Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Asynchronous Interceptors #145

Closed
ghost opened this issue Mar 2, 2016 · 57 comments · Fixed by #439
Closed

Asynchronous Interceptors #145

ghost opened this issue Mar 2, 2016 · 57 comments · Fixed by #439
Milestone

Comments

@ghost
Copy link

ghost commented Mar 2, 2016

Are async interceptors in the pipeline for this library?

This would be such a good feature, as it would allow for async actions before the invocation is proceeded.

I would not mind contributing to this, as long as I could get some architecture advice.

@hammett
Copy link
Contributor

hammett commented Mar 2, 2016

Not sure what you mean?
We have adjusted some of our infrastructure to use async by inspecting the "awaitable" of a return type, and if detected, add a continuation. Not sure this needs to be directly supported by the interceptor, but I'm interested in your proposal.

@joelweiss
Copy link

Even if I try to hook up a continuation like suggested on StackOverflow it will still not work properly if I want to call in the continuation invocation.Proceed() because the interceptor index gets decremented here, so it will call the first inceptor again and again.

@hammett
Copy link
Contributor

hammett commented Mar 2, 2016

I dont think calling proceed in the continuation is a good idea anyways, you wont get a Task instance until you run the actual method.

Looking fwd to your proposal.

@kkozmic
Copy link
Contributor

kkozmic commented Mar 2, 2016

We've had this discussion some time ago. It breaks the abstraction

On Thu, 3 Mar 2016, 10:34 hamilton verissimo notifications@github.com
wrote:

I dont think calling proceed in the continuation is a good idea anyways,
you wont get a Task instance until you run the actual method.

Looking fwd to your proposal.


Reply to this email directly or view it on GitHub
#145 (comment).

sent from my phone
Krzysztof

@joelweiss
Copy link

@hammett You are correct, but you can make it work if you return TaksCompletionSource<>.Task and when you get the final task you can await it and then call taskCompletionSource.SetResult(task.Result).

My scenario is I have an IFooService and when I call fooService.GetData() the first interceptor is a CacheInterceptor that tries to fetch the data from the cache asynchronasly and if it's not in the cache, I want to call invocation.Proceed() to the HttpInterseptor and make an asynchronous http call to get the data.

I was thinking something like this might enable my scenario.

    interceptors[currentInterceptorIndex].Intercept(this);
#if DOTNET40
    if (ReturnValue != null)
    {
        var returnType = Method.ReturnType;
        if (returnType == typeof(Task) || (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)))
        {
            await(Task)ReturnValue;
        }
    }

@kkozmic probably refers to #107 and that's probably more what @mlfletcher1988 is talking about.

@ghost

This comment has been minimized.

@hammett
Copy link
Contributor

hammett commented Mar 4, 2016

this looks as a very specialized use of the interceptors and TPL. I'm not sure it makes sense to add support for this at Castle.Core level, especially since TPL comes with its own pandora's box.

@ghost
Copy link
Author

ghost commented Mar 4, 2016

Not really, it would just be a really good feature to allow async calls to be made before the intercepted invocation is proceeded.

@hammett
Copy link
Contributor

hammett commented Mar 6, 2016

I'll leave this up to @jonorossi and @kkozmic then.

@kkozmic
Copy link
Contributor

kkozmic commented Mar 6, 2016

I see no compelling reason. IMO it's a clear case of cost far outweighing the benefit

@ghost
Copy link
Author

ghost commented Mar 6, 2016

With some architecture advice I would be happy to implement this as a proof of concept.

Is there developer documentation/guidelines for contributing to the project?

@ghost
Copy link
Author

ghost commented Jun 29, 2017

@alex-training
Copy link

Hi everyone,

Is there any progress being made on this?

@ndrwrbgs
Copy link

@alex-training not an optimal solution (over there we also concluded the fixes are best made in Core) but https://github.com/JSkimming/Castle.Core.AsyncInterceptor at least helps with some cases of this.

@jonorossi
Copy link
Member

Is there any progress being made on this?

This stackoverflow question has the current options: https://stackoverflow.com/questions/28099669/intercept-async-method-that-returns-generic-task-via-dynamicproxy

I am not aware of anyone working on DynamicProxy to contribute changes in this area. If someone wanted to put forward a summary of the changes that would need to be made to DP to support this we can consider this.

@kkozmic
Copy link
Contributor

kkozmic commented Feb 13, 2018

My opinion on this hasn't changed since we first looked at this. Are there cases where this solution (or a variation of thereof) won't solve?

@jonorossi
Copy link
Member

@kkozmic not that I'm aware of, I'm of the same opinion, however I've wondered previously whether a helper extension method (e.g. await invocation.ProceedAsync();) would be useful for users.

@alex-training
Copy link

@kkozmic All presented solutions are only workarounds, the biggest challenge is performance.

Because in order to intercept asynchronous method with a result we have to use reflection:

async Task<TResult> ProceedAsynchronous<TResult>(IInvocation invocation)

In the https://github.com/JSkimming/Castle.Core.AsyncInterceptor library it is addressed via hacks around generics:

 Type taskReturnType = returnType.GetGenericArguments()[0];
 MethodInfo method = HandleAsyncMethodInfo.MakeGenericMethod(taskReturnType);
 return (GenericAsyncHandler)method.CreateDelegate(typeof(GenericAsyncHandler));

https://github.com/JSkimming/Castle.Core.AsyncInterceptor/blob/master/src/Castle.Core.AsyncInterceptor/AsyncDeterminationInterceptor.cs#L93

Nowadays asynchronous operations are widespread so I it would be nice to have a native support of them in the Core

@hammett
Copy link
Contributor

hammett commented Feb 13, 2018

Just implement a layer of code generation in a base interceptor, maybe even with lambdas. I really dont see why Castle.Core needs to dabble with TPL.

@alex-training
Copy link

alex-training commented Feb 13, 2018

@hammett Well, code generation is actually what Castle.Core is responsible for.
Bringing that piece of functionality into the base interceptor could overcomplicate the whole system,

Imagine you want to introduce a caching layer for you async methods, such task should be easily implemented as follows:

invocation.ReturnValue = cache.Get<T>(key, () => await Proceed());

I do not say we need some ProceedAsync, rather some efficient way to work with async methods out of the box.

However, anyway, would you be so kind to present any examples of code generation?

@hammett
Copy link
Contributor

hammett commented Feb 13, 2018

application? I said "base interceptor".

Anyways, if you don't know how, that's a valid argument, but then I'd encourage you to work with @JSkimming.

DP doesn't need to know about TPL coz it's not a special IL construct or metadata info. It's a compiler trick, as it was said before. DP is already a complex beast, and we all know each feature comes with a long tail.

@alex-training
Copy link

alex-training commented Feb 13, 2018

@hammett My point is that currently, implementing of such trivial tasks as caching, logging, retrying takes too many efforts with doubtful outcome (performance penalties)

@alex-training
Copy link

alex-training commented Feb 13, 2018

@ndrwrbgs
Copy link

Regarding the message above, I’ve considered code generation in the async interceptor library. There are a couple of problems around how things are implemented in Castle today that require some major hacks from us over there (castle expects that when control comes back to it, the method is finished executing, which isn’t true in async).

We could potentially (between myself and @JSkimming) try to contribute back to Castle, but we both felt the Castle people were much more proficient with such generation, the overall framework, and likely async than we are.

@ndrwrbgs
Copy link

I'll add it to my backlog :) Sadly, as I have a solution that's "good enough" for right now, I don't expect it to get done until end of year :( [well, unless it's dynamic interception for tracing purposes profiles to be too unperformant]

@alex-training, I'd recommend giving the interceptor a try and seeing if it's really not performant enough. Further performance improvements can come later via a nuget upgrade if it's "good enough" perf for you right now. Note though, I'm not sure if we've documented it officially, but it will only work as long as your first "await" is await proceed(invocation). Otherwise we get into some issues with Castle's refcounting

@alex-training
Copy link

@ndrwrbgs I really appreciate your efforts.

Hope that more people will be interested in finding a real performant solution.

I'll definitely give a try to the interceptor, although I have some concerns regarding underlying ConcurrentDictionary.

@JSkimming
Copy link
Contributor

I'm happy to give it a shot it, though I'm not sure what "it" is.

  • Do people want the capability (currently limited) of our extension Castle.Core.AsyncInterceptor brought into Castle.Core?
  • Do people want the current limitations of AsyncInterceptor resolved.
  • Do people want guaranteed performance which has not been demonstrated form AsyncInterceptor?

Background regarding current limitations of AsyncInterceptor

@ndrwrbgs and I have hit somewhat of an impasse with AsyncInterceptor whereby we cannot make true1 asynchronous calls before calling IInvocation.Proceed().

To cut a long story short, this is because AbstractInvocation is state-full specifically currentInterceptorIndex, and returning from IInterceptor.Intercept() before calling IInvocation.Proceed() (which is what we need to do2) caused unpredictable behaviour with race conditions and stack overflows.

We concluded that supporting changes are required in Castle.Core or we need to replace, if that's possible, the implementations of IInvocation with our own.

@ndrwrbgs, please do correct me if I've misrepresented things as you see it.

Background regarding performance of AsyncInterceptor

I've used this library in many live systems, though I fully concede this is anecdotal and YMMV. You should conduct performance testing if you're writing performance critical code, but that statement is true regardless of whether one is using third-party libraries or not.

@ndrwrbgs and I have discussed producing some performance metrics, though it's not been high on my list because the performance of AsyncInterceptor is good enough for my needs.

1. true asynchronous being calls that return an incomplete Task. Task.Yield() is sufficient to exercise the problem Task.FromResult() or Task.CompletedTask is not.
2. It's a complicated explanation, but we attempt to replace the Task that would otherwise be returned by the intercepted code with our own.

@zvolkov
Copy link

zvolkov commented Jan 7, 2019

So, I took @brunoblank 's workaround and adapted it to my case (without IAsyncInvocation's Proceed returning a task but simply using regular IInvocation and returning the Task in Invocation.ReturnValue). Long story short, I think it does work, and I tested it with nested interceptors having an await before calling Proceed. Then I compared Castle's original AbstractInvocation and my version of @brunoblank 's side by side.... And I think the only real difference between the two is the fact that @brunoblank uses Enumerator instead of an AbstractInvocation's index to go over the chain of interceptors - and so in effect @brunoblank logic never decrements the index back as each consecutive interceptor is done with its job and is ready to return up the stack.

So my question: wouldn't it be sufficient to remove the following code to fix AbstractInvocation?

finally { currentInterceptorIndex--; }

If I'm missing something here, can someone explain?

@brunoblank
Copy link

brunoblank commented Jan 7, 2019

@zvolkov I am glad the workaround work for you.

First off, there is a problem with the workaround as it does not detect if there are multiple calls to the Proceed method, this can however be solved with a counter (which is why I think AbstractInvocation uses the index in the first place).

I think the real reason why the workaround works and not AbstractInvocation is that the Invokation.ReturnValue is overwritten as it does not (a)wait the interceptor to complete before continuing. The workaround solves this by returning the value instead, and the Invokation.ReturnValue is set only once.

To implement this fix in Core would be a breaking change as IInterceptor.Proceed is a void method.

If the change was made to return object instead, what should be returned if the return-type is void?

I have pushed a fix for CreateClassProxy<TClass> problem (the error message is not entirely correct, but the code does not go into a infinite loop):

            if (ReferenceEquals(invocation.InvocationTarget, invocation.Proxy))
            {
                throw new InvalidOperationException(
                    "This is a DynamicProxy2 error: invocation.Proceed() has been called more times than expected." +
                    "This usually signifies a bug in the calling code. Make sure that the last interceptor" +
                    " selected for the method '" + Method + "'" +
                    " calls invocation.Proceed() at most once.");
            }

