Dependency Injection with .NETCore for DAL and connection string

18,403

Solution 1

You could follow the options pattern with the configuration framework. This allows you to define a custom type that hold your configuration settings (statically typed) while being restricted to only your actual relevant configuration.

You can use it like this:

public void ConfigureServices(IServiceCollection services)
{
    // register the `Data:DefaultConnection` configuration section as
    // a configuration for the `DatabaseOptions` type
    services.Configure<DatabaseOptions>(Configuration.GetSection("Data:DefaultConnection"));

    // register your database repository
    // note that we don’t use a custom factory where we create the object ourselves
    services.AddScoped<IRepository<IDataModel>, BaseRepository>();
}

This assumes a type DatabaseOptions like this:

public class DatabaseOptions
{
    public string ConnectionString { get; set; }
}

Then, you can just have the DatabaseOptions injected into your BaseRepository:

public class BaseRepository
{
    private readonly DatabaseOptions _options;

    public BaseRepository(IOptions<DatabaseOptions> databaseOptions)
    {
         _options = databaseOptions.Value;
    }
}

Of course, if you have subtypes of that BaseRepository, you need to register those as well and pass the options to the base class:

// register the repository as well in the `ConfigureServices` method
services.AddScoped<PrivacyLevelRepository>();
public class PrivacyLevelRepository : BaseRepository, IRepository<PrivacyLevelDM>
{
    public PrivacyLevelRepository(IOptions<DatabaseOptions> databaseOptions)
        : base(databaseOptions)
    { }
}

I am instantiating and using the repo like I always have. I am not sure how to use a class that I don't instantiate. How do i let this object know it depends on the PrivacyLevelRepository?

PrivacyLevelRepository repo = new PrivacyLevelRepository();
returnValue = repo.GetAllByDomainID(DomainID).ToList();
return returnValue;

You do not appear to understand the idea behind dependency injection yet. Dependency injection with its underlying principle Inversion of Control is simply said about avoiding the use of new to create objects. Instead of actively depending on an implementation (in your example the PrivacyLevelRepository), you are giving up the responsibility and just depend on the outer system to provide you with the dependencies you need.

So instead of creating a new PrivacyLevelRepository, you inject an instance that is created by something somewhere else. That looses coupling on the implementation of your dependency. A very practical example of this is how PrivacyLevelRepository depends on IOptions<DatabaseOptions>. You, as a consumer of that repository, should not need to care to know how to get such an object to be able to create the repository instance. You shouldn’t even need to know how to create a repository instance in the first place.

So your consumer of PrivacyLevelRepository should follow the same idea as the repository itself: The repository does not know how to get those database options; it just depends on the constructing entity to pass such an object on. And your consumer, I assume a controller, should do the same:

public class MyController
{
    private readonly PrivacyLevelRepository _privacyLevelRepository;

    public MyController(PrivacyLevelRepository privacyLevelRepository)
    {
         // instead of *creating* a repository, we just expect to get one
         _privacyLevelRepository = privacyLevelRepository;
    }

    public IActionResult SomeRoute()
    {
         var domainId = "whatever";
         var data = _privacyLevelRepository.GetAllByDomainID(domainId).ToList();
         return View(data);
    }
}

Of course, something has to create the dependencies at some point. But if you embrace dependency injection completely—which ASP.NET Core not only makes very easy but also actively requires you to do so in order to work completely—then you don’t need to care about that part. You just register the types in the ConfigureServices method and then expect the dependencies to be fulfilled where you need them.

For more information, you should definitely check out the dependency injection chapter of the documentation.

Solution 2

You shouldn't be injecting the IConfiguration at all into your classes. The IConfiguration allows access to all configuration values, while a class only requires one (or a few of them). Injecting the IConfiguration is the configuration equivalent of the Service Locator anti-pattern (but for resolving configuration values). It hides the actual used configuration values from the consumer and makes the class harder to use and test.

On top of that, this model makes it much harder to verify the correctness of your configuration file, since individual configuration values are only verified when they are requested for the first time in the application, which could be many mouse 'clicks' into the application.

The solution to this is to load and verify the configuration values at start-up and inject only the configuration value that one class requires, and nothing more. This allows the system to fail-fast and makes it very clear from the class's API what configuration value(s) it requires. Obviously, you could pack configuration values together into a single Value Object, and .NET Core makes this much simpler, which is really nice.

Another thing you should prevent is using base classes. Base classes often become ever changing and growing blocks of code with helper methods and cross-cutting concerns. Their derivatives become much harder to test, because of the hard dependency on the base class.

When you inject the connection string directly into your PrivacyLevelRepository, there is no need to have a base class with a GetSQLConnectionString, since the repository already has the connection string available. There might be other reasons why you have this base class, for instance because you want to do logging or implement security features, but my advice is to not use base classes for this. Instead use decoration and interception, because it allows to keep the 'derived' oblivious of these cross-cutting concerns and even allows a much more modular and flexible system.

UPDATE

This is the way to configure it

string conStr = config["Data:DefaultConnetion:ConnectionString"];

services.AddScoped<IRepository<IDataModel>>(c => new PrivacyLevelRepository(conStr));

Whatever you do, do not let your application components depend on IOptions<T>, since that has quite some bad consequences, as described here.

Solution 3

As Steven mentioned, do not have your application components relying on IOptions<T>.

A more suitable way to access the connection string from the IConfigurationRoot though is done as follows:

string connectionString = configuration.GetConnectionString("DefaultConnection"); //substitute "DefaultConnection" for your named connection.

Where "DefaultConnection" is the object key of you connection string in appsettings.json

Share:
18,403
BLars
Author by

BLars

Updated on June 23, 2022

Comments

  • BLars
    BLars almost 2 years

    I am new to the DI patterns with .NETCore, and I am having trouble getting my connection strings to my DAL.

    I followed the advice given in this thread via the accepted answer, and subsequent comments.

    This is my base class

    public class BaseRepository : IRepository<IDataModel>
    {
        private readonly IConfiguration config;
    
        public BaseRepository(IConfiguration config)
        {
            this.config = config;
        }
    
        public string GetSQLConnectionString()
        {
            return config["Data:DefaultConnetion:ConnectionString"];
        }
    

    This is a snippet of a repository class inheriting the base class

    public class PrivacyLevelRepository : BaseRepository, IRepository<PrivacyLevelDM>
    {
        public PrivacyLevelRepository(IConfiguration config) : base(config) { }
        public void Add(PrivacyLevelDM dataModel)
        {
             ...
        }
    }
    

    This is in my startup.cs

    public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc();
            services.AddScoped<IRepository<IDataModel>>(c => new BaseRepository(Configuration));
        }
    

    However, In my service layer, the instantiation of the repository class is still asking for the (IConfiguration config) to be passed as a parameter.

     PrivacyLevelRepository repo = new PrivacyLevelRepository();
    

    How do I get the IConfiguration loaded directly to my DAL, without having to pass it from Controller > BLL > DAL. This seems extremely inefficient, and incorrect. As the DAL should be determining the connection for an object, not the controller or service layer. They should be agnostic of the datasource, no?

    I figure this is something simple, that I am just not seeing within the DI/IoC paradigm, but I cannot figure it out.

    Edit: I am not using Entity Framework, but a custom Data layer.

    Thanx for any help.

  • BLars
    BLars about 7 years
    Thanx @Steven. I can indeed refactor to remove the base class, as that was only added by information i gathered from the other thread. Also, it does make sense to only inject the connection string instead of the entire IConfiguration. The problem, for me at least, is I don't understand how to inject the connection string into the repository at startup. That is what i tried to do, but it still required it to be passed via parameters on instantiation. services.AddScoped<IRepository<IDataModel>>(c => new BaseRepository(Configuration));
  • BLars
    BLars about 7 years
    I added the above code, and it's still not working. When i instantiate a new repository class, it requires me to pass in the connection string parameter. I did not remove the base class yet, but did update to a connection string instead of the IConfiguration. Also, your code example still used the BaseRepository, so I left it until i can get it working. Here is the error from VS(2017) Error CS7036 There is no argument given that corresponds to the required formal parameter 'conString' of 'PrivacyLevelRepository.PrivacyLevelRepository(string)'
  • BLars
    BLars about 7 years
    Thank @poke. I believe i followed everything above. I like this, but I still have the same issue I had below, when I instantiate PrivacyLevelRepository, I get an error, and it asked for the databaseOptions parameter to be passed in. This seems to be my issue regardless of which approach I try, as it was my issue before i posted, and now after two suggestions, it remains. Also, for anyone who is following this and using the code, there is an extra ">" after the PrivacyLevelDM in the AddScoped method should be: services.AddScoped<IRepository<PrivacyLevelDM>, PrivacyLevelRepository>();
  • poke
    poke about 7 years
    “when I instantiate PrivacyLevelRepository” – How exactly do you do that? The point about Dependency Injection is that you never instantiate something yourself, so there is no need to pass in the options yourself. The DI container does this for you. You just specify that you depend on a IRepository<PrivacyLevelDM> and you will get an already constructed PrivacyLevelRepository without needing to worry about where it came from. (fixed that > error, thanks)
  • BLars
    BLars about 7 years
    I am instantiating and using the repo like I always have. I am not sure how to use a class that I don't instantiate. How do i let this object know it depends on the PrivacyLevelRepository? PrivacyLevelRepository repo = new PrivacyLevelRepository(); returnValue = repo.GetAllByDomainID(DomainID).ToList(); return returnValue;
  • poke
    poke about 7 years
    I’ve expanded my answer to explain that part a bit more in detail. Basically, you need to use dependency injection everywhere. Never create services or repositories manually using new.
  • JCKödel
    JCKödel almost 7 years
    This is wrong, as you are injecting a immutable string (if application.json or environment changes, this code won't see that change)
  • Steven
    Steven almost 7 years
    @JCKodel allowing an application to see configuration changes without a restart is often tricky an hardly ever required. Since config changes are rare (not something that happens every few minutes), requiring an application restart is typically and even the best thing to do. Besides nowever in OPs question does it state he requires this value to be changed while thr application keeps running. In that respect, saying the answer "is wrong" is kinda harsh, don't you think?
  • JCKödel
    JCKödel almost 7 years
    @Steven Not in a cloud environment. Best practices always apply.
  • EGP
    EGP almost 4 years
    This is such a good answer including identifying where we can tend to go wrong switching from the "old" style to Inversion of Control. Thanks!
  • Mohammad Aghazadeh
    Mohammad Aghazadeh about 2 years
    What do we do, If we have more than one connection string? @poke
  • MC9000
    MC9000 about 2 years
    This answer doesn't seem to work for me. When I try and use _conf.ConnString in any class where this is injected, you'll get an error "An object reference is required for the non-static field..." (just when I thought I wrapped my head around DI, I keep getting stuck - ugh!)