AWS Developer Blog

Working with dependency injection in .NET Standard: inject your AWS clients – part 1

Dependency injection (DI) is a central part of any nontrivial application today. .NET has libraries like Ninject for implementing inversion of control (IOC) in their development and, as of .NET Core 1.0 (specifically, .NET Standard 1.1), lightweight DI can be provided by Microsoft.Extensions.DependencyInjection. This was used primarily in the context of developing .NET Core web applications, but it can be used everywhere – GUIs, services, libraries, even simple console applications!

AWS provides the AWSSDK.Extensions.NETCore.Setup library to simplify using dependency injection with AWS clients. It adds extension methods to IConfiguration to extract AWS specific options, and to IServiceCollection to add AWS clients by specifying the client interface.

Demo

The following demo code assumes you’re using a .NET Standard 2.0-compatible runtime. The current version is .NET Standard 2.0 compliant, so it will work with any application running on .NET Core 2.0 or later, or .NET Framework 4.6.1 or later [Compatibility Information].

You will need the following NuGet packages:

Let’s take a look at a simple demo application.

Demo.cs

using System.Threading.Tasks;
using Amazon.DynamoDBv2;
using Amazon.S3;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace WorkingWithDI
{
    public class Demo
    {
        public static async Task Main()
        {
            await NormalDependencyInjection();
        }

        private static async Task NormalDependencyInjection()
        {
            // We can add other sources, such as environment variables. Settings are additive,
            // and the last source in the pipeline wins in case of conflicts.
            var configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build();

            // Simple DI setup
            var services = new ServiceCollection();
            services.AddSingleton<IConfiguration>(configuration);

            // The service clients are singletons
            services.AddAWSService<IAmazonS3>();
            services.AddAWSService<IAmazonDynamoDB>();

            services.AddSingleton<Application>();

            // Set up the container
            var serviceProvider = services.BuildServiceProvider();

            // Get our application, and run it.
            await serviceProvider.GetService<Application>().Run();
        }
    }
}

Here, we are setting up a ServiceCollection with all of the components needed in our app. In this case, “Application” is our main class, and it requires an S3Client and a DynamoDB client to run. When we construct the ServiceProvider, it will create our Application and pass in the needed dependencies.

appsettings.json

{
    "AWS":
    {
        "Region": "us-east-2" 
    }
}

The AWSSDK.Extensions.NETCore.Setup library extracts the AWS options out of the configuration object. These options could be stored as environment variables, XML, JSON, and so on. In this case, we store these settings as JSON. We read them using the Microsoft configuration libraries, and add the Configuration to our DI container.  The AWS configuration is extracted by the library and injected into our AWS clients when they are constructed, which are created and passed to the Application when it’s constructed.

Application.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Amazon.DynamoDBv2;
using Amazon.S3;

namespace WorkingWithDI
{
    class Application : IHostedService, IDisposable
    {
        private readonly IAmazonS3 _amazonS3;
        private readonly IAmazonDynamoDB _amazonDynamoDb;

        public Application(IAmazonS3 amazonS3, IAmazonDynamoDB amazonDynamoDb) =>
            (_amazonS3, _amazonDynamoDb) = (amazonS3, amazonDynamoDb);

        public async Task Run()
        {
            var bucketNames = string.Join(Environment.NewLine, await GetBucketNames());
            Console.WriteLine(bucketNames);

            Console.WriteLine();

            var tableNames = string.Join(Environment.NewLine, await GetTableNames());
            Console.WriteLine(tableNames);
        }

        private async Task<IEnumerable<string>> GetBucketNames() => 
            (await _amazonS3.ListBucketsAsync()).Buckets.Select(bucket => bucket.BucketName);

        private async Task<IEnumerable<string>> GetTableNames() =>
            (await _amazonDynamoDb.ListTablesAsync()).TableNames;

        public void Dispose()
        {
            _amazonS3?.Dispose();
            _amazonDynamoDb?.Dispose();
        }
    }
}
 
       

Our Application simply lists all of the Amazon S3 buckets and Amazon DynamoDB tables in our account. The interesting thing is the constructor: the DI container will inject the clients for us. We never had to create these clients, or even the application. And this is a console app!

Note that the .NET DI only allows constructor injection. This isn’t an issue. You’re not constructing these objects by hand, so adding additional parameters or other refactoring should not be painful. An added benefit to using constructor injection is it makes unit testing with mocks really easy.

Conclusion

Dependency injection lowers the complexity of wiring together nontrivial applications. It also discourages bad practices, like not providing interfaces where appropriate and overusing static helper classes. .NET Standard makes using DI easy, and the AWSSDK.Extensions.NETCore.Setup library simplifies integrating AWS clients into your application.

In part 2 of this blog post, we will look into hosting: WebHost, and in particular the new GenericHost support added into .NET Standard at the release of .NET Core 2.1.