Provider Pattern (simple)

Abstrae el acceso a un recurso o servicio externo intercambiable, configurable en runtime.

#📋 Contexto

Tu aplicación necesita un servicio de almacenamiento de blobs (Azure Blob Storage, AWS S3, u otro) cuya implementación cambia por entorno (local, dev, prod) o por cliente (multi-tenant). El código de dominio no debería conocer los detalles de cada proveedor específico.

#🚨 Problema

  • El cliente (dominio) no debería depender de implementaciones concretas de proveedores.
  • Cambiar de proveedor (ej: de AWS S3 a Azure) no debe requerir cambios en la lógica de negocio.
  • La configuración del proveedor debe ser flexible y centralizada.
  • Tests deben poder usar fácilmente proveedores fake o mock.

#✅ Solución

Define una interfaz común (IBlobStorageProvider), implementaciones para cada proveedor, y resuelve la inyección mediante un builder fluido y DI nativo.

#Arquitectura en esta solución

CSHARP
ProviderPattern.Core
├── IBlobStorageProvider      (interfaz base)
├── BlobStorageBuilder        (builder fluido)
└── ServiceCollectionExtensions (registro en DI)

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

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

#🔧 Componentes Clave

#1. Interfaz Base — 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);
}

Características:

  • Define el contrato común que todos los proveedores deben cumplir.
  • Métodos asincronos (operaciones I/O).
  • Agnóstico de la plataforma: no menciona Azure, AWS, local, etc.

#2. Implementación Concreta — AwsS3BlobStorageProvider

CSHARP
quot;https://{_options.Bucket}.s3.{_options.Region}.amazonaws.com/{blobName}"; } // Resto de métodos... }" 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 es requerido para AWS S3");
        if (string.IsNullOrWhiteSpace(options.Region))
            throw new ArgumentException("Region es requerida para AWS S3");
    }

    public async Task<string> UploadAsync(string containerName, string blobName, Stream stream)
    {
        // Lógica específica de AWS S3
        return $"https://{_options.Bucket}.s3.{_options.Region}.amazonaws.com/{blobName}";
    }

    // Resto de métodos...
}

Características:


#3. Opciones de Configuración — AwsS3BlobStorageOptions

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

Características:


#4. Builder Fluido — 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("Un proveedor debe ser seleccionado");
        return _provider;
    }
}

Características:


#5. Extensión de Builder — 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);
    }
}

Características:


#6. Registro en DI — 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;
    }
}

Características:


#📝 Uso — Ejemplos

#Ejemplo 1: Usar 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}");

#Ejemplo 2: Usar 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);

#Ejemplo 3: Cambiar según configuración (Recomendado para producción)

CSHARP
quot;Proveedor desconocido: {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($"Proveedor desconocido: {providerType}")
    };
});

#🎯 Diferencia con Otros Patrones

Patrón Foco Quién decide Cambio Ejemplo
Provider Recurso/servicio configurable Configuración + DI Reconfig en startup AWS vs Azure
Strategy Algoritmo intercambiable El cliente, en runtime Dinámico en ejecución Algoritmos de sort
Adapter Adaptar API incompatible El desarrollador, una vez Fijo en diseño Convertir XML a JSON
Factory Crear objetos sin exposer clases Factory decide Via factory method Crear DbContext
Decorator Agregar comportamiento Dinámico en ejecución En tiempo de uso Logging alrededor

#📊 Tradeoffs

Ventaja Desventaja
✅ Cambiar proveedor sin tocar el dominio ❌ Interfaz define mínimo común denominador
✅ DI nativo de .NET (no requiere lib externa) ❌ Si proveedores tienen APIs muy distintas, pierdes features
✅ Configuración flexible por entorno ❌ Configuración más compleja en producción
✅ Tests fáciles con providers fake ❌ Tests E2E siguen requiriendo proveedor real
✅ Multi-tenant (un provider por tenant) ❌ Overhead si solo usas un proveedor siempre

#⚠️ Cuándo NO aplicarlo


#🧪 Testing

#Con Provider Fake

CSHARP
quot;{containerName}/{blobName}"] = bytes; return Task.FromResult(
quot;fake://{containerName}/{blobName}"); } // Resto de métodos... } // En 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}");
    }

    // Resto de métodos...
}

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

#📚 Resumen

El Provider Pattern es perfecto para:

Clave: Una interfaz, múltiples implementaciones, resolución por configuración.

#provider #infra #storage #dependency-injection