heringer
heringer

Reputation: 3208

How to configure EF Core connection context to decouple packages dependencies?

I made a library to deal with persistence using the Entity Framework Core. It is used by a Web API targeting a SQL Server database. Furthermore, I test it (unit tests) targeting a SQLite database.

I accomplished it in a straightforward way with a configuration file. So, in my class that extends the DBContext, I implemented OnConfiguring with something like this:

if(isSqlServer) //don't mind where it comes from, it works fine
    optionsBuilder.UseSqlServer(connectionString);
else
    optionsBuilder.UseSqlite(connectionString);

It seems to work fine, but it has an undesired effect. Supposed I have this projects:

  1. PersistenceLayer
  2. PersistenceLayerTests
  3. WebAPI

Since the method UseSqlite is a static extension method in a class of the package Microsoft.EntityFrameworkCore.Sqlite and, similarly, the method UseSqlServer is a static extension method in a class of the package Microsoft.EntityFrameworkCore.SqlServer, two things bother me.

First, I have to include both dependencies in the three projects to avoid the error "System.IO.FileNotFoundException: Could not load file or assembly Microsoft.EntityFrameworkCore.SqlServer (...)" at runtime.

Second, if I need to support a new DBMS (Oracle, MySQL), I have to include new dependencies on the three projects.

I would expect to build a scenario where the project PersistentLayer would not have those dependencies, while PersistentLayerTests would depend solely on Microsoft.EntityFrameworkCore.Sqlite and WebAPI would depend solely on Microsoft.EntityFrameworkCore.SqlServer.

Is there another way to configure the connection context to decouple these packages dependencies?

Notes:

  1. I did that with NHibernate, that uses different drivers implementations for each DBMS.
  2. I tried using reflection, but wasn't elegant and I noticed it would not work if an extension method have different parameters.
  3. I don't need a full solution. If someone can point me another way, it will be very helpful.

Upvotes: 3

Views: 505

Answers (2)

Stefan
Stefan

Reputation: 17678

This seems overkill but, basic strategy in such cases is to put a common interface and separate them in different projects. Not sure if this is the optimal solution for this case.

Then the different projects are independent (except for the common interface) and can easily expanded after time.

The crucial part, is to in you main project only reference to the interface assembly and not to one of the concretes. The concretes (assemblies) must be loaded at run time if you want to avoid the FileNotFoundException: furthermore: you need a mechanism to pick the appopiate one, commonly referred to as a factory.

Lets consider the case in which you have 3 of these (sql, sqllite and oracle):

First thing to do is to create a common interface to communicate with the appropiate Db enginge, e.g.:

//separate assembly
public interface IDbEningeSelector
{
    //added the option builder for simplicity: one could do better.
    void Configure(string connectionString,IOptionsBuilder optionsBuilder);
}

Next, just create 3 separate projects for the concrete implementations, one sql-lite, one sql, and one oracle.

Next: create, in the three projects, a class that implements this:

//3 of these.
public class SqlEnging : IDbEngineSelector
{
    public void Configure(string connectionString,IOptionsBuilder optionsBuilder)
    {
         optionsBuilder.UseSqlServer(connectionString);
    }
}

Ok, now you have a choice to pick one of them.

//the "thing"
IDbEngineSelector selector = null;

selector = //resolve through factory, possibly based on a flag. 

selector.Configure(connectionString, optionsBuilder);

And basically you are done and left with an expandable, dynamically referred database provider system.

For the dynamic assembly resolver, you'll need to load them as described in this article: https://www.codeproject.com/Articles/1194332/Resolving-Assemblies-in-NET-Core

That will actually load the dependencies at run-time, I might be a good idea to reserve a speial path for this purpose where you place those assemblies.

I left out the factory implementation details, I need to check the proper way to do that in .net Core first, hold on...

Upvotes: 2

Adam Simon
Adam Simon

Reputation: 2970

You just need to make the users (PersistenceLayerTests and WebApi projects) of your DbContext responsible for configuring it instead of the DbContext itself.

DbContext provides a constructor accepting a DbContextOptions. You need to expose this constructor:

public class MyDbContext : DbContext
{
    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { }

    // ...
}

Then this enables you to do the following:

var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();

if (isSqlServer) //don't mind where it comes from, it works fine
    optionsBuilder.UseSqlServer(connectionString);
else
    optionsBuilder.UseSqlite(connectionString);

using (var context = new MyDbContext(optionsBuilder.Options))
{
    // ...
}

Upvotes: 2

Related Questions