visit
There are many reasons why it is not possible in the existing language version. Probably, one of the obvious ones is that async methods would return Task
or Task<T>
to be able to properly handle exceptions without crashing the process, and trivially to be able to wait until the async operation is completed.
public class MyObject
{
public MyObject()
{
//diligently initializing
}
}
instance void [System.Runtime]System.Object::.ctor()
where instance
the keyword indicates that the method is an instance method, meaning it is called on an instance of the class, not on the class itself (which would be indicated by the static keyword).
public class MyObject
{
public async MyObject()
{
await InitializeAsync();
}
}
Of course, yes, everything is possible in our world; it only depends on the price and whether it will solve any real problem without introducing new complications and tradeoffs.
I believe that C# language developers have had discussions about it many times, and they clearly understand the feasibility and meaningfulness of incorporating this change.
If we are only allowed to have sync operation in constructors, we can intuitively apply the async to sync approach.
public class MyBestObject
{
public MyBestObject()
{
InitializeAsync().GetAwaiter().GetResult();
}
private async Task InitializeAsync()
{
//diligently initializing
await Task.Delay(100);
}
}
This approach is not so bad if there is no synchronization context in place and the async operation is relatively fast, but in general, it is not a recommended practice since it is based on a lot of assumptions about the executing environment and details of the async operation. It leads to inefficient resource consumption, sudden deadlocks in UI applications, and a common violation of the async programming idea of “async all the way.“
public class MyBestService
{
public async Task InitializeAsync()
{
//diligently initializing
await Task.Delay(100);
}
}
public interface IMyBestServiceFactory
{
Task<MyBestService> CreateAsync(CancellationToken cancellationToken);
}
public sealed class MyBestServiceFactory : IMyBestServiceFactory
{
public MyBestServiceFactory()
{
}
public async Task<MyBestService> CreateAsync(CancellationToken cancellationToken)
{
var service = new MyBestService();
await service.InitializeAsync(cancellationToken);
return service;
}
}
We could either use the static method in MyBestService
class or even specify a dedicated factory for that purpose. The second option is a little bit more compatible with the Dependency Injection pattern since you can request IMyBestServiceFactory
in any class and then just call CreateAsync
the method.
The main drawback of this approach is additional coupling since you (any class uses IMyBestServiceFactory
) need to control the lifetime of a newly created object.
Additionally, it requires adapting solutions that use reflection (Activate.CreateInstace
) or expressions (Expression.New
) to create and initialize instances.
We could do the following trick to avoid problems with the Async Factory pattern.
public class MyBestService
{
private readonly Task _initializeTask;
public MyBestService()
{
_initializeTask = InitializeAsync();
}
public async Task DoSomethingAsync()
{
await _initializeTask;
// Do something async
}
private async Task InitializeAsync()
{
//diligently initializing
await Task.Delay(100);
}
}
As you can see, we are beginning asynchronous initialization in the constructor and saving the reference to the started task. Then, before doing any meaningful operation, we check that _initializeTask
is completed by simply awaiting it.
var myBestService = new MyBestService();
await myBestService.DoSomethingAsync();
However, this approach has several drawbacks:
CancellationToken
.
There are several ways it could be achieved, and one of the most secure ones is utilising the approach with the DefaultAzureCredential
class in .NET
The
DefaultAzureCredential
class provided by the Azure SDK allows apps to use different authentication methods depending on the environment they're run in. This allows apps to be promoted from local development to test environments to production without code changes. You configure the appropriate authentication method for each environment andDefaultAzureCredential
will automatically detect and use that authentication method. The use ofDefaultAzureCredential
should be preferred over manually coding conditional logic or feature flags to use different authentication methods in different environments.
Although there is no huge impact of creating DefaultAzureCredential
every time, to reduce latency and improve efficiency, it is preferable to have a single instance of it for the whole application.
So, to prepare CryptographyClient
for encryption and decryption API, we need to have the following lines
var tokenCredential = new DefaultAzureCredential();
var keyClient = new KeyClient(new Uri(_configuration.KeyVaultUri), tokenCredential);
KeyVaultKey key = await keyClient.GetKeyAsync(_configuration.KeyName);
_cryptographyClient = new CryptographyClient(key.Id, tokenCredential);
To avoid running them on every request, we can utilise Async initialisation described above
internal sealed class DefaultAzureVaultAdapter : IAzureVaultAdapter
{
private readonly AzureVaultConfiguration _configuration;
private EncryptionAlgorithm _encryptionAlgorithm = EncryptionAlgorithm.RsaOaep256;
private CryptographyClient _cryptographyClient = null!;
private Task _initializationTask = null!;
public DefaultAzureVaultAdapter(AzureVaultConfiguration configuration)
{
_configuration = configuration;
_initializationTask = InitializeAsync();
}
public async Task<string> EncryptAsync(string value, CancellationToken cancellationToken)
{
await _initializationTask;
byte[] inputAsByteArray = Encoding.UTF8.GetBytes(value);
EncryptResult encryptResult = await _cryptographyClient.EncryptAsync(_encryptionAlgorithm, inputAsByteArray, cancellationToken);
return Convert.ToBase64String(encryptResult.Ciphertext);
}
public async Task<string> DecryptAsync(string value, CancellationToken cancellationToken)
{
await _initializationTask;
byte[] inputAsByteArray = Convert.FromBase64String(value);
DecryptResult decryptResult = await _cryptographyClient.DecryptAsync(_encryptionAlgorithm, inputAsByteArray, cancellationToken);
return Encoding.Default.GetString(decryptResult.Plaintext);
}
private async Task InitializeAsync()
{
if (_cryptographyClient is not null)
return;
var tokenCredential = new DefaultAzureCredential();
var keyClient = new KeyClient(new Uri(_configuration.KeyVaultUri), tokenCredential);
KeyVaultKey key = await keyClient.GetKeyAsync(_configuration.KeyName);
_cryptographyClient = new CryptographyClient(key.Id, tokenCredential);
}
}
And finally, register our AzureVaultAdapter
with a singleton in IoC container.
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddAzureVaultAdapter(this IServiceCollection services)
{
services.TryAddSingleton<IAzureVaultAdapter, DefaultAzureVaultAdapter>();
return services;
}
}
//github.com/alex-popov-stenn/CSharpAsyncInitPatterns/tree/main