Search code examples
c#blazorblazor-server-side

Is Transient an acceptable scope for injecting a service in Blazor Server that uses DbContext?


I'm working on my first Blazor Server project and I am slowly fixing a lot of initial design errors that I made when I started out. I've been using C# for a while, but I'm new to web development, new to ASP.Net, new to Blazor, and new to web architecture standards, hence why I made so many mistakes early on when I didn't have a strong understanding of how best to implement my project in a way that promotes clean code and long term maintainability.

I've recently restructured my solution so that it follows the "Clean Architecture" outlined in this Microsoft documentation. I now have the following projects, which aim to mirror those described in the document:

  • CoreWebApp: A Blazor project, pages and components live here.
  • Core: A Class Library project, the domain model, interfaces, business logic, etc, live here.
  • Infrastructure: Anything to do with having EF Core access the underlying database lives here, ie ApplicationDbContext, any implementations of Repositories, etc.

I am at a point where I want to move existing implementations of the repository pattern into the Infrastructure project. This will allow me to decouple the Core project from the Infrastructure project by utilising the Dependency Injection system so that any business logic that uses the repositories depends only on the interfaces to those repositories (as defined in Core) and not the actual implementations themselves (to be defined in Infrastructure).

Both the Microsoft documentation linked above, and this video by CodeWrinkles on YouTube make the following two suggestions on how to correctly use DbContext in a Blazor Server project (I'll talk specifically about using DbContext in the context of a repository):

  1. Scope usage of a repository to each individual database request. Basically every time you need the repository you instantiate a new instance, do what needs to be done, and as soon as the use of the repo goes out of scope it is automatically disposed. This is the shortest lived scope for the underlying DbContext and helps to prevent concurrency issues, but also forgoes the benefits of change tracking.
  2. Scope the usage of a repository to the lifecycle of a component. Basically you create an instance of a repository in OnInitialisedAsync, and destroy the repository in the Dispose() method of the component. This allows usage of EF Cores change tracking.

The problem with these two approaches is that they don't allow for use of the DI system, in both cases the repository must be new'd and thus the coupling between Core and Infrastructure remains unbroken.

The one thing that I can't seem to understand is why case 2 can't be achieved by declaring the repository as a Transient service in Program.cs. (I suppose case 1 could also be achieved, you'd just hide spinning up a new DbContext on every access to the repository within the methods it exposes). In both the Microsoft documentation and the CodeWrinkles video they seem to lean pretty heavily on this wording for why the Transient scope isn't well aligned with DbContext:

Transient results in a new instance per request; but as components can be long-lived, this results in a longer-lived context than may be intended.

It seems counterintuitive to make this statement, and then provide a solution to the DbContext lifetime problem that will enable a lifetime that will align with the stated problem.

Scoping a repository to the lifetime of a component seems, to me, to be exactly the same as injecting a Transient instance of a repository as a service. When the component is created a new instance of the service is created, when the user navigates away from the page this instance is destroyed. If the user comes back to the page another instance is created and it will be different to the previous instance due to the nature of Transient services.

What I'd like to know is if there is any reason why I shouldn't create my repositories as Transient services? Is there some deeper aspect to the problem that I've missed? Or is the information that has been provided trying to lead me into not being able to take advantage of the DI system for no apparent reason? Any discussion on this is greatly appreciated!


Solution

  • It's a complex issue. With no silver bullet solution. Basically, you can't have your cake and eat it.

    You either use EF as an [ORM] Object Request Mapper or you let EF manage your complex objects and in the process surrender your "Clean Design" architecture.

    In a Clean Design solution, you map data classes to tables or views. Each transaction uses a "unit of work" Db Context obtained from a DBContextFactory. You only enable tracking on Create/Update/Delete transactions.

    An order is a good example.

    A Clean Design solution has data classes for the order and order items. A composite order object in the Core domain is built by make two queries into the data pipeline. One item query to get the order and one list query to get the order items associated with that order.

    EF lets you build a data class which includes both the order data and a list of order items. You can open that data class in a DbContext, "process" that order by making changes and then call "SaveAsync" to save it back to the database. EF does all the complex stuff in building the queries and tracking the changes. It also holds the DbContext open for a long period.

    Using EF to manage your complex objects closely couples your application domain with your infrastructure domain. Your application is welded to EF and the data stores it supports. It's why you will see some authors asserting that implementing the Repository Pattern with EF is an anti-pattern.

    Taking the Order example above, you normally use a Scoped DI View Service to hold and manage the Order data. Your Order Form (Component) injects the service, calls an async get method to populate the service with the current data and displays it. You will almost certainly only ever have one Order open in an SPA. The data lives in the view service not the UI front end.

    You can use transient services, but you must ensure they:

    1. Don't use DBContexts
    2. Don't implement IDisposable

    Why? The DI container retains a reference to any Transient service it creates that implements IDisposable - it needs to make sure the service is disposed. However, it only disposes that service when the container itself is disposed. You build up redundant instances until the SPA closes down.

    There are some situations where the Scoped service is too broad, but the Transient option isn't applicable such as a service that implements IDisposable. Using OwningComponentBase can help you solve that problem, but it can introduce a new set of problems.

    If you want to see a working Clean Design Repository pattern example there's an article here - https://www.codeproject.com/Articles/5350000/A-Different-Repository-Pattern-Implementation - with a repo.