Search code examples
graphqlazure-functions.net-6.0hotchocolateazure-functions-isolated

Can Function Middleware and HotChocolate Middleware be used together?


Is it possible for Function Middlewares and Hot Chocolate Middlewares to be used together when using Azure Functions in an isolated-process?

For example:

Consider Authentication middleware that is defined at a Function level via IFunctionWorkerMiddleware. When the function is triggered, the Authentication Middleware is triggered and if authentication succeeds, then passes the control to HotChocolate Middleware for Authorization checks, validation checks etc.

both the middleware work in isolation, but when i use them in conjunction, then only the Function Middleware is trigger (as it is the first middleware in the pipeline) but the control is never handed over to HotChocolate middlewares.

I am using HotChocolate v13, with .Net 6 and Function Version v4


Solution

  • In Short: Yes it is possible to use Azure Function Middleware and Hot Chocolate Middleware to be used together, when working with isolated process Azure Functions.

    i decided to write an article about it on Medium: https://medium.com/@vivek.vardhan86/sharing-states-between-function-middleware-and-hot-chocolate-middleware-in-isolated-process-azure-6ea32b752eed

    but if someone cannot read it, then here is the full article:

    Step 1: Define the SharedContext

    The Shared Context serves as a shared storage medium between different middleware components.

    public class SharedContext
    {
        public string SharedData { get; set; }
    }
    

    Step 2: Create an Azure Function Middleware

    Create a middleware class that implements IFunctionsWorkerMiddleware. This middleware sets a value in the Shared Context.

    public class CustomFunctionMiddleware : IFunctionsWorkerMiddleware
    {
        public CustomFunctionMiddleware() { }
    
        public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
        {
           var sharedContext = context.InstanceServices.GetService<SharedContext>()!;
           sharedContext.SharedData = "Some Data";
    
           context.Items["shared_context"] = sharedContext; 
           await next(context);
        }
    }
    

    Since SharedContext will be registered as a scoped service, it should not be injected via the Constructor, since it will be treated as singleton when using isolated-process Azure Functions.

    Step 3: Create a Custom HttpRequestInterceptor in HotChocolate

    This interceptor will be triggered after all the Azure Function Middleware in the pipeline are executed but before the GraphQL middleware and resolvers are invoked .

    Extract the SharedContext from the HttpContext available in DefaultHttpRequestInterceptor or the IHttpRequestInterceptor interface available in Hot Chocolate. Now inject the SharedContext into Hot Chocolate’s GraphQL request pipeline as a Global State.

    public class CustomHttpRequestInterceptor : DefaultHttpRequestInterceptor
    {
        public CustomHttpRequestInterceptor() { }
    
        public override ValueTask OnCreateAsync(HttpContext httpContext, IRequestExecutor requestExecutor,
            IQueryRequestBuilder requestBuilder, CancellationToken cancellationToken)
        {
            var sharedContext = httpContext.Items["shared_context"] as SharedContext;
    
            requestBuilder.AddGlobalState("sharedContext", sharedContext);
            return base.OnCreateAsync(httpContext.Items, requestExecutor, requestBuilder, cancellationToken);
        }
    }
    

    Step 4: Implement GraphQL Middleware

    Now that the Shared Context is added as a Global State it is available to all GraphQL middleware either via Middleware Context or Resolver Context

    public class GraphQLMiddleware
    {
        private readonly FieldDelegate next;
    
        public GraphQLMiddleware(FieldDelegate next)
        {
            this.next = next;
        }
    
        public async Task InvokeAsync(IMiddlewareContext context)
        {
            var sharedContext = context.GetGlobalStateOrDefault<SharedContext>("sharedContext");
            // Your logic here
            await next(context);
        }
    }
    

    Step 5: Register Components

    Register your components in your Program.cs. This is critical for the dependency injection framework to know which implementations to use.

    Make sure that you register the Shared Context as scoped so that each time a Http Trigger is activated via a function call, your SharedContext is scoped to that call. This way every new GraphQL Query or Mutation call will get a new copy of Shared Context and there will be no data bleeding between different GraphQL requests.

    Step 5.1: GraphQL Middleware Registration

    public class QueryType : ObjectType
    {
        protected override void Configure(IObjectTypeDescriptor descriptor)
        {
            descriptor
                .Field("example")
                .Use<GraphQLMiddleware>()
                .Resolve(context =>
                {
                    // Omitted for brevity
                });
        }
    }
    

    Step 5.2: isolated-process Azure Function Host Builder

    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    
    var host = new HostBuilder()
        .ConfigureFunctionsWorkerDefaults(builder =>
        {
            builder.Services
                .AddGraphQLFunction()
                .AddHttpRequestInterceptor<CustomHttpRequestInterceptor>()
                .AddQueryType<QueryType>();
            
            builder.Services.AddScoped<SharedContext>();
            builder.UseMiddleware<CustomFunctionMiddleware>();
        })
        .Build();
    
    host.Run();