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;
})
只需使密钥可配置,创建抽象等
答案 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.UseSqlServer
中AddDbContext<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
时,您必须
IOptions<T>
对象,其中包含连接字符串的详细信息SqlConnection
对象