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

Add new methods ProxyGenerator.CreateDelegateProxy[WithTarget] #403

Closed
wants to merge 10 commits into from

Conversation

stakx
Copy link
Member

@stakx stakx commented Aug 4, 2018

This is in response to #399 (comment):

If both Moq and NSubstitute are doing the same thing for mocking delegates maybe this is something DP should support[.]

and #399 (comment):

If we add delegates proxy support, NSubstitute will not need to use the emit anymore, so it would be a good feature. Likely, the Moq as well[.]

This PR would enable you to do this:

ProxyGenerator proxyGenerator =;
var action = proxyGenerator.CreateDelegateProxy<Action>(interceptors:);
action();

I've taken a look at how this could be implemented in DynamicProxy, and lo and behold! Most of the machinery required for delegate proxying is already there (created by @kkozmic in d3ec3bf for Castle Windsor), just not hooked up to ProxyGenerator yet.

There's a lot of things that still need to be worked out, or worked on:

  • Write unit tests to check whether delegate proxiess' behavior regarding IInvocation.Target, .InvocationTargetMethod, .GetConcreteMethod(), .GetConcreteMethodInvocationTarget() is in line with that of other proxy types.

    Let's skip .GetConcreteMethod() and .GetConcreteMethodInvocationTarget() since those are like the properties but ensure the methods aren't open generic; since it's already impossible to build a delegate proxy for open generic delegate types, there's probably no need to test this.

  • Write unit tests ensuring that delegate proxy types are cached.

  • We need to add documentation (in docs/).

  • Regarding the stability of DelegateProxyGenerator: @fir3pho3nixx: Are you aware of any problems with the use of DelegateProxyGenerator in Windsor?

    I'll be assuming that this hasn't caused any trouble in Windsor until I hear anything to the contrary. See Add new methods ProxyGenerator.CreateDelegateProxy[WithTarget] #403 (comment).

  • We need to decide whether there should be a CreateDelegateProxyWithTarget as well. I have included it for now in a separate commit (which we can easily drop if we want).

    I'll keep that method in the PR for now. See Add new methods ProxyGenerator.CreateDelegateProxy[WithTarget] #403 (comment) below.

  • We might want to deprecate ModuleScope.DefineType and all methods that allow third parties to get hold of DynamicProxy's ModuleBuilders.

    We'll deal with deprecations in a separate PR. See Add new methods ProxyGenerator.CreateDelegateProxy[WithTarget] #403 (comment).

  • The method name: CreateDelegateProxy is in line with the names of other methods on ProxyGenerator, but semantically, I'm not sure if it's correct. Should it be called CreateDelegate or CreateProxyDelegate instead?

    Let's use the name as currently proposed in this PR. See Add new methods ProxyGenerator.CreateDelegateProxy[WithTarget] #403 (comment).

  • I have no idea how stable DelegateProxyGenerator is. I've already found out that it cannot deal with in parameters properly (perhaps because it doesn't copy custom attributes and custom modifiers?).

    DelegateProxyGenerator appears to be reasonably stable, it reuses a lot of the same classes that regular class proxy generators use. I'm adding unit tests to verify the most common operations one would perform on a delegate proxy.

    It might be worthwhile seeing whether Windsor is actually using it

    Yes, it is.

  • We need to add XML documentation.

  • We need to add argument validation.

  • There are tests for DelegateProxyGenerator, but we'd need tests for ProxyGenerator.CreateDelegateProxy.

Reviewers: This is at an early stage, feel free to add any input or additional requirements that you might have.

/cc @zvirja

@stakx stakx force-pushed the delegate-proxies branch 7 times, most recently from 7defa23 to 2f7ca73 Compare August 5, 2018 17:43
@stakx stakx requested a review from jonorossi August 5, 2018 17:45
@stakx stakx changed the title WIP: Add new method ProxyGenerator.CreateDelegateProxy Add new methods ProxyGenerator.CreateDelegateProxy[WithTarget] Aug 5, 2018
throw new ArgumentNullException(nameof(target));
}

return (TDelegate)(object)CreateDelegateProxyImpl(typeof(TDelegate), (Delegate)(object)target, interceptors);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These ugly type casts could be avoided by adding a where TDelegate : Delegate constraint. This would require C# 7.3, which unfortunately Mono does not yet support, even in the current stable version (5.12).

@@ -78,6 +78,14 @@ public Type CreateClassProxyType(Type classToProxy, Type[] additionalInterfacesT
return generator.GetGeneratedType();
}

public Type CreateDelegateProxyType(Type delegateToProxy)
{
AssertValidType(delegateToProxy);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an aside, I noticed a fair amount of code duplication in the methods used for validating arguments.

  • For DefaultProxyBuilder, there's AssertValidType and AssertValidTypes.
  • For ProxyGenerator, there is CheckNotGenericTypeDefinition and CheckNotGenericTypeDefinitions.
  • In ProxyGenerator, I opted not to use the latter because they throw the wrong type of exception (GeneratorException when I needed ArgumentException), so I duplicated the same validation logic once again.

These all do very similar things. Perhaps we could merge them sometime.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm interpreting your 👍 as, "Yes, let's get rid of this code duplication (but in a separate PR)." Let me know if I misinterpreted. :)

/// <returns>
/// New delegate of type <paramref name="delegateToProxy"/> that, when invoked, calls the specified <paramref name="interceptors"/>.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown when the given <paramref name="delegateToProxy"/> object is a null reference (Nothing in Visual Basic).</exception>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forget to mention "[...] or when interceptors is a null reference [...]". Same in other XML documentation comments. Going to rectify this soon.

/// <remarks>
/// Implementers should return a proxy type that contains an <c>Invoke</c> method with the same signature as
/// the <c>Invoke</c> method of the given delegate type <paramref name="delegateToProxy"/>.
/// This <c>Invoke</c> method should delegate all executions to the interceptors passed to the proxy type's constructor.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also mention "[...] delegate all executions to the target and to the interceptors passed to the proxy type's constructor [...]". Going to add this soon.

@jonorossi
Copy link
Member

@stakx it looks great so far. There were a bunch of small things looking over the code but I expect many will be sorted when you finish it.

There's a lot of things that still need to be worked out, or worked on:

What was the plan for the Moq/NSubstitute interface delegates?

Regarding the stability of DelegateProxyGenerator: [..] Are you aware of any problems with the use of DelegateProxyGenerator in Windsor?

It's been there since Windsor 2.5 in 2010. I don't think any of the Windsor typed factory problems are related to this, mostly lifetimes and other things.

We need to decide whether there should be a CreateDelegateProxyWithTarget as well. I have included it for now in a separate commit (which we can easily drop if we want).

I don't really foresee much use, however it might come in handy for someone, especially that you can't implement it yourself (unless if you added an interceptor last) for the target to be called in the middle of the interceptor chain.

We might want to deprecate ModuleScope.DefineType and all methods that allow third parties to get hold of DynamicProxy's ModuleBuilders.

Yep, I think we do that one separately. We've still got the issue open.

The method name: CreateDelegateProxy is in line with the names of other methods on ProxyGenerator, but semantically, I'm not sure if it's correct. Should it be called CreateDelegate or CreateProxyDelegate instead?

Maybe I'm missing the semantics you are seeing, but CreateDelegateProxyWithTarget would be returning a proxy to a delegate (with interceptors) where it calls your delegate, just because it creates a delegate to get there wouldn't make it a generic "create delegate". If I'm completely missing the point more info would be good.

@stakx
Copy link
Member Author

stakx commented Aug 9, 2018

@jonorossi, thanks for reviewing.

Regarding the stability of DelegateProxyGenerator: [...]

It's been there since Windsor 2.5 in 2010. I don't think any of the Windsor typed factory problems are related to this, mostly lifetimes and other things.

I'll tick this one off then... unless and until I hear anything else about it.

We need to decide whether there should be a CreateDelegateProxyWithTarget as well.

I don't really foresee much use, however it might come in handy [...]

I take this to mean that you're not actively against that method. Since I've already implemented it and it doesn't add a lot of extra code to DynamicProxy, I'd say let's include it.

We might want to deprecate [...]

Yep, I think we do that one separately.

Sounds good.

What was the plan for the Moq/NSubstitute interface delegates?

Generally speaking, we'd ideally get things working downstream with the new CreateDelegateProxy before we merge this PR.

I'm hoping @zvirja will chime in and say how he plans to proceed with NSubstitute.

Speaking for Moq, I'm still working out all the required changes caused by going from a custom-emitted container interface type to simply using DP's CreateDelegateProxy. Here's one problem I've encountered:

There's one subtle change that caused me trouble in Moq. When using the new generator.CreateDelegateProxy, Moq's interceptors receive an IInvocation whose .Method now points to the Invoke method in the original (proxied) delegate type (e.g. it points at System.Func<,>.Invoke). When Moq was generating its own container interface type for an Invoke method, and then using CreateInterfaceProxyWithoutTarget on that, invocation.Method would point at that container interface's Invoke method (e.g. Castle.Proxies.Func``2Proxy.Invoke). That can be worked around, but it's a functional change and I'm not sure it is to be expected, or whether DP's delegate proxies misbehave in this regard.

The method name [...]

If you're satisfied with the name CreateDelegateProxy, then so am I. We don't need to make a big deal out of this, I just thought I'd bring it up in case you also found the name weird. But I'm totally happy to put a tick that one off on my list.

@jonorossi
Copy link
Member

There's one subtle change that caused me trouble in Moq. When using the new generator.CreateDelegateProxy, Moq's interceptors receive an IInvocation whose .Method now points to the Invoke method in the original (proxied) delegate type (e.g. it points at System.Func<,>.Invoke). When Moq was generating its own container interface type for an Invoke method, and then using CreateInterfaceProxyWithoutTarget on that, invocation.Method would point at that container interface's Invoke method (e.g. Castle.Proxies.Func``2Proxy.Invoke). That can be worked around, but it's a functional change and I'm not sure it is to be expected, or whether DP's delegate proxies misbehave in this regard.

That sounds like a defect in CreateDelegateProxy. Could you add a unit test for IInvocation.Method and IInvocation.MethodInvocationTarget (they could go into the test file for either delegates or Invocation methods). Probably also worth adding one for IInvocation.GetConcreteMethod, maybe the whole suite of properties and methods should be tested 😉.

@stakx
Copy link
Member Author

stakx commented Aug 9, 2018

I actually just re-read my above post and came to the opposite conclusion: CreateDelegateProxy does the right thing. I am nevertheless going to add these tests to make sure that delegate proxies' behavior are in agreement with other proxy types.

Fortunately for us, @kkozmic already wrote the logic behind this in
d3ec3bf (in the form of `DelegateProxyGenerator`), so it's mostly a
matter of creating a more convenient API for it in `ProxyGenerator`.
The tests in there are really about `DelegateProxyGenerator`.
Create a new fixture (having the same name as the one we previously
renamed) with tests for the new `CreateDelegateProxy` functionality.

The tests targeting `in` parameters are currently failing. We'll need
to fix this in subsequent commits.
It turns out that the tests from the previous commit fail due to a bug
in the runtime (`in` parameters + generic types cause `ldtoken` IL to
reference a non-existent method) that we've previously encountered.

Skip these tests and replace them with similar ones that do not rely
on a generic delegate type. These will pass just fine.
... and tests verifying that the proxy calls (interceptors and) the
target as it should.
Note that these tests don't opt into the same testing pattern used
with other proxy kinds; i.e. we don't expand the `ProxyKind` enum be-
cause the tests using it often wouldn't work for delegate proxies.
This is because they are more limited than other proxy types and don't
support most of the usual things like hooks or additional interfaces.
These tests attempt to document what `IInvocation.Method` and `IInvoc-
ation.MethodInvocationTarget` should be for the various proxy types.

These tests essentially document two facts:

 * `IInvocation.Method` points to the corresponding method in the
   proxied type.

 * `IInvocation.MethodInvocationTarget` points to the method that will
   get invoked when invocation proceeds to the target, or `null` if
   there is no target.

No tests are added for `IInvocation.GetConcreteMethod()` and `IInvoc-
ation.GetConcreteMethodInvocationTarget()`. These deal mostly with
open generics and don't appear to be relevant for delegate proxies.
@stakx
Copy link
Member Author

stakx commented Aug 9, 2018

@jonorossi - Here's a summary of the latest changes:

  • Fixed the glitches in XML documentation comments that I mentioned in review comments above.
  • Added tests for IInvocation.Method and IInvocation.MethodInvocationTarget.
  • For good measure, threw in some extra tests verifying type caching, as well as updated written documentation.

This PR looks more or less finished for me, let me know if you find anything else that needs changing.

@@ -70,7 +70,7 @@ public void InterfaceProxyWithTarget_MethodInvocationTarget_should_be_methodOnTa
}

[Test]
public void InterfaceProxyWithTarget_MethodInvocationTarget_should_be_null()
public void InterfaceProxyWithoutTarget_MethodInvocationTarget_should_be_null()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed a typo here; otherwise unrelated to this PR.

using NUnit.Framework;

[TestFixture]
public class InvocationMethodTestCase : BasePEVerifyTestCase
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole file is more or less a copy of InvocationMethodInvocationTargetTestCase.cs adjusted to .Method (instead of .MethodInvocationTarget). I removed a few non-delegate proxy tests where I'm simply not sufficiently knowledgeable to know what the expected behavior should be. We can always add these back at a later time.

@stakx
Copy link
Member Author

stakx commented Sep 12, 2018

@jonorossi: You've given us a lot to think about. I'm hoping we don't have to take such a large step back... but if we do, that's OK. I'll need to think about this carefully for some time.

In the meantime, another possible approach for discussion: we could introduce a ProxyGenerationOptions.AdditionalMethods. This would work similarly to AdditionalInterfaces and allow one to add any methods with the same signatures (name, return type, parameter list, calling convention) as some specified existing methods. For example:

options.AdditionalMethods.Add(typeof(SomeDelegateType).GetMethod("Invoke"));
generator.Create...(..., options); // business as usual

That's likely the smallest additive change to the public API I can come up with. Quite similar to mixins. It might also be useful in scenarios we don't even know yet (DLR scenarios maybe?).

It would mean mocking libraries still need to call Delegate.CreateDelegate themselves for the additional method on the returned proxy object... but that should be fairly trivial.

If we went that way we might want to limit AdditionalMethods such that any possible conflict with existing methods causes an exception right away, otherwisewe might run into some tricky questions of semantics.

@jonorossi
Copy link
Member

You've given us a lot to think about. I'm hoping we don't have to take such a large step back

I wasn't intending for it to be a step backwards but a step forward, we improve the existing DelegateProxyGenerator so we can get ProxyGenerationOptions into it and get a set of proper overloads on ProxyGenerator for delegates which handle the Delegate.CreateDelegate for you. I suspect I dumped a little too much info into a single comment, I sort of dumped all my notes while taking a look at where we could go.

In the meantime, another possible approach for discussion: we could introduce a ProxyGenerationOptions.AdditionalMethods

Interesting, however I'm struggling to see a good use case for this other than using it for delegates here, we want users adding a contract (interface or mixin). This would still require you specify a base class or interface and how would you specify the target?

If we went that way we might want to limit AdditionalMethods such that any possible conflict with existing methods causes an exception right away, otherwisewe might run into some tricky questions of semantics.

This is where I think it gets a little ugly, we'd be introducing something that we don't really want, and then asking all the mocking libraries to use it, and then we'd have to support it.

@jonorossi
Copy link
Member

If we take some of what I spoke about and did one step at a time. I'm thinking we want 4 methods something like these:

TDelegate CreateDelegateProxy<TDelegate>(TDelegate target, params IInterceptor[] interceptors) where TDelegate : System.Delegate;
TDelegate CreateDelegateProxy<TDelegate>(TDelegate target, ProxyGenerationOptions options, params IInterceptor[] interceptors) where TDelegate : System.Delegate;

object CreateDelegateProxy(Type delegateToProxy, object target, params IInterceptor[] interceptors);
object CreateDelegateProxy(Type delegateToProxy, object target, ProxyGenerationOptions options, params IInterceptor[] interceptors);

Straight away the code in the pull request can do the first of the pairs. We just need to look at getting the ProxyGenerationOptions through (the proxy class already has a field for it) and beef up DelegateProxyGenerator to do our bidding. We can work through each property of the options class if you feel this is a good way forward.

@stakx
Copy link
Member Author

stakx commented Sep 12, 2018

I wasn't intending for it to be a step backwards but a step forward [...]

Sorry for not being more precise, I meant "taking a step back" in the sense of "looking at the big picture & at the fundamentals", not in the sense of "regressing to a worse state". Going back to look at the very basics and work forward from there just seems like a ton of work and I was hoping we wouldn't have to go back quite that far. But I agree it might become necessary eventually, and I'm ready for it.

Interesting, however I'm struggling to see a good use case for this other than using it for delegates here

Same here. But it has the advantage of being a generic mechanism (as opposed to the earlier, overly specific CreateDelegateWithInvokeMethodCompatibleWith) that should mostly integrate fairly seamlessly with the rest of DP, and be fairly logical/non-contradictory (cf. below). I just thought I'd mention it.

This is where I think it gets a little ugly, [...]

True. Probably best indeed to drop this idea. 😁

Regarding your latest comment, that brings us back to where we were before: CreateDelegateProxy being a leaky abstraction IMO. You ask for a delegate (something "invokable"), yet you hand over options for a type (something "instantiable"), thus DP is revealing how delegate proxy creation works internally.

Sure, we can follow this solution, if we're all fine with this (IMO) minor API design flaw. This approach is entirely practical, after all. I guess it wouldn't be too hard to propagate the ProxyGenerationOptions further, all the way to to type emitting code in DP.

@stakx
Copy link
Member Author

stakx commented Sep 12, 2018

Btw. like with any other class proxy, I believe a target wouldn't be strictly required. Sometimes you just want to create a delegate whose implementation is provided by the interceptors, without ever .Proceed()-ing to (i.e. invoking) some target delegate. So perhaps we'd want both CreateDelegateProxy, and a CreateDelegateProxyWith[out]Target (depends on what the default would be).

@jonorossi
Copy link
Member

Sorry for not being more precise

No problem. It just felt a little like we are going around in circles so I was trying to work out what we need the implementation to have to support the required scenarios.

But it has the advantage of being a generic mechanism (as opposed to the earlier, overly specific CreateDelegateWithInvokeMethodCompatibleWith) that should mostly integrate fairly seamlessly with the rest of DP, and be fairly logical/non-contradictory (cf. below). I just thought I'd mention it.

Hmmm yes, there was something about the CreateDelegateWithInvokeMethodCompatibleWith proposal I didn't like but couldn't put my finger on, and I think that is it isn't generic especially that the method doesn't need to be called Invoke it can be named anything.

I'm definitely liking ProxyGenerationOptions.AdditionalMethods over CreateDelegateWithInvokeMethodCompatibleWith.

Regarding your latest comment, that brings us back to where we were before: CreateDelegateProxy being a leaky abstraction IMO. You ask for a delegate (something "invokable"), yet you hand over options for a type (something "instantiable"), thus DP is revealing how delegate proxy creation works internally.

Very true, I guess we've come down to the point where there is no way to provide the control over the type without exposing the fact that the type exists in the first place.

Btw. like with any other class proxy, I believe a target wouldn't be strictly required.

But class proxies always have targets, either you provide one and virtual members will go there or the proxy's base class is essentially the target.

Sometimes you just want to create a delegate whose implementation is provided by the interceptors, without ever .Proceed()-ing to (i.e. invoking) some target delegate. So perhaps we'd want both CreateDelegateProxy, and a CreateDelegateProxyWith[out]Target (depends on what the default would be).

