【问题标题】:How to resolve specific DbContext based on query argument in GraphQL?如何根据 GraphQL 中的查询参数解析特定的 DbContext?
【发布时间】:2021-06-22 14:38:39
【问题描述】:

我想使用 GraphQL 和 Entity Framework Core 来查询多个数据库。每个数据库都链接到一个被许可人,因此所有查询都会收到一个查询参数licenseeId。现在我需要指示 DI 在服务请求 DbContext 时(例如通过构造函数参数或服务定位器)以某种方式基于 licenseeId 解析 DbContext。这真的可能吗?

以下是当前实施的相关部分:

存储库类

public MyRepository
{
    public MyDbContext DbContext { get; set; }

    public MyRepository(MyDbContext dbContext)
    {
    }
}

查询类

public class MainQuery : ObjectGraphType
{
    public MainQuery()
    {
        objectGraph.FieldAsync<ListGraphType<MyModel>>("items",
            arguments: new QueryArguments(
                new QueryArgument<NonNullGraphType<GuidGraphType>> { Name = "licenseeId" }
            ),
            resolve: async context => {
                var licenseeId = resolveFieldContext.GetArgument<Guid>("licenseeId");

                // *1, create dbContext based on licenseeId manually via factory
                var dbContext = ...;

                var repository = resolveFieldContext.ResolveServices.GetRequiredService<CucumberRepository>();

                // *2, assign context manually
                repository.DbContext = dbContext;

                return await repository.GetAllAsync();
            });
    }
}

如您所见,我目前需要使用工厂 (*1) 手动创建 DbContext,然后将此实例分配给存储库属性 (*2)。

我想在这里使用纯 DI。我的想法是在 Startup 中以某种方式使用服务工厂

services.AddDbContext<MyDbContext>((serviceProvider, dbContextOptionsBuilder) => {
  var query = serviceProvider.GetRequiredService<?>();
  
  var connectionString = $"...;Catalog=MyDatabase_{query.GetLicenseeId()}";

  dbContextOptionsBuilder.UseSqlServer(connectionString, ...);
});

这将使我能够像这样定义存储库类

public MyRepository
{
    public MyRepository(MyDbContext dbContext)
    {
    }
}

在解析回调中我可以简单地写

objectGraph.FieldAsync<ListGraphType<MyModel>>(...,
    resolve: async context => {
        var repository = resolveFieldContext.ResolveServices.GetRequiredService<MyRepository>();

        return await repository.GetAllAsync();
    });

这是个好主意吗?

【问题讨论】:

  • 存储库模式在这里是个坏主意。
  • @SvyatoslavDanyliv 是的,我知道,但我无法改变这一点。让我们专注于实际问题;)
  • @SvyatoslavDanyliv 您指出存储库模式在这里是一个坏主意。你有这个说法的参考吗?是不是因为 GraphQL 无法优化查询?
  • 使用 EF 总是坏主意。我的意思是通用存储库模式。例如:rob.conery.io/2014/03/04/…

标签: c# dependency-injection graphql entity-framework-core


【解决方案1】:

我提出以下方法(假设许可是 int)。请注意,缓存 DbContextOptions 很重要,因为 EF Core 会缓存基于此对象的 LINQ 查询。

public interface ILicenseOptionFactory
{
    public DbContextOptions GetOptions(int licenseId);
}

public class LicenseOptionFactory : ILicenseOptionFactory
{
    private ConcurrentDictionary<int, DbContextOptions> _options = new ConcurrentDictionary<int, DbContextOptions>();

    public DbContextOptions GetOptions(int licenseId)
    {
        var options = _options.GetOrAdd(licenseId, lid =>
        {
            // any other way how to retrieve connections string based on licenseId
            string cs;
            switch (lid)
            {
                case 0:
                    cs = "connectionString0";
                    break;
                case 1:
                    cs = "connectionString1";
                    break;
                default:
                    throw new Exception($"Invalid licenseId: {lid}");
            }

            return new DbContextOptionsBuilder().UseSqlServer(cs).Options;
        });

        return options;
    }
}
public interface ILicenseConnectionFactory<TContext> : IDisposable, IAsyncDisposable
    where TContext : DbContext
{
    TContext GetContext(int licenseId);
}

public class LicenseConnectionFactory<TContext> : ILicenseConnectionFactory<TContext> where TContext : DbContext
{
    private readonly ILicenseOptionFactory _optionFactory;
    private static Dictionary<int, TContext> _contexts;

    public LicenseConnectionFactory(ILicenseOptionFactory optionFactory)
    {
        _optionFactory = optionFactory;
    }

    public TContext GetContext(int licenseId)
    {
        _contexts ??= new Dictionary<int, TContext>();
        if (_contexts.TryGetValue(licenseId, out var ctx))
            return ctx;

        var options = _optionFactory.GetOptions(licenseId);
        ctx = (TContext)Activator.CreateInstance(typeof(TContext), options);
        _contexts.Add(licenseId, ctx);
        return ctx;
    }

    public void Dispose()
    {
        if (_contexts == null)
            return;

        foreach (var dbContext in _contexts.Values)
        {
            dbContext.Dispose();   
        }

        _contexts = null;
    }

    public async ValueTask DisposeAsync()
    {
        if (_contexts == null)
            return;

        foreach (var dbContext in _contexts.Values)
        {
            await dbContext.DisposeAsync();   
        }

        _contexts = null;
    }
}

