diff --git a/test/Autofac.Extras.DynamicProxy.Test/ClassInterceptorsWithAttributeFilteringFixture.cs b/test/Autofac.Extras.DynamicProxy.Test/ClassInterceptorsWithAttributeFilteringFixture.cs new file mode 100644 index 0000000..cb0017c --- /dev/null +++ b/test/Autofac.Extras.DynamicProxy.Test/ClassInterceptorsWithAttributeFilteringFixture.cs @@ -0,0 +1,169 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Features.AttributeFilters; +using Castle.DynamicProxy; + +namespace Autofac.Extras.DynamicProxy.Test; + +/// +/// Documents the interaction between WithAttributeFiltering and class +/// interception, including a known limitation tracked by +/// issue #56. +/// +/// +/// +/// EnableClassInterceptors rewrites the implementation type to a Castle +/// DynamicProxy subclass, replicating the original constructor parameter +/// attributes onto the proxy constructor. Castle reproduces an enum +/// argument passed to an attribute parameter that is typed +/// (as KeyFilterAttribute(object key) is) using the enum's underlying +/// integer type rather than the original enum type. As a result the key on +/// the replicated no longer matches the +/// registered keyed service, and filtering silently fails. +/// +/// +/// This is an upstream Castle DynamicProxy bug tracked at +/// castleproject/Core#748. +/// It only affects non-string keys under class interception; string keys and +/// interface interception are unaffected. The tests below pin the current +/// behavior so we get a signal if/when the upstream behavior changes. +/// +/// +public class ClassInterceptorsWithAttributeFilteringFixture +{ + public enum LoggerKey + { + First, + Second, + } + + [Fact] + public void EnumKeyFilteringWorksWithoutInterception() + { + // Baseline: WithAttributeFiltering honors an enum [KeyFilter] when + // interception is NOT enabled. + var builder = new ContainerBuilder(); + builder.Register(c => "first").Keyed(LoggerKey.First); + builder.Register(c => "second").Keyed(LoggerKey.Second); + builder.RegisterType().WithAttributeFiltering(); + var container = builder.Build(); + + var resolved = container.Resolve(); + + Assert.Equal("first", resolved.Value); + } + + [Fact] + public void EnumKeyFilteringIsNotHonoredWithClassInterception() + { + // KNOWN LIMITATION (issue #56 / castleproject/Core#748): with class + // interception the replicated [KeyFilter] loses its enum key type, so the + // filter no longer matches the keyed registration and the unkeyed + // fallback is injected instead of the "First" keyed value. + // + // This test documents the *current, incorrect* behavior. If it starts + // failing because "first" is resolved, Castle has likely fixed the + // underlying bug and this limitation (and the docs/comments) can be + // removed. + var builder = new ContainerBuilder(); + builder.RegisterType(); + builder.Register(c => "first").Keyed(LoggerKey.First); + builder.Register(c => "fallback"); + builder + .RegisterType() + .WithAttributeFiltering() + .EnableClassInterceptors() + .InterceptedBy(typeof(NullInterceptor)); + var container = builder.Build(); + + var resolved = container.Resolve(); + + Assert.Equal("fallback", resolved.Value); + } + + [Fact] + public void StringKeyFilteringWorksWithClassInterception() + { + // WORKAROUND: a string key round-trips through the object-typed attribute + // parameter losslessly, so filtering works with class interception. + var builder = new ContainerBuilder(); + builder.RegisterType(); + builder.Register(c => "first").Keyed("first-key"); + builder.Register(c => "fallback"); + builder + .RegisterType() + .WithAttributeFiltering() + .EnableClassInterceptors() + .InterceptedBy(typeof(NullInterceptor)); + var container = builder.Build(); + + var resolved = container.Resolve(); + + Assert.Equal("first", resolved.Value); + } + + [Fact] + public void EnumKeyFilteringWorksWithInterfaceInterception() + { + // WORKAROUND: interface interception does not rewrite the constructor, so + // the original [KeyFilter] is read intact and the enum key matches. + var builder = new ContainerBuilder(); + builder.RegisterType(); + builder.Register(c => "first").Keyed(LoggerKey.First); + builder.Register(c => "fallback"); + builder + .RegisterType() + .WithAttributeFiltering() + .As() + .EnableInterfaceInterceptors() + .InterceptedBy(typeof(NullInterceptor)); + var container = builder.Build(); + + var resolved = container.Resolve(); + + Assert.Equal("first", resolved.Value); + } + + public interface IHaveValue + { + string Value + { + get; + } + } + + public class HasEnumKeyedParameter : IHaveValue + { + public HasEnumKeyedParameter([KeyFilter(LoggerKey.First)] string value) + { + Value = value; + } + + public virtual string Value + { + get; + } + } + + public class HasStringKeyedParameter + { + public HasStringKeyedParameter([KeyFilter("first-key")] string value) + { + Value = value; + } + + public virtual string Value + { + get; + } + } + + private class NullInterceptor : IInterceptor + { + public void Intercept(IInvocation invocation) + { + invocation.Proceed(); + } + } +}