/Invio.Extensions.DependencyInjection

Extensions for DependencyInjection in ASP.NET Core

Primary LanguageC#MIT LicenseMIT

Invio.Extensions.DependencyInjection

Appveyor Travis CI NuGet Coverage

This library implements the factory pattern as extension methods to Microsoft's ASP.NET Core Dependency Injection library. It is .NET Standard 1.3 compliant for cross-platform use.

Installation

The latest version of this package is available on NuGet. To install, run the following command:

PM> Install-Package Invio.Extensions.DependencyInjection

Usage

In Microsoft's Dependency Injection framework, a developer adds a collection of ServiceDescriptor objects to an IServiceCollection during an ASP.NET application's initial call to ConfigureServices in the Startup class. Say, for example, we had a service with the interface of IExampleService which in turn had an implementation of ExampleService. This could be registered in a variety of ways, such as:

// Simple implementationType example

public void ConfigureServices(IServiceCollection services) {
    services.AddSingleton<IExampleService, ExampleService>();
    services.AddSingleton<IExampleService>(new ExampleService());
    services.AddSingleton(typeof(IExampleService), typeof(ExampleService));
}

Sometimes, the hydration of a service may be dependent on some level of configuration. For example, maybe a string from your appsettings.json file is needed to determine how to provide an IExampleService. This could be defined with an implementationFactory lambda, which receives an implementation of IServiceProvider in and expects a valid implementation of IExampleService back out. This can be registered in a variety of ways, such as:

// Simple implementationFactory example

public IConfigurationRoot Configuration { get; }

public void ConfigureServices(IServiceCollection services) {
    var myConfig = Configuration["MyConfigurationString"];

    services.AddSingleton<IExampleService>(provider => new ExampleService(myConfig));
    services.AddSingleton(typeof(IExampleService), provider => new ExampleService(myConfig));
}

This lambda works well for trivial dependencies, but what if the creation of ExampleService was complex? Perhaps it requires a few dependencies that are defined in your IServiceProvider as well as some decisions based upon configuration defined in appsettings.json that have been modularized into Microsoft's Options framework? It could become unwieldy, like this:

// Complex implementationFactory example

public IConfigurationRoot Configuration { get; }

public void ConfigureServices(IServiceCollection services) {
    services.Configure<DependentConfiguration>(Configuration.GetSection("Dependent"));
    services.AddSingleton<IDependencyFoo, DependencyFoo>();
    services.AddTransient<IDependencyBar, DependencyBar>();

    services.AddSingleton(
        typeof(IExampleService),
        provider => {
            var configuration = provider.GetRequiredService<IOptions<DependentConfiguration>>().Value;
            var fooService = provider.GetRequiredService<IDependencyFoo>();
            var barService = provider.GetRequiredService<IDependencyBar>();

            if (configuration.IsFooWrapped) {
                fooService = new Wrapper<IDependencyFoo>(fooService);
            }

            if (!configuration.IsImmutable) {
                barService.Mood = Mood.Depressed;
                barService.SadTimes = true;
            }

            return new ExampleServiceBehavior(
                new ExampleService(fooService, barService)
            );
        }
    );

}

Now we're introducing real complexity into an area that is difficult to test. This is where this library's utilization of the factory pattern can be employed. This library adds a new interface, IFactory<T>, that has a single method, Provide(), that returns a service of type T. By defining the factory as another service within the IServiceCollection, we can reroute initializationFactory lambda's complexity into something that can be unit tested, like so:

public class ExampleServiceFactory : IFactory<IExampleService> {

    private IOptions<DependentConfiguration> options { get; }
    private IDependencyFoo fooService { get; }
    private IDependencyBar barService { get; }

    public ExampleServiceFactory(
        IOptions<DependentConfiguration> options,
        IDependencyFoo fooService,
        IDependencyBar barService) {

        if (options == null) {
            throw new ArgumentNullException(nameof(options));
        } else if (fooService == null) {
            throw new ArgumentNullException(nameof(fooService));
        } else if (barService == null) {
            throw new ArgumentNullException(nameof(barService));
        }

        this.options = options;
        this.fooService = fooService;
        this.barService = barService;
    }

    public T Provide() {
        var configuration = options.Value;

        if (configuration.IsFooWrapped) {
            fooService = new Wrapper<IDependencyFoo>(fooService);
        }

        if (!configuration.IsImmutable) {
            barService.Mood = Mood.Depressed;
            barService.SadTimes = true;
        }

        return new ExampleServiceBehavior(
            new ExampleService(fooService, barService)
        );
    }

}

Now our ConfigureServices method in the Startup class can be defined to use the IFactory<IExampleService> implementation to provide an IExampleService like so.

// Refactored implementationFactory using IFactory<IExampleService>

public IConfigurationRoot Configuration { get; }

public void ConfigureServices(IServiceCollection services) {
    services.Configure<DependentConfiguration>(Configuration.GetSection("Dependent"));
    services.AddSingleton<IDependencyFoo, DependencyFoo>();
    services.AddTransient<IDependencyBar, DependencyBar>();

    // The factory is another service now.
    services.AddTransient<IFactory<IExampleService>, ExampleServiceFactory>();

    services.AddSingleton(
        typeof(IExampleService),
        provider => provider.GetRequiredService<IFactory<IExampleService>>().Provide()
    );
}

That's a good step, but we can go further. Use the IServiceCollection extension methods provided in this library to further drop down the amount of boilerplate code, like so:

// Refactored boilerplate implementationFactory into extension method

public IConfigurationRoot Configuration { get; }

public void ConfigureServices(IServiceCollection services) {
    services.Configure<DependentConfiguration>(Configuration.GetSection("Dependent"));
    services.AddSingleton<IDependencyFoo, DependencyFoo>();
    services.AddTransient<IDependencyBar, DependencyBar>();

    services.AddSingletonWithFactory<IExampleService, ExampleServiceFactory>();
}

That's it. <3