Your AdditionalMethods idea isn't looking too bad with how complicated this will get and this replicates the normal proxy functionality for something fairly uncommon in most applications.

I don't think AdditionalMethods can be of type MethodInfo because semantically it is a bit wrong as that belongs to another type and it'll cause problems like we used to have before changing AdditionalAttributes to IList<CustomAttributeInfo>.

I wonder if we can change mixins to support this rather than another property which works similar.

How do you see the API being used, just throwing something together at the moment:

// Example: Moq mocking MyClass

var options = new ProxyGenerationOptions();
// add any attributes
options.AdditionalMethods.Add(typeof(SomeDelegate).GetMethod("Invoke"));

Type t = typeof(IMocked<>).MakeGenericType(typeof(MyClass));
var proxy = generator.CreateInterfaceProxyWithoutTarget(t, options, interceptors);

var @delegate = Delegate.CreateDelegate(typeof(SomeDelegate), proxy, "Invoke");
@delegate.Invoke();

With AdditionalMethods it would always require the user to have a class or interface in the mix, where as the current DelegateProxyGenerator does not require either.

@jonorossi
Copy link
Member

Btw. like with any other class proxy, I believe a target wouldn't be strictly required. Sometimes you just want to create a delegate whose implementation is provided by the interceptors, without ever .Proceed()-ing to (i.e. invoking) some target delegate. So perhaps we'd want both CreateDelegateProxy, and a CreateDelegateProxyWith[out]Target (depends on what the default would be).

I think we should just allow the target to be null rather than another set of overloads/methods. CreateClassProxy and CreateClassProxyWithoutTarget is very wordy, and they are actually implemented quite differently (inheritance vs composition).

@stakx
Copy link
Member Author

stakx commented Sep 12, 2018

With AdditionalMethods it would always require the user to have a class or interface in the mix, where as the current DelegateProxyGenerator does not require either.

Right. Having a container type in the mix might give some clients more control than they need (FakeItEasy, Windsor), but at the same time doesn't overly restrict others (Moq, and to some degree NSubstitute) and won't cause anyone much additional work to actually create a delegate if that's all they need. (We could provide a convenience extension method for that.)

How do you see the API being used

  • For Moq, your code example looks about right.

  • NSubstitute (if I understood correctly) would use options.AdditionalAttributes instead of an additional interface.

  • FakeItEasy (if I understood correctly) wouldn't use either of those as they map types outside of the generated proxy type.

  • Similarly for Windsor.

I don't think AdditionalMethods can be of type MethodInfo

Right. That was probably a little too simplistic. We'd perhaps need to define some kind of AdditionalMethodInfo type:

sealed class AdditionalMethodInfo
{
    public MethodInfo TemplateMethodForSignature { get; }

    // later, perhaps also (for NSubstitute):
    public IList<CustomAttributeInfo> AdditionalAttributes { get; }
    ...
}

I wonder if we can change mixins to support this

Perhaps by special-casing the mixin facility so it accepts delegate types as mixins? Delegate types don't implement any explicit interface but they do fulfill a well-known contract (of which we'd probably be forgiven for cherrypicking justInvoke and ignoring [Begin|End]Invoke).

(Btw., I don't have a clear favourite solution so far. They all have their distinct [dis]advantages.)

@blairconrad
Copy link
Contributor

FakeItEasy (if I understood correctly) wouldn't use either of those as they map types outside of the generated proxy type.

We'd started mapping types outside of the generated proxy type specifically for delegates, because our hand-rolled delegates didn't lend themselves to intrinsic mapping to the "fake manager". Previously we'd used a mechanism similar to Moq's, if I understand its usage.

I'd have to confirm with @thomaslevesque, but I don't think FakeItEasy would reject changing the mechanism we use to link proxies to our managers.

@thomaslevesque
Copy link
Contributor

I'd have to confirm with @thomaslevesque, but I don't think FakeItEasy would reject changing the mechanism we use to link proxies to our managers.

I have no objection to change it if it makes sense 👍

@jonorossi
Copy link
Member

Sorry for the delay, I too didn't know which way to go.

With AdditionalMethods it would always require the user to have a class or interface in the mix, where as the current DelegateProxyGenerator does not require either.

Right. Having a container type in the mix might give some clients more control than they need (FakeItEasy, Windsor), but at the same time doesn't overly restrict others (Moq, and to some degree NSubstitute) and won't cause anyone much additional work to actually create a delegate if that's all they need. (We could provide a convenience extension method for that.)

I was wrong, just realised there is no reason you couldn't just do CreateClassProxy with typeof(object), no methods, no additional interfaces.

I don't think AdditionalMethods can be of type MethodInfo

Right. That was probably a little too simplistic. We'd perhaps need to define some kind of AdditionalMethodInfo type:

For some reason I was actually thinking we couldn't make it of type MethodInfo because that would cause problems because MethodInfo defines so much stuff that only relates to the method on another type, however we'd obviously just document the few things we read off the template (name, return parameter, return type, generic arguments, parameters) and what isn't supported (custom attributes), and throw for constructors, abstract and static. We can't read custom attributes from the MethodInfo because some can't be decoded from the reflection API (i.e. why we moved to our own CustomAttributeInfo.

I wonder if we can change mixins to support this

Perhaps by special-casing the mixin facility so it accepts delegate types as mixins? Delegate types don't implement any explicit interface but they do fulfill a well-known contract (of which we'd probably be forgiven for cherrypicking justInvoke and ignoring [Begin|End]Invoke).

