Skip to content

Instantly share code, notes, and snippets.

@dolphinspired
Last active October 31, 2024 14:33
Show Gist options
  • Save dolphinspired/796d26ebe1237b78ee04a3bff0620ea0 to your computer and use it in GitHub Desktop.
Save dolphinspired/796d26ebe1237b78ee04a3bff0620ea0 to your computer and use it in GitHub Desktop.
FunctionContextAccessor example

IFunctionContextAccessor Implementation

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.

  1. Create your interface. You must include both get and set on this interface.
public interface IFunctionContextAccessor
{
    FunctionContext FunctionContext { get; set; }
}
  1. 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;
    }
}
  1. 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);
    }
}
  1. 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
    }
}
@marcsstevenson
Copy link

Have just spent the day working through the same problem regarding trying to use scoped dependencies in Azure Function Middleware (isolated .Net7). Have also spent 20+ years coding with C# and have never heard of AsyncLocal before.

THANK YOU for this gist as it's implementation of AsyncLocal was simple to add to my solution. Have also used a throw exception guard because this really cannot leak in a multi-tenanted system!

@sayeed1999
Copy link

sayeed1999 commented Sep 25, 2023

Great thing!👍👍👍

@jmelosegui
Copy link

@dolphinspired, have you seen this comment form @davidfowl
Azure/azure-functions-dotnet-worker#950 (comment)
How are you addressing those concerns?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment