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"
.