(Btw., I don't have a clear favourite solution so far. They all have their distinct [dis]advantages.)

Having more of a look at mixins I don't think there will even be much special casing for MulticastDelegate (see this article why you should pretend System.Delegate does not exist) especially since reflection doesn't hide that delegates are just a class. We could even decide to call AddMixinInstance but it might be safer (and more discoverable) if we add a AddMixinDelegate or AddDelegateMixin method. Right now the ProxyGenerationOptions and MixinData classes don't do much checking, MixinData has one line we could change to populate the contract from the delegate type rather than the object instance's interface. Then come to the emitted proxy type, we could emit slightly different code depending on if there was a target to proceed to but ultimately the main structure of the mixin fields and the likes seem pretty close to what we want.

Here is a runnable test case with a mixin, along with a bunch of comments for what I'm thinking:

using System;
using Castle.DynamicProxy;

public delegate int AdditionDelegate(int x, int y);

class Program
{
    static void Main(string[] args)
    {
        var options = new ProxyGenerationOptions();
        options.AddMixinInstance(new Addition()); // working mixin for running code
        //options.AddMixinInstance(typeof(AdditionDelegate));              // without target
        //options.AddMixinInstance(new AdditionDelegate((x, y) => x + y)); // with target

        // MixinData.MixinInterfaces[0] - the contract is the delegate type
        // MixinData.Mixins[0]          - the delegate instance if one, or null

        var generator = new ProxyGenerator();
        var proxy = generator.CreateClassProxy(typeof(object), options, new HelloInterceptor());

        // Obviously not required, but we can just document that all delegates used as mixins will
        // be given the method name "Invoke" since that is the name in the metadata of the class,
        // however would be nice to have a method so a user doesn't need to call CreateDelegate
        var @delegate = (AdditionDelegate)Delegate.CreateDelegate(
            typeof(AdditionDelegate), (object)proxy, "Invoke");
        //var @delegate = ProxyUtil.CreateDelegateToMixin<AdditionDelegate>(proxy);
        //var @delegate = ProxyUtil.CreateDelegateToMixin(proxy, typeof(AdditionDelegate));

        int z = @delegate.Invoke(10, 20);
        Console.WriteLine(z);
    }
}

// Working mixin implementation that we'd be able to do via just a delegate
public interface IAddition
{
    int Invoke(int x, int y);
}
public class Addition : IAddition
{
    public int Invoke(int x, int y)
    {
        Console.WriteLine("Calculating...");
        return x + y;
    }
}

public class HelloInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Console.WriteLine("Before");
        invocation.Proceed();
        Console.WriteLine("After");
    }
}

It doesn't allow you to emit custom attributes on the proxy's methods (which sounded like a nice to have, not a requirement), but it does still allow custom attributes on the proxy type, as well as doing whatever you like with the container in terms of base class, additional interfaces, etc. The implementation of this should be quite easy because the InvokeMethodOnTarget will call on the mixin field typed after the delegate (rather than an interface like normal), which is much cleaner than having a new set of fields if we went with the AdditionalMethods option. I think this way will also prevent users misusing something like AdditionalMethods doing something undesirable.

Does this proposal fit everyone's requirements? If you are not sure, please just ask. Please let me know which bits you like or dislike.

@stakx
Copy link
Member Author

stakx commented Sep 19, 2018

@jonorossi - apologies for being a bit passive myself regarding this issue. I'm still quite busy settling into my new daytime job, trying to be new best buddies with PHP. (It's challenging. 😃)

Does this proposal fit everyone's requirements?

Speaking for Moq, this proposal should work.

Please let me know which bits you like or dislike.

  • I like options.AddMixinInstance(new AdditionDelegate((x, y) => x + y)).

  • options.AddMixinInstance(typeof(AdditionDelegate)) on the other hand, not so much. Calling a Type object an "instance" seems contradictory. I'd prefer something without Instance in the method name, e.g. options.AddMixin(typeof(…)) or options.AddDelegateMixin(typeof(…)). Perhaps delegate mixins should generally use a different method name for consistency.

  • ProxyUtil.CreateDelegateToMixin(proxy, typeof(AdditionDelegate)) sounds like a useful helper method to have, too, because most clients are going to have to do the Delegate.CreateDelegate step one way or another. Having such a helper method in DynamicProxy would relieve clients from having to map delegate signatures to the generated method.

The last point brings me to...

// Obviously not required, but we can just document that all delegates used as mixins will
// be given the method name "Invoke" since that is the name in the metadata of the class,
// however would be nice to have a method so a user doesn't need to call CreateDelegate

As noted above, if we added a method such as ProxyUtil.CreateDelegateToMixin, we wouldn't even need to document the name(s) of the generated delegate methods. DynamicProxy would take care of mapping a delegate type to the right method.

That being said, I believe it would be very reasonable to call all delegate mixins' methods Invoke, and officially document that, too. Only then would ProxyUtil.CreateDelegateToMixin be a truly optional helper method (as suggested by the Util in ProxyUtil) that one can choose to use or not use.

Calling all delegate mixin methods Invoke should work fine as long as noone tries to add mixins for two delegates having the same signature. Say, if someone does this:

options.AddMixinInstance(new Func<object, bool>(obj => true));
options.AddMixinInstance(new Predicate<object>(obj => true));

Not sure why anyone would want to do this, but we'd probably have to identify the signature collision and throw an InvalidOperationException (or similar) for the second call. (Alternatively, we could do the same that we do for interface methods and switch to an "explicit implementation" name whenever a name/signature collision occurs. Not sure that makes any sense though.)

// MixinData.MixinInterfaces[0] - the contract is the delegate type
// MixinData.Mixins[0]          - the delegate instance if one, or null

I cannot comment on this bit yet, as I'm still somewhat unfamiliar with how mixins are kept track of internally. Will look into it.

@stakx
Copy link
Member Author

stakx commented Sep 19, 2018

P.S. I am not sure whether the "with target" / "without target" mapping to AddMixinInstance(delegate) / AddMixinInstance(delegateType) is obvious enough. Up until now, it has always been possible to Proceed for invocations of mixed-in methods; that'll no longer be the case for AddMixinInstance(delegateType). (Are we all comfortable with that change btw.?) Just saying this as another argument why we might possibly want to call that method overload something different.

@thomaslevesque
Copy link
Contributor

Does this proposal fit everyone's requirements?

To be honest, I didn't follow very closely and I'm not sure what you're proposing exactly... With your proposal, how should we create a delegate proxy?

@stakx
Copy link
Member Author

stakx commented Sep 19, 2018

@thomaslevesque, I'll attempt an answer by example. Following @jonorossi's proposal, if all you needed were a delegate proxy, then usage might look like this in the most basic case:

// You'll need distinct `options` per delegate type. Specify additional options as needed:
var options = new ProxyGenerationOptions();
options.AddMixinInstance(typeof(SomeDelegate));

// Then use it to generate a proxy. That proxy will have an `Invoke` method with the specified
// delegate type's signature. Proxy type, base class, interceptors etc. are chosen as usual:
var proxy = proxyGenerator.CreateClassProxy(typeof(object), options, new MyInterceptor(...));

// Either call `Delegate.CreateDelegate` for the generated `Invoke` method yourself, or use a
// helper method provided by DynamicProxy:
var someDelegate = (SomeDelegate)ProxyUtil.CreateDelegateToMixin(proxy, typeof(SomeDelegate));

// Invoking the returned delegate will trigger the interceptor(s) specified above:
someDelegate(...)

@blairconrad
Copy link
Contributor

@stakx, thanks for the summary (and @jonorossi for the proposal!). It seems like an easy enough recipe to follow, and I've no doubt that FakeItEasy could use it.

I've never used DynamicProxy outside of FakeItEasy, and have no experience with the mixin functionality, so maybe this is all idiomatic, but the CreateDelegateToMixin invocation sounds funny. What does ToMixin mean here? It sounds more like FromMixin to me…

@thomaslevesque
Copy link
Contributor

@stakx, thanks. I guess we could use this, but it doesn't seem very straightforward, especially compared to the CreateDelegateProxy that was suggested at some point.

@blairconrad
Copy link
Contributor

@thomaslevesque, it's true, but if I understand things, it does offer the ability to have the proxy implement an interface, which would be helpful if we want to use that mechanism for providing a link to the FakeManager…

@jonorossi
Copy link
Member

I going to start by explaining how the current mixin functionality works, I obviously assumed that was obvious which it isn't and isn't even a common or well documented feature.

DynamicProxy Mixins aren't very complicated. You add an object instance as a "mixin" during proxy creation, DP will look at the interfaces the class implements and it'll additionally implement those interfaces on the proxy class and pass calls to those methods through to the mixin object. Proxies that have a target obviously hold a reference to the target (in DP it is just a field of the proxy class), the same occurs with mixin "targets", it creates a field for each interface the mixin implements. You can't cast the proxy instance to the mixin class but you can to any interface the mixin implements.

My proposal is to do the same sort of thing with delegates. Since we can't explicitly expose the delegate's contract on a proxy class (a bit like the mixin isn't explicitly exposed) the proxy class would implement the delegate contract and optionally hold a reference to a target delegate if you do "Proceed" to the delegate. I think this proposal is cleaner than adding a completely different type of proxy for delegates.

@stakx I know I wrote a lot in my last comment, but yes I agree we'd want a different method name for adding the delegate type. I didn't mention it in my last comment but one feature of mixins that should have been supported was to specify which interfaces you want DP to implement for you rather than it just adding all, I think this would fit in well with the add mixin method we'd add for delegates.

@stakx Mixin support already requires the interfaces of mixins to be unique, i.e. two mixins can't implement the same interface; and we'd do the same thing here so you can't add two delegates with the same signature to the same proxy.

@blairconrad CreateDelegateToMixin or CreateDelegateFromMixin, could probably go either way, but my thinking is that it is creating a delegate pointing to a mixin.

@thomaslevesque At first it might seem more complicated than the CreateDelegateProxy proposal without knowing DP mixins, but this way there is no need to document a special "delegate matched container type". Does my explanation above help make the proposal clearer that this results in essentially the same proxy type because we would have had to hold on to an optional delegate target too, but this way you can generate the proxy just as you would a class or interface and the extra method(s) get mixed in?

@thomaslevesque
Copy link
Contributor

Does my explanation above help make the proposal clearer that this results in essentially the same proxy type because we would have had to hold on to an optional delegate target too, but this way you can generate the proxy just as you would a class or interface and the extra method(s) get mixed in?

Yes, it's perfectly clear. Thanks @jonorossi!

@blairconrad
Copy link
Contributor

Ah, yes. That is very clear. Thanks for detailed explanation, @jonorossi. I'm sorry you had to put so much time into it!

@stakx
Copy link
Member Author

stakx commented Oct 1, 2018

@jonorossi:

Apologies once again for the long delay. Above, you wrote:

one feature of mixins that should have been supported was to specify which interfaces you want DP to implement for you rather than it just adding all, I think this would fit in well with the add mixin method we'd add for delegates.

Let me attempt a sketch for a new mixin API:

public void AddMixin(object instance, params Type[] mixedInTypes)

This should cover what you mentioned. mixedInTypes would have to be either:

  1. one or more interface types, all of which must be implemented by instance's runtime type (directly, or via any of its base classes).
  2. a single delegate type, which must be identical to instance.GetType().
  3. possibly an empty array, in which case the method would assume that you want to mix in all interface types implemented by instance's runtime type.
public void AddMixin(object instance, Type mixedInType)

This wouldn't be strictly necessary, as it is a special case of the previous method. It would be especially useful when you want to specify only one type to be mixed in, as with delegate mixins. It could also prevent an unnecessary params array allocation in other cases.

public void AddMixin(Type mixedInType)

This would be needed for delegate mixins when you do not want to specify an instance (aka target).

We could optionally relax constraints a little further and allow mixedInType to be an interface type, which would turn this API into an alternative to AdditionalInterfaces (but at the level of the proxy generation options vs. single Create…Proxy… calls). (This might be useful for mocking libraries such as Moq that let all their proxies implement a specific interface.)

public void AddMixinInstance(object instance)

This is what we have today. We could deprecate it in favor of (reimplement it by delegating to) the first method listed above.

Finally, we could go a little further and have a MixinCollection Mixins { get; } in ProxyGenerationOptions so you could write options.Mixins.Add(...) instead of options.AddMixin(...). That's mostly cosmetics, though.

Any thoughts? If this sounds about right, I could attempt an implementation for it.

@stakx
Copy link
Member Author

stakx commented Mar 13, 2019

Closing in favour of the PR referenced above.

@stakx stakx closed this Mar 13, 2019
@jonorossi
Copy link
Member

@stakx apologies I didn't respond to your questions here on 2 Oct 2018, not sure how I missed it.

@jonorossi
Copy link
Member

@stakx were you planning to make any of your proposed changes on 2 Oct 2018 around being able to add a mixin and specifying the interfaces? If so, did you want to create another issue so we can look over the API changes.

@stakx
Copy link
Member Author

stakx commented Mar 29, 2019

@jonorossi:

were you planning to make any of your proposed changes on 2 Oct 2018 around being able to add a mixin and specifying the interfaces?

TBH, the mixin API in general isn't super-dear to me. I cannot say whether it gets used much at all, but I suspect not. So the time spent on improving it (purely for its own sake) would perhaps be better invested elsewhere.

When I prepared my delegate mixin PR I decided on more specifically-named methods, because it's more obvious to the end user what can be mixed in. With the proposal I originally made above, you'd have had to check the XML documentation to figure out that you can use it to mix-in delegates.

That being said, if you'd like, I'll open a new issue so we can track this properly.

@jonorossi
Copy link
Member

TBH, the mixin API in general isn't super-dear to me. I cannot say whether it gets used much at all, but I suspect not. So the time spent on improving it (purely for its own sake) would perhaps be better invested elsewhere.

When I prepared my delegate mixin PR I decided on more specifically-named methods, because it's more obvious to the end user what can be mixed in. With the proposal I originally made above, you'd have had to check the XML documentation to figure out that you can use it to mix-in delegates.

That being said, if you'd like, I'll open a new issue so we can track this properly.

Good answer, that is what I expected. I'm happy for us to improve things in the future when/if there is demand.

@stakx stakx deleted the delegate-proxies branch April 3, 2019 07:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants