需要异步入口点来自定义DbContext

时间:2019-04-03 16:11:05

标签: c# asp.net-core entity-framework-core

Net核心和EF核心不像完整框架一样直接支持AAD令牌。您可以在SqlConnection上设置访问令牌,这是一个工作问题。检索令牌是异步操作。因此,我需要一个异步的通用入口点。在我的DbContext的构造函数中,我可以注入并执行内容,但是我不能异步执行它,因此效果不够好。

有什么想法吗?谢谢

internal class DbTokenConfig : IDbContextConfig
{
    private readonly ITokenProvider _tokenProvider;

    public DbTokenConfig(ITokenProvider tokenProvider)
    {
        _tokenProvider = tokenProvider;
    }

    public async Task Config(MyDbContext context)
    {
        var conn = context.Database.GetDbConnection() as SqlConnection;
        conn.AccessToken = await _tokenProvider.GetAsync();
    }
}

我需要一个异步入口点才能执行它,这是一般的做法,因此任何注入DbContext的服务都将使其应用

编辑:基本上就是这样做

public class MyCommandHandler : ICommandHandler<MyCommand> 
{
   private readonly DbContext _ctx;

   public MyCommandHandler(DbContext ctx) 
   {
      _ctx = ctx;
   }

   public async Task Handle(MyCommand cmd) 
   {
      await _ctx.Set<Foo>().ToListAsync(); //I want my access token to be applied before it opens connection
   }
}

编辑:有效的解决方案

.AddDbContext<MyDbContext>(b => b.UseSqlServer(Configuration.GetConnectionString("MyDb")))
.AddScoped<DbContext>(p =>
{
    var ctx = new AuthenticationContext("https://login.microsoftonline.com/xxx");
    var result = ctx.AcquireTokenAsync("https://database.windows.net/", new ClientCredential("xxx", "xxx"))
        .ConfigureAwait(false)
        .GetAwaiter()
        .GetResult();

    var db = p.GetService<MyDbContext>();
    ((SqlConnection)db.Database.GetDbConnection()).AccessToken = result.AccessToken;
    return db;
})

只需使密钥可配置,创建抽象等

2 个答案:

答案 0 :(得分:1)

a Github issue about this,所以这并不是很清楚。由于当前没有内置支持different issue tracks this,该问题已解决。

原始问题描述了一种巧妙的解决方法。首先,UseSqlBuilder具有一个接受现有DbConnection的重载。可以使用AAD令牌配置此连接。如果关闭,EF将根据需要打开并关闭它。一个人可以写:

services.AddDbContext<MyDBContext>(options => {
            SqlConnection conn = new SqlConnection(Configuration["ConnectionString"]);
            conn.AccessToken = (new AzureServiceTokenProvider()).GetAccessTokenAsync("https://database.windows.net/")
                        .Result;
            options.UseSqlServer(conn);
});

棘手的部分是如何处理该连接。

Brian Ball发布的一个聪明的解决方案是在DbContext上实现一个接口,并将那个注册为具有工厂功能的控制器使用的服务。 DbContext仍使用其具体类型进行注册。工厂函数获取该上下文,并将AAD令牌设置为其连接:

services.AddDbContext<MyDbContext>(builder => builder.UseSqlServer(connectionString));

services.AddScoped<IMyDbContext>(serviceProvider => {
  //Get the configured context
  var dbContext = serviceProvider.GetRequiredService<MyDbContext>();  

  //And set the AAD token to its connection
  var connection = dbContext.Database.GetDbConnection() as System.Data.SqlClient.SqlConnection;
  if(connection == null) {/*either return dbContext or throw exception, depending on your requirements*/}
  connection.AccessToken = //code used to acquire an access token;

  return dbContext;
});

这样,上下文的生存期仍由EF Core管理。 AddScoped<IMyDbContext>充当获取该上下文并设置AAD令牌的过滤器

下一个问题是如何写//code used to acquire an access token;,使其不会阻塞。

这并不是什么大问题,因为根据the docs

  

AzureServiceTokenProvider类将令牌缓存在内存中,并在到期前从Azure AD检索令牌。

此代码可以提取到工厂方法中,甚至可以作为依赖项注入。

移动目标位置

主要问题是构造函数还不能异步 ,因此构造函数注入不能异步检索令牌。

可以完成的 是注册一个在控制器的异步操作中调用的异步Func<>工厂或服务,而不是在构造函数中调用。假设:

//Let's inject configuration too
//Defaults stolen from AzureServiceTokenProvider's source
public class TokenConfig
{
    public string ConnectionString {get;set;};    
    public string AzureAdInstance {get;set;} = "https://login.microsoftonline.com/";

    public string TennantId{get;set;}
    public string Resource {get;set;}
}

class DbContextWithAddProvider
{
    readonly AzureServiceTokenProvider _provider;
    readonly TokenConfig _config;
    readonly IServiceProvider _svcProvider;

    public DbContextWithAddProvider(IServiceProvider svcProvider, IOption<TokenConfig> config)
    {
        _config=config;
        _provider=new AzureServiceTokenProvider(config.ConnectionString,config.AzureAdInstance);
        _svcProvider=svcProvider;
    }

    public async Task<T> GetContextAsync<T>() where T:DbContext
    {
        var token=await _provider.GetAccessTokenAsync(_config.Resource,_config.TennantId);
        var dbContext = _svcProvider.GetRequiredService<T>();  

        var connection = dbContext.Database.GetDbConnection() as System.Data.SqlClient.SqlConnection;
        connection.AccessToken = token;
        return dbContext;
    }
}

此服务应注册为单例,因为除了缓存令牌(我们要做要保留的令牌)之外,它不保留任何状态。

现在可以将其注入构造函数中,并在异步操作中调用:

class MyController:Controller
{
    DbContextWithAddProvider _ctxProvider;

    public MyController(DbContextWithAddProvider ctxProvider)
    {
        _ctxProvider=ctxProvider;
    }

    public async Task<IActionResult> Get()
    {
        var dbCtx=await _ctxProvider.GetContextAsync<MyDbContext>();
        ...
    }
}

答案 1 :(得分:0)

大约2年前,我经历了类似的过程,在我的上一份工作中,我们决定为DbContext对象实施凭据的动态刷新,该对象是在应用程序初次启动时从Key Vault检索到的,然后缓存凭据,如果连接失败,则假定凭据已更改或过期,并且它将再次检索它们并刷新SqlConnection对象(快乐路径方案,很显然,还有其他原因导致连接到失败)。

那么,在这种情况下,问题是IServiceCollection没有可用的异步方法,该方法允许您调用异步委托,因此在使用异步逻辑注册服务时必须使用.Result先决条件。

您可以做的是使用访问令牌创建一个SqlConnection对象,并将其传递给SqlServerDbContextOptionsExtensions.UseSqlServerAddDbContext<T>服务注册内的ConfigureServices。这样可以确保创建的每个DbContext都将分配一个访问令牌,并且默认情况下,它的作用域为每个请求都将有一个新的令牌。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddScoped<ITokenProvider, TokenProvider>();
    services.AddScoped<ISqlConnectionProvider, SqlConnectionProvider>();

    services.AddDbContext<TestDbContext>((provider, options) =>
    {
        var connectionTokenProvider = provider.GetService<ITokenProvider>();
        var sqlConnectionProvider = provider.GetService<ISqlConnectionProvider>();

        var accessToken = connectionTokenProvider.GetAsync().Result; // Yes, I consider this to be less than elegant, but marking this delegate as async & awaiting would result in a race condition.
        var sqlConnection = sqlConnectionProvider.CreateSqlConnection(accessToken);

        options.UseSqlServer(sqlConnection);
    });
}

ISqlConnectionProvider的界面是

internal interface ISqlConnectionProvider
{
    SqlConnection CreateSqlConnection(string accessToken);
}

在实施ISqlConnectionProvider时,您必须

  1. 注入一个IOptions<T>对象,其中包含连接字符串的详细信息
  2. 构建或分配连接字符串
  3. 分配访问令牌
  4. 返回SqlConnection对象