Инъекции DbContext с асинхронным конструктор зависимостей


Цель состоит в том, чтобы построить DbContext с подключение, использующее маркер доступа. Маркер доступа приобретается с Адал (библиотеки проверки подлинности Active Directory с).

Проблема в том, что приобретая маркер доступа является асинхронной операции. К счастью, Адал использует ConfigureAwait(false) для асинхронных вызовов, поэтому она должна быть безопасной, не-по-асинхронный, не рискуя взаимоблокировок.

Полный код приведен ниже. Фокус на этот вопрос на синхронизации через асинхронный код и способ регистрации DbContextно замечания других программ приветствуются тоже.

Конфигурация контейнера

private Container CreateContainer(AppSettings.Root appSettings)
{
    var container = new Container();
    /*
     * AsyncScopedLifestyle is recommended for Web API applications.
     * https://simpleinjector.readthedocs.io/en/latest/lifetimes.html#asyncscoped-vs-webrequest
     */
    container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();

    container.Register<ITokenProvider>(
        () => new TokenProvider(appSettings.Azure.TenantId, appSettings.Azure.ClientId, appSettings.Azure.ClientCertificateSDN),
        Lifestyle.Singleton
        );

    container.Register<IConnectionFactory, ConnectionFactory>(Lifestyle.Singleton);

    container.Register(
        () => new FooDbContext(container.GetInstance<IConnectionFactory>().CreateConnection(appSettings.Foo.Database.ConnectionString)),
        Lifestyle.Scoped
        );

    /*
     * Calling Verify is not required, but is highly encouraged.
     * https://simpleinjector.readthedocs.io/en/latest/using.html#verifying-the-container-s-configuration
     */
    container.Verify();

    return container;
}

Поставщик маркер

public interface ITokenProvider
{
    Task<string> GetDatabaseAccessToken();
}

public sealed class TokenProvider
    : ITokenProvider
{
    /*
     * Asynchronous calls have ConfigureAwait(false) to allow sync-over-async without risking a deadlock, e.g. when a token is required during construction.
     * ADAL supports this as per https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/issues/504.
     */

    private readonly string tenantId;

    private readonly string clientId;

    private readonly X509Certificate2 clientCertificate;

    public TokenProvider(
        string tenantId,
        string clientId,
        string clientCertificateSDN
        )
    {
        this.tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId));
        this.clientId = clientId ?? throw new ArgumentNullException(nameof(clientId));

        using (var certificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser))
        {
            certificateStore.Open(OpenFlags.ReadOnly);
            this.clientCertificate = certificateStore.Certificates
                .Find(X509FindType.FindBySubjectDistinguishedName, clientCertificateSDN ?? throw new ArgumentNullException(nameof(clientCertificateSDN)), false)
                [0];
        }
    }

    private async Task<string> GetAccessToken(string resource)
    {
        var authenticationContext = new AuthenticationContext($"https://login.microsoftonline.com/{tenantId}");
        var clientAssertionCertificate = new ClientAssertionCertificate(this.clientId, this.clientCertificate);
        var authenticationResult = await authenticationContext.AcquireTokenAsync(resource, clientAssertionCertificate).ConfigureAwait(false);

        return authenticationResult.AccessToken;
    }

    public async Task<string> GetDatabaseAccessToken()
    {
        return await this.GetAccessToken("https://database.windows.net/").ConfigureAwait(false);
    }
}

Фабрика связи

public interface IConnectionFactory
{
    SqlConnection CreateConnection(string connectionString);
}

public sealed class ConnectionFactory
    : IConnectionFactory
{
    private readonly ITokenProvider tokenProvider;

    public ConnectionFactory(ITokenProvider tokenProvider)
    {
        this.tokenProvider = tokenProvider ?? throw new ArgumentNullException(nameof(tokenProvider));
    }

    public SqlConnection CreateConnection(string connectionString)
    {
        return new SqlConnection(connectionString)
        {
            AccessToken = this.tokenProvider.GetDatabaseAccessToken().Result
        };
    }
}

DbContext можно

public partial class FooDbContext
    : DbContext
{
    public FooDbContext(SqlConnection connection)
        : base(connection, false)
    {
    }
}


Комментарии
1 ответ

Если вы не возражаете, что я спрашиваю, по какой причине вы хотите зарегистрировать DbContext, который напрямую? Я знаю .Объем ядра, кажется, хочет, чтобы вы это сделали, но мне не нравится МОК контейнеров решать, когда оставить открытым или закрыть соединения с БД. Insetead, я бы просто сделала IFooDbContextFactory и использовать "CreateAsync" способ. Этот завод может принять ваш IAppSettings или IDbContextSettings или что-то получить строку подключения. Затем вы можете позвонить поставщику асинхронного просто отлично, и в вашей конкретной кода Вы бы просто сделать:

public class FooDbContextFactory : IFooDbContextFactory
{
private readonly IFooDbContextSettings _settings;
private readonly ITokenProvider _tokenProvider;

public FooDbContextFactory(IFooDbContextSettings settings, ITokenProvider tokenProvider)
{
_settings = settings;
_tokenProvider = tokenProvider;
}

public async Task<FooDbContext> CreateAsync()
{
var token = await _tokenPRovider.GetToken(...).ConfigureAwait(false);
return new FooDbContext(...);
}
}

public class SomeRepository
{
private reaonly IFooDbContextFactory _factory;
public SomeRepository(IFooDbContextFactory factory)
{
_factory = factory;
}

public async Task<ICollection<SomeEntity>> GetSomeList()
{
using (var context = await _factory.CreateAsync().ConfigureAwait(false))
{
return await context.Somes().ToListAsync().ConfigureAwait(false);
}
}
}

2
ответ дан 9 февраля 2018 в 09:02 Источник Поделиться