In today's cloud-native world, vendor lock-in poses a significant risk, particularly due to high costs. A modern application should be adaptable, capable of running on AWS, Azure, or GCP, and be able to switch between databases like PostgreSQL and SQL Server with minimal friction. I recently implemented this functionality into a .NET Core Web API project, and here's a breakdown of the approach.
The Goal: Configuration-Driven Infrastructure
The idea is to make the cloud provider and database technology a configuration choice, not a hard-coded decision. The application loads itself based on predefined settings in the appsettings.json
or environment variables.
How It Works: A Look at the Code
The magic happens in Program.cs
. The application starts by building a configuration that can be overridden by an environment variable (APP_CONFIG_FILE
), crucial for differing environments (dev, staging, prod).
csharp
Load configuration file (supports APP_CONFIG_FILE env override)
var configFile = Environment.GetEnvironmentVariable("APP_CONFIG_FILE")
?? $"appsettings.{builder.Environment.EnvironmentName}.json"
?? "appsettings.json";
builder.Configuration.AddConfiguration(configuration);
Next, it binds strongly-typed option classes for service and cloud settings. This is where we decide our fate for this deployment.
csharp
region Bind required options
var serviceOption = configuration.GetSection("ServiceOptions").Get<ServiceOptions>()
?? throw new ApplicationException(...);
var cloudConfig = configuration.GetSection("CloudServiceOptions").Get<CloudServiceOptions>()
?? throw new ApplicationException(...);
The database provider is using a simple switch
statement, registering the appropriate validator and services. This pattern is easily extendable to new databases (e.g., MySQL, SQLite).
csharp
// Register DB Validators based on provider
switch (serviceOption.DbProvider)
{
case DatabaseProvider.PostgreSQL:
builder.Services.AddSingleton<IDatabaseConnectionValidator, PostgresConnectionValidator>();
break;
case DatabaseProvider.SqlServer:
builder.Services.AddSingleton<IDatabaseConnectionValidator, SqlServerConnectionValidator>();
break;
default:
throw new NotSupportedException($"Unsupported DB provider: {serviceOption.DbProvider}");
}
The most powerful part is the Strategy Pattern for cloud providers. A factory, based on the config, creates the specific configurator (e.g., AwsConfigurator
, AzureConfigurator
). This object then handles all provider-specific setup: secrets management, blob storage, etc.
csharp
#region Cloud Provider Strategy
var cloudConfigurator = CloudConfiguratorFactory.Create(cloudConfig.Provider);
await cloudConfigurator.ConfigureAsync(builder, configuration);
Finally, comprehensive health checks (/health/live, /health/ready) ensure all components—whether an AWS RDS PostgreSQL instance or an Azure SQL Database—are responsive before the application accepts traffic.
Scenarios Where This Shines
- Multi-Cloud Deployments: Run the exact same codebase in different clouds for redundancy, compliance, or to leverage best-of-breed services.
- Mitigating Vendor Lock-in: It gives you the negotiating power and freedom to migrate if a provider's costs increase or services decline.
- Development & Testing: Developers can run the API locally with SQLite, while QA tests against a production-like PostgreSQL container, and staging uses the real Azure infrastructure.
- Disaster Recovery (DR): Your DR plan can involve spinning up the application in a secondary cloud provider with a simple configuration change.
Pros
- Flexibility & Portability: The biggest win. Your infrastructure is no longer rigid.
- Easier Testing: Isolate and mock dependencies more cleanly based on configuration.
- Future-Proofing: Onboarding a new cloud provider or database means adding a new implementation, not refactoring the entire codebase.
- Clear Separation of Concerns: Application logic is decoupled from infrastructure logic.
Cons
- Initial Complexity: The setup is more complex than a simple, single-provider app. You need to abstract away provider-specific nuances.
- Maintenance Overhead: You must maintain multiple client SDKs and implementations. A change in one cloud provider's SDK needs testing.
- Potential for "Lowest Common Denominator": To remain portable, you might avoid using powerful, unique features of a specific cloud provider (e.g., Azure Cosmos DB's serverless capacity, AWS Aurora's specific integrations).
- Testing Matrix Expansion: You need to test all supported configurations, which can multiply the number of test scenarios.
Conclusion
Building a .NET Core Web API with support for multiple clouds and databases is an investment in architectural flexibility. While it introduces upfront complexity, the long-term benefits of portability, resilience, and avoiding vendor lock-in are immense for any serious application destined for a cloud environment. The key is to use the .NET configuration system and patterns like Strategy and Dependency Injection to your advantage, just as the code above demonstrates.
Note: The code provided here is not meant to serve as a tutorial, but rather as a basic overview and conceptual abstraction.
Top comments (0)