Using DbContext SaveChanges with Transactions

As MSDN confirms , in EF 5 onwards, the DbContext class is a combination of Unit-Of-Work and Repository patterns. "In the web applications that I create, I try to implement repository templates and units of measure on top of the existing DbContext class. Recently, like many others, I have found that this is too much in my script. Iโ€™m not worried about the main storage mechanism, that has ever changed with SQL Server, and although I appreciate the benefits that unit testing will bring, I still have a lot to learn before implementing it in a real application.

Thus, my solution is to use the DbContext class directly as a repository and Unit-Of-Work, and then use StructureMap to enter one instance per request into separate service classes, which allows them to do work in context. Then in my controllers I add each service I need and accordingly process the methods needed for each action. In addition, each request ends with a transaction created with DbContext at the beginning of the request, and either rolls back if there is any type of exception (be it an EF error or an application error), or committed if all is well. The following is an example code script.

This example uses the Territory and Shipper tables from the Northwind Sample Database. In this control controller, the territory and the shipper are added at the same time.

controller

public class AdminController : Controller { private readonly TerritoryService _territoryService; private readonly ShipperService _shipperService; public AdminController(TerritoryService territoryService, ShipperService shipperService) { _territoryService = territoryService; _shipperService = shipperService; } // all other actions omitted... [HttpPost] public ActionResult Insert(AdminInsertViewModel viewModel) { if (!ModelState.IsValid) return View(viewModel); var newTerritory = // omitted code to map from viewModel var newShipper = // omitted code to map from viewModel _territoryService.Insert(newTerritory); _shipperService.Insert(newShipper); return RedirectToAction("SomeAction"); } } 

Territory Service

 public class TerritoryService { private readonly NorthwindDbContext _dbContext; public TerritoryService(NorthwindDbContext dbContext) { _dbContext = dbContext; } public void Insert(Territory territory) { _dbContext.Territories.Add(territory); } } 

Sender Service

 public class ShipperService { private readonly NorthwindDbContext _dbContext; public ShipperService(NorthwindDbContext dbContext) { _dbContext = dbContext; } public void Insert(Shipper shipper) { _dbContext.Shippers.Add(shipper); } } 

Creating a transaction on Application_BeginRequest ()

 // _dbContext is an injected instance per request just like in services HttpContext.Items["_Transaction"] = _dbContext.Database.BeginTransaction(System.Data.IsolationLevel.ReadCommitted); 

Rollback or commit transaction on Application_EndRequest

 var transaction = (DbContextTransaction)HttpContext.Items["_Transaction"]; if (HttpContext.Items["_Error"] != null) // populated on Application_Error() in global { transaction.Rollback(); } else { transaction.Commit(); } 

Now all this works well, but the only question I have now is where is the best place to call the SaveChanges() function in DbContext? Should I call it in every service level method?

 public class TerritoryService { // omitted code minus changes to Insert() method below public void Insert(Territory territory) { _dbContext.Territories.Add(territory); _dbContext.SaveChanges(); // <== Call it here? } } public class ShipperService { // omitted code minus changes to Insert() method below public void Insert(Shipper shipper) { _dbContext.Shippers.Add(shipper); _dbContext.SaveChanges(); // <== Call it here? } } 

Or should I leave the methods of the Insert () service class as they are and just call SaveChanges () before the transaction?

 var transaction = (DbContextTransaction)HttpContext.Items["_Transaction"]; // HttpContext.Items["_Error"] populated on Application_Error() in global if (HttpContext.Items["_Error"] != null) { transaction.Rollback(); } else { // _dbContext is an injected instance per request just like in services _dbContext.SaveChanges(); // <== Call it here? transaction.Commit(); } 

Everything is good? Is it safe to call SaveChanges () more than once since it is wrapped in a transaction? Are there any problems that I encounter while doing this? Or is it best to call SaveChanges () only once before a transaction? I personally would rather just call him at the end right before the transaction, but I want to be sure that I will not miss any errors with the transactions or something is wrong? If you read this, thanks for taking the time to help. I know this was a long question.

+6
source share
2 answers

You should call SaveChanges() when it's time to do a one-time, atomic save. Since your services do not know about each other or depend on each other, internally they cannot guarantee that one or the other intends to make changes. Therefore, in this installation, I assume that each of them will have to make its own changes.

This, of course, leads to the problem that these operations may not be individually atomic. Consider this scenario:

 _territoryService.Insert(newTerritory); // success _shipperService.Insert(newShipper); // error 

In this case, you partially transferred the data, leaving the system in a slightly unknown state.

Which object in this scenario controls the atomicity of the operation? In web applications, I find that usually a controller. This is, after all, a request made by the user. In most scenarios (of course, there are exceptions), I assume that you can expect the entire request to be successful or unsuccessful.

If this is the case, and your atomicity is at the request level, then I would recommend getting DbContext from the IoC container at the controller level and passing it to services. (They already require this on their designers, so there are not big changes.) These services can work in context, but never capture the context. Then the consumer code (controller) can transmit it (or cancel it or refuse it, etc.), as soon as all services complete their operations.

While different business objects, services, etc. should internally support their own logic, I believe that usually objects that own the atomicity of operations are at the application level, driven by business processes called by users.

+4
source

Basically you create a repository, not a service.

To answer your question, you can simply ask yourself another question. "How will I use this functionality?"

You add a couple of records, deleting some records, updating some records. We could say that you call your various methods about 30 times. If you call SaveChanges 30 times, you make 30 return flights to the database, causing a lot of traffic and overhead that you CAN avoid.

I usually recommend that you make as few database visits as possible and limit the number of calls to SaveChanges (). Therefore, I recommend that you add the Save () method to your repository / service level and call it at the level that your repository / service level calls.

If it is not required to save something before doing something else, you should not call it 30 times. You must call him 1 time. If you need to save something before doing something else, you can still call SaveChanges at that absolute moment of the request in the layer invoking your repository / service level.

Summary / TL; DR: Create a Save () method at your repository / service level instead of calling SaveChanges () in each repository / service method. This will increase your productivity and save you from unnecessary overhead.

0
source

Source: https://habr.com/ru/post/972338/


All Articles