DEV Community

Cheyzan
Cheyzan

Posted on

Embracing Cloud Independence & Multi-Database in .NET Core Web API

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);
Enter fullscreen mode Exit fullscreen mode

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(...);

Enter fullscreen mode Exit fullscreen mode

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}");
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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

  1. Multi-Cloud Deployments: Run the exact same codebase in different clouds for redundancy, compliance, or to leverage best-of-breed services.
  2. Mitigating Vendor Lock-in: It gives you the negotiating power and freedom to migrate if a provider's costs increase or services decline.
  3. 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.
  4. Disaster Recovery (DR): Your DR plan can involve spinning up the application in a secondary cloud provider with a simple configuration change.

Pros

  1. Flexibility & Portability: The biggest win. Your infrastructure is no longer rigid.
  2. Easier Testing: Isolate and mock dependencies more cleanly based on configuration.
  3. Future-Proofing: Onboarding a new cloud provider or database means adding a new implementation, not refactoring the entire codebase.
  4. Clear Separation of Concerns: Application logic is decoupled from infrastructure logic.

Cons

  1. Initial Complexity: The setup is more complex than a simple, single-provider app. You need to abstract away provider-specific nuances.
  2. Maintenance Overhead: You must maintain multiple client SDKs and implementations. A change in one cloud provider's SDK needs testing.
  3. 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).
  4. 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)