-
Notifications
You must be signed in to change notification settings - Fork 44
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
Discussion: Directions to take async interception #60
Comments
When using the Castle.Core - IInterceptor for synchronous operations is straight forward and works very well. So having the IAsyncInterceptor align with Castle.Core being asynchronous would look like this note: that there must be two separate methods for synchronous vs asynchronous as we do not want to introduce sync-over-async or async-over-sync problems public interface IAsyncInterceptor
{
void Intercept(IInvocation invocation);
Task InterceptAsync(IInvocation invocation);
} However, when trying to use the same concept for asynchronous is hard. Accessing the ReturnValue and casting it is problematic as public interface IAsyncInterceptor
{
void Intercept(IInvocation invocation);
Task InterceptAsync(IInvocation invocation);
Task InterceptAsync<TResult>(IInvocation invocation);
} So when implementing an interceptor based on this interface (not addressing the async before proceed problem) is when can/should I think this should be handled by AsyncInterceptor. This can be solved by taking care of return-values which would make this IAsyncInterceptor look like this: public interface IAsyncInterceptor
{
void Intercept(IInvocation invocation);
TResult Intercept<TResult>(IInvocation invocation);
Task InterceptAsync(IInvocation invocation);
Task<TResult> InterceptAsync<TResult>(IInvocation invocation);
} From the discussions in PR 428 - castle.core.
Now we make break out the public interface IAsyncInterceptor
{
void Intercept(IInvocation invocation, Action proceed);
TResult Intercept<TResult>(IInvocation invocation, Func<TResult> proceed);
Task InterceptAsync(IInvocation invocation, Func<Task> proceed);
Task<TResult> InterceptAsync<TResult>(IInvocation invocation, Func<Task<TResult>> proceed);
} The the last touch is to make an public interface IAsyncInterceptor
{
void Intercept(IAsyncInvocation invocation, Action proceed);
TResult Intercept<TResult>(IAsyncInvocation invocation, Func<TResult> proceed);
Task InterceptAsync(IAsyncInvocation invocation, Func<Task> proceed);
Task<TResult> InterceptAsync<TResult>(IAsyncInvocation invocation, Func<Task<TResult>> proceed);
} |
I agree that it would be nice to make your library have a very similar API surface to Castle's. @brunoblank's design comes close to how I would model these interfaces. In addition, I would like to suggest the following:
|
Btw. thank you @JSkimming for asking for my opinion... I appreciate it, even though I have hardly been involved in this project up until now. I am happy to draw up a more detailed draft similar to how @brunoblank did above sometime during the weekend, if that would be deemed welcome/useful. |
Thanks for the comments @brunoblank & @stakx.
The more, the merrier 😃
Yes please 👍 I've read both of your detailed comments, but I don't have the head-space to think things through at the moment, I'll try and give it some time this weekend, though I've tried and failed to meet that commitment in the past. 🤷♂️ I have given some thought on what I'd like to achieve. I want to provide a layering of features, where consumers of this library can take as much or as little as they like. So at the lowest level something like the AsyncDeterminationInterceptor, (I don't like the name, but still can't think of anything better) where it adds very little overhead to a pure Castle.Core implementation. Then add further functionality on top of that, to make implementing common scenarios easier. For instance AsyncTimingInterceptor, which was a problem I'd solve several times on different projects by lifting the same code. Also, one thing I'd like to avoid, which is why I want to remove AsyncInterceptorBase, is having a detrimental effect on implementing synchronous interception within the same interceptor. Thanks again for the input. |
Also, @stakx if you're interested, I'd be happy to have you join the collaborators. |
@JSkimming, thank you very much for the offer. Interested, always (I love technical riddles. 😃), right now I hardly know this project's code base, so I suggest I'll start the humble way and simply submit PRs if I see something worth doing. Once I'm more familiar with the project, I'll be happy to join! In the meantime, let me just put out an idea or two.
What follows in the code block below is an illustration of what I suggested above. If I were to build an async interceptor base class, and I wanted to stay as close as possible to DynamicProxy's API, this is how I would probably do it: // No `IAsyncInterceptor` interface; users of your library are using it precisely
// because they do not want to implement async interception themselves!
public abstract partial class AsyncInterceptor : IInterceptor
{
protected AsyncInterceptor() { }
// Those must be implemented by users. Note the lack of an `InterceptAsync`
// method returning a `Task<T>`. Interceptors do not return a return value
// (as there may not be one in every case); this holds for both the sync and
// async case.
protected abstract void Intercept(IInvocation invocation);
protected abstract Task InterceptAsync(AsyncInvocation invocation);
// Implemented by the base class. The async machinery here looks at the
// method and decides which of the above methods to delegate to.
void IInterceptor.Intercept(IInvocation invocation) { ... }
}
// Note that this does not implement Core's `IInvocation`, which would be
// bad as it would inherit "dangerous" members like `Proceed` (which may
// malfunction in the async case), `ReturnValue` (which might not take any
// effect in the calling code), `SetArgumentValue` (by-ref arguments don't
// play well with `async/await`), etc. Instead, it *wraps* an `IInvocation`!
//
// This type could possibly be hidden behind an `IAsyncInvocation` interface,
// or be made abstract, although the benefits of that are perhaps small.
public sealed partial class AsyncInvocation
{
private readonly IInvocation invocation;
// Note the `readonly`. Should be captured in the ctor before any async.
private readonly IInvocationProceedInfo proceed;
internal AsyncInvocation(IInvocation invocation) { ... }
// Have properties/methods that delegate to the wrapped invocation
// when it is safe to do so:
public MethodInfo Method => this.invocation.Method;
public object Proxy => this.invocation.Proxy;
...
// Convert `object[] Arguments` into a read-only collection, like is the
// case with the underlying `ReturnValue`, modifying by-ref `Arguments`
// will have no effect after the first `await`.
public IReadOnlyList<object> Arguments => this.invocation.Arguments;
// A return value buffer of its own. This is to guarantee that each
// invocation gets its own `Result` preserved and not constantly
// overwritten. This also means we don't need to constantly convert
// between `T` and `Task<T>` when reading from / writing to the underlying
// `ReturnValue`.
//
// The semantics here are that for an intercepted method with a return
// type of `Task<T>`, this property would hold a value of type `T`. (Therefore,
// setting this for a method returning `Task` should throw an exception.)
// That's also the reason why it's called `Result`, not `ReturnValue`.
public object Result { get; set; }
// Replaces the `Proceed` method. Takes care to store the underlying
// `ReturnValue` before proceeding, then extracting the new return value
// after proceeding and storing it in its own `Result` (above), and
// finally restoring the original underlying `ReturnValue`.
public Task ProceedAsync() { ... }
} See this Gist for a working (but simplistic) implementation. Additional abstractions may be added inside the async machinery, e.g. if you want it to be able to deal with awaitable types other than the framework's own (Not sure how all of this can be done without adding a ton of complexity and runtime overhead, so perhaps I'd have several base classes of
I suppose facilities provided by the library might not all build on top of the same basis. For example, would you build a timing interceptor on top of DynamicProxy's solution to this problem would perhaps be to separate different concerns into separate interceptors and compose them into a pipeline. That is, solve the problem through composition, not inheritance. It should be possible to freely mix and match |
Good breakdown @stakx! Personally I really dislike partial classes from external libraries and would prefer an interface. Having the The major pro is that if the interceptor isn't interested in the actual value then it does not have to do anything with it. I think the return value approach is easier to understand and work with. The thin
Yes agree, this is definitely something to investigate. I am throwing an idea into the mix here to see what you think. One "scary" part of this is the rather large surface to touch to cover everything in the |
Hmm @brunoblank, I think for once I cannot agree. (Or I may simply misunderstand what you wrote.)
There's no such thing as a "partial class from external libraries". (I only used the
... except that the problems you described for a Granted, in the async case, you usually do need to return a Both approaches have their pros and cons. My suggestion was simply to stay close to how DynamicProxy works.
Are you talking of present-day
Say you write an interceptor that performs an async log operation. It only wants to log the invocation; it never wants to return a value. You now have to write the same identical interceptor at least twice, once for |
Yes you are right. I was going to write I dislike partial classes as a concept and avoid using them, and I prefer interfaces from external libraries instead of classes. Thanks for pointing it out (I often don't get what I think into what I write :( ) Lets consider this interface: public interface IAsyncInterceptor
{
Task<TResult> InterceptAsync<TResult>(IAsyncInvocation<TResult> invocation);
} So if have an implementation that proceeds it would look like this: public class AsyncInterceptor: IAsyncInterceptor
{
public async Task<TResult> InterceptAsync<TResult>(IAsyncInvocation<TResult> invocation)
{
...
TResult result = await invocation.Proceed();
...
return result;
}
} And yes you would be required to capture and return the result from the inner proceed. And if you don't want to proceed you would have to return something, and returning On the other hand with the Result property I would have to do this : public class MyAsyncInterceptor: AsyncInterceptor
{
protected override async Task InterceptAsync(AsyncInvocation invocation)
{
...
await invocation.Proceed();
object result = invocation.Result;
...
}
} Aha, I now understand that the Result property is the unwrapped result (the Why not call it In the first approach it relies on generics and do the MethodInfo.Invoke under the surface and you get two methods to implement (only considering Task based methods). The second approach only have one method to implement but you have to do the MethodInfo.Invoke yourself (if you need to). However, it still does it under the surface but does not expose it to the user. I would say the second approach aligns closer with Castle (and leaves the messy MethodInfo.Invoke to the user).
That implementation would just implement all interfaces. |
That was from my example above - four methods to implement. |
Hey everyone, I'd like to share something with you that I cooked up just now. A little earlier today, I was looking at the code produced by the C# compiler for If you're interested and if you would like to take a look, here's the resulting code: https://github.com/stakx/DynamicProxy.AsyncInterceptor. (It's only a draft at this stage, though it appears to work, at least in the few simple cases that I've tested.) |
Hi, I don't fully understand your above discussion, and I'm not sure if below is a novel idea... I observe that the implementation of A new interface with proceed func as injected param public interface ISimpleAsyncInterceptor
{
void InterceptSynchronous(IInvocation invocation);
Task InterceptAsynchronous(IInvocation invocation, Func<Task> proceed);
Task<TResult> InterceptAsynchronous<TResult>(IInvocation invocation, Func<Task<TResult>> proceed);
} The standard impl of the new interface is simplified to public class MyInterceptor : ISimpleAsyncInterceptor
{
public async Task InterceptAsynchronous(IInvocation invocation, Func<Task> proceed)
{
// Step 1. Do something prior to invocation.
await proceed();
// Step 2. Do something after invocation.
}
public async Task<TResult> InterceptAsynchronous<TResult>(IInvocation invocation, Func<Task<TResult>> proceed)
{
// Step 1. Do something prior to invocation.
TResult result = await proceed();
// Step 2. Do something after invocation.
return result;
}
public void InterceptSynchronous(IInvocation invocation)
{
// Step 1. Do something prior to invocation.
invocation.Proceed();
// Step 2. Do something after invocation.
}
} That can be transformed to public class BridgingSimpleAsyncInterceptor : IAsyncInterceptor
{
private ISimpleAsyncInterceptor _simpleAsyncInterceptor;
public BridgingSimpleAsyncInterceptor(ISimpleAsyncInterceptor simpleAsyncInterceptor)
{
_simpleAsyncInterceptor = simpleAsyncInterceptor;
}
public void InterceptSynchronous(IInvocation invocation)
{
_simpleAsyncInterceptor.InterceptSynchronous(invocation);
}
public void InterceptAsynchronous(IInvocation invocation)
{
async Task proceed()
{
invocation.Proceed();
await (Task)invocation.ReturnValue;
}
invocation.ReturnValue = _simpleAsyncInterceptor.InterceptAsynchronous(invocation, proceed);
}
public void InterceptAsynchronous<TResult>(IInvocation invocation)
{
async Task<TResult> proceed()
{
invocation.Proceed();
return await (Task<TResult>)invocation.ReturnValue;
}
invocation.ReturnValue = _simpleAsyncInterceptor.InterceptAsynchronous<TResult>(invocation, proceed);
}
} |
About my last comment, I just discover that |
Hi @samuelpsfung, thanks for taking the time to jump in. It is appreciated. When I raised this ticket, we had just released PR #54 to fix a longstanding issue with calling asynchronous operations before At the time, there were several discussions about the best way to solve the problem, potentially adding async support to Castle.Core. Where things current stand is we have a solid working solution, I'm thinking of closing this discussion as it petered out, though happy to leave it open should people see value in sparking things up again. |
Hi @JSkimming This comment says using Then I don't understand why you think |
You are correct and have identified a comment outlining the problem. In the following exact circumstances,
Therefore:
EDIT: To answer your final question.
I consider it a mistake, because the attempt to make async interception more concise, has introduced this issue. There may be better ways of achieving conciseness. |
We've had several discussions on issues and pull requests on this repo, and on Castle.Core, and I wanted to bring it together (if possible).
Firstly, I want to thank you guys for getting involved. I imagine we've all got day jobs, so any time we can give to this is fantastic.
Here is my current thinking, it's by no means an exhaustive list, it's just what's top of mind at the moment.
AsyncInterceptorBase
is a mistake.The current alpha introduces some breaking changes, and I believe we must introduce more, given point 1. I think now is an excellent time to tackle some other gnarly issues if you guys are happy to get involved?
All views regretfully received.
The text was updated successfully, but these errors were encountered: