Home Solution Patterns Provider Pattern (single)
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
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
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:
Encapsulates AWS-specific logic.
Receives AwsS3BlobStorageOptions with provider-specific configuration.
Validates options in constructor (fail-fast).
Translates generic methods to AWS operations.
# 3. Configuration Options — AwsS3BlobStorageOptions
public class AwsS3BlobStorageOptions
{
public string ? Region { get ; set ; }
public string ? Bucket { get ; set ; }
}
Characteristics:
Provider-specific POCO.
Easily serializable from appsettings.json or environment variables.
Each provider defines its own options.
# 4. Fluent Builder — BlobStorageBuilder
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:
Enables fluent configuration: builder.UseAwsS3(...)
Validation at build time.
Reusable builder pattern for other services.
# 5. Builder Extension — AwsS3BuilderExtensions
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:
Extends the builder without modifying it.
Instantiates the provider and its options.
Allows fluent lambda configuration.
# 6. DI Registration — ServiceCollectionExtensions
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:
Orchestrates the builder and registers the interface in DI.
Configurable lifecycle (Singleton by default).
Allows using services.AddBlobStorage(x => x.UseAwsS3(...)).
# 📝 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
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);
# Example 3: Switch based on configuration (Recommended for production)
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
Only one implementation for life: If you never change providers, it's overengineering.
Providers with very different capabilities: If AWS has features that Azure doesn't, the common interface becomes useless.
Critical performance pressure: The extra indirection (virtual calls) is minimal but exists.
# 🧪 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:
✅ Interchangeable external services (storage, auth, cache, email, SMS).
✅ Multi-tenant applications.
✅ Switch provider via configuration (not code).
✅ Integration with native .NET DI.
Key takeaway: One interface, multiple implementations, resolution by configuration.
#provider #infra #storage #dependency-injection