Search code examples
c#dependency-injectionautofacserilog

Is it possible to get the requesting service type at resolution time in Autofac?


I'm using Serilog and Autofac and I would like to register an ILogger using .ForContext() at resolution time with the type of the object to be injected with the ILogger. Using the debugger, I can walk the IComponentContext and see what I want but the types to get to it are all internal so I can't (as far as I'm aware) actually get to it in my code. My registration code (which doesn't work as stated above) looks like this:

builder.RegisterType<SerilogLoggerFactory>().As<ISerilogLoggerFactory>().SingleInstance();
builder.Register(context =>
{
    var defaultResolveRequestContext = (Autofac.Core.Resolving.Pipeline.DefaultResolveRequestContext)context;
    var resolveOperation = (Autofac.Core.Resolving.ResolveOperation)defaultResolveRequestContext.Operation;
    var initiatingRequestService = (Autofac.Core.TypedService)resolveOperation.InitiatingRequest.Service;
    return context.Resolve<ISerilogLoggerFactory>().Create().ForContext(initiatingRequestService.ServiceType);
});

DefaultResolveRequestContext, ResolveOperation, and InitiatingRequest are all inaccessible due to their protection level.

Any thoughts or ideas would be appreciated.


Solution

  • Thanks to @Travis Illig's response, we created the following implementation:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    
    using Autofac.Core;
    using Autofac.Core.Resolving.Pipeline;
    
    using Serilog;
    
    namespace YOUR_COMPANY_NAME.Core.Logging.Serilog;
    
    /// <summary>
    /// Injects resolved types with <see cref="ILogger.ForContext(Type)"/> for <see cref="ILogger"/>.
    /// Supports both constructor and property injection.
    /// Inspired by <see href="link">https://autofac.readthedocs.io/en/latest/examples/log4net.html</see>.
    /// </summary>
    public class AutofacSerilogMiddleware : IResolveMiddleware
    {
        private readonly ILogger _rootLogger;
        private readonly Dictionary<Type, List<PropertyInfo>> _loggerPropertiesCache = new();
    
        public PipelinePhase Phase => PipelinePhase.ParameterSelection;
    
        public AutofacSerilogMiddleware(ILogger rootLogger)
        {
            _rootLogger = rootLogger;
        }
    
        public void Execute(ResolveRequestContext context, Action<ResolveRequestContext> next)
        {
            context.ChangeParameters(context.Parameters.Union(new[] { new ResolvedParameter((p, i) => p.ParameterType == typeof(ILogger), (p, i) => p.Member.DeclaringType != null ? _rootLogger.ForContext(p.Member.DeclaringType) : _rootLogger), }));
    
            // Continue the resolve.
            next(context);
    
            // Has an instance been activated?
            if (!context.NewInstanceActivated || context.Instance == null)
            {
                return;
            }
    
            var instanceType = context.Instance.GetType();
    
            if (!_loggerPropertiesCache.TryGetValue(instanceType, out var propertyInfos))
            {
                // Get all the injectable properties to set.
                propertyInfos = instanceType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.PropertyType == typeof(ILogger) && p.CanWrite && p.GetIndexParameters().Length == 0).ToList();
    
                _loggerPropertiesCache[instanceType] = propertyInfos;
            }
    
            if (!propertyInfos.Any())
            {
                return;
            }
    
            var contextLogger = _rootLogger.ForContext(instanceType);
    
            foreach (var propertyInfo in propertyInfos)
            {
                //Performance could be improved by generating and caching setter delegates instead of using PropertyInfo.SetValue
                propertyInfo.SetValue(context.Instance, contextLogger, null);
            }
        }
    }