This is a brief tutorial on how to create a dependency-injectable FunctionContext accessor for Azure Functions running on the dotnet-isolated runtime (.NET 5 and up). This will work very similarly to IHttpContextAccessor - it will allow you to access details about the current Function invocation and pass arbitrary values between injected services that are scoped to this invocation.
- Create your interface. You must include both
get
andset
on this interface.
public interface IFunctionContextAccessor
{
FunctionContext FunctionContext { get; set; }
}
- Create an implementation of that interface. This is modeled after the ASP .NET Core implementation of HttpContextAccessor and will allow you to store a FunctionContext instance that's scoped to the current Task chain (i.e. Function invocation).
public class FunctionContextAccessor : IFunctionContextAccessor
{
private static AsyncLocal<FunctionContextRedirect> _currentContext = new AsyncLocal<FunctionContextRedirect>();
public virtual FunctionContext FunctionContext
{
get
{
return _currentContext.Value?.HeldContext;
}
set
{
var holder = _currentContext.Value;
if (holder != null)
{
// Clear current context trapped in the AsyncLocals, as its done.
holder.HeldContext = null;
}
if (value != null)
{
// Use an object indirection to hold the context in the AsyncLocal,
// so it can be cleared in all ExecutionContexts when its cleared.
_currentContext.Value = new FunctionContextRedirect { HeldContext = value };
}
}
}
private class FunctionContextRedirect
{
public FunctionContext HeldContext;
}
}
- Create a middleware to that will set the FunctionContext on each Function invocation.
public class FunctionContextAccessorMiddleware : IFunctionsWorkerMiddleware
{
private IFunctionContextAccessor FunctionContextAccessor { get; }
public FunctionContextAccessorMiddleware(IFunctionContextAccessor accessor)
{
FunctionContextAccessor = accessor;
}
public Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
if (FunctionContextAccessor.FunctionContext != null)
{
// This should never happen because the context should be localized to the current Task chain.
// But if it does happen (perhaps the implementation is bugged), then we need to know immediately so it can be fixed.
throw new InvalidOperationException($"Unable to initalize {nameof(IFunctionContextAccessor)}: context has already been initialized.");
}
FunctionContextAccessor.FunctionContext = context;
return next(context);
}
}
- Register your accessor and middleware on startup.
public static void Main(string[] args)
{
var host = Host.CreateDefaultBuilder()
.ConfigureServices((host, services) =>
{
// The accessor itself should be registered as a singleton, but the context
// within the accessor will be scoped to the Function invocation
services.AddSingleton<IFunctionContextAccessor, FunctionContextAccessor>();
})
.ConfigureFunctionsWorkerDefaults(app =>
{
app.UseMiddleware<FunctionContextAccessorMiddleware>();
})
.Build();
host.Run();
}
You're done! You can now inject this accessor into your Functions or injected services. Here's an example:
public class UserRepository : IUserRepository
{
private IFunctionContextAccessor FunctionContextAccessor { get; }
private ILogger Logger { get; }
public UserRepository(IFunctionContextAccessor accessor, ILogger<UserRepository> logger)
{
FunctionContextAccessor = accessor;
Logger = logger;
}
public async Task<User> GetUserAsync(string userId)
{
var context = FunctionContextAccessor.FunctionContext;
Logger.LogInformation($"Getting users for function invocation: {context.InvocationId}");
context.Items.Add("UserRepositoryAccessed", true);
// Idk, return a user or something
}
}
@dolphinspired, have you seen this comment form @davidfowl
Azure/azure-functions-dotnet-worker#950 (comment)
How are you addressing those concerns?