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)
{
}
}
TokenProvider
class has a dependency on aX509Certificate2
. Is this intended to be lazy loaded? I would probably generate the certificate inside yourCreateContainer
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\$