0

I am trying to implement the service and repository patterns in my ASP.NET Core 6 based web application (using EF Core), but I think I am doing a few things wrong. In my controllers I inject a service instance which I can then use like this to get, create, update or delete entities:

[HttpPost]
public async Task<IActionResult> CreateProject([FromBody] Project body)
{
    int projectId = await this.projectService.CreateProjectAsync(body);

    return CreatedAtAction(nameof(GetProject), new { id = projectId }, null);
}

The CreateProjectAsync function then performs validations (if necessary) and calls the corresponding CreateProjectAsync function of the ProjectRepository class. One important thing to note is that the Project class is created by myself and serves as view model, too. It is mapped to the corresponding EF Core type (such as TblProject) in the repository before it is created/updated in the database or after it has been read from the database.

This approach works fine in many cases but I often encounter problems when I need to use transactions. One example would be that in addition to projects, I also have related entities which I want to create at the same time when creating a new project. I only want this operation to be successful when the project and the related entities were both successfully created, which I cannot do without using transactions. However, my services are not able to create a transaction because they are not aware of the EF Core DbContext class, so my only option right now is to create the transactions directly in the repository. Doing this would force me to create everything in the same repository, but every example I've seen so far suggests to not mix up different entities in a single repository.

How is this usually done in similar projects? Is there anything wrong with my architecture which I should consider changing to make my life easier with this project?

Chris
  • 1,417
  • 4
  • 21
  • 53
  • Your question is rooted in the repository pattern that keeps you from saving entire object graphs in one `SaveChanges` call in one EF context. If the pattern makes that impossible, either modify it or ditch it. – Gert Arnold Jan 25 '22 at 10:15

1 Answers1

1

Great question! By default, the EF context is registered as a scoped dependency - so one per request. You could create a simple transaction service (registered as a scoped dependency) that would expose transaction handling. This service could be injected into both your repositories and other service layers as need be.

By returning the transaction object when a transaction is begun, you could grant each layer autonomy over committing or rolling back it's own transaction.

You could also add some cleanup logic to the Dispose method of the transaction service to commit or rollback any open transactions when the service is being disposed.

Edit for personal practice: I have abandoned the repository pattern for several projects except for frequent get operations. Using the EF context directly in service layers allows me to take advantage of navigation properties to perform complex multi-table inserts or edits. I also get the added benefit of a single round trip to the database to execute these operations, as well as implicit transaction wrapping. Personally, it gets more and more difficult to make the case for the repository pattern on EF projects - other than you get really locked in to EF because it is all over the lower service layers.

John Glenn
  • 1,469
  • 8
  • 13
  • Thanks for the answer! I just did some more reasearch and came across `TransactionScope` which seems to be exactly what I was looking for. Do you know if that is the right approach or should I rather implement it as you suggested? – Chris Feb 01 '22 at 15:17
  • 1
    @Chris - Microsoft recommends moving away from TransactionScope for most use cases. The details of why (e.g., the limitations and operational differences) get covered here: https://learn.microsoft.com/en-us/ef/ef6/saving/transactions#using-transactions-with-other-features – John Glenn Feb 02 '22 at 04:13
  • I see, but I am still having problems wrapping my head around how to implement your suggestion in my project. In some cases I have repository functions where I insert data into two tables. For inserting into the second table I need the ID that was assigned when inserting into the first table, so I call `SaveChanges()` after the first insert to be able to get the ID. How would the transaction service look like so I can still keep doing this while also being able to create transactions in my service layers? Do you have an example that would make this more clear? – Chris Feb 02 '22 at 10:06
  • If you use navigation properties in Entity Framework, you don't need to get the ID - EF will do it for you. However, you should still be able to get the ID after saving changes as long as you are within the transaction. Are your tests showing otherwise? – John Glenn Feb 04 '22 at 03:32
  • No, but because of missing primary keys in some tables in the database I was not able to use the navigation properties everywhere, so I sometimes had to manually call `SaveChanges()` to get the ID. However, yesterday I was finally able to clarify this with the database designer so it has been fixed now. – Chris Feb 04 '22 at 08:17