dependency injection multiple interface for the same storage (with different connectionString)

Viewed 844

I want to add two or more(depends on how many azure storage container i want to add to my app) services in Startup.cs

My appsettings.json:

"AzureBlobStorageConfiguration": {
    "Storages": {
      "Storage1": {
        "StorageName": "Storage1",
        "ConnString": "connString",
        "AzureBlobContainerName": "containerName"
      },
      "Storage2": {
        "StorageName": "Storage2",
        "ConnString": "connString",
        "AzureBlobContainerName": "containerName"
      },
      "Storage3": {
        "StorageName": "Storage3",
        "ConnString": "connString",
        "AzureBlobContainerName": "containerName"
      }
    }

Next in Startup.cs im adding service with method:

 public static IServiceCollection AddAzureStorage1(this IServiceCollection services, IConfiguration configuration)
        {
            var options = new ABlobStorageConfigurationOptionsDTO();
            configuration.GetSection("AzureBlobStorageConfiguration").GetSection("Storages").GetSection("Storage1").Bind(options);

            services.AddTransient<IAzureBlobStorage1, AzureBlobStorage1>(isp =>
            {
                var client = new BlobServiceClient(options.ConnString);
                var container = client.GetBlobContainerClient(options.AzureBlobContainerName);
                var containerName = options.AzureBlobContainerName;
                var storageName = options.StorageName;

                return new AzureBlobStorage1(container, containerName, storageName);
            }
                );
            return services;
        }

My IAzureBlobStorage1 looks like:

public interface IAzureBlobStorage1
    {
        string AzureBlobContainerName { get; }
        string StorageName { get; }

        public Task<Stream> DownloadStreamAsyns(string fileName);

        public Task Upload(string fileId, Stream stream);

    }

and AzureBlobStorage1 :

    public class AzureBlobStorage1 : IAzureBlobStorage1
    {
        private BlobContainerClient _client;
        private string _containerName;
        private string _storageName;
        public string StorageName => _storageName;
        public string AzureBlobContainerName => _containerName;

        public AzureBlobStorage1(BlobContainerClient client, string containerName, string storageName)
        {
            _client = client;
            _containerName = containerName;
            _storageName = storageName;
        }


        public async Task<Stream> DownloadStreamAsyns(string fileName)
        {
            return await _client.GetBlobClient(fileName).OpenReadAsync();
        }

        public async Task Upload(string fileId, Stream stream)
        {
            await _client.GetBlobClient(fileId).UploadAsync(stream);
        }
    }

After this i can injection interface in my constructor controller class :

        public Controller(IAzureBlobStorage1 azureStorage)
        {
            _azureStorage1 = azureStorage;

        }

But if i want to add many storages (i have 3 in appsetings.json) i have to:

  1. Create interface IAzureBlobStorage2 (looking the same like IAzureBlobStorage1 - only name change)

  2. Create class AzureBlobStorage2 (looking the same like AzureBlobStorage1 - only name change)

  3. copy-paste method with changed class names

public static IServiceCollection AddAzureStorage2(this IServiceCollection services, IConfiguration configuration)
        {
            var options = new ABlobStorageConfigurationOptionsDTO();
            configuration.GetSection("AzureBlobStorageConfiguration").GetSection("Storages").GetSection("Storage2").Bind(options);

            services.AddTransient<IAzureBlobStorage2, AzureBlobStorage2>(isp =>
            {
                var client = new BlobServiceClient(options.ConnString);
                var container = client.GetBlobContainerClient(options.AzureBlobContainerName);
                var containerName = options.AzureBlobContainerName;
                var storageName = options.StorageName;

                return new AzureBlobStorage2(container, containerName, storageName);
            }
                );
            return services;
        }

Now i can get it in controller by

  public Controller(IAzureBlobStorage2 azureStorage)
        {
            _azureStorage2 = azureStorage;

        }

If i want add my third storage i need to copy-paste third time my code.

For me this solution looks very bad and im thinking how i can resolve it and make my code clean.

1 Answers

Unsure if this is a best practice or not, but you could design a named service provider, maybe? Either that, or you could just a generic parameter to differentiate them, but that generic parameter wouldn't mean much except as a way to differentiate..

Anyways, here's a really basic implementation using some kind of named provider?:

public interface INamedService {
    string Identifier { get; }
}

public interface IAzureBlobStorage : INamedService
{
    string AzureBlobContainerName { get; }
    string StorageName { get; }
    Task<Stream> DownloadStreamAsyns(string fileName);
    Task Upload(string fileId, Stream stream);
}

public class NamedServiceProvider<T> 
    where T : INamedService
{
    readonly IReadOnlyDictionary<string, T> Instances;

    public NamedServiceProvider(
        IEnumerable<T> instances) 
    {
        Instances = instances?.ToDictionary(x => x.Identifier) ??
            throw new ArgumentNullException(nameof(instances));
    }

    public bool TryGetInstance(string identifier, out T instance) {
        return Instances.TryGetValue(identifier, out instance);
    }
}

public class AzureBlobStorage : IAzureBlobStorage
{
    public string Identifier { get; }
    private BlobContainerClient _client;
    private string _containerName;
    private string _storageName;
    public string StorageName => _storageName;
    public string AzureBlobContainerName => _containerName;

    public AzureBlobStorage(string identifier, BlobContainerClient client, string containerName, string storageName)
    {
        Identifier = identifier;
        _client = client;
        _containerName = containerName;
        _storageName = storageName;
    }


    public async Task<Stream> DownloadStreamAsyns(string fileName)
    {
        return await _client.GetBlobClient(fileName).OpenReadAsync();
    }

    public async Task Upload(string fileId, Stream stream)
    {
        await _client.GetBlobClient(fileId).UploadAsync(stream);
    }
}

And then the static extension method:

public static IServiceCollection AddAzureStorage(
    this IServiceCollection services, 
    IConfiguration configuration,
    string identifier)
{
    var options = new ABlobStorageConfigurationOptionsDTO();
    configuration
        .GetSection("AzureBlobStorageConfiguration")
        .GetSection("Storages")
        .GetSection(identifier)
        .Bind(options);

    return services
        .TryAddTransient<NamedServiceProvider<IAzureBlobStorage>>()
        .AddTransient<IAzureBlobStorage, AzureBlobStorage>(isp =>
    {
        var client = new BlobServiceClient(options.ConnString);
        var container = client.GetBlobContainerClient(options.AzureBlobContainerName);
        var containerName = options.AzureBlobContainerName;
        var storageName = options.StorageName;

        return new AzureBlobStorage(identifier, container, containerName, storageName);
    });
}

Then you could call use it like so:

public Controller(NamedServiceProvider<IAzureBlobStorage> azureStorage)
{
    _ = azureStorage ?? throw new ArgumentNullException(nameof(azureStorage));
    _azureStorage2 = azureStorage.TryGetInstance("Storage2", out var instance) ? instance : throw new Exception("Something about the identifier not being found??");
}

I coded this outside of an intellisense environment, so sorry if there are any smaller mispellings or bugs. There may be a better way to do this, but this seemed at least somewhat ok-ish? Oh, and I only changed what I had to in order to make it work generically. I didn't want to touch any other logic..

Related