diff --git a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs index 222ed48..966f875 100644 --- a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs +++ b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs @@ -25,14 +25,24 @@ public static class RegistrationExtensions private static readonly ProxyGenerator _proxyGenerator = new(); /// - /// Enable class interception on the target type. Interceptors will be determined - /// via Intercept attributes on the class or added with InterceptedBy(). + /// Enable class interception on the target type. Interceptors will be + /// determined via on the class or added + /// with + /// . /// Only virtual methods can be intercepted this way. /// - /// Registration limit type. - /// Registration style. - /// Registration to apply interception to. - /// Registration builder allowing the registration to be configured. + /// + /// Registration limit type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// Registration builder allowing the registration to be configured. + /// public static IRegistrationBuilder EnableClassInterceptors( this IRegistrationBuilder registration) { @@ -40,15 +50,59 @@ public static IRegistrationBuilder - /// Enable class interception on the target type. Interceptors will be determined - /// via Intercept attributes on the class or added with InterceptedBy(). + /// Enable class interception on the target type, conditionally based on the + /// implementation type. Interceptors will be determined via + /// on the class or added with + /// . /// Only virtual methods can be intercepted this way. /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// Registration to apply interception to. - /// Registration builder allowing the registration to be configured. + /// + /// Registration limit type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// A predicate, evaluated against each candidate implementation type, that + /// determines whether interception is applied. Types for which the + /// predicate returns are registered without + /// interception. + /// + /// + /// Registration builder allowing the registration to be configured. + /// + public static IRegistrationBuilder EnableClassInterceptors( + this IRegistrationBuilder registration, + Func shouldIntercept) + { + return EnableClassInterceptors(registration, ProxyGenerationOptions.Default, shouldIntercept); + } + + /// + /// Enable class interception on the target type. Interceptors will be + /// determined via on the class or added + /// with + /// . + /// Only virtual methods can be intercepted this way. + /// + /// + /// Registration limit type. + /// + /// + /// Activator data type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// Registration builder allowing the registration to be configured. + /// public static IRegistrationBuilder EnableClassInterceptors( this IRegistrationBuilder registration) where TConcreteReflectionActivatorData : ConcreteReflectionActivatorData @@ -57,19 +111,105 @@ public static IRegistrationBuilder - /// Enable class interception on the target type. Interceptors will be determined - /// via Intercept attributes on the class or added with InterceptedBy(). + /// Enable class interception on the target type, conditionally based on the + /// implementation type. Interceptors will be determined via + /// on the class or added with + /// . + /// Only virtual methods can be intercepted this way. + /// + /// + /// Registration limit type. + /// + /// + /// Activator data type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// A predicate, evaluated against the implementation type, that determines + /// whether interception is applied. When the predicate returns + /// the type is registered without interception. + /// + /// + /// Registration builder allowing the registration to be configured. + /// + public static IRegistrationBuilder EnableClassInterceptors( + this IRegistrationBuilder registration, + Func shouldIntercept) + where TConcreteReflectionActivatorData : ConcreteReflectionActivatorData + { + return EnableClassInterceptors(registration, ProxyGenerationOptions.Default, shouldIntercept); + } + + /// + /// Enable class interception on the target type with specific options and + /// additional interfaces. Interceptors will be determined via + /// on the class or added with + /// . /// Only virtual methods can be intercepted this way. /// - /// Registration limit type. - /// Registration style. - /// Registration to apply interception to. - /// Proxy generation options to apply. + /// + /// Registration limit type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// Proxy generation options to apply. + /// + /// + /// Additional interface types. Calls to their members will be proxied as well. + /// + /// + /// Registration builder allowing the registration to be configured. + /// + public static IRegistrationBuilder EnableClassInterceptors( + this IRegistrationBuilder registration, + ProxyGenerationOptions options, + params Type[] additionalInterfaces) + { + return EnableClassInterceptors(registration, options, null, additionalInterfaces); + } + + /// + /// Enable class interception on the target type, conditionally based on the + /// implementation type, with specific options and additional interfaces. + /// Interceptors will be determined via on + /// the class or added with + /// . + /// Only virtual methods can be intercepted this way. + /// + /// + /// Registration limit type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// Proxy generation options to apply. + /// + /// + /// An optional predicate, evaluated against each candidate implementation type, + /// that determines whether interception is applied. Types for which the predicate + /// returns are registered without interception. When + /// all types are intercepted. + /// /// Additional interface types. Calls to their members will be proxied as well. /// Registration builder allowing the registration to be configured. public static IRegistrationBuilder EnableClassInterceptors( this IRegistrationBuilder registration, ProxyGenerationOptions options, + Func? shouldIntercept, params Type[] additionalInterfaces) { if (registration == null) @@ -77,33 +217,100 @@ public static IRegistrationBuilder rb.EnableClassInterceptors(options, additionalInterfaces)); + registration.ActivatorData.ConfigurationActions.Add((t, rb) => rb.EnableClassInterceptors(options, shouldIntercept, additionalInterfaces)); return registration; } /// - /// Enable class interception on the target type. Interceptors will be determined - /// via Intercept attributes on the class or added with InterceptedBy(). + /// Enable class interception on the target type with specific options and + /// additional interfaces. Interceptors will be determined via + /// on the class or added with + /// . /// Only virtual methods can be intercepted this way. /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// Registration to apply interception to. - /// Proxy generation options to apply. - /// Additional interface types. Calls to their members will be proxied as well. + /// + /// Registration limit type. + /// + /// + /// Activator data type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// Proxy generation options to apply. + /// + /// + /// Additional interface types. Calls to their members will be proxied as well. + /// /// Registration builder allowing the registration to be configured. public static IRegistrationBuilder EnableClassInterceptors( this IRegistrationBuilder registration, ProxyGenerationOptions options, params Type[] additionalInterfaces) where TConcreteReflectionActivatorData : ConcreteReflectionActivatorData + { + return EnableClassInterceptors(registration, options, null, additionalInterfaces); + } + + /// + /// Enable class interception on the target type, conditionally based on the + /// implementation type, with specific options and additional interfaces. + /// Interceptors will be determined via on + /// the class or added with + /// . + /// Only virtual methods can be intercepted this way. + /// + /// + /// Registration limit type. + /// + /// + /// Activator data type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// Proxy generation options to apply. + /// + /// + /// An optional predicate, evaluated against the implementation type, that + /// determines whether interception is applied. When the predicate returns + /// the type is registered without interception. When + /// the type is intercepted. + /// + /// + /// Additional interface types. Calls to their members will be proxied as well. + /// + /// + /// Registration builder allowing the registration to be configured. + /// + public static IRegistrationBuilder EnableClassInterceptors( + this IRegistrationBuilder registration, + ProxyGenerationOptions options, + Func? shouldIntercept, + params Type[] additionalInterfaces) + where TConcreteReflectionActivatorData : ConcreteReflectionActivatorData { if (registration == null) { throw new ArgumentNullException(nameof(registration)); } + // Class interception rewrites the implementation type to a proxy subclass + // at registration time, so the decision to intercept is made here, per type. + // When the predicate rejects the type the registration is left untouched. + if (shouldIntercept != null && !shouldIntercept(registration.ActivatorData.ImplementationType)) + { + return registration; + } + registration.ActivatorData.ImplementationType = _proxyGenerator.ProxyBuilder.CreateClassProxyType( registration.ActivatorData.ImplementationType, @@ -143,17 +350,99 @@ public static IRegistrationBuilder - /// Enable interface interception on the target type. Interceptors will be determined - /// via Intercept attributes on the class or interface, or added with InterceptedBy() calls. + /// Enable interface interception on the target type. Interceptors will be + /// determined via on the class or + /// interface, or added with + /// . /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// Registration to apply interception to. - /// Proxy generation options to apply. - /// Registration builder allowing the registration to be configured. + /// + /// Registration limit type. + /// + /// + /// Activator data type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// Proxy generation options to apply. + /// + /// + /// Registration builder allowing the registration to be configured. + /// public static IRegistrationBuilder EnableInterfaceInterceptors( this IRegistrationBuilder registration, ProxyGenerationOptions? options = null) + { + return EnableInterfaceInterceptors(registration, options, null); + } + + /// + /// Enable interface interception on the target type, conditionally based on + /// the resolved implementation type. Interceptors will be determined via + /// on the class or interface, or added with + /// . + /// + /// + /// Registration limit type. + /// + /// Activator data type. + /// + /// Registration style. + /// + /// Registration to apply interception to. + /// + /// + /// A predicate, evaluated against the resolved implementation type, that + /// determines whether interception is applied. When the predicate returns + /// the resolved instance is returned without a proxy. + /// + /// + /// Registration builder allowing the registration to be configured. + /// + public static IRegistrationBuilder EnableInterfaceInterceptors( + this IRegistrationBuilder registration, + Func shouldIntercept) + { + return EnableInterfaceInterceptors(registration, null, shouldIntercept); + } + + /// + /// Enable interface interception on the target type, conditionally based on + /// the resolved implementation type, with specific options. Interceptors + /// will be determined via on the class or + /// interface, or added with + /// . + /// + /// + /// Registration limit type. + /// + /// + /// Activator data type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// Proxy generation options to apply. + /// + /// + /// A predicate, evaluated against the resolved implementation type, that + /// determines whether interception is applied. When the predicate returns + /// the resolved instance is returned without a proxy. + /// + /// + /// Registration builder allowing the registration to be configured. + /// + public static IRegistrationBuilder EnableInterfaceInterceptors( + this IRegistrationBuilder registration, + ProxyGenerationOptions? options, + Func? shouldIntercept) { if (registration == null) { @@ -164,11 +453,21 @@ public static IRegistrationBuilder ctx.ResolveService(s)) .Cast() .ToArray(); @@ -195,15 +494,29 @@ public static IRegistrationBuilder - /// Allows a list of interceptor services to be assigned to the registration. + /// Assigns a list of interceptor services to a registration by service. /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// Registration to apply interception to. - /// The interceptor services. - /// Registration builder allowing the registration to be configured. - /// or . + /// + /// Registration limit type. + /// + /// + /// Activator data type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// The interceptor services. + /// + /// + /// Registration builder allowing the registration to be configured. + /// + /// + /// Thrown when or is . + /// public static IRegistrationBuilder InterceptedBy( this IRegistrationBuilder builder, params Service[] interceptorServices) @@ -224,15 +537,29 @@ public static IRegistrationBuilder InterceptedBy } /// - /// Allows a list of interceptor services to be assigned to the registration. + /// Assigns a list of interceptor services to a registration by name. /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// Registration to apply interception to. - /// The names of the interceptor services. - /// Registration builder allowing the registration to be configured. - /// or . + /// + /// Registration limit type. + /// + /// + /// Activator data type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// The names of the interceptor services. + /// + /// + /// Registration builder allowing the registration to be configured. + /// + /// + /// Thrown when or is . + /// public static IRegistrationBuilder InterceptedBy( this IRegistrationBuilder builder, params string[] interceptorServiceNames) @@ -246,15 +573,30 @@ public static IRegistrationBuilder InterceptedBy } /// - /// Allows a list of interceptor services to be assigned to the registration. + /// Assigns a list of interceptor services to a registration by interceptor + /// type. /// - /// Registration limit type. - /// Activator data type. - /// Registration style. - /// Registration to apply interception to. - /// The types of the interceptor services. - /// Registration builder allowing the registration to be configured. - /// or . + /// + /// Registration limit type. + /// + /// + /// Activator data type. + /// + /// + /// Registration style. + /// + /// + /// Registration to apply interception to. + /// + /// + /// The types of the interceptor services. + /// + /// + /// Registration builder allowing the registration to be configured. + /// + /// + /// Thrown when or is . + /// public static IRegistrationBuilder InterceptedBy( this IRegistrationBuilder builder, params Type[] interceptorServiceTypes) diff --git a/test/Autofac.Extras.DynamicProxy.Test/ConditionalInterceptionFixture.cs b/test/Autofac.Extras.DynamicProxy.Test/ConditionalInterceptionFixture.cs new file mode 100644 index 0000000..acfed34 --- /dev/null +++ b/test/Autofac.Extras.DynamicProxy.Test/ConditionalInterceptionFixture.cs @@ -0,0 +1,177 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Builder; +using Autofac.Features.Scanning; +using Castle.DynamicProxy; +using IInvocation = Castle.DynamicProxy.IInvocation; + +namespace Autofac.Extras.DynamicProxy.Test; + +public class ConditionalInterceptionFixture +{ + [Fact] + public void NullRegistration_Throws() + { + IRegistrationBuilder concrete = null!; + IRegistrationBuilder scanning = null!; + Func predicate = _ => true; + + Assert.Throws(() => concrete.EnableInterfaceInterceptors(predicate)); + Assert.Throws(() => concrete.EnableClassInterceptors(predicate)); + Assert.Throws(() => scanning.EnableClassInterceptors(predicate)); + } + + [AttributeUsage(AttributeTargets.Class)] + private sealed class InterceptMeAttribute : Attribute + { + } + + public interface IPublicInterface + { + string PublicMethod(); + } + + [Fact] + public void InterfaceInterception_AppliesProxyWhenPredicateMatches() + { + var builder = new ContainerBuilder(); + builder.RegisterType(); + builder + .RegisterType() + .EnableInterfaceInterceptors(t => t.IsDefined(typeof(InterceptMeAttribute), false)) + .InterceptedBy(typeof(StringMethodInterceptor)) + .As(); + var container = builder.Build(); + + var obj = container.Resolve(); + + Assert.Equal("intercepted-PublicMethod", obj.PublicMethod()); + } + + [Fact] + public void InterfaceInterception_SkipsProxyWhenPredicateDoesNotMatch() + { + var builder = new ContainerBuilder(); + builder.RegisterType(); + builder + .RegisterType() + .EnableInterfaceInterceptors(t => t.IsDefined(typeof(InterceptMeAttribute), false)) + .InterceptedBy(typeof(StringMethodInterceptor)) + .As(); + var container = builder.Build(); + + var obj = container.Resolve(); + + // Predicate returns false, so the raw instance is returned without a proxy. + Assert.Equal("PublicMethod", obj.PublicMethod()); + Assert.IsType(obj); + } + + [Fact] + public void InterfaceInterception_PredicateAppliesPerScannedType() + { + var builder = new ContainerBuilder(); + builder.RegisterType(); + builder + .RegisterAssemblyTypes(typeof(ConditionalInterceptionFixture).Assembly) + .Where(t => t == typeof(Intercepted) || t == typeof(NotIntercepted)) + .As() + .EnableInterfaceInterceptors(t => t.IsDefined(typeof(InterceptMeAttribute), false)) + .InterceptedBy(typeof(StringMethodInterceptor)); + var container = builder.Build(); + + var all = container.Resolve>().ToList(); + + // The attributed type is proxied; the other is returned untouched. + Assert.Contains(all, o => o.PublicMethod() == "intercepted-PublicMethod"); + Assert.Contains(all, o => o.PublicMethod() == "PublicMethod"); + } + + [Fact] + public void ClassInterception_AppliesProxyWhenPredicateMatches() + { + var builder = new ContainerBuilder(); + builder.RegisterType(); + builder + .RegisterType() + .EnableClassInterceptors(t => t.IsDefined(typeof(InterceptMeAttribute), false)) + .InterceptedBy(typeof(StringMethodInterceptor)); + var container = builder.Build(); + + var obj = container.Resolve(); + + Assert.Equal("intercepted-VirtualMethod", obj.VirtualMethod()); + } + + [Fact] + public void ClassInterception_SkipsProxyWhenPredicateDoesNotMatch() + { + var builder = new ContainerBuilder(); + builder.RegisterType(); + builder + .RegisterType() + .EnableClassInterceptors(t => t.IsDefined(typeof(InterceptMeAttribute), false)) + .InterceptedBy(typeof(StringMethodInterceptor)); + var container = builder.Build(); + + var obj = container.Resolve(); + + // Predicate returns false, so the type is registered without a proxy. + Assert.Equal("VirtualMethod", obj.VirtualMethod()); + Assert.Same(typeof(NotInterceptedClass), obj.GetType()); + } + + [Fact] + public void ClassInterception_PredicateAppliesPerScannedType() + { + var builder = new ContainerBuilder(); + builder.RegisterType(); + builder + .RegisterAssemblyTypes(typeof(ConditionalInterceptionFixture).Assembly) + .Where(t => t == typeof(InterceptedClass) || t == typeof(NotInterceptedClass)) + .EnableClassInterceptors(t => t.IsDefined(typeof(InterceptMeAttribute), false)) + .InterceptedBy(typeof(StringMethodInterceptor)); + var container = builder.Build(); + + Assert.Equal("intercepted-VirtualMethod", container.Resolve().VirtualMethod()); + Assert.Equal("VirtualMethod", container.Resolve().VirtualMethod()); + } + + [InterceptMe] + public class Intercepted : IPublicInterface + { + public string PublicMethod() => "PublicMethod"; + } + + public class NotIntercepted : IPublicInterface + { + public string PublicMethod() => "PublicMethod"; + } + + [InterceptMe] + public class InterceptedClass + { + public virtual string VirtualMethod() => "VirtualMethod"; + } + + public class NotInterceptedClass + { + public virtual string VirtualMethod() => "VirtualMethod"; + } + + private class StringMethodInterceptor : IInterceptor + { + public void Intercept(IInvocation invocation) + { + if (invocation.Method.ReturnType == typeof(string)) + { + invocation.ReturnValue = "intercepted-" + invocation.Method.Name; + } + else + { + invocation.Proceed(); + } + } + } +}