6

I'm using .Net Core 2.1. I'm using more than one DbContext. I'm creating a DbContextFactory for every context. But, I want to do this in a generic way. I want to create only one DbContextFactory. How can I achieve this?

MyDbContextFactory.cs

public interface IDbContextFactory<TContext> where TContext : DbContext
{
    DbContext Create();
}

public class MyDbContextFactory : IDbContextFactory<MyDbContext>
{
    public IJwtHelper JwtHelper { get; set; }

    public MyDbContextCreate()
    {
        return new MyDbContext(this.JwtHelper);
    }

    DbContext IDbContextFactory<MyDbContext>.Create()
    {
        throw new NotImplementedException();
    }
}

UnitOfWork.cs

public class UnitOfWork<TContext> : IUnitOfWork<TContext> where TContext : DbContext
{
    public static Func<TContext> CreateDbContextFunction { get; set; }
    protected readonly DbContext DataContext;

    public UnitOfWork()
    {
        DataContext = CreateDbContextFunction();
    }
 }

MyDbContext.cs

public class MyDbContext : DbContext
{
    private readonly IJwtHelper jwtHelper;

    public MyDbContext(IJwtHelper jwtHelper) : base()
    {
        this.jwtHelper = jwtHelper;
    }
}
14
  • 3
    I think you will find some commenters that state that DbContext is already a unit of work. stackoverflow.com/questions/17390545/… Commented Feb 18, 2019 at 14:07
  • I think you don't need this UnitOfWork. Moreover, in your current implementation you actually don't dispose DbContext and make it live much longer, than it should live. You just need a factory registered as singleton, which can create multiple database contexts and that's it. You may want to create an abstract factory to easily test it. Commented Feb 18, 2019 at 14:11
  • Not DbContext is UnitOfWork. Because, if you change something from two different context, then you can rollback by UnitOfWork. But you can't do this by DbContext @Neil Commented Feb 18, 2019 at 14:12
  • You can see my structure from stackoverflow.com/questions/54741937/inherit-from-generic-class @YeldarKurmangaliyev . Why my DbContext don't dispose? Can you explain? What is my mistake? Commented Feb 18, 2019 at 14:15
  • 1
    Yep. Totally agree with @Neil. There's no such thing as cross-database transactions. If you're depending on being able to work with multiple databases at once in a transactional way, your data integrity is doomed from the start. Commented Feb 18, 2019 at 16:37

2 Answers 2

12

So you have a database, and a class that represent this database: your DbContext, it should represent the tables and the relations between the tables that are in your database, nothing more.

You decided to separate the operations on your database from the database itself. That is a good thing, because if several users of your database want to do the same thing, they can re-use the code to do it.

For instance, if you want to create "an Order for a Customer with several OrderLines, containing ordered Products, agreed Prices, amount, etc", you'll need to do several things with your database: check if the customer already exists, check if all products already exist, check if there are enough items, etc.

These things are typically things that you should not implement in your DbContext, but in a separate class.

If you add a function like: CreateOrder, then several users can re-use this function. You'll only have to test this only once, and if you decide to change something in your order model, there is only one place where you'll have to change the creation of an Order.

Other advantages of separating the class that represents your database (DbContext) from the class that handles this data is that will be easier to change the internal structure without having to change the users of your database. You can even decide to change from Dapper to Entity Framework without having to change usage. This makes it also easier to mock the database for test purposes.

Functions like CreateOrder, QueryOrder, UpdateOrder already indicate that they are not generic database actions, they are meant for an Ordering database, not for a School database.

This might have the effect that unit-of-work might not be a proper name for the functionality you want in the separate class. A few years ago, unit-of-work was mainly meant to represent actions on a database, not really a specific database, I'm not really sure about this, because I saw fairly soon that a real unit-of-work class would not enhance functionality of my DbContext.

Quite often you see the following:

  • A DbContext class that represents your Database: the database that you created, not any generic idea of databases
  • A Repository class that represent the idea of storing your data somewhere, how this is stored, it could be a DbContext, but also a CSV-file, or a collection of Dictionary classes created for Test purposes. This Repository has an IQueryable, and functions to Add / Update / Remove (as far as needed
  • A class that represents your problem: the ordering model: Add Order / Update Order / Get Order of a Customer: this class really knows everything about an Order, for instance that it has an OrderTotal, which is probably nowhere to be found in your Ordering database.

Outside DbContext you sometimes may need SQL, for instance to improve efficiency of a call. Outside Repository it should not be seen that you are using SQL

Consider to separate the concerns: how to save your data (DbContext), how to CRUD (create, fetch, update, etc) the data (Repository), how to use the data (combine the tables)

I think what you want to do in your unit-of-work should be done inside the repository. Your Ordering class should create the Repository (which creates the DbContext), query several items to check the data it has to Add / Update, do the Add and Update and save the changes. After that your ordering class should Dispose the Repository, which in turn will Dispose the DbContext.

The Repository class will look very similar to the DbContext class. It has several sets that represent the tables. Every set will implement IQueryable<...> and allow to Add / Update / Remove, whatever is needed.

Because of this similarity in functions you could omit the Repository class and let your Ordering class use the DbContext directly. However, keep in mind, that changes will be bigger if in future you decide that you don't want to use entity framework anymore but some newer concept, or maybe return back to Dapper, or even more low level. SQL will seep through into your Ordering class

What to choose

I think you should answer several questions for yourself:

  • Is there really only one database that should be represented by your DbContext, could it be that the same DbContext should be used in a 2nd database with the same layout. Think of a test database, or a development database. Wouldn't it be easer / better testable / better changeable, to let your program create the DbContext that is to be used?
  • Is there really one group of Users of your DbContext: should everyone have the possibility to Delete? to Create? Could it be that some programs only want to query data (the program that e-mails the orders), and that order programs need to add Customers. And maybe another program needs to Add and Update Products, and the amount of Products in the warehouse. Consider Creating different Repository classes for them. Each Repository gets the same DbContext, because they are all accessing the same database
  • Similarly: only one data processing class (the above mentioned ordering class): should the process that handles Orders, be able to change product prices and add items to the stock?

The reason that you need the factories, is because you don't want to let your "main" program decide what items it should create for the purpose it is running right now. Code would be much easier if you created the items yourself:

Creation sequence for an Ordering Process:

 IJwtHelper jwtHelper = ...;

 // The product database: all functionality to do everything with Products and Orders
 ProductDbContext dbContext = new ProductDbContext(...)
 {
    JwtHelper = jwtHelper,
    ...
 };

 // The Ordering repository: everything to place Orders,
 // It can't change ProductPrices, nor can it stock the wharehouse
 // So no AddProduct, not AddProductCount,
 // of course it has TakeNrOfProducts, to decrease Stock of ordered Products
 OrderingRepository repository = new OrderingRepository(...) {DbContext = dbContext};

 // The ordering process: Create Order, find Order, ...
 // when an order is created, it checks if items are in stock
 // the prices of the items, if the Customer exists, etc.
 using (OrderingProcess process = new OrderingProcess(...) {Repository = repository})
{
     ... // check if Customer exists, check if all items in stock, create the Order
     process.SaveChanges();
}

When the Process is Disposed, the Repository is Disposed, which in turns Disposes the DbContext.

Something similar for the process that e-mails the Orders: It does not have to check the products, nor create customers, it only has to fetch data, and maybe update that an order has been e-mailed, or that e-mailing failed.

 IJwtHelper jwtHelper = ...;

 // The product database: all functionality to do everything with Products and Orders
 ProductDbContext dbContext = new ProductDbContext(...) {JwtHelper = jwtHelper};

 // The E-mail order repository: everything to fetch order data
 // It can't change ProductPrices, nor can it stock the wharehouse
 // It can't add Customers, nor create Orders
 // However it can query a lot: customers, orders, ...
 EmailOrderRepository repository = new EmailOrderRepository(...){DbContext = dbContext};

 // The e-mail order process: fetch non-emailed orders,
 // e-mail them and update the e-mail result
 using (EmailOrderProcess process = new EmailOrderProcess(...){Repository = repository}
 {
     ... // fetch the orders that are not e-mailed yet
         // email the orders
         // warning about orders that can't be emailed
         // update successfully logged orders
     repository.SaveChanges();

See how much easier you make the creation process, how much more versatile you make it: give the DbContext a different JwtHelper, and the data is logged differently, give the Repository a different DbContext and the data is saved in a different database, give the Process a different Repository, and you'll use Dapper to execute your queries.

Testing will be easier: create a Repository that uses Lists to save the tables, and testing your process with test data will be easy

Changes in databases will be easier. If for instance you later decide to separate your databases into one for your stock and stock prices and one for Customers and Orders, only one Repository needs to change. None of the Processes will notice this change.

Conclusion

Don't let the classes decide which objects they need. Let the creator of the class say: "hey, you need a DbContext? Use this one!" This will omit the need of factories and such

Separate your actual database (DbContext) from the concept of storing and retrieving data (Repository), use a separate class that handles the data without knowing how this data is stored or retrieved (The process class)

Create several Repositories that can only access the data they need to perform the task (+data that can be foreseen in future after expected changed). Don't make too much Repositories, but also not one that can do everything.

Create process classes that do only what they are meant to do. Don't create one process class with 20 different tasks. It will only make it more difficult to describe what it should do, more difficult to test it and more difficult to change the task

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks a lot for your detailed answer @HaraldCoppoolse. You helped me a lot. I have to use more than one context because my databases created 3 years ago and I can't change it. So, I found a solution in this url. github.com/raghav-rosberg/UnitOfWorkWithMultipleDBContext . And copy paste to my NetCore project. And I added DbContextFactory by your helps.
But after you and others write, I fear that there will be management problems in the future of the project. Beause, my project very huge and has more than 1.000 page and millions of data in my tables. Please can you check that project in that url? Is that project written by true way? Otherwise, I will change my repository structure. @HaraldCoppoolse
The problem with several databases is another good example why it would be wise to create a separate Repository class that hides how and where the data is saved. This Repository class holds all Dbcontexts and exposes the IQueryables that you need. As long as you query data from one database, or Create / Update / Delete, there is no difference with one Dbcontext. Only if you need to join tables from two databases, then the Database Management System can't do that, so your Process class will have to use AsEnumerable() to move the selected data to your process, where it can do the (group-)join.
I haven't got a clue about the nuget project. As the Repository class that I described seems fairly easy, I advise to use that approach. Almost everything in the Repository is the same as if you only has one database. I think there won't be a lot of joins of tables from several databases, as you can't control the foreign keys
Thanks again for your helps @HaraldCoppoolse
3

If you want to reuse the existing implementation, EF Core 5 provides DbContext Factory out of the box now: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-5.0/whatsnew#dbcontextfactory

Make sure you dispose it correctly, as it's instances are not managed by the application's service provider.

See Microsoft documentation Using a DbContext factory

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.