Home > Net >  DI repository which depends on factory with an asynchronous resolve method
DI repository which depends on factory with an asynchronous resolve method

Time:11-14

I have a UserRepository which depends on IDynamoDbClientFactory. The problem is that IDynamoDbClientFactory has one method and it is asynchronous. The ServiceCollections DI framework doesn't allow me to have an async provider. I'm not allowed to change DynamoDbClientFactory as it's in an external library.

How do I deal with that in better way than what I did below using .GetAwaiter().GetResult()?

services.AddSingleton<IDynamoDbClientFactory, DynamoDbClientFactory>();

services.AddSingleton<IUserRepository>(provider =>
{
    var dbClientFactory = provider.GetRequiredService<IDynamoDbClientFactory>();
    var dynamoDb = dbClientFactory.GetClientAsync().GetAwaiter().GetResult();
    return new UserRepository(dynamoDb);
});

I found this similar question which suggests using an adapter in order to hide the async operation from the application code.

I removed the not important code for the sake of simplicity.

public interface IDynamoDbClientFactory
{
    Task<IAmazonDynamoDB> GetClientAsync();
}

public sealed class DynamoDbClientFactory : IDynamoDbClientFactory
{
    private readonly IConfiguration _configuration;
    private IAmazonDynamoDB? _client;
    private DateTime _clientTimeout;

    public DynamoDbClientFactory(IConfiguration configuration)
    {
        _configuration = configuration;
        _clientTimeout = DateTime.Now;
    }

    public async Task<IAmazonDynamoDB> GetClientAsync()
    {
        var cutoff = _clientTimeout - TimeSpan.FromMinutes(5);

        if (_client != null && cutoff > DateTime.Now)
        {
            return _client;
        }

        var assumeRoleArn = _configuration.GetValue<string>("AWS:AssumeRole");

        if (assumeRoleArn == null)
        {
            _client = new AmazonDynamoDBClient();
            _clientTimeout = DateTime.MaxValue;
        }
        else
        {
            var credentials = await GetCredentials(assumeRoleArn);
            _client = new AmazonDynamoDBClient(credentials);
            _clientTimeout = credentials!.Expiration;
        }

        return _client;
    }

    private async Task<Credentials?> GetCredentials(string roleArn)
    {
        using var client = new AmazonSecurityTokenServiceClient();
        var response = await client.AssumeRoleAsync(new AssumeRoleRequest
        {
            RoleArn = roleArn,
            RoleSessionName = "configManagerApi",
        });

        if (response.HttpStatusCode == System.Net.HttpStatusCode.OK)
        {
            return response.Credentials;
        }

        throw new ApplicationException($"Could not assume role {roleArn}");
    }
}
public sealed class UserRepository : IUserRepository
{
    private readonly IAmazonDynamoDB _dynamoDb;

    public UserRepository(IAmazonDynamoDB dynamoDb)
    {
        _dynamoDb = dynamoDb;
    }

    public async Task<UserDto?> GetAsync(string hashKey, string sortKey)
    {
        ...
    }

    public async Task<bool> CreateAsync(UserDto userDto)
    {
        ...
    }

    public async Task<bool> UpdateAsync(UserDto userDto)
    {
        ...
    }

    public async Task<bool> DeleteAsync(string hashKey, string sortKey)
    {
        ...
    }
}

The following is unused.

// The adapter hides the details about GetClientAsync from the application code.
// It wraps the creation and connection of `MyClient` in a `Lazy<T>`,
// which allows the client to be connected just once, independently of in which order the `GetClientAsync`
// method is called, and how many times.
public class DynamoDbClientAdapter : IDisposable
{
    private readonly Lazy<Task<IDynamoDbClientFactory>> _factories;

    public DynamoDbClientAdapter(IConfiguration configuration)
    {
        _factories = new Lazy<Task<IDynamoDbClientFactory>>(async () =>
        {
            var client = new DynamoDbClientFactory(configuration);
            await client.GetClientAsync();
            return client;
        });
    }

    public void Dispose()
    {
        if (_factories.IsValueCreated)
        {
            _factories.Value.Dispose();
        }
    }
}

CodePudding user response:

Try the following:

services.AddSingleton<IDynamoDbClientFactory, DynamoDbClientFactory>();
services.AddScoped<IUserRepository, DynamicClientUserRepositoryAdapter>();

Where DynamicClientUserRepositoryAdapter is an adapter inside your Composition Root that creates the real UserRepository lazily on demand based on its required IAmazonDynamoDB:

public class DynamicClientUserRepositoryAdapter : IUserRepository
{
    private readonly IDynamoDbClientFactory factory;
    private IUserRepository repository;

    public DynamicClientUserRepositoryAdapter(IDynamoDbClientFactory factory) =>
        this.factory = factory;

    private async Task<IUserRepository> GetRepositoryAsync()
    {
        if (this.repository is null)
        {
            var client = await this.factory.GetClientAsync();
            this.repository = new UserRepository(client);
        }
        
        return this.repository;
    }
    
    public async Task<UserDto?> GetAsync(string hashKey, string sortKey)
    {
        var repository = await this.GetRepositoryAsync();
        return await repository.GetAsync(hashKey, sortKey);
    }

    public async Task<bool> CreateAsync(UserDto userDto)
    {
        var repository = await this.GetRepositoryAsync();
        return await repository.CreateAsync(userDto);
    }

    public async Task<bool> UpdateAsync(UserDto userDto)
    {
        var repository = await this.GetRepositoryAsync();
        return await repository.UpdateAsync(userDto);
    }

    public async Task<bool> DeleteAsync(string hashKey, string sortKey)
    {
        var repository = await this.GetRepositoryAsync();
        return await repository.DeleteAsync(hashKey, sortKey);
    }
}

NOTE: I'm assuming that IAmazonDynamoDB is not thread safe, which is why I registered DynamicClientUserRepositoryAdapter as scoped.

The rational of why to design things like this is explained in the answer you already referred to in your question.

  • Related