Passing in the type of the declaring class for NLog using Autofac
Following on from this question I would like autofac to inject the type 开发者_开发问答of the declaring object into the constructor of my NLog service, so that it can correctly log which type is logging entries.
My NLogService class looks like this...
public class NLogService : ILogService
{
private readonly Logger _logger;
public NLogService(Type t)
{
var consumerType = t.DeclaringType.FullName;
_logger = LogManager.GetLogger(consumerType);
}
However it fails on app startup because it obviously cannot work out what to inject into the constructor of the NLogService with the following error...
None of the constructors found with 'Public binding flags' on type 'MyProduct.Domain.Services.Logging.NLogService' can be invoked with the available services and parameters: Cannot resolve parameter 'System.Type t' of constructor 'Void .ctor(System.Type)'.
So, my question is - how do i instruct autofac to inject the type of the calling class?
I tried this...
public NLogService(Type t)
{
var method = MethodBase.GetCurrentMethod();
Type consumingType = method.DeclaringType;
var consumerType = consumingType.FullName;
var consumerType = t.DeclaringType.FullName;
_logger = LogManager.GetLogger(consumerType);
}
But i just end up with MyProduct.Domain.Services.Logging.NLogService
What i want is the type of the class that is doing the actual logging.
i have already tried this suggestion and it didnt work for me either.
Could make your NLogService generic, i.e. NLogService<T>
and use Autofac's open generics support?
Then you could do this:
public class NLogService<T> : ILogger<T>
{
private readonly Logger _logger;
public NLogService()
{
_logger = LogManager.GetLogger(typeof(T).FullName);
}
}
There is no real good way to do this with Autofac, because does not have support for 'context based injection' (which is what you are trying to do). There is a workaround, but it aint pretty...
What you can do is revert to property injection and define a base class or interface for that ILogService
property. For instance, you can define the following interface:
public interface ILoggerContainer
{
public ILogService Logger { get; set; }
}
Now you can implement this interface on all types that need a logger:
public class Consumer : IConsumer, ILoggerContainer
{
public ILogService Logger { get; set; }
}
With this in place you can configure Autofac as follows:
builder.RegisterType<ILoggerContainer>()
.OnActivating(e =>
{
var type = typeof(LogService<>)
.MakeGenericType(e.Instance.GetType());
e.Instance.Logger = e.Context.Resolve(type);
});
Another workaround, that you may find cleaner is to inject an ILogger<T>
with the same type as the type of the parent type:
public class Consumer : IConsumer
{
public Consumer(ILogger<Consumer> logger) { }
}
This makes the configuration much easier and prevents you from having to have a base class. Which one is most appropriate is up to you.
As I said, these are workarounds, but to be honest, you might need to reconsider your logging strategy in your application. Perhaps you are logging at too many places. In the applications I write there is hardly ever a need to log, and when I do, I write an logging message that is expressive enough so that there is no need to communicate the type that triggered the event. And when you log exception, you will always have a complete stack trace (and exception logging should almost only happen in the outer layer of your application and not within services anyway).
The following technique works well in our experience:
Create an attribute like below, which can be applied at class level or at the injection site:
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Class)] public class LoggerAttribute : Attribute { public readonly string Name; public LoggerAttribute(string name) { Name = name; } }
Create an Autofac module that you register with the
ContainerBuilder
:
public class LogInjectionModule : Module { protected override void AttachToComponentRegistration(IComponentRegistry registry, IComponentRegistration registration) { registration.Preparing += OnComponentPreparing; } static void OnComponentPreparing(object sender, PreparingEventArgs e) { var typePreparing = e.Component.Activator.LimitType; // By default, the name supplied to the logging instance is the name of the type in which it is being injected into. string loggerName = typePreparing.FullName; //If there is a class-level logger attribute, then promote its supplied name value instead as the logger name to use. var loggerAttribute = (LoggerAttribute)typePreparing.GetCustomAttributes(typeof(LoggerAttribute), true).FirstOrDefault(); if (loggerAttribute != null) { loggerName = loggerAttribute.Name; } e.Parameters = e.Parameters.Union(new Parameter[] { new ResolvedParameter( (p, i) => p.ParameterType == typeof (Logger), (p, i) => { // If the parameter being injected has its own logger attribute, then promote its name value instead as the logger name to use. loggerAttribute = (LoggerAttribute) p.GetCustomAttributes(typeof(LoggerAttribute),true).FirstOrDefault(); if (loggerAttribute != null) { loggerName = loggerAttribute.Name; } // Return a new Logger instance for injection, parameterised with the most appropriate name which we have determined above. return LogManager.GetLogger(loggerName); }), // Always make an unamed instance of Logger available for use in delegate-based registration e.g.: Register((c,p) => new Foo(p.TypedAs<Logger>()) new TypedParameter(typeof(Logger), LogManager.GetLogger(loggerName)) }); } }
You can now inject a named Logger in any one of these ways depending on individual scenarios:
By default, the injected logger name will be given the full type name of the class it is injected into:
public class Foo { public Foo(Logger logger) { } }
Use a constructor parameter [Logger] attribute to override the logger name:
public class Foo { public Foo([Logger("Meaningful Name")]Logger logger) { } }
Use a class-level [Logger] attribute to set the same logger name override for all constructor overloads:
[Logger("Meaningful Name")] public class Foo { public Foo(Logger logger, int something) { } public Foo(Logger logger, int something, DateTime somethingElse) { } }
Use constructor parameter [Logger] attributes on each constructor overload to set different logger names depending on the context of how you were constructed:
public class Foo { public Foo(Logger("Meaningful Name")]Logger logger, int something) { } public Foo(Logger("Different Name")]Logger logger, int something, DateTime somethingElse) { } }
IMPORTANT NOTE: If you register types to be resolved with logger constructor injection using Autofac's delegate registration, you MUST use the two parameter overload like so: Register((c,p) => new Foo(p.TypedAs<Logger>())
.
Hope this helps!
It is possible to do this without generics.
However, please note that in Autofac 6.x, the resolution process has changed to use a resolve pipeline. This doesn't matter for most scenarios, but it does when you want to use the lifetime events like OnPreparing
, etc. Most of the answers here on SO around overriding the Preparing
event are very old and are now outdated. You can't override Preparing
directly anymore.
There is an example on the Autofac documentation site doing this for log4net, and it works with NLog with only minor changes. Here is the basic idea:
public class Log4NetMiddleware : IResolveMiddleware
{
public PipelinePhase Phase => PipelinePhase.ParameterSelection;
public void Execute(ResolveRequestContext context, Action<ResolveRequestContext> next)
{
// Add our parameters.
context.ChangeParameters(context.Parameters.Union(
new[]
{
new ResolvedParameter(
(p, i) => p.ParameterType == typeof(ILog),
(p, i) => LogManager.GetLogger(p.Member.DeclaringType)
),
}));
// Continue the resolve.
next(context);
// Has an instance been activated?
if (context.NewInstanceActivated)
{
var instanceType = context.Instance.GetType();
// Get all the injectable properties to set.
// If you wanted to ensure the properties were only UNSET properties,
// here's where you'd do it.
var properties = instanceType
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.PropertyType == typeof(ILog) && p.CanWrite && p.GetIndexParameters().Length == 0);
// Set the properties located.
foreach (var propToSet in properties)
{
propToSet.SetValue(context.Instance, LogManager.GetLogger(instanceType), null);
}
}
}
}
Please also note that you have to understand how middleware works in Autofac. The documentation is a good place to start.
精彩评论