paint-brush
Asynchronous Initialization in C#: Overcoming Constructor Limitationsby@fairday
20,449 reads
20,449 reads

Asynchronous Initialization in C#: Overcoming Constructor Limitations

by AlekseiDecember 27th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Solutions to enable asynchronous initialization in C# constructors, overcoming limitations for efficient code execution.
featured image - Asynchronous Initialization in C#: Overcoming Constructor Limitations
Aleksei HackerNoon profile picture

What is the problem?

Async await syntax gives us the most straightforward way to write asynchronous code.

However, such construction cannot be used in certain scenarios. For instance, using async await keywords is prohibited in constructors.


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.


And if we simply take the following class, build the library containing it, and then look at disassembled code.

public class MyObject
{
    public MyObject()
    {
        //diligently initializing
    }
}


We will see the following line.

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).


So, technically, it calls some method (constructor) on the already existing object to initialize it.

Could it be made asynchronous hypothetically?

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.


However, even without language in-built such a feature, we can solve this problem.

Ways to overcome

There are plenty of approaches that could be used to achieve the asynchronous initialization.

Async to sync

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.“

Async factory

A quite standard way for solving this problem is to use factories.

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.

Async initialization

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.

The using of this approach is very usual both in cases with a self-instantiating way of objects and IoC container.

var myBestService = new MyBestService();
await myBestService.DoSomethingAsync();

However, this approach has several drawbacks:

  • Lack of control over the initialization process. For instance, we cannot simply pass CancellationToken.
  • It gives the caller an incompletely instantiated object for a while. Although it does not seem a problem in general, it could lead to more complicated debugging if the initialization operation takes too long. Also, calling operation timeout might be less than the required amount of time for this particular object which apparently will lead to issues during the execution.

Real example

For applications to access Azure services like storage, key vault, or cognitive services, authenticating with Azure is mandatory, regardless of their deployment environment - on Azure, on-premises, or locally during development.


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


That’s what Microsoft says about it


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 and DefaultAzureCredential will automatically detect and use that authentication method. The use of DefaultAzureCredential should be preferred over manually coding conditional logic or feature flags to use different authentication methods in different environments.


So, in order to securely store PII (Personally Identifiable Information) which can be either Full name, Phone Number, Credit card number, or Social security number, we would like to encrypt it before storing it somewhere and provide viewing original value with only limited access.


One of the possible ways is to use AzureKeyVault API for encryption and decryption.


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

Conclusion

In this article, I covered several approaches for asynchronous object initialisation. Understanding requirements is the key to choosing the right approach. You can find the source code used to describe these patterns in this repository.


https://github.com/alex-popov-stenn/CSharpAsyncInitPatterns/tree/main


Thank you for reading! See you next time!