Provider Pattern (single)

Abstracts access to an external resource or service that is interchangeable and configurable at runtime.

#📋 Context

Your application needs a blob storage service (Azure Blob Storage, AWS S3, or other) whose implementation changes by environment (local, dev, prod) or by client (multi-tenant). The domain code should not know the details of each specific provider.

#🚨 Problem

  • The client (domain) should not depend on concrete provider implementations.
  • Switching providers (e.g., from AWS S3 to Azure) should not require changes to business logic.
  • Provider configuration must be flexible and centralized.
  • Tests should easily use fake or mock providers.

#✅ Solution

Define a common interface (IBlobStorageProvider), implementations for each provider, and resolve the injection using a fluent builder and native DI.

#Architecture in this solution

CSHARP
ProviderPattern.Core
├── IBlobStorageProvider      (base interface)
├── BlobStorageBuilder        (fluent builder)
└── ServiceCollectionExtensions (DI registration)

ProviderPattern.Core.AwsS3
├── AwsS3BlobStorageProvider
├── AwsS3BlobStorageOptions
└── AwsS3BuilderExtensions

ProviderPattern.Core.AzureBlobStorage
├── AzureBlobStorageProvider
├── AzureBlobStorageOptions
└── AzureBlobStorageBuilderExtensions

#🔧 Key Components

#1. Base Interface — IBlobStorageProvider

CSHARP
public interface IBlobStorageProvider
{
    Task<string> UploadAsync(string containerName, string blobName, Stream stream);
    Task<Stream> DownloadAsync(string containerName, string blobName);
    Task<bool> DeleteAsync(string containerName, string blobName);
    Task<bool> ExistsAsync(string containerName, string blobName);
    Task<IEnumerable<string>> ListBlobsAsync(string containerName);
}

Characteristics:

  • Defines the common contract that all providers must fulfill.
  • Asynchronous methods (I/O operations).
  • Platform agnostic: does not mention Azure, AWS, local, etc.

#2. Concrete Implementation — AwsS3BlobStorageProvider

CSHARP
quot;https://{_options.Bucket}.s3.{_options.Region}.amazonaws.com/{blobName}"; } // Rest of methods... }" aria-label="Copiar código"> Copiar
public class AwsS3BlobStorageProvider : IBlobStorageProvider
{
    private readonly AwsS3BlobStorageOptions _options;

    public AwsS3BlobStorageProvider(AwsS3BlobStorageOptions options)
    {
        _options = options ?? throw new ArgumentNullException(nameof(options));
        
        if (string.IsNullOrWhiteSpace(options.Bucket))
            throw new ArgumentException("Bucket is required for AWS S3");
        if (string.IsNullOrWhiteSpace(options.Region))
            throw new ArgumentException("Region is required for AWS S3");
    }

    public async Task<string> UploadAsync(string containerName, string blobName, Stream stream)
    {
        // AWS S3 specific logic
        return $"https://{_options.Bucket}.s3.{_options.Region}.amazonaws.com/{blobName}";
    }

    // Rest of methods...
}

Characteristics:


#3. Configuration Options — AwsS3BlobStorageOptions

CSHARP
public class AwsS3BlobStorageOptions
{
    public string? Region { get; set; }
    public string? Bucket { get; set; }
}

Characteristics:


#4. Fluent Builder — BlobStorageBuilder

CSHARP
public class BlobStorageBuilder
{
    private IBlobStorageProvider? _provider;

    public BlobStorageBuilder UseProvider(IBlobStorageProvider provider)
    {
        _provider = provider ?? throw new ArgumentNullException(nameof(provider));
        return this;
    }

    public IBlobStorageProvider Build()
    {
        if (_provider == null)
            throw new InvalidOperationException("A provider must be selected before building");
        return _provider;
    }
}

Characteristics:


#5. Builder Extension — AwsS3BuilderExtensions

CSHARP
public static class AwsS3BuilderExtensions
{
    public static BlobStorageBuilder UseAwsS3BlobStorageProvider(
        this BlobStorageBuilder builder, 
        Action<AwsS3BlobStorageOptions>? options = null)
    {
        var awsOptions = new AwsS3BlobStorageOptions();
        options?.Invoke(awsOptions);

        var provider = new AwsS3BlobStorageProvider(awsOptions);
        return builder.UseProvider(provider);
    }
}

Characteristics:


#6. DI Registration — ServiceCollectionExtensions