@zvolkov
Copy link

zvolkov commented Jan 8, 2019

Hey @brunoblank thanks for getting back.

As for detecting multiple calls to Proceed, I do not believe it is framework's responsibility to enforce the proper use of design. I'm fine with having this responsibility lie on developer, as long as the restriction is clearly documented.

As for overwriting ReturnValue, I think I disagree with your assessment both conceptually and based on my real testing. Conceptually, an await operator operates on an object (IAwaitable or really anything that implements GetAwaiter() method or extension method) - not on an expression (inv.ReturnValue). So once the await has evaluated the expression and got its object, even if the value of the expression changes (as the nested interceptors overwrite ReturnValue) the await will still "look" at the original object it "saw" the first time. This is the behavior I see in my real testing, with nested interceptors updating ReturnValue to their respective Tasks, as they bubble up the stack, and I'm 99% sure it works correctly even after the innermost task completes and the chain of continuations "magically" continues with the next line after the innermost await. Each continuation completes its own Task which is what the next outer await "remembers" and "looks at" since it saw it the first time.

In short, I believe Castle's separation between void IInterceptor.Proceed and object ReturnValue continues to work just fine for async scenarios and does not need to change. Based on my research and testing it does look like decrementing the index is the only problem with AbstractInvocation. I wonder if there is a way to prove that conclusively with a series of unit-tests?

CreateClassProxy remains a problem (as does any method that creates InheritanceInvocation instead of CompositionInvocation). It's nice that you submitted a fix to detect the infinite loop and throw a proper exception. Of course, it would be much nicer if Castle's multiple implementations of IInvocation.InvocationTarget did not violate Liskov Substitution Principle and returned the ultimate Target regardless of whether it's Inheritcance- or Composition-style proxy. Something for the core developers to think about, I guess.

@brunoblank
Copy link

@zvolkov good analyze! I have reviewed and I agree with you regarding the ReturnValue, that is not the problem.

I have done some testing with removing the finally { currentInterceptorIndex--; }.

This makes the straight forward async case work, but then there is an unit-test failing:
Interceptor_can_proceed_multiple_times

I checked the test and realized that the workaround does not actually work the same way and has to be rewritten a bit. I think it is best explained with this example.
Lets say you want to implement a retry interceptor. when calling the Proceed method the second time it will no longer call the same interceptor (or target) again because it has not done the "currentInterceptorIndex--"

My initial though on this is that it could possibly be solved using a recursive method keeping track of the "next in line" instead of a counting up / down the currentInterceptorIndex.

I will post an update after once I have had time to do some implementation and testing.

@brunoblank
Copy link

I have now updated the workaround to be "recursive style" commit. My testing so far indicates it works for async and also calling proceed multiple times invoke the expected interceptor/target.

I have also removed the IAsyncInvokation and IAsyncInterceptor as per @zvolkov insights that the Invokation.ReutrnValue is not the problem.

This should hopefully give the core developers and god idea on how to implement a fix in Core.

I also agree with @zvolkov:

CreateClassProxy remains a problem (as does any method that creates InheritanceInvocation instead of CompositionInvocation). It's nice that you submitted a fix to detect the infinite loop and throw a proper exception. Of course, it would be much nicer if Castle's multiple implementations of IInvocation.InvocationTarget did not violate Liskov Substitution Principle and returned the ultimate Target regardless of whether it's Inheritcance- or Composition-style proxy. Something for the core developers to think about, I guess.

@zvolkov
Copy link

zvolkov commented Jan 8, 2019

@brunoblank you are completely right about calling Proceed multiple times in case of multiple retries happening inside a resilient interceptor. It is the scenario that decrementing the interceptor index was meant to address.

I see your recursive logic instantiates new Invocation for every level of nesting. That's a nice way to keep track of the index, with zero changes to the user API (IInvocation/IInterceptor) but I'm not sure about performance impact of creating multiple instances of Invocation.

It would be nice to find a way to support the interceptor retries without instantiating a separate Invocation object for each level of interception nesting and with minimal changes to the IInvocation/IInterceptor API...

Here is one idea I have:

public void Proceed(IInterceptor currentInterceptor)
{
        this.currentInterceptorIndex = Array.IndexOf(this.interceptors, currentInterceptor);
        this.Proceed();
}

basically, add a special overload of Proceed that restarts the invocation chain from a given interceptor. This would be called by those interceptors that need to await/invoke Proceed multiple times.

@brunoblank
Copy link

@zvolkov I like your idea and was thinking in the same direction. That would be a breaking change and that would maybe be something for the next major release.

I see your recursive logic instantiates new Invocation for every level of nesting. That's a nice way to keep track of the index, with zero changes to the user API (IInvocation/IInterceptor) but I'm not sure about performance impact of creating multiple instances of Invocation.

I totally agree, I don't think this is the solution more like giving some ideas on how to adress it.

I have been looking at how asp.net core implements middleware which is working very similar comparing Proceed() with next()

I think there are nice features that could be implemented if the ProxyGenerator would return a IInterceptionBuilder (compared with IApplicationBuilder) consider this psuedo code

            var proxy = ProxyGenerator
                .CreateClassProxyWithTarget(new TargetClass())
                .Use(async (invocation, next) =>
                {
                    Console.WriteLine("-- before --");
                    await next();
                    Console.WriteLine("--- after ---");
                })
                .UseInterceptor<MyOtherInterceptor>()
                .Build();
public class MyOtherInterceptor
{
    private readonly RequestDelegate _next;

    public MyOtherInterceptor(RequestDelegate next)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task Intercept(IInvocation invocation)
    {
        Console.WriteLine("--- before ---");
        await _next(invocation);
        Console.WriteLine("--- after ---");
    }
}

@brunoblank
Copy link

@zvolkov I have updated the workaround project with parts of the changes discussed above (commit).

@jonorossi please have a look at the workaround example, I think it would be fairly straight forward to implement this in Core, however this is a breaking change (void Proceed() is moved from IInvocation to IInterceptor.Intercept):

public delegate void InvocationDelegate(IInvocation invocation);

public interface IInterceptor
{
    void Intercept(IInvocation invocation, InvocationDelegate proceed);
}

@JSkimming
Copy link
Contributor

I created an example PR #337 last year to demonstrate the issue (see my comment above). I've just rebased it off master and incorporated @brunoblank PR #428 to see if the changes address my example.

It does, great work @brunoblank 👍 see my new PR #429 for the results.

@jonorossi
Copy link
Member

Many thanks guys for continuing to work on this, sorry for the silence on my end.

Great to see the changes are quite minimal, however this breaking change scares me because IInterceptor is likely the most user-implemented interface across all Castle projects, this is a breaking change I'm not convinced on making.

We'd need to either support this callback IInterceptor style side-by-side, or go with a non-breaking API alternative.

I know I've said earlier in this thread that I'd prefer not to create an IInvocation instance for each interceptor, but I'd prefer that rather than this breaking change. The performance of a new invocation instance for each interceptor probably isn't significant and most people don't usually have too many interceptors. We might also be able to create a simple IInvocation class that wraps the real IInvocation class and holds the reference to the next interceptor target for Proceed, that way arguments, mixins, etc are only stored in the one IInvocation instance.

Thoughts? Opinions?

@brunoblank
Copy link

brunoblank commented Jan 21, 2019

Good feedback @jonorossi, I understand and share your concerns regarding changing the IInterceptor interface.

I tried the wrapper approach first but got issues with IChangeProxyTarget as many tests started to fail, I am not sure how this works, and also the InvokeMethodOnTarget is protected protected abstract void InvokeMethodOnTarget();

