Rory Primrose

Learn from my mistakes, you don't have time to make them yourself

View project on GitHub

Dependency Injection and ILogger in Azure Functions

Azure Functions is a great platform for running small quick workloads. I have been migrating some code over to Azure Functions where the code was written with dependency injection and usages of ILogger<T> in the lower level dependencies. This post will go through how to support these two requirements in Azure Functions.

Dependency Injection

There are already several posts around that provide a solution for dependency injection in Azure Functions v2. They use an extensibility point in Azure Functions for binding values to the parameters on the static entry point of the function. The code below is based on this post.

It starts with the creation of a marker attribute.

[Binding]
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class InjectAttribute : Attribute
{
    public InjectAttribute(Type type)
    {
        Type = type;
    }

    public Type Type { get; }
}

The extension point uses IExtensionConfigProvider to register a binder for function parameters that are resolved using an IoC container. The bulk of this class is creating the Autofac container in a thread safe way to make sure that the container is only created once.

public class InjectConfiguration : IExtensionConfigProvider
{
    private static readonly object _syncLock = new object();
    private static IContainer _container;

    public void Initialize(ExtensionConfigContext context)
    {
        InitializeContainer(context);

        context
            .AddBindingRule<InjectAttribute>()
            .BindToInput<dynamic>(i => _container.Resolve(i.Type));
    }

    private void InitializeContainer(ExtensionConfigContext context)
    {
        if (_container != null)
        {
            return;
        }

        lock (_syncLock)
        {
            if (_container != null)
            {
                return;
            }

            _container = ContainerConfig.BuildContainer(context.Config.LoggerFactory);
        }
    }
}

Creating the container is where there starts to be custom code based on the project. This is a fairly general configuration of a container. In this case the types of the current assembly are registered in the container using their interfaces. Then a logger factory and module are registered (more on this later).

public static class ContainerConfig
{
    public static IContainer BuildContainer(ILoggerFactory factory)
    {
        var builder = new ContainerBuilder();

        var assemblyTypes = Assembly.GetExecutingAssembly().GetTypes().Where(x => x.GetInterfaces().Any()).ToArray();

        builder.RegisterTypes(assemblyTypes).AsImplementedInterfaces();

        builder.RegisterInstance(factory).As<ILoggerFactory>();
        builder.RegisterModule<LoggerModule>();

        return builder.Build();
    }
}

The way this works is that Azure Functions will invoke the extension point in order to bind values to the marked parameters of the static entry point of the function. The binder will return the parameter value by resolving the target type from the Autofac container.

This simple setup provides great flexibility for developing Azure Functions with dependency injection. It is less than ideal that Azure Functions uses a static member as the entry point so this is as good as it gets for now. It is possible that this will change in the future.

ILogger

The default Azure Functions template in Visual Studio uses TraceWriter for logging. This works well when developing locally with Visual Studio and also writes log entries out to the Azure Portal log section for the function when deployed to Azure. I have already been using ILogger and ILogger<T> in my code so prefer it over TraceWriter.

Azure Functions natively support binding to a ILogger parameter on the static method of the function however there are currently some disadvantages. It does not support binding ILogger<T> or ILoggerFactory parameters, does not write entries to the local dev console and does not write to the Azure Portal log section. It does however write the log entries to Application Insights in production and that is enough for me.

I have been using an Autofac module to dynamically create logger instances for the target types being created with the logger dependency. Originally this module would create log4net log instances and now creates ILogger and ILogger<T> instances.

public class LoggerModule : Module
{
    private static readonly ConcurrentDictionary<Type, object> _logCache = new ConcurrentDictionary<Type, object>();

    private interface ILoggerWrapper
    {
        object Create(ILoggerFactory factory);
    }

    protected override void AttachToComponentRegistration(
        IComponentRegistry componentRegistry,
        IComponentRegistration registration)
    {
        Ensure.Any.IsNotNull(registration, nameof(registration));

        // Handle constructor parameters.
        registration.Preparing += OnComponentPreparing;
    }

    private static object GetLogger(IComponentContext context, Type declaringType)
    {
        return _logCache.GetOrAdd(
            declaringType,
            x =>
            {
                var factory = context.Resolve<ILoggerFactory>();
                var loggerName = "Function." + declaringType.FullName + ".User";

                return factory.CreateLogger(loggerName);
            });
    }

    private static void OnComponentPreparing(object sender, PreparingEventArgs e)
    {
        var t = e.Component.Activator.LimitType;

        if (t.FullName.IndexOf(nameof(FunctionApp1), StringComparison.OrdinalIgnoreCase) == -1)
        {
            return;
        }

        if (t.FullName.EndsWith("[]", StringComparison.OrdinalIgnoreCase))
        {
            // Ignore IEnumerable types
            return;
        }

        e.Parameters = e.Parameters.Union(
            new[]
            {
                new ResolvedParameter((p, i) => p.ParameterType == typeof(ILogger), (p, i) => GetLogger(i, t))
            });
    }
}

The line of code to note above is the filter against the namespace of the target type requested of the Autofac container. This module will only create log instances for target types that exist under the specified base namespace. This is hard-coded against a namespace using the nameof(FunctionApp1) call and will need to be updated if you use this in your own projects. This could be refactored to use the base namespace of the assembly containing the module but this will not always catch all scenarios.

This module requires an ILoggerFactory to be already registered in the container. The module must resolve this dependency to create logger instances. Thankfully this is available in the InjectConfiguration.InitializeContainer extensibilty point via context.Config.LoggerFactory which is then passed to the ContainerConfig class. The factory is then registered in the container making it available to the LoggerModule.

All this now works as expected because of Autofac tying everything together. This means for example that the following code works perfectly.

public static class Function1
{
    [FunctionName("Function1")]
    public static void Run(
        [TimerTrigger("0 */1 * * * *")]TimerInfo myTimer,
        [Inject(typeof(ISomething))]ISomething something,
        ILogger log)
    {
        log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");

        var value = something.GetSomething();

        log.LogInformation("Found value " + value);
    }
}

public interface ISomething
{
    string GetSomething();
}

public class Something : ISomething
{
    private readonly ILogger<Something> _logger;

    public Something(ILogger<Something> logger)
    {
        _logger = logger;
    }

    public string GetSomething()
    {
        _logger.LogInformation("Hey, we are doing something");

        return Guid.NewGuid().ToString();
    }
}

Updated: 27/May/2018

Azure Functions (v2 beta) applies a filter out of the box for ILogger. The filter looks at whether the logger name is either Function.Something or Function.Something.User. Any logger that does not have its name matching this format will have its log messages filtered out.

The way ILogger<T> is created by LogFactory uses the name derived from typeof(T).FullName. This does not conform to either of the values that Azure Functions likes so the log messages are filtered out. What this means is that currently (since the code from 19th December 2017 was released) Azure Functions will not work when using ILogger<T>. Additionally, the LoggerModule originally posted above also didn’t work for ILogger because it used factory.CreateLogger(declaringType) which creates the logger name from declaringType.FullName. This also does not match the expected filter and log messages are also filtered out.

I’ve updated the module above so that it no longer supports dependency injection of ILogger<T> and ILogger instances are still created with the declaring type but also in a format that would allow Azure Functions to write the log entries. This is done by creating loggers with the name derived from "Function." + declaringType.FullName + ".User".

Written on April 5, 2018