Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Determine the target type for a dependency during resolution

Tags:

c#

autofac

It seems to be impossible to determine the type for which a dependency is resolved:

containerBuilder.Register(context =>
{
   // What is the type for which this component is resolved?
   var type = default(Type); // TBD
   return context.Resolve<ILoggerFactory>().CreateLogger(type);
});

The goal here is to create the .NET Core logger with the right category for the type that it is applied to.

The example in the Autofac documentation describes how to pull this off with a Middleware component, and I was successful. But it seems adding a pipeline to every registration has performance implications, and I've yet to discover a way to only apply the pipeline to registrations for components that have a dependency on ILogger.

Motivation: the obvious choice seems to be to change the dependency to be of type ILogger<T> where T is the type on which this dependency is applied, like so:

public class Component
{
    public Component(ILogger<Component> logger)...
}

but experience tells me a lot of developers hastily copy and paste components and forget to change the type parameter, resulting in confusing logs. In the current code, where we still use Common.Logging, our component would simply need a non-generic ILog:

public class Component
{
    public Component(ILog log)...
}

In our previous DI container, Castle.Windsor, it would be as easy as this:

public class LoggerSubDependencyResolver : ISubDependencyResolver
{
    public bool CanResolve(CreationContext context, ISubDependencyResolver contextHandlerResolver, ComponentModel model, DependencyModel dependency)
    {
        return dependency.TargetType == typeof(ILog);
    }

    public object Resolve(CreationContext context, ISubDependencyResolver contextHandlerResolver, ComponentModel model, DependencyModel dependency)
    {
        return CanResolve(context, contextHandlerResolver, model, dependency) ? LogManager.GetLogger(model.Implementation) : null;
    }
}

Is there an easier way to accomplish this? Or this is the way to do it and I'm overly concerned about the performance implications?

like image 946
Dave Van den Eynde Avatar asked May 19 '21 06:05

Dave Van den Eynde


1 Answers

Yes, but it's undocumented so use it at your own risk

containerBuilder.Register(ctx =>
{
    var rc = ctx as ResolveRequestContext;    
    var operation = rc.Operation as IDependencyTrackingResolveOperation;
    //this is not going to work for controllers, unless you register them as services
    var service = operation.RequestStack.Skip(1).First().Service as TypedService;   
    return LogManager.GetLogger(service.ServiceType);
});

The middleware approach is the way to do it if you stick to the documentation. And it is, in this case, almost a direct alternative to the CastleWindsor resolvers (Notice: in CW resolvers are also called for each registeration). And you can set up middleware only for classes that depend on ILog using reflection. Also if the performance is a concern you might want to cache LogManager.GetLogger calls as mentioned in the documentation.

public class Log4NetMiddleware : IResolveMiddleware
{
    //Caching LogManager.GetLogger(type)
    private ILog _log;

    public Log4NetMiddleware(ILog log)
    {            
        _log = log;
    }

    public PipelinePhase Phase => PipelinePhase.ParameterSelection;

    public void Execute(ResolveRequestContext context, Action<ResolveRequestContext> next)
    {
        context.ChangeParameters(context.Parameters.Union(
            new[]
            {
                new ResolvedParameter(
                    (p, i) => p.ParameterType == typeof(ILog), //This is your CanResolve
                    (p, i) => _log //Resolve
                ),
            }));

        next(context);

        //This code below can be removed if you don't need injection via properties
        if (context.NewInstanceActivated)
        {
            var instanceType = context.Instance.GetType();

            //This is your CanResolve
            var properties = instanceType
                .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(p => p.PropertyType == typeof(ILog) && p.CanWrite && p.GetIndexParameters().Length == 0);

            foreach (var propToSet in properties)
            {
                //This is your Resolve
                propToSet.SetValue(context.Instance, _log, null);
            }
        }
    }
}

And registration would look like this

public void ConfigureContainer(ContainerBuilder containerBuilder)
{

    containerBuilder.ComponentRegistryBuilder.Registered += (sender, args) =>
    {
        var type = args.ComponentRegistration
            .Activator
            .LimitType;

        var constructors = type
            .GetConstructors(BindingFlags.Instance | BindingFlags.Public);

        if (constructors.Any(x => x.GetParameters().Any(p => p.ParameterType == typeof(ILog))))
        {
            args.ComponentRegistration.PipelineBuilding += (sender2, pipeline) =>
            {
                pipeline.Use(new Log4NetMiddleware(LogManager.GetLogger(type)));
            };
            return;
        }

        //the code below can be removed if you don't inject via properties
        var properties = type
          .GetProperties(BindingFlags.Public | BindingFlags.Instance)
          .Where(p => p.PropertyType == typeof(ILog) && p.CanWrite && p.GetIndexParameters().Length == 0);

        if (properties.Any())
        {
            args.ComponentRegistration.PipelineBuilding += (sender2, pipeline) =>
            {
                pipeline.Use(new Log4NetMiddleware(LogManager.GetLogger(type)));
            };
        }
    };
}
like image 79
Alexander Mokin Avatar answered Nov 19 '22 11:11

Alexander Mokin