4
\$\begingroup\$

The goal is to construct a DbContext with a connection that uses an access token. The access token is acquired with ADAL (Active Directory Authentication Library).

The problem is that acquiring an access token is an async operation. Luckily, ADAL uses ConfigureAwait(false) for its async calls, so it should be safe to do sync-over-async without risking deadlocks.

Full code is provided below. The focus for this question is on the sync-over-async code and method of registering the DbContext, but remarks for the other code are welcome too.

Container configuration

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;
}

Token provider

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);
    }
}

Connection factory

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)
    {
    }
}
\$\endgroup\$
2
  • 2
    \$\begingroup\$ It seems your TokenProvider class has a dependency on a X509Certificate2. Is this intended to be lazy loaded? I would probably generate the certificate inside your CreateContainer method and add that to the container as a singleton. This way if it fails, it "fails fast" on startup, and potentially other future classes have access to that certificate. \$\endgroup\$
    – Brad M
    Feb 5, 2018 at 14:20
  • \$\begingroup\$ @BradM Good point, the certificate will eventually be used by other parts of the application, so indeed it makes sense to retrieve it in the container. Thanks! \$\endgroup\$
    – Stijn
    Feb 5, 2018 at 14:43

1 Answer 1

3
\$\begingroup\$

If you don't mind me asking, any reason you want to register the DbContext directly? I know .NET Core seems to want you to do that, but I don't like IoC containers deciding when to leave open or close my DB connections. Insetead, I'd just do IFooDbContextFactory and have that use a "CreateAsync" method. This factory can take your IAppSettings or IDbContextSettings or something to get the connection string. Then you can call the provider async just fine and in your specific code you'd just do:

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);
       }
    }
}
\$\endgroup\$
5
  • \$\begingroup\$ I don't like IoC containers deciding when to leave open or close my DB connections and they don't if you tell them not to. Four ways to dispose IDisposables in ASP.NET Core or with Autofac you can mark a dependency as ExternallyOwned. \$\endgroup\$
    – t3chb0t
    Apr 11, 2018 at 8:07
  • 1
    \$\begingroup\$ People can make mistakes with this configuration. With a factory and using statement there is no question when this gets disposed and you don't have to worry about other applications using this code configure it incorrectly😓 \$\endgroup\$ Apr 11, 2018 at 20:28
  • \$\begingroup\$ This is a very good argument... to kill every idea because people can make mistakes with just anything :-| With a properly configured DI container I have less worries then by maintaining my own solutions. I like the smiley ;-) \$\endgroup\$
    – t3chb0t
    Apr 12, 2018 at 12:19
  • \$\begingroup\$ I just like having control over my database connections. I know when I'm done so I kill it as soon as I'm done with it. \$\endgroup\$ Apr 13, 2018 at 12:01
  • \$\begingroup\$ Plus if you are doing calls in parallel you'd need multiple instances of dbcontext since you can't share them. Thus you'd need a factory for that anyway. Plus you can have more control over the configuration on it. \$\endgroup\$ Apr 13, 2018 at 12:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.