AWS SDK for .NET: Multi-Tenant Dependency Injection
Building multi-tenant applications with AWS SDK for .NET requires dynamic client configuration based on tenant context. Let's explore how to connect to different regions and accounts using middleware and scoped dependency injection.
Building multi-tenant applications with AWS often means connecting to resources spread across different regions or even different AWS accounts—all depending on which tenant is making the request.
If you try to use the standard singleton dependency injection pattern for AWS SDK clients, you'll quickly hit a wall.
Static configuration doesn't work when you need to dynamically switch regions or credentials based on incoming HTTP requests.
In this post, let's explore how to handle AWS SDK dependency injection for multi-tenant scenarios where resources span different regions and accounts.
Thanks to AWS for sponsoring this article in my .NET on AWS Series.
The Challenge: Static Configuration in Multi-Tenant Scenarios
Consider a typical ASP.NET API application where you've registered DynamoDB client using the standard approach:
builder.Services.AddAWSService<IAmazonDynamoDB>();builder.Services.AddSingleton<IDynamoDBContext, DynamoDBContext>();app.MapGet("/movies", async (int year, IDynamoDBContext dbContext) =>{var movies = await dbContext.QueryAsync<Movie>(year).GetRemainingAsync();return Results.Ok(movies.Select(m => m.Title));});
This works perfectly for single-region applications. The configuration is set at startup, and all requests use the same region — based on your local AWS profile.
But what if you have DynamoDB tables in multiple regions? Let's say you have:
- A
Movietable inap-southeast-2(Sydney) - Another
Movietable inap-south-1(Mumbai)
Different tenants need to connect to different tables based on a request parameter—maybe a header value indicating the tenant's region.
With the current setup, you're stuck with a single region. The configuration happens at startup in Program.cs, and there's no way to change it dynamically inside your endpoint handlers.
AWS SDK for .NET: How to Register Clients with Dependency Injection

