From 12742a972554a5841f1c496704f9dda4870d6d16 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Tue, 23 Jun 2026 14:07:00 -0700 Subject: [PATCH] Fix interface interception with open generic assembly scanning (#27) EnsureInterfaceInterceptionApplies previously inspected every service exposed by the registration and threw if any was a non-interface. When scanning an assembly with AsClosedTypesOf, the concrete type is also registered as a service, causing the check to fail even when resolving the closed interface. Use the service actually being resolved (ResolveRequestContext.Service) to decide whether interface interception applies, rather than every service on the registration. The non-interface guard is preserved for direct resolution of a concrete service. --- .../RegistrationExtensions.cs | 15 +-- ...OpenGenericInterfaceInterceptionFixture.cs | 97 +++++++++++++++++++ 2 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 test/Autofac.Extras.DynamicProxy.Test/OpenGenericInterfaceInterceptionFixture.cs diff --git a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs index 68989f2..222ed48 100644 --- a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs +++ b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs @@ -164,7 +164,7 @@ public static IRegistrationBuilder InterceptedBy return InterceptedBy(builder, interceptorServiceTypes.Select(t => new TypedService(t)).ToArray()); } - private static void EnsureInterfaceInterceptionApplies(IComponentRegistration componentRegistration) + private static void EnsureInterfaceInterceptionApplies(Service service, IComponentRegistration componentRegistration) { - if (componentRegistration.Services - .OfType() - .Select(s => new Tuple(s.ServiceType, s.ServiceType.GetTypeInfo())) - .Any(s => !s.Item2.IsInterface || !ProxyUtil.IsAccessible(s.Item1))) + // Only the service actually being resolved needs to be a public interface. + // A registration may expose additional services (for example, the concrete + // type alongside an interface when scanning with AsClosedTypesOf) that are + // not interfaces; those are irrelevant when interception is applied to an + // interface service. See issue #27. + if (service is IServiceWithType serviceWithType && + (!serviceWithType.ServiceType.GetTypeInfo().IsInterface || !ProxyUtil.IsAccessible(serviceWithType.ServiceType))) { throw new InvalidOperationException( string.Format( diff --git a/test/Autofac.Extras.DynamicProxy.Test/OpenGenericInterfaceInterceptionFixture.cs b/test/Autofac.Extras.DynamicProxy.Test/OpenGenericInterfaceInterceptionFixture.cs new file mode 100644 index 0000000..72ed67e --- /dev/null +++ b/test/Autofac.Extras.DynamicProxy.Test/OpenGenericInterfaceInterceptionFixture.cs @@ -0,0 +1,97 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Core; +using Castle.DynamicProxy; +using IInvocation = Castle.DynamicProxy.IInvocation; + +namespace Autofac.Extras.DynamicProxy.Test; + +public class OpenGenericInterfaceInterceptionFixture +{ + public interface ICommandHandler + { + TResult Handle(TCommand command); + } + + [Fact] + public void InterceptsClosedTypesOfScannedOpenGenericInterface() + { + // Issue #27: When scanning an assembly and registering closed types of an + // open generic interface, interface interception should still work even + // though the scan also registers the concrete type as a service. + var builder = new ContainerBuilder(); + builder.RegisterType(); + builder + .RegisterAssemblyTypes(typeof(OpenGenericInterfaceInterceptionFixture).Assembly) + .AsClosedTypesOf(typeof(ICommandHandler<,>)) + .EnableInterfaceInterceptors() + .InterceptedBy(typeof(StringMethodInterceptor)); + var container = builder.Build(); + + var handler = container.Resolve>(); + + Assert.Equal("intercepted-Handle", handler.Handle("input")); + } + + [Fact] + public void InterceptsInterfaceServiceWhenRegistrationAlsoExposesConcreteType() + { + // A registration can expose both the concrete type and an interface. When + // resolved by the interface, interception should apply even though the + // concrete type is not an interface. + var builder = new ContainerBuilder(); + builder.RegisterType(); + builder + .RegisterType() + .AsSelf() + .As>() + .EnableInterfaceInterceptors() + .InterceptedBy(typeof(StringMethodInterceptor)); + var container = builder.Build(); + + var handler = container.Resolve>(); + + Assert.Equal("intercepted-Handle", handler.Handle("input")); + } + + [Fact] + public void ThrowsWhenResolvingConcreteServiceWithInterfaceInterception() + { + // Resolving the non-interface service directly cannot be proxied via + // interface interception and must still throw, even when the registration + // also exposes an interface service. + var builder = new ContainerBuilder(); + builder.RegisterType(); + builder + .RegisterType() + .AsSelf() + .As>() + .EnableInterfaceInterceptors() + .InterceptedBy(typeof(StringMethodInterceptor)); + var container = builder.Build(); + + var dx = Assert.Throws(() => container.Resolve()); + Assert.IsType(dx.InnerException); + } + + public class StringCommandHandler : ICommandHandler + { + public string Handle(string command) => command; + } + + private class StringMethodInterceptor : IInterceptor + { + public void Intercept(IInvocation invocation) + { + if (invocation.Method.ReturnType == typeof(string)) + { + invocation.ReturnValue = "intercepted-" + invocation.Method.Name; + } + else + { + invocation.Proceed(); + } + } + } +}