autofac / Autofac

An addictive .NET IoC container
https://autofac.org
MIT License
4.5k stars 837 forks source link

Register service with Interceptor, then register multiple decorator, auto apply Interceptor to the outermost decorator #1416

Open idiotsky opened 7 months ago

idiotsky commented 7 months ago

Register service with Interceptor, then register multiple decorator, auto apply Interceptor to the outermost decorator Actually this is the default behaviors when I use autofac 4.x. after upgrade version 8.0.0, this not working any more. I know explicit configure interceptor on the outermost decorator make it more clarity and flexibility. but I thinks we should keep the original behaviors some how, even it not the default behaviors. This behaviors will benefit in case when add new decorator, we don't need remove the interceptor and add the interceptor to the new decorator.

tillig commented 7 months ago

The last version of Autofac 4.x was v4.9.4 released in 2019. A lot has happened since then to accommodate for .NET Core and some compatibility issues.

If there is a problem you're seeing, please express it in the form of a unit test or something we can try to reproduce. There may be something broken, but there it may be that there are things that just work differently now. However, it's somewhat difficult to say without seeing code. The text/prose explanation is not entirely clear and before responding it'd be better to see exactly what you're talking about in concrete code.

Please make sure that repro is as absolutely minimal as possible. No extra classes, no crazy indirection with extra methods running registration, no Autofac modules, no assembly scanning, no full application stack. As absolutely minimal as possible.

idiotsky commented 7 months ago
public interface IService
{
    void DoWork();
}

public class Service : IService
{
    public void DoWork()
    {
        Console.WriteLine("Service is doing work.");
    }
}

public class ServiceDecorator(IService decoratedService) : IService
{
    public void DoWork()
    {
        Console.WriteLine("Before decorated service work.");
        decoratedService.DoWork();
        Console.WriteLine("After decorated service work.");
    }
}

public class LoggingInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Console.WriteLine($"Intercepting call: {invocation.Method.Name}");
        invocation.Proceed();
        Console.WriteLine($"Completed call: {invocation.Method.Name}");
    }
}

// Program.cs
var builder = new ContainerBuilder();

// Register the service and interceptor
builder.RegisterType<Service>().As<IService>().EnableInterfaceInterceptors().InterceptedBy(typeof(LoggingInterceptor));
builder.RegisterDecorator<ServiceDecorator, IService>();

// Enable class interceptors and register the logging interceptor to be applied to the decorator
builder.RegisterType<LoggingInterceptor>();

IContainer? container = builder.Build();

// Resolve and use the service
var service = container.Resolve<IService>();
service.DoWork();

if we use autofac 4.x and Autofac.Extras.DynamicProxy 4.5.0 LoggingInterceptor will act on ServiceDecorator(if multiple decorator it will act on the outmost decorator) and output is Intercepting call: DoWork Before decorated service work. Service is doing work. After decorated service work. Completed call: DoWork

if we use autofact 8.0 and Autofac.Extras.DynamicProxy 7.1.0 (actually is since 5+ the behaviors changed) LoggingInterceptor will act on Service and output is Before decorated service work. Intercepting call: DoWork Service is doing work. Completed call: DoWork After decorated service work.

and It also put the sample on github https://github.com/idiotsky/AutofacWithDecoratorAndInterceptor

and I saw this closed issue too, it should be similar issue as mine. https://github.com/autofac/Autofac/issues/1296

thanks for your reply

tillig commented 7 months ago

This is kind of a tough one.

I haven't dug into it super deep, but I'm guessing that the order of operations changed here due to the changes in the way the registration pipelines got updated. All of those internals changed for .NET Core for compatibility and to try fixing issues where there wasn't a consistent order of operations, especially when it came to trying to add things like diagnostics and such.

I'll admit we've always had troubles when it came to trying to put all the things together all at once - interceptors and decorators and adapters and everything. I'm not sure we can really "win" on the order of operations. On one hand, if we do the interception after all the decoration, we have problems like the fact that the interceptor is on the service registration rather than the decorator registration so it's not really obvious that the interceptor will "move" during decoration. On the other hand, I can see the desire to intercept at the resolved service level, which is potentially considered to be the "outermost" object in that hierarchy.

While I recognize we appear to have changed things as of around five years ago, I'm not sure we'll change it back now. It's been broken, we released the major version for the breaking change, and it's been running pretty steady like this for five years now.

I wonder if there's a way we can "fail forward," as it were?

For example, is there a way we can allow interceptors to be added to decorator registrations? This wouldn't solve the problem of totally switching the order of operations, but it would allow you to at least manage where the interception happens in a more intentional fashion. It'd also allow the registration to be more readable - the interceptor would be attached to the thing being intercepted, not "magically" moved around.

idiotsky commented 7 months ago

ok, question, Is it possible register interceptor on decorator for now?

tillig commented 7 months ago

I'm not in a place where I can test. Did you try it?

idiotsky commented 7 months ago

I try this

builder.RegisterType<ServiceDecorator>()
       .EnableInterfaceInterceptors()
       .InterceptedBy(typeof(LoggingInterceptor));

but the Interceptor dose not apply. still in trying if i can make it working, any suggestion? thanks.

tillig commented 7 months ago

I don't have any suggestions right now. If it works out of the box, it would be on the RegisterDecorator<> line since the decorator isn't registered as a separate service. If that's not supported, I might look at how the code in the Autofac DynamicProxy library works and see if there's a way to use that to set up the interception pipeline. Without digging in here at the code level - something I cannot do at this very specific moment in time - I can't provide additional guidance.

