Skip to content

Instantly share code, notes, and snippets.

@dolphinspired
Last active March 29, 2024 10:49
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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
    }
}
@benrobot
Copy link

benrobot commented Nov 7, 2021

This was so useful I created a NuGet package that I'm consuming in other projects.

https://www.nuget.org/packages/Functions.Worker.ContextAccessor/

@dolphinspired
Copy link
Author

Awesome, glad you found it useful!

@CarlosGomesMarel
Copy link

How does this handle simultaneous azure function calls if _currentContext is static?
private static AsyncLocal<FunctionContextRedirect> _currentContext

@dolphinspired
Copy link
Author

@CarlosGomesMarel AsyncLocal will return the value for the current Task chain, or control flow. So while _currentContext is static, the values it references are local to each Task. I can't explain it too well, but maybe this example will help.

A little more background: This is the approach that HttpContextAccessor uses in ASP.NET Core, so I followed this pattern for FunctionContext. What I ultimately ended up doing was creating an abstract class AsyncAccessor<T>, which let me create a Task-local value wrapper for any type that I might need. Very useful!

@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!👍👍👍

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