Нужна асинхронная точка входа для пользовательской конфигурации DbContext - PullRequest
0 голосов
/ 03 апреля 2019

Net Core и EF core не поддерживают токены 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, применила ее

edit: Так в основном, когда делаешь

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 ]

1 голос
/ 03 апреля 2019

Есть проблема Github по этому поводу , так что это определенно неясно. Проблема закрыта, потому что в настоящее время нет встроенной поддержки, другая проблема отслеживает эту .

Оригинальная проблема описывает умный обходной путь, хотя. Прежде всего, 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);
});

Самое сложное - как избавиться от этого соединения.

Умное решение, опубликованное Брайаном Боллом, заключается в реализации интерфейса в 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;, чтобы оно не блокировалось.

Это не такая большая проблема, потому что, согласно документам :

Класс 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>();
        ...
    }
}
0 голосов
/ 03 апреля 2019

Я прошел через аналогичный процесс почти 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 вам потребуется

  1. Внедрить объект IOptions<T>, содержащий подробности строки подключения
  2. Построить или назначитьстрока подключения
  3. Назначение токена доступа
  4. Возвращение объекта SqlConnection
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...