1

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();
        }
    }
}
nop
  • 4,711
  • 6
  • 32
  • 93
  • The repo pattern is useful when it _removes duplicated ORM search code_ (e.g. `GetAllSeniorsWithCovid()`) from the codebase not act as a simple CRUD interface, otherwise it could write a single CRUD repo generic handling all types. –  Nov 14 '22 at 07:43
  • 2
    Can you show a little bit more code? Why is `GetClientAsync` async? – ProgrammingLlama Nov 14 '22 at 07:45
  • Can't you pass a Task into your UserTradeRepository constructor? – Klamsi Nov 14 '22 at 07:45
  • @ProgrammingLlama, edited the question with that code. – nop Nov 14 '22 at 07:52
  • Why doesn't the similar question you linked to provide you with an answer? It's unclear to me what you're still missing. – Steven Nov 14 '22 at 07:52
  • @Steven, I don't know how to wrap things up. – nop Nov 14 '22 at 07:53
  • If there some scenario where you have something to await in your `GetClientAsync` method, because at the moment it's running 100% synchronously, and needn't return a `Task` or be `async`. – ProgrammingLlama Nov 14 '22 at 07:54
  • @ProgrammingLlama, I'm not allowed to change that method. It should remain like that. (it's an external library). – nop Nov 14 '22 at 07:55
  • Ah, gotcha. That's annoying. – ProgrammingLlama Nov 14 '22 at 07:56

1 Answers1

1

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.

Steven
  • 166,672
  • 24
  • 332
  • 435