autofacnlogautofac-module

How does the logging module for Autofac and NLog work?


I am still fairly new to Autofac and Nlog and I need some help in understanding what is taking place in my Autofac LoggingModule for Nlog. It works as expected thanks to following the injecting-nlog-with-autofacs-registergeneric. But rather than just copy paste, I would like to make sure I understand what is occurring in each method (Load & AttachToComponentRegistration). If you could review my thoughts and further clarify anything I have incorrect (quite a bit I am sure), I would greatly appreciate it. Thank you in advance!


LoggingModule

public class LoggingModule : Module
{

    protected override void Load(ContainerBuilder builder)
    {
        builder
            .Register((c, p) => new LogService(p.TypedAs<Type>()))
            .AsImplementedInterfaces();
    }

    protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration)
    {
        registration.Preparing +=
            (sender, args) =>
            {
                var forType = args.Component.Activator.LimitType;

                var logParameter = new ResolvedParameter(
                    (p, c) => p.ParameterType == typeof(ILog),
                    (p, c) => c.Resolve<ILog>(TypedParameter.From(forType)));

                args.Parameters = args.Parameters.Union(new[] { logParameter });
            };
    }

}

My understanding of the code within Load()

c - The parameter c, provided to the expression, is the component context(an IComponentContext object) in which the component is being created. The context in which a service can be accessed or a component's dependencies resolved.

p - An IEnumerable with the incoming parameter set

AsImplementedInterfaces - Autofac allows its users to register the types explicitly or implicitly. While "As" is used for explicit registrations, "AsImplementedInterfaces" and "AsSelf" are used for implicit ones. In other words, the container automatically registers the implementation against all the interfaces it implements.

Thoughts: The Load method code registers a new LogService class (which represents "c") with the type of logger (which represents "p") as the constructor parameter for the LogService class

Questions:


My understanding of the code within AttachToComponentRegistration()

AttachToComponentRegistration method - Override to attach module-specific functionality to a component registration.

AttachToComponentRegistration Parameters:

registration.Preparing - Fired when a new instance is required. The instance can be provided in order to skip the regular activator, by setting the Instance property in the provided event arguments.


var forType = args.Component.Activator.LimitType;

args = Autofac.Core.PreparingEventArgs - Fired before the activation process to allow parameters to be changed or an alternative instance to be provided.

Component = PreparingEventArgs.Component Property - Gets the component providing the instance being activated

Activator = IComponentRegistration.Activator Property - Gets the activator used to create instances.

LimitType = IInstanceActivator.LimitType Property - Gets the most specific type that the component instances are known to be castable to.

Thoughts on forType - As I understand it, this variable holds the Name and FullName of the calling class from where the logging service is being called?

forType Debugger Image

Questions:


var logParameter = new ResolvedParameter(
                    (p, c) => p.ParameterType == typeof(ILog),
                    (p, c) => c.Resolve<ILog>(TypedParameter.From(forType)));

ResolvedParameter - can be used as a way to supply values dynamically retrieved from the container, e.g. by resolving a service by name.

Thoughts on logParameter - This is where I start to get lost. So does, it check that the Parameter is of Type ILog and if so it will then resolve it with the constructor parameter and pass in forType variable?

Questions:


args.Parameters = args.Parameters.Union(new[] { logParameter });

args.Parameters = PreparingEventArgs.Parameters Property - Gets or sets the parameters supplied to the activator.

