DEV Community

loading...

Dependency Injection with Connection Strings

Justin J Stark
Hello
Updated on ・1 min read

One issue with dependency injection is non-class dependencies such as connection strings cannot be automatically resolved by the IoC container. If you use Dapper or straight ADO.NET, you have likely run into this. Fortunately, there is a pattern to use that is simple and works with every container.

Let's say we are building a student gradebook which connects to a database to store grades. It also connects to a student registration system over HTTP. So we have a couple components defined.

public class GradeRepository : IGradeRepository
{
    private readonly string _gradebookConnectionString;
    private readonly ICacheService _cacheService;

    public GradeRepository(string gradebookConnectionString, ICacheService cacheService)
    {
        _gradebookConnectionString = gradebookConnectionString;
        _cacheService = cacheService;
    }
}

public class RegistrationRepository : IRegistrationRepository
{
    private readonly string _registrationApiBaseUrl;
    private readonly string _registrationClientId;
    private readonly ICacheService _cacheService;

    public RegistrationRepository(string registrationApiBaseUrl, string registrationClientId, ICacheService cacheService)
    {
        _registrationApiBaseUrl= registrationConnectionString;
        _registrationClientId = registrationClientId;
        _cacheService = cacheService;
    }
}

We are using constructor injection to pass the dependent configuration and utility to each component. Each component takes an ICacheService that will handle caching the responses.

Now, let's wire up our two repositories with an IoC container. In this demonstration we will use Autofac but this pattern works with any container.

var builder = new ContainerBuilder();

builder.RegisterType<CacheService>().As<ICacheService>();

builder.Register(ctx => new GradeRepository
    (
        gradebookConnectionString: ConfigurationManager.ConnectionStrings["GradebookConnectionString"].ConnectionString,
        cacheService: ctx.Resolve<ICacheService>()
    ))
    .As<IGradeRepository>();

builder.Register(ctx => new RegistrationRepository
    (
        registrationApiBaseUrl: ConfigurationManager.AppSettings["RegistrationApiBaseUrl"],
        registrationClientId: ConfigurationManager.AppSettings["RegistrationClientId"],
        cacheService: ctx.Resolve<ICacheService>()
    ))
    .As<IRegistrationRepository>();

var container = builder.Build();

This works but it is a little ugly. It starts getting really ugly as we add more and more components with string parameters. Every registration requires a factory function where we look up the configuration variables.

The problem is the IoC container does not know how to resolve strings. There are several ways to improve this. For one, we could try to resolve the string parameters using some fancy container functionality that injects a value based on the parameter name and type. But this is finicky and support varies by container.

Instead, let's get rid of our string dependencies altogether. We will move all configuration into a single configuration class and then pass the class into every component that needs it.

public class ConfigurationProvider : IConfigurationProvider
{
    private readonly string _gradebookConnectionString;
    private readonly string _registrationApiBaseUrl;
    private readonly string _registrationClientId;

    public ConfigurationProvider(string gradebookConnectionString, string registrationApiBaseUrl, string registrationClientId)
    {
        _gradebookConnectionString = gradebookConnectionString;
        _registrationApiBaseUrl= registrationApiBaseUrl;
        _registrationClientId = registrationClientId;
    }
}

This is pretty clean and gives us a single place where our app-level configuration variables are stored. It will also, hopefully, prevent our colleagues and future code maintainers from trying to set configuration variables in different parts of the application in different, inconsistent ways.

Now that our configuration class is defined, let's pass it into our components.

public class GradeRepository : IGradeRepository
{
    private readonly IConfigurationProvider _configurationProvider;
    private readonly ICacheService _cacheService;

    public GradeRepository(IConfigurationProvider configurationProvider, ICacheService cacheService)
    {
        _configurationProvider = configurationProvider;
        _cacheService = cacheService;
    }
}

public class RegistrationRepository : IRegistrationRepository
{
    private readonly IConfigurationProvider _configurationProvider;
    private readonly ICacheService _cacheService;

    public RegistrationRepository(IConfigurationProvider configurationProvider, ICacheService cacheService)
    {
        _configurationProvider = configurationProvider;
        _cacheService = cacheService;
    }
}

Notice the number of dependencies our component takes has decreased. If our component requires 10 different configuration variables, its signature is the same.

Now let's update the container registration. We no longer need to specify a factory function for components that require configuration variables.

builder.Register(ctx => new ConfigurationProvider(
        gradebookConnectionString: ConfigurationManager.ConnectionStrings["GradebookConnectionString"].ConnectionString,
        registrationApiBaseUrl: ConfigurationManager.AppSettings["RegistrationApiBaseUrl"],
        registrationClientId: ConfigurationManager.AppSettings["RegistrationClientId"]
    ))
    .As<IConfigurationProvider>()
    .SingleInstance();

builder.RegisterType<CacheService>().As<ICacheService>();
builder.RegisterType<GradeRepository>().As<IGradeRepository>();
builder.RegisterType<RegistrationRepository>().As<IRegistrationRepository>();

That's it. We can keep registering more and more components without messy factory functions. In addition, all our application configuration is pulled from the configuration files in one spot when our application starts.

I have tried a lot of different ways to inject configuration variables into my components. This is the simplest method and the easiest to configure with any IoC container.

Discussion (1)

Collapse
ngnasr1123 profile image
ngnasr1123

Hi Justin. Great article. Very helpful. What do you do in cases of primitive types which are not retrieved from app settings? Let's say we want to construct an object that is dependent on a customer id. If the construction of our dependency graph is meant to happen in the root class/entry point of our application, how would we go about injecting this parameter?