注册示例,注意SingletonScoped是这些服务所必需的:

var serviceCollection = new ServiceCollection();

serviceCollection.AddSingleton<ILicenseOptionFactory, LicenseOptionFactory>();
serviceCollection
    .AddScoped<ILicenseConnectionFactory<MyDbContext>, LicenseConnectionFactory<MyDbContext>>();

存储库示例(但最好完全删除此抽象)

public class MyRepository
{
    private readonly ILicenseConnectionFactory<MyDbContext> _factory;
    private MyDbContext _dbContext;

    public MyDbContext DbContext
    {
        get => _dbContext ?? throw new Exception("Repository is not initialized.");
    }

    public MyRepository(ILicenseConnectionFactory<MyDbContext> factory)
    {
        _factory = factory;
    }

    public void SetLicenseId(int licnseId)
    {
        _dbContext = _factory.GetContext(licnseId);
    }
}

以及最终用法。我不知道 resolveFieldContext 是什么是否可以通过 DI 解决 - 您可以在不使用 SetLicenseId 的情况下简化存储库初始化。

public class MainQuery : ObjectGraphType
{
    public MainQuery()
    {
        objectGraph.FieldAsync<ListGraphType<MyModel>>("items",
            arguments: new QueryArguments(
                new QueryArgument<NonNullGraphType<GuidGraphType>> { Name = "licenseeId" }
            ),
            resolve: async context => {
                var licenseeId = resolveFieldContext.GetArgument<Guid>("licenseeId");

                // *1, create dbContext based on licenseeId manually via factory
                var repository = resolveFieldContext.ResolveServices.GetRequiredService<CucumberRepository>();

                // *2, assign context manually
                repository.SetLicenseId(licenseeId);

                return await repository.GetAllAsync();
            });
    }
}

【讨论】:

  • 这基本上是我已经拥有的,但你的代码更复杂,+1。
【解决方案2】:

在做了更多研究之后,我得出的结论是,仅通过 AddDbContext / 服务工厂解析 DbContext 是有风险的,而且几乎是个坏主意,因为这需要将 DbContext 注册为临时服务。这带来了副作用,其中一些提到了here,请参阅 cmets,其中一个 cmets 链接到 https://github.com/aspnet/DependencyInjection/issues/456,其中还讨论了瞬态问题。

DbContext 需要是临时的,因为 GraphQL 查询可以包含多个字段,因此可以包含多个被许可人 ID,例如

{
  addressTypes(licenseeId: "02050fd1-b312-4571-baaf-4ee40a54eae5") {
    id,
    name,
    timestamp
  },
  banks(licenseeId: "77047070-3CBE-4E58-9805-1EA8DA621C74") {
    id,
    accountNumber
  }
}

因此在同一个HttpRequest 请求中,DbContext 需要针对字段addressTypesbanks 进行不同的解析,因此它必须是瞬态的。但这也意味着其他代码部分需要知道DbContext 现在是瞬态的。关于最初的问题和存储库模式 - 现在每个存储库实例都会注入一个新的 DbContext 实例,这是(未经测试的)一个坏主意。

无论如何,我在挖掘 GraphQL 源代码后发现的选项之一是从 HttpContext 检索当前查询,用 IDocumentBuilder 解析它,用 IDocumentValidator 验证它,最后构建字段参数.所有这些都是由IDocumentExecutor 中的 GraphQL 包实现的,是的,它非常复杂。我不建议这样做,因为您需要复制大部分部分并对其进行自定义。

另一个选项是使用FieldMiddleware 将当前的被许可人 ID 存储到提供程序实例中。然后在AddDbContext 中使用提供程序来构建连接字符串并返回瞬态DbContext 实例。下面是一些sn-ps的代码:

要记住的提供程序类LicenseeId

public class LicenseeIdProvider
{
    public Guid LicenseeId { get; set; }
}

services.AddScoped<LicenseeIdProvider>();

带有 FieldMiddleware 的架构可在提供程序实例中记住 licenseeId

public class DefaultSchema : Schema
{
    public DefaultSchema(IServiceProvider serviceProvider)
        : base(serviceProvider)
    {
        ...

        FieldMiddleware.Use(next =>
        {
            return resolveFieldContext =>
            {
                if (resolveFieldContext.HasArgument("LicenseeId"))
                {
                    var licenseeId = resolveFieldContext.GetArgument<Guid>("LicenseeId");
                    var licenseeIdProvider= resolveFieldContext.RequestServices.GetService<LicenseeIdProvider>();

                    licenseeIdProvider.LicenseeId = licenseeId;
                }

                var result = next(resolveFieldContext);

                return result;
            };
        });
    }
}

向服务工厂注册 DbContext 以根据 licenseeId 创建实例

services.AddDbContext<MyDbContext>((sp, dbContextOptionsBuilder) =>
{
    var licenseeIdProvider = sp.GetService<LicenseeIdProvider>();
    var licenseeId = licenseeIdProvider.LicenseeId;

    var connectionString = $"...;Catalog=MyDatabase_{licenseeId}";

    dbContextOptionsBuilder.UseSqlServer(connectionString, ...);
}, ServiceLifetime.Transient);

【讨论】:

    猜你喜欢
    • 2019-01-09
    • 1970-01-01
    • 2022-07-24
    • 2020-01-04
    • 1970-01-01
    • 2019-09-26
    • 1970-01-01
    • 2018-12-21
    • 2018-06-14
    相关资源
    最近更新 更多