args.Parameters.Union = Produces the set union of two sequences by using the default equality comparer. Returns an System.Collections.Generic.IEnumerable`1 that contains the elements from both input sequences, excluding duplicates.

Thoughts on args.Parameters - I really do not know at this point other than to guess that it returns a collection of Parameters and removes duplicates?

Questions:

logParameter Debugger Image Nlog Database Table Image


LogService class

public class LogService : ILog
{
    private readonly ILogger _log;

    public LogService(Type type)
    {
        _log = LogManager.GetLogger(type.FullName);
    }

    public void Debug(string message, params object[] args)
    {
        Log(LogLevel.Debug, message, args);
    }

    public void Info(string message, params object[] args)
    {
        Log(LogLevel.Info, message, args);
    }

    public void Warn(string message, params object[] args)
    {
        Log(LogLevel.Warn, message, args);
    }

    public void Error(string message, params object[] args)
    {
        Log(LogLevel.Error, message, args);
    }

    public void Error(Exception ex)
    {
        Log(LogLevel.Error, null, null, ex);
    }

    public void Error(Exception ex, string message, params object[] args)
    {
        Log(LogLevel.Error, message, args, ex);
    }

    public void Fatal(Exception ex, string message, params object[] args)
    {
        Log(LogLevel.Fatal, message, args, ex);
    }

    private void Log(LogLevel level, string message, object[] args)
    {
        _log.Log(typeof(LogService), new LogEventInfo(level, _log.Name, null, message, args));
    }

    private void Log(LogLevel level, string message, object[] args, Exception ex)
    {
        _log.Log(typeof(LogService), new LogEventInfo(level, _log.Name, null, message, args, ex));
    }

}

ILog interface

public interface ILog
{
    void Debug(string message, params object[] args);
    
    void Info(string message, params object[] args);
    
    void Warn(string message, params object[] args);

    
    void Error(string message, params object[] args);
    void Error(Exception ex);

    void Error(Exception ex, string message, params object[] args);

    void Fatal(Exception ex, string message, params object[] args);
}

Solution

  • There's a lot to unpack here. You're not really asking for an answer to a specific question so much as a code walkthrough and explanation of an existing solution that works, so I might suggest posting to StackExchange Code Review if you need much more than what I'm going to give you here. Not trying to be unhelpful, but, like, if your question is, "Is my thinking right?" and the answer is "sort of," there's a lot of discussion on each individual point to explain why "sort of" is the answer (or "no," or "yes," as the case may be). It can turn into a lengthy answer, followed up by additional questions for clarification, which require yet additional answers... and StackOverflow isn't really a discussion forum capable of those sorts of things.

    [i.e., I'll take probably an hour and write up an answer here... but I can't promise I'll actually be back to follow up on anything because there are other questions to answer and other things I need to allocate time to. StackOverflow is really more about "How do I...?" or other things that have a single, reasonably concrete answer.]

    First, I recommend diving in yourself with a debugger on some breakpoints to actually see what's going on. For example, you asked what's in LimitType in one area - you could pretty easily answer that one by just sticking a breakpoint on that line and looking at the value. This will be a good way to follow up for additional clarification yourself - breakpoints for the win.

    Second, I recommend spending some time with the Autofac docs. There's a lot of documentation out there that can answer questions.

    Given the docs can round out some things that may not be clear, rather than try to address each "are my thoughts correct" item, let me just heavily annotate the module and hope that clarifies things.

    // General module documentation is here:
    // https://autofac.readthedocs.io/en/latest/configuration/modules.html
    public class LoggingModule : Module
    {
      // Load basically registers types with the container just like
      // if you were doing it yourself on the ContainerBuilder. It's
      // just a nice way of packaging up a set of registrations so
      // they're not all in your program's "Main" method or whatever.
      protected override void Load(ContainerBuilder builder)
      {
        // This is a lambda registration. Docs here:
        // https://autofac.readthedocs.io/en/latest/register/registration.html#lambda-expression-components
        // This one uses both the component context (c) and the incoming
        // set of parameters (p). In this lambda, the parameters are NOT the set of constructor
        // parameters that Autofac has resolved - they're ONLY things that
        // were MANUALLY specified. In this case, it's assuming a TypedParameter
        // with a System.Type value is being provided manually. It's not going
        // to try resolving that value from the container. This is going hand-in-hand
        // with the logParameter you see in AttachToComponentRegistration.
        // Parameter docs are here:
        // https://autofac.readthedocs.io/en/latest/resolve/parameters.html
        // In general if you resolve something that has both manually specified parameters
        // and things that can be resolved by Autofac, the manually specified parameters
        // will take precedence. However, in this lambda it's very specifically looking
        // for a manually specified parameter.
        // You'll want to keep this as a default InstancePerDependency because you probably
        // want this to live as long as the thing using it and no longer. Likely
        // NLog already has object pooling and caching built in so this isn't as
        // expensive as you think, but I'm no NLog expert. log4net does handle
        // that for you.
        builder
          .Register((c, p) => new LogService(p.TypedAs<Type>()))
          .AsImplementedInterfaces();
      }
    
      // This method attaches a behavior (in this case, an event handler) to every
      // component registered in the container. Think of it as a way to run a sort
      // of "global foreach" over everything registered.
      protected override void AttachToComponentRegistration(
        IComponentRegistry componentRegistry,
        IComponentRegistration registration)
      {
        // The Preparing event is called any time a new instance is needed. There
        // are docs for the lifetime events but Preparing isn't on there. Here are the
        // docs and the issue I filed on your behalf to get Preparing documented.
        // https://autofac.readthedocs.io/en/latest/lifetime/events.html
        // https://github.com/autofac/Documentation/issues/69
        // You can see the Preparing event here:
        // https://github.com/autofac/Autofac/blob/6dde84e5b0a3f82136a0567a84da498b04e1fa2d/src/Autofac/Core/IComponentRegistration.cs#L83
        // and the event args here:
        // https://github.com/autofac/Autofac/blob/6dde84e5b0/src/Autofac/Core/PreparingEventArgs.cs
        registration.Preparing +=
          (sender, args) =>
            {
              // The Component is the thing being resolved - the thing that
              // needs a LogService injected. The Component.Activator is the
              // thing that is actually going to execute to "new up" an instance
              // of the Component. The Component.Activator.LimitType is the actual
              // System.Type of the thing being resolved.
              var forType = args.Component.Activator.LimitType;
    
              // The docs above explain ResolvedParameter - basically a manually
              // passed in parameter that can execute some logic to determine if
              // it satisfies a constructor or property dependency. The point of
              // this particular parameter is to provide an ILog to anything being
              // resolved that happens to have an ILog constructor parameter.
              var logParameter = new ResolvedParameter(
    
                // p is the System.Reflection.ParameterInfo that describes the
                // constructor parameter that needs injecting. c is the IComponentContext
                // in which the resolution is being done (not used here). If this
                // method evaluates to true then this parameter will be used; if not,
                // it will refuse to provide a value. In this case, if the parameter
                // being injected is an ILog, this ResolvedParameter will tell Autofac
                // it can provide a value.
                (p, c) => p.ParameterType == typeof(ILog),
    
                // p and c are the same here, but this time they're used to actually
                // generate the value of the parameter - the ILog instance that should
                // be injected. Again, this will only run if the above predicate evaluates
                // to true. This creates an ILog by manually resolving from the same
                // component context (the same lifetime scope) as the thing that
                // needs the ILog. Remember earlier that call to p.AsTyped<Type>()
                // to get a parameter? The TypedParameter thing here is how that
                // value gets poked in up there. This Resolve call will effectively
                // end up calling the lambda registration.
                (p, c) => c.Resolve<ILog>(TypedParameter.From(forType)));
    
              // The thing being resolved (the component that consumes ILog) now
              // needs to be told to make use of the log parameter, so add it into
              // the list of parameters that can be used when resolving that thing.
              // If there's an ILog, Autofac will use this specified parameter to
              // fulfill the requirement.
              args.Parameters = args.Parameters.Union(new[] { logParameter });
            };
        }
    }
    

    Something missing from this that's present in the log4net module example is the ability to do property injection for the logger. However, I'm not going to solve that here; you can look at the example right in the documentation and take that as an exercise to work on if you need that functionality.

    I hope that helps. I'll probably not be coming back to follow up on additional questions, so if this isn't enough, I very, very much recommend setting some breakpoints, maybe setting up some tiny minimal-repro unit tests, that sort of thing, and do some deeper exploration to get clarity. Honestly, it's one thing to have someone else explain it, but it's another to actually see it in action and dive into the source of various projects. You'll come out with a fuller understanding with the latter approach, even if it's potentially not as fast.