CSHARP
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddBlobStorage(
        this IServiceCollection services,
        Action<BlobStorageBuilder> configure, 
        ServiceLifetime serviceLifetime = ServiceLifetime.Singleton)
    {
        var builder = new BlobStorageBuilder();
        configure(builder);
        var provider = builder.Build();
        
        services.Add(new ServiceDescriptor(
            typeof(IBlobStorageProvider), 
            _ => provider, 
            serviceLifetime));
        
        return services;
    }
}

Characteristics:


#📝 Usage — Examples

#Example 1: Using AWS S3

CSHARP
quot;URL: {url}");" aria-label="Copiar código"> Copiar
var services = new ServiceCollection();

services.AddBlobStorage(x => x
    .UseAwsS3BlobStorageProvider(opts => {
        opts.Bucket = "my-bucket";
        opts.Region = "us-east-1";
    }));

var provider = services.BuildServiceProvider();
var blobProvider = provider.GetRequiredService<IBlobStorageProvider>();

var url = await blobProvider.UploadAsync("my-bucket", "document.pdf", stream);
Console.WriteLine($"URL: {url}");

#Example 2: Using Azure Blob Storage

CSHARP
var services = new ServiceCollection();

services.AddBlobStorage(x => x
    .UseAzureBlobStorageProvider(opts => {
        opts.ConnectionString = "DefaultEndpointsProtocol=https;...";
    }));

var provider = services.BuildServiceProvider();
var blobProvider = provider.GetRequiredService<IBlobStorageProvider>();

var url = await blobProvider.UploadAsync("images", "photo.jpg", stream);
CSHARP
quot;Unknown provider: {providerType}") }; });" aria-label="Copiar código"> Copiar
// appsettings.json
{
  "BlobStorage": {
    "Provider": "AWS",
    "Aws": {
      "Bucket": "my-bucket",
      "Region": "us-east-1"
    }
  }
}

// Program.cs
var config = builder.Configuration;
var providerType = config["BlobStorage:Provider"];

services.AddBlobStorage(x =>
{
    return providerType switch
    {
        "AWS" => x.UseAwsS3BlobStorageProvider(opts =>
        {
            config.GetSection("BlobStorage:Aws").Bind(opts);
        }),
        "Azure" => x.UseAzureBlobStorageProvider(opts =>
        {
            config.GetSection("BlobStorage:Azure").Bind(opts);
        }),
        _ => throw new InvalidOperationException($"Unknown provider: {providerType}")
    };
});

#🎯 Difference with Other Patterns

Pattern Focus Who decides Change Example
Provider Configurable resource/service Configuration + DI Reconfig at startup AWS vs Azure
Strategy Interchangeable algorithm The client, at runtime Dynamic during execution Sorting algorithms
Adapter Adapt incompatible API The developer, once Fixed in design Convert XML to JSON
Factory Create objects without exposing classes Factory decides Via factory method Create DbContext
Decorator Add behavior Dynamic at runtime At usage time Logging wrapper

#📊 Tradeoffs

Advantage Disadvantage
✅ Switch provider without touching domain ❌ Interface defines lowest common denominator
✅ Native .NET DI (no external lib needed) ❌ If providers have very different APIs, you lose features
✅ Flexible configuration per environment ❌ More complex configuration in production
✅ Easy tests with fake providers ❌ E2E tests still need real provider
✅ Multi-tenant support (one provider per tenant) ❌ Overhead if you only use one provider forever

#⚠️ When NOT to apply it


#🧪 Testing

#With Fake Provider

CSHARP
quot;{containerName}/{blobName}"] = bytes; return Task.FromResult(
quot;fake://{containerName}/{blobName}"); } // Rest of methods... } // In tests services.AddBlobStorage(x => x.UseProvider(new FakeBlobStorageProvider()));" aria-label="Copiar código"> Copiar
public class FakeBlobStorageProvider : IBlobStorageProvider
{
    private readonly Dictionary<string, byte[]> _store = new();

    public Task<string> UploadAsync(string containerName, string blobName, Stream stream)
    {
        var bytes = new byte[stream.Length];
        stream.Read(bytes);
        _store[$"{containerName}/{blobName}"] = bytes;
        return Task.FromResult($"fake://{containerName}/{blobName}");
    }

    // Rest of methods...
}

// In tests
services.AddBlobStorage(x => x.UseProvider(new FakeBlobStorageProvider()));

#📚 Summary

The Provider Pattern is perfect for:

Key takeaway: One interface, multiple implementations, resolution by configuration.

#provider #infra #storage #dependency-injection