The Manual Approach: Creating Clients Per Request
One way to solve this is by manually creating clients inside your endpoint handlers:
app.MapGet("/movie/by-header-region", async (int year, HttpContext context) =>{if (!context.Request.Headers.TryGetValue("regionEndpoint", out var regionHeader)){return Results.BadRequest("Missing 'regionEndpoint' header");}var regionEndpoint = Amazon.RegionEndpoint.GetBySystemName(regionHeader);var dynamoDbClient = new AmazonDynamoDBClient(regionEndpoint);var dbContext = new DynamoDBContext(dynamoDbClient);var movies = await dbContext.QueryAsync<Movie>(year).GetRemainingAsync();return Results.Ok(new {region = regionHeader.ToString(),movies = movies.Select(m => m.Title)});});
This works—you can now pass different region values via the regionEndpoint header:
GET https://localhost:5001/movie/by-header-region?year=2019regionEndpoint: ap-southeast-2
Change the header to ap-south-1, and you'll connect to Mumbai instead of Sydney.
However, this approach has serious problems:
- Code duplication across every endpoint that needs AWS services
- Manual client creation scattered throughout your application
- Difficult to test and maintain
- Doesn't leverage dependency injection benefits
If you're connecting to multiple AWS services (S3, SQS, SNS, etc.), you'd need to wire up this manual creation logic everywhere.
This becomes tedious and error-prone very quickly.
Building a Dynamic Configuration Solution
The AWS SDK for .NET provides a better approach through delayed configuration using function overloads for AddDefaultAWSOptions.
This solution was described in detail in an AWS blog post by Philip Chuang, with contributions from the community addressing various edge cases.
The core idea is to:
- Create a factory that provides AWS options dynamically
- Use ASP.NET middleware to configure the factory based on the HTTP request
- Register AWS clients with scoped lifetime instead of singleton
Let's walk through the implementation step by step.
Step 1: Create the AWS Options Factory
First, create an interface and implementation for an AWS options factory:
public interface IAWSOptionsFactory{Func<AWSOptions> AWSOptionsBuilder { get; set; }}public class AWSOptionsFactory : IAWSOptionsFactory{public Func<AWSOptions> AWSOptionsBuilder { get; set; }}
This factory exposes a function property that, when invoked, returns AWSOptions configured for the current request.
Step 2: Register the Factory as Scoped
Register the factory as a scoped service in your dependency injection container:
builder.Services.AddScoped<IAWSOptionsFactory, AWSOptionsFactory>();
Using scoped lifetime is critical—it ensures each HTTP request gets a fresh instance of the factory, allowing different configuration per request.
Step 3: Configure AddDefaultAWSOptions with the Factory
Update the AWS SDK registration to use the factory's function:
builder.Services.AddDefaultAWSOptions(sp => sp.GetService<IAWSOptionsFactory>().AWSOptionsBuilder(),ServiceLifetime.Scoped);
Instead of hardcoding AWSOptions, we're passing a function that resolves the factory and invokes its builder function. The scoped lifetime ensures this is evaluated per request.
Step 4: Create Middleware to Set the Options Builder
Now create middleware that reads the HTTP request context and configures the options builder:
public class AWSOptionsMiddleware(RequestDelegate next){public async Task InvokeAsync(HttpContext context,IAWSOptionsFactory optionsFactory){optionsFactory.AWSOptionsBuilder = () =>{var awsOptions = new AWSOptions();if (context.Request.Headers.TryGetValue("regionEndpoint", out var regionEndpoint)){awsOptions.Region = RegionEndpoint.GetBySystemName(regionEndpoint);}else{// Default region if header not providedawsOptions.Region = RegionEndpoint.APSoutheast2;}return awsOptions;};await next(context);}}
This middleware:
- Reads the
regionEndpointheader from the request - Sets the
AWSOptionsBuilderfunction to return options with the appropriate region - Calls the next middleware in the pipeline
Step 5: Register AWS Services as Scoped
Update your AWS service registrations to use scoped lifetime:
builder.Services.AddAWSService<IAmazonDynamoDB>(lifetime: ServiceLifetime.Scoped);builder.Services.AddScoped<IDynamoDBContext>(sp =>new DynamoDBContext(sp.GetService<IAmazonDynamoDB>()));
This is a crucial change—singleton instances can't be reconfigured per request. With scoped lifetime, each request gets new client instances configured with the appropriate region.
Step 6: Register the Middleware
Finally, register the middleware in the application pipeline:
app.UseMiddleware<AWSOptionsMiddleware>();
Place this before your endpoints so the options are configured before dependency resolution happens.
How It All Works Together
Here's the request flow:
- HTTP request arrives with
regionEndpoint: ap-south-1header - Middleware executes and sets the
AWSOptionsBuilderfunction to return options with Mumbai region - Controller handler runs and requests
IDynamoDBContextfrom DI - DI container resolves dependencies:
- Resolves scoped
IAmazonDynamoDBclient - Calls
AddDefaultAWSOptionsfunction, which invokes the factory's builder - Builder returns
AWSOptionswith Mumbai region - Creates DynamoDB client configured for Mumbai
- Creates
DynamoDBContextwrapping the client
- Resolves scoped
- Handler executes query against Mumbai DynamoDB table
- Request completes, scoped instances are disposed
Each request gets independently configured AWS clients based on its header values.
Your Endpoint Code Stays Clean
The benefit of this approach is that your endpoint handlers remain simple and clean:
app.MapGet("/movies", async (int year, IDynamoDBContext dbContext) =>{var movies = await dbContext.QueryAsync<Movie>(year).GetRemainingAsync();return Results.Ok(movies.Select(m => m.Title));});
No manual client creation, no region selection logic—just inject IDynamoDBContext and use it.
The middleware handles all the dynamic configuration transparently.
While scoped instances have some overhead compared to singletons, they provide the flexibility needed for multi-tenant scenarios. For high-traffic applications, consider implementing client pooling to balance dynamic configuration with resource efficiency.
This approach eliminates manual client creation throughout your codebase and leverages ASP.NET's middleware pipeline for clean separation of concerns—making your code more testable and maintainable.
References: