Reputation: 3208
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:
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:
Upvotes: 3
Views: 505
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
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