Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
idiotsky opened this issue Apr 1, 2024 · 13 comments

Comments

@idiotsky
Copy link

idiotsky commented Apr 1, 2024

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
Copy link
Member

tillig commented Apr 1, 2024

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.

@tillig tillig added the pending-input Pending input or update label Apr 1, 2024
@idiotsky
Copy link
Author

idiotsky commented Apr 1, 2024

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.
#1296

thanks for your reply

@tillig tillig removed the pending-input Pending input or update label Apr 1, 2024
@tillig
Copy link
Member

tillig commented Apr 1, 2024

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
Copy link
Author

idiotsky commented Apr 1, 2024

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

@tillig
Copy link
Member

tillig commented Apr 1, 2024

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

@idiotsky
Copy link
Author

idiotsky commented Apr 1, 2024

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
Copy link
Member

tillig commented Apr 1, 2024

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
Copy link
Author

idiotsky commented Apr 1, 2024

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
Copy link
Member

tillig commented Apr 1, 2024

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
Copy link
Author

idiotsky commented Apr 1, 2024

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
Copy link
Author

idiotsky commented Apr 2, 2024

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
Copy link
Member

tillig commented Apr 2, 2024

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:

  • If this changes the signature of an existing method (e.g., by adding another parameter) then that will probably not be accepted. Adding a new overload is a better solution in that case because it won't be breaking. Perhaps that means a bit of internal refactoring to have a method like EnableDecoratorInterfaceInterception or something similar. (That's not a recommendation for a name, I'm just saying, a different name to differentiate rather than a parameter may be something to consider.)
  • I'm OK if we start out only enabling interface interceptors for decorators and adding the class interception later if folks ask for it.
  • It might be worth looking harder at the pipeline mechanism if possible. I think trying to handle the event is going to create some of that order-of-operations stuff we had problems with before due to things trying to handle the same event and having registration order problems affecting the execution. With the pipeline we can somewhat control order - a big reason why that got introduced in the first place. I do not have tips or tricks off the top of my head for this.

@idiotsky
Copy link
Author

idiotsky commented Apr 3, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants