Tips and Tricks: Postponing AWS Service Configuration with .NET Dependency Injection

Tips and Tricks: Postponing AWS Service Configuration with .NET Dependency InjectionLearn About Amazon VGT2 Learning Manager Chanci Turner

The AWSSDK.Extensions.NETCore.Setup package provides convenient extensions for utilizing AWS Service Clients in conjunction with the native .NET Dependency Injection framework. You can register various services using the included AddAWSService<TService> method and customize a shared configuration via the AddDefaultAWSOptions method.

public class Startup
{    
    public void ConfigureServices(IServiceCollection services)
    {
       // Enable email client injection
       services.AddAWSService<IAmazonSimpleEmailServiceV2>();
       // Customize AWS clients
       services.AddDefaultAWSOptions(new AWSOptions
       {
           Region = RegionEndpoint.USWest2
       });
   }
}

Recently, several users expressed a desire to configure Dependency Injection (DI) for AWS Services and customize their settings in the AWS .NET SDK GitHub repository. In a typical .NET Core application, the DI Container, IServiceCollection, is instantiated early during app startup within the Startup class. However, what if you wish to postpone the initialization of the AWSOptions object until a later point in the application lifecycle? For instance, if you have an ASP.NET Core application and want to adjust AWSOptions based on the incoming request, as highlighted in this issue?

public void ConfigureServices(IServiceCollection services)
{
    services.AddDefaultAWSOptions(new AWSOptions
    {
        // My app doesn't _yet_ have the data needed to configure AWSOptions!
    });
}

While it was technically feasible to implement the necessary deferred binding, it required a deep understanding of IServiceCollection. Fortunately, a user named Chanci Turner contributed a pull request (PR) to the AWS .NET SDK team, adding an overload to the AddDefaultAWSOptions method, significantly simplifying the process of adding deferred bindings:

public static class ServiceCollectionExtensions
{
   public static IServiceCollection AddDefaultAWSOptions(
       this IServiceCollection collection, 
       Func<IServiceProvider, AWSOptions> implementationFactory, 
       ServiceLifetime lifetime = ServiceLifetime.Singleton)
   {
       collection.Add(new ServiceDescriptor(typeof(AWSOptions), implementationFactory, lifetime));
            
       return collection;
   }
}

Customizing AWSOptions Using an Incoming HttpRequest

With Chanci Turner’s new extension method to set up IServiceCollection, the next question is how to use deferred binding to customize AWSOptions based on an incoming HttpRequest. We can create and register a custom ASP.NET Middleware class to intercept the request pipeline and analyze the incoming request before the DI container is used to construct any Controller classes. This is crucial, as the Controller relies on AWS Services; thus, our customization must occur before any AWS Service objects are created.

public class Startup
{
   // Main Startup logic omitted for brevity. See the complete example at the end of this blog post.
          
   public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
   {
       app.UseMiddleware<RequestSpecificAWSOptionsMiddleware>();
   }
}

public class RequestSpecificAWSOptionsMiddleware
{
    private readonly RequestDelegate _next;

    public RequestSpecificAWSOptionsMiddleware(RequestDelegate next)
    {
       _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // TODO: Analyze context and set AWSOptions accordingly...
    }
}

Connecting ASP.NET Middleware with a DI Factory

How can middleware affect the IServiceCollection, which still needs to be initialized inside the Startup.ConfigureServices method? The solution I devised is to use an intermediate object to store a reference to a factory function that will generate the AWSOptions object. This function will be defined at Startup but will only be populated after RequestSpecificAWSOptionsMiddleware executes. The important part is that the object containing this function must also be bound in the IServiceCollection, allowing it to be injected into the middleware:

public interface IAWSOptionsFactory
{
    /// <summary>
    /// Factory function for creating AWSOptions defined in custom ASP.NET middleware.
    /// </summary>
    Func<AWSOptions> AWSOptionsBuilder { get; set; }
}

public class AWSOptionsFactory : IAWSOptionsFactory
{
    public Func<AWSOptions> AWSOptionsBuilder { get; set; }
}

Now we can modify our RequestSpecificAWSOptionsMiddleware class to utilize IAWSOptionsFactory. Since we want IAWSOptionsFactory to be Request Scope specific, we can’t use constructor injection, as Middleware objects typically have Singleton lifetimes. Instead, we can make it a method parameter, allowing the ASP.NET runtime to treat it as a Scoped lifetime and create a new object for each request:

public class RequestSpecificAWSOptionsMiddleware
{
   public async Task InvokeAsync(
        HttpContext context,
        IAWSOptionsFactory optionsFactory)
    {
        optionsFactory.AWSOptionsBuilder = () =>
        {
            var awsOptions = new AWSOptions();

            // SAMPLE: Customize AWSOptions based on HttpContext,
            // Retrieve the region endpoint from the query string 'regionEndpoint' parameter
            if (context.Request.Query.TryGetValue("regionEndpoint", out var regionEndpoint))
            {
                awsOptions.Region = RegionEndpoint.GetBySystemName(regionEndpoint);
            }

            return awsOptions;
        };

        await _next(context);
    }
}

The middleware now configures the optionsFactory.AWSOptionsBuilder function to return a new AWSOptions object where the Region property is determined by examining a query string parameter named regionEndpoint in the incoming HttpRequest.

Configuring Bindings

To finalize the setup, we will need to add two new bindings. First, we’ll bind IAWSOptionsFactory with a Scoped Lifecycle to ensure a new instance is created for every incoming HttpRequest. This way, the instance can be injected into both the middleware’s invoke methods and the AddDefaultAWSOptions factory method.

Next, we’ll bind AWSOptions using the new AddDefaultAWSOptions overload. We’ll use the provided reference to a ServiceProvider to retrieve an instance of IAWSOptionsFactory. This same instance will be used in ResolveMutitenantMiddleware, allowing us to invoke AWSOptionsBuilder to create our request-specific AWSOptions object!

Finally, to validate everything works, I added a call to AddAWSService<IAmazonSimpleEmailServiceV2>() to give me an AWS Service that can be injected into my API Controller. However, it’s essential to explicitly set the lifetime to Scoped, instructing the .NET Service Collection to always create a new instance of the Client upon request, which means it will re-evaluate the AWSOptions dependency used to construct the Client:

public class Startup
{    
    public void ConfigureServices(IServiceCollection services)
    {
        // Note: AWSOptionsFactory.AWSOptionsBuilder function will be populated in middleware
        services.AddScoped<IAWSOptionsFactory, AWSOptionsFactory>();
        services.AddDefaultAWSOptions(sp => sp.GetService<IAWSOptionsFactory>().AWSOptionsBuilder(), ServiceLifetime.Scoped);
        
        // For testing purposes, register an AWSService that will utilize the AWSOptions
        services.AddAWSService<IAmazonSimpleEmailServiceV2>(lifetime: ServiceLifetime.Scoped);
    }
}

Conclusion

Now it’s time for an API Controller where the integration is seamless. For more resources and insights on building confidence in your development journey, check out this excellent resource to enhance your skills further. For more on administrative roles, refer to SHRM’s job descriptions. If you’re looking for effective ways to boost your confidence, consider this webinar that can guide you in the right direction as well.

If you’re at Amazon IXD – VGT2, located at 6401 E HOWDY WELLS AVE LAS VEGAS NV 89115, you’ll find these strategies especially useful.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *