diff --git a/CHANGELOG.md b/CHANGELOG.md index 52c310d909..dfc6561584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Enhancements: - Added trace logging level below Debug; maps to Trace in log4net/NLog, and Verbose in Serilog (@pi3k14, #404) - Recognize read-only parameters by the `In` modreq (@zvirja, #406) - DictionaryAdapter: Exposed GetAdapter overloads with NameValueCollection parameter in .NET Standard (@rzontar, #423) +- Ability to add delegate mixins to proxies using `ProxyGenerationOptions.AddDelegate[Type]Mixin`. You can bind to the mixed-in `Invoke` methods on the proxy using `ProxyUtil.CreateDelegateToMixin`. (@stakx, #436) - New `IInvocation.CaptureProceedInfo()` method to enable better implementations of asynchronous interceptors (@stakx, #439) Deprecations: diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/DelegateMixinTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/DelegateMixinTestCase.cs new file mode 100644 index 0000000000..ace7091cf6 --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/DelegateMixinTestCase.cs @@ -0,0 +1,311 @@ +// Copyright 2004-2010 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Castle.DynamicProxy.Tests +{ + using System; + using System.Reflection; + + using NUnit.Framework; + + [TestFixture] + public class DelegateMixinTestCase : BasePEVerifyTestCase + { + [Test] + public void ProxyGenerationOptions_AddDelegateTypeMixin_when_given_null_throws_ArgumentNullException() + { + var options = new ProxyGenerationOptions(); + Assert.Throws(() => options.AddDelegateTypeMixin(null)); + } + + [Test] + public void ProxyGenerationOptions_AddDelegateTypeMixin_when_given_non_delegate_type_throws_ArgumentException() + { + var options = new ProxyGenerationOptions(); + Assert.Throws(() => options.AddDelegateTypeMixin(typeof(Exception))); + } + + [Test] + public void ProxyGenerationOptions_AddDelegateTypeMixin_when_given_delegate_type_succeeds() + { + var options = new ProxyGenerationOptions(); + options.AddDelegateTypeMixin(typeof(Action)); + } + + [Test] + public void ProxyGenerationOptions_AddDelegateMixin_when_given_null_throws_ArgumentNullException() + { + var options = new ProxyGenerationOptions(); + Assert.Throws(() => options.AddDelegateMixin(null)); + } + + [Test] + public void ProxyGenerationOptions_AddDelegateMixin_when_given_delegate_succeeds() + { + var options = new ProxyGenerationOptions(); + options.AddDelegateMixin(new Action(() => { })); + } + + + [Test] + public void ProxyGenerator_CreateClassProxy_can_create_delegate_proxy_without_target() + { + var options = new ProxyGenerationOptions(); + options.AddDelegateTypeMixin(typeof(Action)); + var _ = new Interceptor(); + var proxy = generator.CreateClassProxy(typeof(object), options, _); + } + + [Test] + public void ProxyGenerator_CreateInterfaceProxyWithoutTarget_can_create_delegate_proxy_without_target() + { + var options = new ProxyGenerationOptions(); + options.AddDelegateTypeMixin(typeof(Action)); + var _ = new Interceptor(); + var proxy = generator.CreateInterfaceProxyWithoutTarget(typeof(IComparable), options, _); + } + + [Test] + public void ProxyGenerator_CreateInterfaceProxyWithTarget_can_create_delegate_proxy_without_target() + { + var target = new Target(); + + var options = new ProxyGenerationOptions(); + options.AddDelegateTypeMixin(typeof(Action)); + var _ = new Interceptor(); + var proxy = generator.CreateInterfaceProxyWithTarget(typeof(IComparable), target, options, _); + } + + [Test] + public void ProxyGenerator_CreateClassProxy_can_create_callable_delegate_proxy_without_target() + { + var options = new ProxyGenerationOptions(); + options.AddDelegateTypeMixin(typeof(Action)); + + var interceptor = new Interceptor(); + + var proxy = generator.CreateClassProxy(typeof(object), options, interceptor); + var action = ProxyUtil.CreateDelegateToMixin(proxy); + Assert.NotNull(action); + + action.Invoke(); + Assert.AreSame(typeof(Action).GetTypeInfo().GetMethod("Invoke"), interceptor.LastInvocation.Method); + } + + [Test] + public void ProxyGenerator_CreateInterfaceProxyWithoutTarget_can_create_callable_delegate_proxy_without_target() + { + var options = new ProxyGenerationOptions(); + options.AddDelegateTypeMixin(typeof(Action)); + + var interceptor = new Interceptor(); + + var proxy = generator.CreateInterfaceProxyWithoutTarget(typeof(IComparable), options, interceptor); + var action = ProxyUtil.CreateDelegateToMixin(proxy); + Assert.NotNull(action); + + action.Invoke(); + Assert.AreSame(typeof(Action).GetTypeInfo().GetMethod("Invoke"), interceptor.LastInvocation.Method); + } + + [Test] + public void ProxyGenerator_CreateInterfaceProxyWithTarget_can_create_callable_delegate_proxy_without_target() + { + var target = new Target(); + + var options = new ProxyGenerationOptions(); + options.AddDelegateTypeMixin(typeof(Action)); + + var interceptor = new Interceptor(); + + var proxy = generator.CreateInterfaceProxyWithTarget(typeof(IComparable), target, options, interceptor); + var action = ProxyUtil.CreateDelegateToMixin(proxy); + Assert.NotNull(action); + + action.Invoke(); + Assert.AreSame(typeof(Action).GetTypeInfo().GetMethod("Invoke"), interceptor.LastInvocation.Method); + } + + [Test] + public void ProxyGenerator_CreateClassProxy_cannot_proceed_to_delegate_type_mixin() + { + var options = new ProxyGenerationOptions(); + options.AddDelegateTypeMixin(typeof(Action)); + + var interceptor = new Interceptor(shouldProceed: true); + + var proxy = generator.CreateClassProxy(typeof(object), options, interceptor); + var action = ProxyUtil.CreateDelegateToMixin(proxy); + Assert.NotNull(action); + + Assert.Throws(() => action.Invoke()); + } + + [Test] + public void ProxyGenerator_CreateInterfaceProxyWithoutTarget_cannot_proceed_to_delegate_type_mixin() + { + var options = new ProxyGenerationOptions(); + options.AddDelegateTypeMixin(typeof(Action)); + + var interceptor = new Interceptor(shouldProceed: true); + + var proxy = generator.CreateInterfaceProxyWithoutTarget(typeof(IComparable), options, interceptor); + var action = ProxyUtil.CreateDelegateToMixin(proxy); + Assert.NotNull(action); + + Assert.Throws(() => action.Invoke()); + } + + [Test] + public void ProxyGenerator_CreateInterfaceProxyWithTarget_cannot_proceed_to_delegate_type_mixin() + { + var target = new Target(); + + var options = new ProxyGenerationOptions(); + options.AddDelegateTypeMixin(typeof(Action)); + + var interceptor = new Interceptor(shouldProceed: true); + + var proxy = generator.CreateInterfaceProxyWithTarget(typeof(IComparable), target, options, interceptor); + var action = ProxyUtil.CreateDelegateToMixin(proxy); + Assert.NotNull(action); + + Assert.Throws(() => action.Invoke()); + } + + [Test] + public void ProxyGenerator_CreateClassProxy_can_proceed_to_delegate_mixin() + { + var target = new Target(); + + var options = new ProxyGenerationOptions(); + options.AddDelegateMixin(new Action(target.Method)); + + var interceptor = new Interceptor(shouldProceed: true); + + var proxy = generator.CreateClassProxy(typeof(object), options, interceptor); + var action = ProxyUtil.CreateDelegateToMixin(proxy); + Assert.NotNull(action); + + action.Invoke(); + Assert.True(target.MethodInvoked); + } + + [Test] + public void ProxyGenerator_CreateInterfaceProxyWithoutTarget_can_proceed_to_delegate_mixin() + { + var target = new Target(); + + var options = new ProxyGenerationOptions(); + options.AddDelegateMixin(new Action(target.Method)); + + var interceptor = new Interceptor(shouldProceed: true); + + var proxy = generator.CreateInterfaceProxyWithoutTarget(typeof(IComparable), options, interceptor); + var action = ProxyUtil.CreateDelegateToMixin(proxy); + Assert.NotNull(action); + + action.Invoke(); + Assert.True(target.MethodInvoked); + } + + [Test] + public void ProxyGenerator_CreateInterfaceProxyWithTarget_can_proceed_to_delegate_mixin() + { + var target = new Target(); + + var options = new ProxyGenerationOptions(); + options.AddDelegateMixin(new Action(target.Method)); + + var interceptor = new Interceptor(shouldProceed: true); + + var proxy = generator.CreateInterfaceProxyWithTarget(typeof(IComparable), target, options, interceptor); + var action = ProxyUtil.CreateDelegateToMixin(proxy); + Assert.NotNull(action); + + action.Invoke(); + Assert.True(target.MethodInvoked); + } + + [Test] + public void Can_mixin_several_different_delegate_types_simultaneously() + { + var options = new ProxyGenerationOptions(); + options.AddDelegateTypeMixin(typeof(Action)); + options.AddDelegateTypeMixin(typeof(Action)); + + var interceptor = new Interceptor(); + + var proxy = generator.CreateClassProxy(typeof(object), options, interceptor); + + var action = ProxyUtil.CreateDelegateToMixin(proxy); + Assert.NotNull(action); + action.Invoke(); + + var intAction = ProxyUtil.CreateDelegateToMixin>(proxy); + Assert.NotNull(action); + intAction.Invoke(42); + } + + [Test] + public void Cannot_mixin_several_delegate_types_with_same_signature() + { + var options = new ProxyGenerationOptions(); + options.AddDelegateTypeMixin(typeof(Func)); + options.AddDelegateTypeMixin(typeof(Predicate)); + Assert.Throws(() => options.Initialize()); + + } + + [Serializable] + public sealed class Target : IComparable + { + public bool CompareToInvoked { get; set; } + + public bool MethodInvoked { get; set; } + + public int CompareTo(object obj) + { + CompareToInvoked = true; + return 123; + } + + public void Method() + { + MethodInvoked = true; + } + } + + private sealed class Interceptor : IInterceptor + { + private readonly bool shouldProceed; + + public Interceptor(bool shouldProceed = false) + { + this.shouldProceed = shouldProceed; + } + + public IInvocation LastInvocation { get; set; } + + public void Intercept(IInvocation invocation) + { + LastInvocation = invocation; + if (shouldProceed) + { + invocation.Proceed(); + } + } + } + } +} diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/MixinDataTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/MixinDataTestCase.cs index b4d703dcab..e8fe9d9526 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/MixinDataTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/MixinDataTestCase.cs @@ -193,5 +193,97 @@ public void TwoMixinsWithSameInterfaces() new MixinData(new object[] { mixin1, mixin2 }) ); } + + [Test] + public void Equals_false_for_two_instances_having_different_delegate_type_mixins() + { + MixinData mixinData1 = new MixinData(new object[] { typeof(Action) }); + MixinData mixinData2 = new MixinData(new object[] { typeof(Action) }); + Assert.AreNotEqual(mixinData1, mixinData2); + } + + [Test] + public void Equals_true_for_two_instances_having_same_delegate_type_mixins() + { + MixinData mixinData1 = new MixinData(new object[] { typeof(Action) }); + MixinData mixinData2 = new MixinData(new object[] { typeof(Action) }); + Assert.AreEqual(mixinData1, mixinData2); + } + + [Test] + public void GetHashCode_equal_for_two_instances_having_same_delegate_type_mixins() + { + MixinData mixinData1 = new MixinData(new object[] { typeof(Action) }); + MixinData mixinData2 = new MixinData(new object[] { typeof(Action) }); + Assert.AreEqual(mixinData1.GetHashCode(), mixinData2.GetHashCode()); + } + + // This test appears to be incorrect. However, remember that GetHashCode is allowed to + // sacrifice accuracy for speed. It's not a full replacement for the more expensive + // Equals check; the only requirement is that equal object have equal hashcodes. + [Test] + public void GetHashCode_equal_for_two_instances_having_different_delegate_type_mixins() + { + MixinData mixinData1 = new MixinData(new object[] { typeof(Action) }); + MixinData mixinData2 = new MixinData(new object[] { typeof(Action) }); + Assert.AreEqual(mixinData1.GetHashCode(), mixinData2.GetHashCode()); + } + + // This is a current limitation that means DynamicProxy will might not recognize + // suitable cached proxy types for such compatible delegate scenarios, and regenerate + // an identical type. This might not even matter that much in practice. + [Test] + public void Equals_false_for_two_instances_having_compatible_delegate_type_mixins() + { + MixinData mixinData1 = new MixinData(new object[] { typeof(Func) }); + MixinData mixinData2 = new MixinData(new object[] { typeof(Predicate) }); + Assert.AreNotEqual(mixinData1, mixinData2); + } + + [Test] + public void Ctor_succeeds_when_mixing_regular_mixin_instances_with_delegate_mixins() + { + var mixinData = new MixinData(new object[] + { + new NotADelegate(), + new Action(() => { }), + }); + } + + [Test] + public void Ctor_succeeds_when_mixing_regular_mixin_instances_with_delegate_type_mixins() + { + var mixinData = new MixinData(new object[] + { + new NotADelegate(), + typeof(Action), + }); + } + + [Test] + public void Ctor_throws_when_multiple_delegate_mixins_for_same_Invoke_signature() + { + Assert.Throws(() => new MixinData(new object[] + { + new NotADelegate(), + new Func(_ => true), + new Predicate(_ => false), + })); + } + + [Test] + public void Ctor_throws_when_multiple_delegate_type_mixins_for_same_Invoke_signature() + { + Assert.Throws(() => new MixinData(new object[] + { + typeof(Func), + new NotADelegate(), + typeof(Predicate) + })); + } + + public class NotADelegate : INotADelegate { } + + public interface INotADelegate { } } } \ No newline at end of file diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ProxyUtilTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ProxyUtilTestCase.cs index 18f116b1bc..f0cc7b4f7c 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ProxyUtilTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ProxyUtilTestCase.cs @@ -24,6 +24,63 @@ namespace Castle.DynamicProxy.Tests [TestFixture] public class ProxyUtilTestCase { + [Test] + public void CreateDelegateToMixin_when_given_null_for_proxy_throws_ArgumentNullException() + { + var _ = typeof(Action); + Assert.Throws(() => ProxyUtil.CreateDelegateToMixin(null, _)); + } + + [Test] + public void CreateDelegateToMixin_when_given_null_for_delegateType_throws_ArgumentNullException() + { + var _ = new object(); + Assert.Throws(() => ProxyUtil.CreateDelegateToMixin(_, null)); + } + + [Test] + public void CreateDelegateToMixin_when_given_non_delegate_type_throws_ArgumentException() + { + var _ = new object(); + Assert.Throws(() => ProxyUtil.CreateDelegateToMixin(_, typeof(Exception))); + } + + [Test] + public void CreateDelegateToMixin_when_given_valid_arguments_succeeds() + { + var proxy = new FakeProxyWithInvokeMethods(); + Assert.NotNull(ProxyUtil.CreateDelegateToMixin(proxy, typeof(Action))); + } + + [Test] + public void CreateDelegateToMixin_throws_MissingMethodException_if_no_suitable_Invoke_method_found() + { + var proxy = new FakeProxyWithInvokeMethods(); + Assert.Throws(() => ProxyUtil.CreateDelegateToMixin>(proxy)); + } + + [Test] + public void CreateDelegateToMixin_returns_invokable_delegate() + { + var proxy = new FakeProxyWithInvokeMethods(); + var action = ProxyUtil.CreateDelegateToMixin(proxy); + action.Invoke(); + } + + [Test] + public void CreateDelegateToMixin_can_deal_with_multiple_Invoke_overloads() + { + var proxy = new FakeProxyWithInvokeMethods(); + + var action = ProxyUtil.CreateDelegateToMixin(proxy); + action.Invoke(); + Assert.AreEqual("Invoke()", proxy.LastInvocation); + + var intAction = ProxyUtil.CreateDelegateToMixin>(proxy); + intAction.Invoke(42); + Assert.AreEqual("Invoke(42)", proxy.LastInvocation); + } + [TestCaseSource(nameof(AccessibleMethods))] public void IsAccessible_Accessible_Method_Returns_True(MethodBase method) { @@ -123,5 +180,20 @@ public void APublicMethod() { } } + + private sealed class FakeProxyWithInvokeMethods + { + public string LastInvocation { get; set; } + + public void Invoke() + { + LastInvocation = "Invoke()"; + } + + public void Invoke(int arg) + { + LastInvocation = $"Invoke({arg})"; + } + } } } diff --git a/src/Castle.Core/DynamicProxy/Contributors/ClassProxyInstanceContributor.cs b/src/Castle.Core/DynamicProxy/Contributors/ClassProxyInstanceContributor.cs index 1acc266392..82ebf0f3eb 100644 --- a/src/Castle.Core/DynamicProxy/Contributors/ClassProxyInstanceContributor.cs +++ b/src/Castle.Core/DynamicProxy/Contributors/ClassProxyInstanceContributor.cs @@ -187,7 +187,7 @@ private bool VerifyIfBaseImplementsGetObjectData(Type baseType, IList CollectElementsToProxyInternal( { foreach (var @interface in interfaces) { - var item = new InterfaceMembersCollector(@interface); + MembersCollector item; + if (@interface.GetTypeInfo().IsInterface) + { + item = new InterfaceMembersCollector(@interface); + } + else + { + Debug.Assert(@interface.IsDelegateType()); + item = new DelegateTypeMembersCollector(@interface); + } item.CollectMembersToProxy(hook); yield return item; } diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs index 732fd8b81a..0be0c0345a 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/ClassEmitter.cs @@ -16,9 +16,12 @@ namespace Castle.DynamicProxy.Generators.Emitters { using System; using System.Collections.Generic; + using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; + using Castle.DynamicProxy.Internal; + public class ClassEmitter : AbstractTypeEmitter { internal const TypeAttributes DefaultAttributes = @@ -48,7 +51,14 @@ public ClassEmitter(ModuleScope modulescope, String name, Type baseType, IEnumer { foreach (var inter in interfaces) { - TypeBuilder.AddInterfaceImplementation(inter); + if (inter.GetTypeInfo().IsInterface) + { + TypeBuilder.AddInterfaceImplementation(inter); + } + else + { + Debug.Assert(inter.IsDelegateType()); + } } } diff --git a/src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs b/src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs index bc8ee03960..bb6e67bbdc 100644 --- a/src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs +++ b/src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs @@ -226,6 +226,14 @@ public static MemberInfo[] Sort(MemberInfo[] members) return sortedMembers; } + /// + /// Checks whether the specified is a delegate type (i.e. a direct subclass of ). + /// + internal static bool IsDelegateType(this Type type) + { + return type.GetTypeInfo().BaseType == typeof(MulticastDelegate); + } + private static bool CloseGenericParametersIfAny(AbstractTypeEmitter emitter, Type[] arguments) { var hasAnyGenericParameters = false; diff --git a/src/Castle.Core/DynamicProxy/MixinData.cs b/src/Castle.Core/DynamicProxy/MixinData.cs index dce7286e77..999907721d 100644 --- a/src/Castle.Core/DynamicProxy/MixinData.cs +++ b/src/Castle.Core/DynamicProxy/MixinData.cs @@ -15,13 +15,19 @@ namespace Castle.DynamicProxy { using System; - using System.Reflection; using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Reflection; + + using Castle.DynamicProxy.Generators; + using Castle.DynamicProxy.Internal; public class MixinData { private readonly Dictionary mixinPositions = new Dictionary(); private readonly List mixinsImpl = new List(); + private int delegateMixinCount = 0; /// /// Because we need to cache the types based on the mixed in mixins, we do the following here: @@ -38,10 +44,29 @@ public MixinData(IEnumerable mixinInstances) { var sortedMixedInterfaceTypes = new List(); var interface2Mixin = new Dictionary(); + delegateMixinCount = 0; foreach (var mixin in mixinInstances) { - var mixinInterfaces = mixin.GetType().GetInterfaces(); + Type[] mixinInterfaces; + object target; + if (mixin is Delegate) + { + ++delegateMixinCount; + mixinInterfaces = new[] { mixin.GetType() }; + target = mixin; + } + else if (mixin is Type delegateType && delegateType.IsDelegateType()) + { + ++delegateMixinCount; + mixinInterfaces = new[] { delegateType }; + target = null; + } + else + { + mixinInterfaces = mixin.GetType().GetInterfaces(); + target = mixin; + } foreach (var inter in mixinInterfaces) { @@ -49,17 +74,47 @@ public MixinData(IEnumerable mixinInstances) if (interface2Mixin.TryGetValue(inter, out var interMixin)) { - var message = string.Format( - "The list of mixins contains two mixins implementing the same interface '{0}': {1} and {2}. An interface cannot be added by more than one mixin.", - inter.FullName, - interMixin.GetType().Name, - mixin.GetType().Name); + string message; + if (interMixin != null) + { + message = string.Format( + "The list of mixins contains two mixins implementing the same interface '{0}': {1} and {2}. An interface cannot be added by more than one mixin.", + inter.FullName, + interMixin.GetType().Name, + mixin.GetType().Name); + } + else + { + Debug.Assert(inter.IsDelegateType()); + message = string.Format( + "The list of mixins already contains a mixin for delegate type '{0}'.", + inter.FullName); + } throw new ArgumentException(message, "mixinInstances"); } + interface2Mixin[inter] = target; + } + } - interface2Mixin[inter] = mixin; + if (delegateMixinCount > 1) + { + // If at least two delegate mixins have been added, we need to ensure that + // the `Invoke` methods contributed by them don't have identical signatures: + var invokeMethods = new HashSet(); + foreach (var mixedInType in interface2Mixin.Keys) + { + if (mixedInType.IsDelegateType()) + { + var invokeMethod = mixedInType.GetMethod("Invoke"); + if (invokeMethods.Contains(invokeMethod, MethodSignatureComparer.Instance)) + { + throw new ArgumentException("The list of mixins contains at least two delegate mixins for the same delegate signature.", nameof(mixinInstances)); + } + invokeMethods.Add(invokeMethod); + } } } + sortedMixedInterfaceTypes.Sort((x, y) => x.FullName.CompareTo(y.FullName)); for (var i = 0; i < sortedMixedInterfaceTypes.Count; i++) @@ -107,14 +162,26 @@ public override bool Equals(object obj) return false; } + if (delegateMixinCount != other.delegateMixinCount) + { + return false; + } + for (var i = 0; i < mixinsImpl.Count; ++i) { - if (mixinsImpl[i].GetType() != other.mixinsImpl[i].GetType()) + if (mixinsImpl[i]?.GetType() != other.mixinsImpl[i]?.GetType()) { return false; } } + if (delegateMixinCount > 0) + { + var delegateMixinTypes = mixinPositions.Select(m => m.Key).Where(TypeUtil.IsDelegateType); + var otherDelegateMixinTypes = other.mixinPositions.Select(m => m.Key).Where(TypeUtil.IsDelegateType); + return Enumerable.SequenceEqual(delegateMixinTypes, otherDelegateMixinTypes); + } + return true; } @@ -124,7 +191,7 @@ public override int GetHashCode() var hashCode = 0; foreach (var mixinImplementation in mixinsImpl) { - hashCode = 29*hashCode + mixinImplementation.GetType().GetHashCode(); + hashCode = unchecked(29 * hashCode + mixinImplementation?.GetType().GetHashCode() ?? 307); } return hashCode; diff --git a/src/Castle.Core/DynamicProxy/ProxyGenerationOptions.cs b/src/Castle.Core/DynamicProxy/ProxyGenerationOptions.cs index e92f300a75..78918d34c0 100644 --- a/src/Castle.Core/DynamicProxy/ProxyGenerationOptions.cs +++ b/src/Castle.Core/DynamicProxy/ProxyGenerationOptions.cs @@ -16,15 +16,18 @@ namespace Castle.DynamicProxy { using System; using System.Collections.Generic; + using System.Reflection; using System.Reflection.Emit; #if FEATURE_SERIALIZATION using System.Runtime.Serialization; #endif - using Castle.Core.Internal; #if DOTNET40 using System.Security; #endif + using Castle.Core.Internal; + using Castle.DynamicProxy.Internal; + #if FEATURE_SERIALIZATION [Serializable] #endif @@ -123,19 +126,52 @@ public MixinData MixinData } } + /// + /// Adds a delegate type to the list of mixins that will be added to generated proxies. + /// That is, generated proxies will have a `Invoke` method with a signature matching that + /// of the specified . + /// + /// The delegate type whose `Invoke` method should be reproduced in generated proxies. + /// is . + /// is not a delegate type. + public void AddDelegateTypeMixin(Type delegateType) + { + if (delegateType == null) throw new ArgumentNullException(nameof(delegateType)); + if (!delegateType.IsDelegateType()) throw new ArgumentException("Type must be a delegate type.", nameof(delegateType)); + + AddMixinImpl(delegateType); + } + + /// + /// Adds a delegate to be mixed into generated proxies. The + /// will act as the target for calls to a `Invoke` method with a signature matching that + /// of the delegate. + /// + /// The delegate that should act as the target for calls to `Invoke` methods with a matching signature. + /// is . + public void AddDelegateMixin(Delegate @delegate) + { + if (@delegate == null) throw new ArgumentNullException(nameof(@delegate)); + + AddMixinImpl(@delegate); + } + public void AddMixinInstance(object instance) { - if (instance == null) - { - throw new ArgumentNullException("instance"); - } + if (instance == null) throw new ArgumentNullException(nameof(instance)); + if (instance is Type) throw new ArgumentException("You may not mix in types using this method.", nameof(instance)); + AddMixinImpl(instance); + } + + private void AddMixinImpl(object instanceOrType) + { if (mixins == null) { mixins = new List(); } - mixins.Add(instance); + mixins.Add(instanceOrType); mixinData = null; } diff --git a/src/Castle.Core/DynamicProxy/ProxyUtil.cs b/src/Castle.Core/DynamicProxy/ProxyUtil.cs index efb86044d3..3321f37912 100644 --- a/src/Castle.Core/DynamicProxy/ProxyUtil.cs +++ b/src/Castle.Core/DynamicProxy/ProxyUtil.cs @@ -26,11 +26,67 @@ namespace Castle.DynamicProxy #endif using Castle.Core.Internal; + using Castle.DynamicProxy.Generators; + using Castle.DynamicProxy.Internal; public static class ProxyUtil { private static readonly SynchronizedDictionary internalsVisibleToDynamicProxy = new SynchronizedDictionary(); + /// + /// Creates a delegate of the specified type to a suitable `Invoke` method + /// on the given instance. + /// + /// The proxy instance to which the delegate should be bound. + /// The type of delegate that should be created. + /// + /// The does not have an `Invoke` method that is compatible with + /// the requested type. + /// + public static TDelegate CreateDelegateToMixin(object proxy) + { + return (TDelegate)(object)CreateDelegateToMixin(proxy, typeof(TDelegate)); + } + + /// + /// Creates a delegate of the specified type to a suitable `Invoke` method + /// on the given instance. + /// + /// The proxy instance to which the delegate should be bound. + /// The type of delegate that should be created. + /// + /// The does not have an `Invoke` method that is compatible with + /// the requested . + /// + public static Delegate CreateDelegateToMixin(object proxy, Type delegateType) + { + if (proxy == null) throw new ArgumentNullException(nameof(proxy)); + if (delegateType == null) throw new ArgumentNullException(nameof(delegateType)); + if (!delegateType.IsDelegateType()) throw new ArgumentException("Type is not a delegate type.", nameof(delegateType)); + + var invokeMethod = delegateType.GetMethod("Invoke"); + var proxiedInvokeMethod = + proxy + .GetType() + .GetMember("Invoke", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Cast() + .FirstOrDefault(m => MethodSignatureComparer.Instance.EqualParameters(m, invokeMethod)); + + if (proxiedInvokeMethod == null) + { + throw new MissingMethodException("The proxy does not have an Invoke method " + + "that is compatible with the requested delegate type."); + } + else + { +#if FEATURE_NETCORE_REFLECTION_API + return proxiedInvokeMethod.CreateDelegate(delegateType, proxy); +#else + return Delegate.CreateDelegate(delegateType, proxy, proxiedInvokeMethod); +#endif + } + } + public static object GetUnproxiedInstance(object instance) { #if FEATURE_REMOTING