If it's not supported right out of the box, like if builder.RegisterDecorator<X, Y>().EnableInterfaceInterceptors() doesn't work - then it will not be like two lines of code. It will require some spelunking into the Autofac DynamicProxy integration to see how it builds up the metadata and pipeline stuff to allow interception to work. But, again, I can't provide specific guidance, hints, or suggestions at this point in time.

idiotsky commented 7 months ago

builder.RegisterDecorator<X, Y>() return void, so we can not use builder.RegisterDecorator<X, Y>().EnableInterfaceInterceptors()

I try this still not working, may be it does not support now. the interceptor does not take affect use the following code.

// Register the base service normally.
builder.RegisterType<Service>().Named<IService>("baseService");

// Register the LoggingInterceptor.
builder.RegisterType<LoggingInterceptor>();

// Register the decorator to wrap the base service and enable interception on the decorator.
builder.RegisterDecorator<IService>(
              (context, parameters, service) => new ServiceDecorator(service),
              fromKey: "baseService")
       .EnableInterfaceInterceptors() // Enable interception on the decorator
       .InterceptedBy(typeof(LoggingInterceptor)); // Specify the interceptor

var container = builder.Build();

// Resolve and use the service.
var service = container.Resolve<IService>();
service.DoWork();

Ideally we should support two mode interceptor on decorator or interceptor on service, both has its user case, or even more complicate, we can support interceptor on decorator and interceptor on service the same time.

tillig commented 7 months ago

I've marked this as an enhancement. I'm guessing the changes will be in the Autofac.Extras.DynamicProxy library for the most part, but I'm not positive.

We would entertain a pull request for this feature if it can be done without breaking the API; or breaking things very minimally (e.g., changing a void return type to return an object of some type). I'm not sure we'd be interested in supporting something terribly complex like "dual mode interception" or lots of parameters/config options - complexity adds overhead in testing, maintenance, and support, and given this is fairly edge-case, keeping that to a minimum is important.

Given this is all volunteer work and that there is no "development team" or anything like that, there is no "promised timeline" or schedule for this. If folks want this, the fastest way to get it done will be a pull request rather than waiting for a maintainer to get time to take it on.

idiotsky commented 7 months ago

I'm not familiar with the source code of Autofac.Extras.DynamicProxy, I think add an .InterceptedOnDecorator() method like this

builder.RegisterType<Service>().As<IService>().EnableInterfaceInterceptors().InterceptedBy(typeof(LoggingInterceptor))
       .InterceptedOnDecorator()

use this it won't break the current API, and can support the interceptor on decorator mode. any way I'll take an look Autofac.Extras.DynamicProxy to see if I can do something. thanks for your reply.

idiotsky commented 7 months ago
public static IRegistrationBuilder<TLimit, TActivatorData, TSingleRegistrationStyle> EnableInterfaceInterceptors<TLimit, TActivatorData, TSingleRegistrationStyle>(
        this IRegistrationBuilder<TLimit, TActivatorData, TSingleRegistrationStyle> registration, ProxyGenerationOptions? options = null, bool createProxyOnDecorator = false)
    {
        ArgumentNullException.ThrowIfNull(registration);

        if (createProxyOnDecorator)
        {
            registration.OnActivated(e =>
                                     {
                                         var componentContext = (ResolveRequestContext)e.Context;
                                         CreateProxy(componentContext);
                                     });
        }
        else
        {
            registration.ConfigurePipeline(p => p.Use(PipelinePhase.ScopeSelection, MiddlewareInsertionMode.StartOfPhase, (ctx, next) =>
                                           {
                                               next(ctx);
                                               CreateProxy(ctx);
                                           }));
        }

        void CreateProxy(ResolveRequestContext ctx)
        {
            EnsureInterfaceInterceptionApplies(ctx.Registration);

            // The instance won't ever _practically_ be null by the time it gets here.
            var proxiedInterfaces = ctx.Instance!
                .GetType()
                .GetInterfaces()
                .Where(ProxyUtil.IsAccessible)
                .ToArray();

            if (proxiedInterfaces.Length == 0)
            {
                return;
            }

            var theInterface = proxiedInterfaces.First();
            var interfaces = proxiedInterfaces.Skip(1).ToArray();

            var interceptors = GetInterceptorServices(ctx.Registration, ctx.Instance.GetType())
                .Select(s => ctx.ResolveService(s))
                .Cast<IInterceptor>()
                .ToArray();

            ctx.Instance = options == null
                ? ProxyGenerator.CreateInterfaceProxyWithTarget(theInterface, interfaces, ctx.Instance, interceptors)
                : ProxyGenerator.CreateInterfaceProxyWithTarget(theInterface, interfaces, ctx.Instance, options, interceptors);
        }

        return registration;
    }

I have made a change to the Autofac.Extras.DynamicProxy.RegistrationExtensions class and it is working. Initially, I considered adding a middleware to the service pipelines, but I couldn't find a way to integrate the service middleware using this method. Therefore, I opted to use OnActivated instead. I'm uncertain if there's a need to modify EnableClassInterceptors. @tillig, if this adjustment is acceptable, could you please implement it? If it's not suitable, I would appreciate any suggestions for improvement, and I'll make the necessary corrections.

tillig commented 7 months ago

The right way to suggest a fairly large code change like this is to submit a pull request. That way we can verify that all the unit tests still pass and we can get additional eyes on it to catch things that I may not catch - @alistairjevans sometimes sees stuff I don't, for example. I won't be able to take this large block of code and verify it works or suggest alternatives.

I will say:

idiotsky commented 7 months ago

already submit 2 pull request one is on autofac, add these extension methods will help remove the duplicate code in the second pull request on Autofac.Extras.DynamicProxy. any feed back let me know, I'll try to fix it.