Azure / azure-functions-dotnet-worker

Azure Functions out-of-process .NET language worker
MIT License
429 stars 184 forks source link

Make FunctionContext available as a Dependency Type ( Like IHttpContextAccessor) #950

Open jp1482 opened 2 years ago

jp1482 commented 2 years ago

Enhancement Introduction

Currently current FunctionContext available as a input parameter to Function.

[Function("HelloWorld")]
public async Task<OutputResponse> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "send")] HttpRequestData req
            ,FunctionContext functionContext)

Now if there is any dependency that called as part of above function implementation then It required explicit pass of FunctionContext. If compare is with asp.net core web api structure then we have IHttpContextAccessor. In can be available to access in downstream call. For example if there is to create HttpClient from IHttpClientFactory and It has custom message handler which required FunctionContext.

services.AddHttpClient("Call", client =>
         {
             client.BaseAddress = new Uri("http://localhost");
         }).AddHttpMessageHandler<ExampleHttpMessageHandler>();

ExampleMessageHandler looks like following.

public class ExampleHttpMessageHandler : DelegatingHandler
{
    private readonly FunctionContext functionContext;

    public ExampleHttpMessageHandler(FunctionContext functionContext)
    {
        this.functionContext = functionContext;
    }
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {   
        return base.SendAsync(request, cancellationToken);
    }
}

Currently above will not work as FunctionContext is not available as dependency.

Possible Solution

public interface IFunctionContextAccessor
{
    public FunctionContext? FunctionContext { get; set; }
}
internal sealed class DefaultFunctionContextAccessor
    : IFunctionContextAccessor
{
    private static readonly AsyncLocal<FunctionContextHolder> _functionContextCurrent = new AsyncLocal<FunctionContextHolder>();

    public FunctionContext? FunctionContext
    {
        get
        {
            return _functionContextCurrent.Value?.Context;
        }
        set
        {
            var holder = _functionContextCurrent.Value;
            if (holder != null)
            {               
                holder.Context = null;
            }

            if (value != null)
            {              
                _functionContextCurrent.Value = new FunctionContextHolder { Context = value };
            }
        }
    }

    private class FunctionContextHolder
    {
        public FunctionContext? Context;
    }
}

Current Approach ( As Middleware is only approach )

public class FunctionContextMiddleware : IFunctionsWorkerMiddleware
{
    public Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
    {
        var _functionContextAccessor = context.InstanceServices.GetService<IFunctionContextAccessor>();
        if (_functionContextAccessor != null)
        {
            _functionContextAccessor.FunctionContext = context;
        }
        return next(context);
    }
}

This is how it is being used.

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(worker =>
    {
        worker.UseMiddleware<FunctionContextMiddleware>();
    })
    .ConfigureServices(services =>
    {
        services.AddSingleton<IFunctionContextAccessor, DefaultFunctionContextAccessor>();     
    })
    .Build();

host.Run();

Suggestion

davidfowl commented 2 years ago

Don't do this unless:

rvdginste commented 2 years ago

@davidfowl

This code looks very similar to FunctionContextAccessor and is a translation of the HttpContextAccessor to the FunctionContext.

I understand about the "not disposable", because it would not be properly disposed. But I do not understand about the immutable/readonly/threadsafe part?

FunctionContext encapsulates the information about a function execution and the Items property is used for a key/value collection that can be used to share data within the scope of an invocation.

So, from that, I understand that it is only used within one function call and that its use is similar to the HttpContext in ASP.NET Core. As long as you don't write multi-threaded code in a function execution (that would concurrently access the FunctionContext), I don't see how it could cause problems?

davidfowl commented 2 years ago

I wrote an essay here https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#asynclocalt đŸ˜„

rvdginste commented 2 years ago

@davidfowl

"essay" is uh.. fitting, but very informative, thank you!

jp1482 commented 2 years ago

@rvdginste Thanks for asking this. @davidfowl Thanks for providing information.

As per my understanding code for worker, It create functioncontext and it is remaining same for that request so it is good if available like httpcontextaccessor.

nulltoken commented 1 year ago

Issue #1288 has been closed in favor of this one.

Context is (for instance) we'd need to decorate Application Insights telemetry with metadata related to the user invoking invoking an Http trigger. In AspNet.Core, we'd rely on an IHttpContextAccessor.

How can this be done in isolated worker world?

mathiasi commented 1 year ago

Would appreciate an update on this - facing the same question

gha-zund commented 9 months ago

Note that FunctionContext is not available in durable entities with isolated model at the moment. See also: https://github.com/Azure/azure-functions-durable-extension/discussions/2740

If FunctionContext would be available in the (scoped) ServiceProvider, or if there was something like the suggested FunctionContextAccessor, it would be available for durable entities too without additional effort.

Dorrro commented 3 months ago

Like @nulltoken, we want to enhance our Application Insights telemetry with some metadata that is available only via the FunctionContext. Is there any update on that matter?

fabiocav commented 3 months ago

@Dorrro there are no current plans to implement this feature.

We're keeping this open as we review and reevaluate items like this periodically (and it's a good way to continue to gather feedback and discuss), but the open issue is not an indication of a plan/commitment to add this support.