I also got into some tricky parts that I did not solve for the create new interceptor per proceed approach, the currentIntercptorIndex needed to be passed to the ctor when creating a new instance which is part of the generated code.

One enabling approach could be to make the InvokeMethodOnTarget available in the IInvocation interface opening up for a workaround implementation in dependent projects without reflection-invoke performance penalty.

@stakx
Copy link
Member

stakx commented Mar 21, 2019

I've studied this issue and looked at some linked code, however without being able to try things out right now, so please apologize if I still misunderstand.

It appears that the one single problem to be solved in DP was, and still is, the statefulness of AbstractInvocation specifically currentInterceptorIndex, right?

You have discussed various workarounds around this central issue, e.g. building a chain of several distinct invocation objects instead of just one, or giving Proceed an additional delegate-typed parameter. All of these workarounds would appear to have the problem of putting additional pressure on the GC since more objects get created on the heap.

What I am wondering, without having studied this completely through, is whether it would be possible to extract currentInterceptorIndex from the invocation into some cheap-to-copy InterceptionPipelineProgress value type. IInterceptor.Intercept would receive an instance of this type along with an IInvocation, and IInvocation.Proceed would demand such an instance (passed by-ref).

This would relieve GC pressure, but still be a breaking change, so as @jonorossi said it would possibly have to be done side-by-side to the existing API. Given interceptors that support some new IInterceptor2 interface, DP would then hit a slightly different codegen path.

This is just an idea off the top of my head, I'll think about this some more ASAP.

@JSkimming
Copy link
Contributor

@stakx yes, that's a good summary.

One thing worth adding, which is solved by the proposal by @brunoblank, is the Task (or Task<>) returned by the underlying implementation (e.g. the method being intercepted) needs to be replaced by the one the interceptor creates.

This only manifests as an issue if the interceptor executes asynchronously before calling Proceed() because then interceptor cannot replace the ReturnValue property before it is passed back through the call chain.

The idea of creating a side-by-side implementation seems like a reasonable compromise given the constraints.

Something like @brunoblank's implementation could work, though I like the idea of changing IInvocation to something like this:

public interface IInvocation2
{
    // All the same properties and methods as IInvocation except ...

    // Remove ReturnValue.
    //object ReturnValue { get; set; }

    // Change Proceed to return the return value, or null for void methods.
    object Proceed();
}
public interface IInterceptor2
{
    // Intercept now needs to return the return value.
    object Intercept(IInvocation2 invocation);
}

A no-op implementation of IInterceptor2 could be like this:

public class NoOpInterceptor : IInterceptor2
{
    public object Intercept(IInvocation2 invocation)
    {
        return invocation.Proceed();
    }
}

This would allow an interceptor to do something like this.

public class PointlessInterceptor : IInterceptor2
{
    public object Intercept(IInvocation2 invocation)
    {
        return JustToBeAsync(invocation);
    }

    public async Task JustToBeAsync(IInvocation2 invocation)
    {
        await Task.Yield();

        // Assume the return value is a Task.
        Task realReturnValue = (Task) invocation.Proceed();

        await realReturnValue;

        await Task.Yield();
    }
}

What do you think?

@stakx
Copy link
Member

stakx commented Mar 21, 2019

What do you think?

It's still too early for me to make any definite judgement on concrete new APIs. For now, I just want to get a full understanding of the problem(s) that we're trying to solve... so thank you for reminding me of the return value aspect.

   // Remove ReturnValue.
   //object ReturnValue { get; set; }

That being said, pulling the return value out of IInvocation, and shifting it to Intercept's and Proceed's return value "parameter", doesn't feel quite right. While it might simplify some aspects of dealing with async interception, it also breaks the abstraction of IInvocation being a complete, self-contained description of one invocation, and that doesn't seem desirable... unless there simply isn't any feasible alternative.

As soon as I find a good chunk of thinking time, I'll check out @brunoblank's proposal & the other open PR to better understand how you guys arrived at this solution.

