Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save ddieppa/dfff9a0ad1b2ce258a89b0ef36f1fd12 to your computer and use it in GitHub Desktop.

Select an option

Save ddieppa/dfff9a0ad1b2ce258a89b0ef36f1fd12 to your computer and use it in GitHub Desktop.

Revisions

  1. @benmccallum benmccallum created this gist Oct 8, 2020.
    54 changes: 54 additions & 0 deletions UseResolverScopedMediatorAttribute.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,54 @@
    using System;
    using System.Collections.Generic;
    using System.Reflection;
    using HotChocolate.Types;
    using HotChocolate.Types.Descriptors;
    using MediatR;
    using Microsoft.Extensions.DependencyInjection;

    namespace MyCompany.GraphQL
    {
    /// <summary>
    /// This attribute allows us to tap into the field resolution middleware
    /// so that we can dynamically create an <see cref="IServiceScope"/> just for
    /// this field resolver method, and create an <see cref="IMediator"/> instance inside it.
    ///
    /// This is used to solve a problem with using EF Core's DbContextPool, which scopes
    /// DbContext instances per http request and will cause HotChocolate resolvers running
    /// in parallel to execute on the same DbContext, which isn't allowed.
    ///
    /// By resolving our <see cref="IMediator"/> instance inside an isolated scope,
    /// we ensure that if it depends itself on a DbContext that it will be unique in that scope
    /// and there won't be multiple threads using it simultaneously.
    /// </summary>
    [AttributeUsage(AttributeTargets.Method)]
    public class UseResolverScopedMediatorAttribute : ObjectFieldDescriptorAttribute
    {
    private static readonly string _injectedArgumentName = "mediator";

    private static readonly HashSet<string> _localSchemaNames = new HashSet<string>
    {
    SchemaNames.Content,
    SchemaNames.ShopTyre,
    SchemaNames.Task
    };

    public override void OnConfigure(
    IDescriptorContext descriptorContext,
    IObjectFieldDescriptor descriptor,
    MemberInfo member)
    {
    descriptor.Use(next => async context =>
    {
    // Temp workaround for: https://github.com/ChilliCream/hotchocolate/issues/2246
    using var scope = _localSchemaNames.Contains(context.Schema.Name)
    ? context.Service<Microsoft.AspNetCore.Http.IHttpContextAccessor>().HttpContext.RequestServices.CreateScope()
    : context.Service<IServiceProvider>().CreateScope();
    var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
    context.ModifyScopedContext(c => c.SetItem(_injectedArgumentName, mediator));

    await next(context);
    });
    }
    }
    }
    8 changes: 8 additions & 0 deletions _Usage.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    [UseResolverScopedMediator]
    public async Task<string> GetAccountsAsync(
    IResolverContext context,
    [ScopedState] IMediator mediator)
    => await mediator.Send(new SomeRequest());

    // Note: Here mediator is scoped, and when the handler is instantiated, all its dependencies are generated in the same scope,
    // including DbContext instances, so no multi-thread usage :)