blog post image
Andrew Lock avatar

Andrew Lock

~4 min read

Configuring environment specific services for dependency injection in ASP.NET Core

In this short post I show how you can configure dependency injection so that different services will be injected depending if you are in a development or production environment.

tl;dr - save the IHostingEnvironment injected in your Startup constructor for use in your ConfigureServices method.

Why would you want to?

There are a whole number of possibilities for why you might want to do this, but fundamentally it's becuase you want things to work differently in production than in development. For example when you're running and testing in a development environment:

  • You probably don't want emails to be sent to customers
  • You might not want to rely on an external service
  • You might not want to use live authentication details for an external service
  • You might not want to have to use 2FA every time you login to your app.

Many of these issues can be handled with simple configuration - for example you may point to a local development mail server instead of the production mail server for email. This is simple to do with the new configuration system and is a great option in many cases.

However sometimes configuration just can't handle everything you need. For example, say call an external api to retrieve currency rates. If that api costs money, you obviously don't want to be calling it in development. However you can't necessarily just use configuration to point to a different endpoint - you would need a different endpoint that delivers data in the same format etc which is likely not available to you.

Instead, a better way to handle the issue would be to use a facade around the external api call, and creating two separate implementations - one that uses the external api, the other that just returns some dummy data. In production you can use the live api, while in development you can use the dummy service, without having to worry about the external service at all.

For example, you might create code similar to the following:

public interface ICurrencyRateService
{
    ICollection<CurrencyRate> GetCurrencyRates();
}

public class ExternalCurrencyRateService : ICurrencyRateService
{
    private readonly IExternalService _service;
    public ExternalCurrencyRateService(IExternalService service)
    {
        _service = service;
    }

    ICollection<CurrencyRate> GetCurrencyRates()
    {
        return _service.GetRates();
    }
}

public class DummyCurrencyRateService : ICurrencyRateService
{
    public ICollection<CurrencyRate> GetCurrencyRates()
    {
        return new [] {
            new CurrencyRate {
                Currency = "GBP",
                Rate = 1.00
            },
            new CurrencyRate {
                Currency = "USD",
                Rate = 1.31
            }
            // ...more currencies
        };
    }
}

In these classes we define an interface, a live implementation which uses the IExternalService, and a dummy service which just returns back some dummy data.

A first attempt

In my first attempt to hook these two services up, I tried just injecting the IHostingEnvironment directly into the ConfifgureServices call in my Startup class, like so:

public void ConfigureServices(IServiceCollection services, IHostingEnvironment env)
{
    // Add required services.
}

Many methods in the ASP.NET Core framework allow this kind of dependency injection at the method level. For example you can inject services into the Startup.Configure method when configuring your app, or into the Invoke method when creating custom middleware. Unfortunately in this case, the method must be exactly as described, otherwise your app will crash on startup with the following error:

Unhandled Exception: System.InvalidOperationException: The ConfigureServices method 
must either be parameterless or take only one parameter of type IServiceCollection.

Doh! This does kind of make sense as until you have configured the services by calling the method, you don't have a service provider to use to inject them!

The right way

Luckily, we have a simple alternative. The Startup class itself may contain a constructor which accepts an instance of IHostingEnvironment. By convention this method creates the IConfigurationRoot using a ConfigurationBuilder and saves it to a property on Startup called Configuration.

We can easily take a similar approach for IHostingEnvironment by saving it to a property on Startup, for use later in ConfigureServices. Our Startup method would look something like this:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        Configuration = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .Build();

        HostingEnvironment = env;
    }

    public IConfigurationRoot Configuration { get; }
    public IHostingEnvironment HostingEnvironment { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        if (HostingEnvironment.IsDevelopment())
        {
            services.AddTransient<ICurrencyRateService, DummyCurrencyRateService>();
        }
        else
        {
            services.AddTransient<ICurrencyRateService, ExternalCurrencyRateService>();
        }

        // other services
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
            // middleware configuration
    }
}

As you can see, we simply save the inject IHostingEnvironment in the constructor to the HostingEnvironment property. Later, in the ConfigureServices method, we can check the environment, and if in a development setting, inject the appropriate ICurrencyRateService.

Summary

The technique shown above will not always be necessary, and generally speaking configuration will probably be a simple and more intuitive route to handling different behaviour based on environment. However the dependency injection container is also a great point to switch out your services. If you are using a third party container there are often other ways of achieving the same effect with their native APIs, for example the profiles feature in StructureMap.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?