Problem:
We have a Blazor server app with a DevExpress grid component, showing data directly from the DB. We want all the operations - filtering, grouping, etc. - to take place on the DB layer, that’s why we don’t use a Service layer to fetch the data, rather we hook directly onto the DB context.
Let’s say we have 2 users, looking at the same grid, each in his own browser (that implicitly means 2 different SignalR connections). User 1 changes the state, but user 2 isn’t aware of that, even if he refreshes the grid. Only when user 2 refreshes the page (F5) are the differences shown.
Explanation:
DB contexts are “scoped DI” by default. In a classic HTTP request-response architecture, that means that for the duration of a request, one and the same instance of the DB context is provided by the DI to all who request it. In the example above, data would be refreshed, because each request will instantiate a new DB context.
In a Blazor app, things are different. DB context in our case is not refreshed with each WEB request. Actually, the term ‘request’ doesn’t even exist in SignalR (WebSocket) and WebAssembly. So, what happens in our example? As long as the SignalR connection is alive, user 2 has the same instance of the DB context. If another user changes state in his own instance of the context, these changes aren’t propagated to other context instances. Roughly, this means that a ‘scoped’ DB context actually becomes a ‘singleton’ (well, almost, singleton in the scope of a user / session / signalR connection).
Links:
- https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection
- https://docs.microsoft.com/en-us/aspnet/core/blazor/blazor-server-ef-core
- https://www.thinktecture.com/blazor/dependency-injection-scopes-in-blazor/
Thoughts:
- Our service layer is stateless, so it isn't a problem. DB contexts are problematic
- Blazor doesn’t have a concept of a ‘scoped’ service
- ‘Scoped’ is actually a singleton in the scope of a single connection
- ‘Singleton’ provides the same service for all the connections
- There is an approximation of a scoped service, scoped to the ‘component’ level
- Each razor component will use the same instances in its lifetime
- But this lifetime can be long lived nonetheless
- Another, similar approximation
- If truth be told, things are pretty similar to the classic request-response architecture: if 2 requests would happen at exactly the same time, there would be 2 DB context instances with different states. This surely can happen, but the probability of it is low, so it’s not such a problem
- Having a ‘transient’ DB context also isn’t OK
- we want our API (service layer) methods to be a “unit of work” (1 API - 1 DB transaction)
- one API can call multiple BL functions, each in a separate ServiceBL class - those should have the same DB context instances
Solutions:
- Scoped is already treated almost the same as a singleton. What if we would register DB contexts as singletons?
- Sounds like a bad idea - everybody would use one long-lived instance, it would/could present a bottleneck, what about thread safety?
- "EF Core does not support multiple parallel operations being run on the same context instance"
- ‘Page refresh‘ in the right places can be a substitute for ‘scopes’
- await JSRuntime.InvokeVoidAsync("location.reload");
- NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true);
- When the ‘refresh data grid’ button is clicked, we can create a new instance of the DB context
- This is only a solution for this specific case, though. The underlying problem still exists, multiple users have different instances of DB contexts which will sooner or later blow up in our faces
- API methods are our unit-of-work. We could manually create a DI scope, use it for the duration of the API and then dispose of it. But that would mean we would have to bubble the services (at least DB context) down to each and every class that would need them :/
Any ideas would be much appreciated