@stakx
Copy link
Member

stakx commented Mar 25, 2019

TL;DR:

We don't need to make breaking changes to DynamicProxy's API to enable async interceptors. The only thing stopping us appears to be the Proceed-after-await malfunction (which can be fixed with e.g. #428 or #439). Apart from that, follow a few basic rules (below) and you should be fine.

Do we need an AsyncInterceptor base class in DynamicProxy?

I think that we should prioritize making asynchronous interceptors possible; but DynamicProxy doesn't necessarily have to provide an AsyncInterceptor base class of its own—that's something that can go in a separate contrib package.

The reason for this is that today, the number of awaitable types is potentially unlimited. If it were still 2010, with .NET 4.0 the latest, we might have written an AsyncInterceptor base class that could deal with exactly Task and Task<T>. Today, we would either have to add support for ValueTask<T> (and force a dependency on the System.Threading.Tasks.Extensions package on downstream clients), or base an implementation on duck-typing and the generic .GetAwaiter() machinery... which isn't actually feasible because awaiter types only allow you to wait for completion of, but not to complete, awaitables. (JSkimming/Castle.Core.AsyncInterceptor and its use of TaskCompletionSource<>, or the explanations further below, demonstrate why that would be necessary.)

Any AsyncInterceptor class that we write therefore runs the risk of not covering all awaitable types that people require.

One argument for putting an AsyncInterceptor class in Castle.Core would be that writing it is so difficult that people are likely to get it wrong. A shared open-source implementation might reduce that risk.

Regardless of where async interceptors are implemented, what's stopping us?

It still seems to me that the only roadblock is invocation.Proceed not working as expected when preceded by any await. PRs #428 and #439 address this issue in different ways. While #428 might be more thread-safe than #439, it causes a much larger breaking change in DynamicProxy's API. For that reason I think we should favour #439 (or something similar it).

Some guidelines for dealing with invocation.ReturnValue

@JSkimming, you mentioned the problem of return values:

One thing worth adding [...] is the Task (or Task<>) returned by the underlying implementation (e.g. the method being intercepted) needs to be replaced by the one the interceptor creates.

This only manifests as an issue if the interceptor executes asynchronously before calling Proceed() because then interceptor cannot replace the ReturnValue property before it is passed back through the call chain.

Apart from the fixable Proceed-after-await malfunction, what needs to happen when a method with an awaitable return type (say Task<>) gets intercepted appears entirely feasible today, without any further API changes:

  1. If the interceptor does not .Proceed(), it must provide the intercepted method's implementation, and therefore set a return value; i.e. it must set ReturnValue to a suitable Task<> instance.

  2. If the interceptor awaits anything, it must set ReturnValue before the first await. This can always be done using e.g. a Task<> derived from a TaskCompletionSource<>. That task's result can then be set at any time, even after any number of awaits.

  3. If the interceptor does .Proceed(), any one of the subsequent interceptors or the proxy target object (doesn't matter which) might set ReturnValue to a Task<> Y (as per rule 1). (If that doesn't happen, and our interceptor cannot come up with its own return value, it may abort with a NotImplementedException like DynamicProxy would in similar synchronous scenarios.) The interceptor needs to await Y, but (as per rule 2) not before it has re-set ReturnValue to a Task<> X of its own. Once Y completes, the interceptor gets its return value, optionally does some computations with it, then sets the result of X accordingly.

  4. All other interceptors follow the same set of rules.

Please correct me if I've forgotten anything, or made a mistake.

@stakx stakx added this to the v4.4.0 milestone Mar 26, 2019
@jonorossi
Copy link
Member

Many thanks @stakx for pushing ahead on this one. I agree we need to fix the Process-after-await malfunction to allow people to use DP with async code and include a bunch of documentation/guidance on how to do it, then in the future look at add built-in support if desired.

I'll add my comments to #439.

@htzhang2
Copy link

Is AsyncInterceptor ready?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.