-
Notifications
You must be signed in to change notification settings - Fork 133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Button on Grid with selectable row causing threading problems for Entity Framework #25
Comments
Event propagation stopping is not enabled on .net Core 3.0. My sample changes the page to avoid this issue. But if you remain on the same page, .net core doesn't stop event propagation. It´s a known bug, but Microsoft couldn't implement a solution for the 3.0 release. They have just implemented it on 3.1 Preview 2: We should have to wait until Preview 2 release to verify that it works as expected. |
.NET Core 3.1 Preview 2 has been released: Look at the "Stop event propagation in Blazor apps" section |
Good news regarding stopping the event propagation in Core 3.1. However, I wonder.....I also have a "traditional" web application that fires lots of AJAX calls based on UI events (some instigated by the user, others not) and these all pull data from our DB. With Blazor and the connected SignalR approach, I can't see what will prevent multiple calls against the same EF context (unless we implement locking). |
I assume that you are using dependency injection for the DbContext. In this case, how are you registering the DbContext in Startup.cs? As a transient service or a scope one? Scoped service should be enough. But if you are currently using a scoped service, try with a transient one. |
I tried with Transient, but that resulted in an error, so using
|
Have you tried this? services.AddDbContext<MyContext>(ServiceLifetime.Transient);
services.AddTransient<MyService>(); |
I hadn't tried that combination, and t appears to work as expected, which is great (Thanks!). But which is the better option? To instantiate a new Service and Context for each "call-back", or to have a longer running Service with locking? I think the transient way is the better....anyone disagree? |
As an FYI: an alternative to:
Is to use
|
Thanks for the info. I'm currently developing CRUD functionality for the GridBlazor component: https://github.com/gustavnavar/Grid.Blazor/tree/crud And in this case, neither Razor Page
@inject MyService Service
Startup
services.AddDbContext<MyContext>(ServiceLifetime.Transient);
services.AddTransient<MyService>(); nor Razor Page
@inherits OwningComponentBase<Data.MyService>
Startup
services.AddDbContext<MyContext>();
services.AddScoped<MyService>(); work as expected on Blazor Server App projects. The best result I got was manually controlling the live time of the DbContext object. Service is not using DI for the DbContext. It is unsing DI for DbContextOptions What I'm currently using for the CRUD branch is: Startup
--------
services.AddDbContext<NorthwindDbContext>(options =>
{
options.UseSqlServer(ConnectionString);
});
services.AddScoped<IOrderService, OrderService>(); Razor Page
------------
@inject IOrderService orderService OrderService
--------------
public class OrderService : IOrderService
{
private readonly DbContextOptions<NorthwindDbContext> _options;
public OrderService(DbContextOptions<NorthwindDbContext> options)
{
_options = options;
}
public ItemsDTO<Order> GetOrdersGridRows(Action<IGridColumnCollection<Order>> columns,
QueryDictionary<StringValues> query)
{
using (var context = new NorthwindDbContext(_options))
{
var repository = new OrdersRepository(context);
var server = new GridServer<Order>(repository.GetAll(), new QueryCollection(query),
true, "ordersGrid", columns)
.Sortable()
.WithPaging(10)
.Filterable()
.WithMultipleFilters()
.Groupable(true)
.Searchable(true, false);
// return items to displays
var items = server.ItemsToDisplay;
// uncomment the following lines are to test null responses
//items = null;
//items.Items = null;
//items.Pager = null;
return items;
}
}
public ItemsDTO<Order> GetOrdersGridRows(QueryDictionary<StringValues> query)
{
using (var context = new NorthwindDbContext(_options))
{
var repository = new OrdersRepository(context);
var server = new GridServer<Order>(repository.GetAll(), new QueryCollection(query),
true, "ordersGrid", null).AutoGenerateColumns();
// return items to displays
return server.ItemsToDisplay;
}
}
public ItemsDTO<OrderDetail> GetOrderDetailsGridRows(Action<IGridColumnCollection<OrderDetail>> columns,
object[] keys, QueryDictionary<StringValues> query)
{
using (var context = new NorthwindDbContext(_options))
{
int orderId;
int.TryParse(keys[0].ToString(), out orderId);
var repository = new OrderDetailsRepository(context);
var server = new GridServer<OrderDetail>(repository.GetForOrder(orderId), new QueryCollection(query),
true, "orderDetailssGrid" + keys[0].ToString(), columns)
.Sortable()
.WithPaging(10)
.Filterable()
.WithMultipleFilters();
// return items to displays
var items = server.ItemsToDisplay;
return items;
}
}
public async Task<Order> GetOrder(int OrderId)
{
using (var context = new NorthwindDbContext(_options))
{
var repository = new OrdersRepository(context);
return await repository.GetById(OrderId);
}
}
public async Task UpdateAndSave(Order order)
{
using (var context = new NorthwindDbContext(_options))
{
var repository = new OrdersRepository(context);
await repository.Update(order);
repository.Save();
}
}
public async Task<Order> Get(params object[] keys)
{
using (var context = new NorthwindDbContext(_options))
{
int orderId;
int.TryParse(keys[0].ToString(), out orderId);
var repository = new OrdersRepository(context);
return await repository.GetById(orderId);
}
}
public async Task Insert(Order item)
{
using (var context = new NorthwindDbContext(_options))
{
var repository = new OrdersRepository(context);
await repository.Insert(item);
repository.Save();
}
}
public async Task Update(Order item)
{
using (var context = new NorthwindDbContext(_options))
{
var repository = new OrdersRepository(context);
await repository.Update(item);
repository.Save();
}
}
public async Task Delete(params object[] keys)
{
using (var context = new NorthwindDbContext(_options))
{
var order = Get(keys);
var repository = new OrdersRepository(context);
await repository.Delete(order);
repository.Save();
}
}
} Each service method creates a DbContext to control its livetime using: using (var context = new NorthwindDbContext(_options))
{ } |
Oh, very interesting... When you say:
what was the problem? Were you getting the same threading issue, or did it present a different issue? Either way, I'm gonig to adopt your approach of controlling the lifetime (and keep |
The issues are not related to threading and getting InvalidOperationException. The issues where related to showing data on the grid that has not been committed. I'm using a component for CRUD that call to several subcomponents to paint the grid and the 4 crud forms. But its only one component that changes its state to show one of 5 subcomponents:
So it's important that the changes are committed when changing from one subcomponent to another one. Using Transient services one can not control that each subcomponent uses a new and fresh DbContext and that it is disposed after finishing the operation. I had 3 issues depending on using Scope, Transient or OwningComponentBase:
Neither of these issues were present in Blazor WebAssembly. And with any combination of Scope, Transient or OwningComponentBase configuration I got at least one of the issues. If you use this version of the code: As soon as I started controlling the lifetime of DBContext, all issues disappeared and the behavior was the same for Blazor WebAssembly and Blazor Server App projects. You can see how it works with this version of the code: |
I had many of the issues that @gustavnavar had, and only creating a separate context for each EF call resolved them. It seems like a bit hacky, because we are overriding the lifetime of the service when it really should be set in startup. I can't help feeling that there is still not enough guidance on EF and Blazor. Despite reading all the info on blog posts and github issues regarding this, only the above implementation worked, both transient and OwningComponent caused different issues. |
i also experience similar issues as described by @gustavnavar so i agree with @robalexclark that more guidance on the "best practice" using EF with blazor is needed. |
So the pattern that seems to work for us is to instantiate the Context manually, as follows: Startup
MyContext
MyService
|
This probably isn't a bug, more likely to be a hole in my understanding..
I am using the Blazor Server architecture, .NET Core 3.1 (currently still beta).
Setup
I have my EntityFramework's Context, and configure this in my startup using:
services.AddDbContext<MyContext>();
I have a Service, which contains all my EF queries. This is configured in my startup using:
services.AddScoped<MyService>();
I rely on the Blazor framework to DI the MyContext into my MyService, and MyService into my razor page:
and "@Inject MyService Service" in the razor page.
When I create my Grid, I set it to be searchable and have a column that includes an edit button:
c.Add().RenderComponentAs<GridButton>(new List<Action<object>> { this.ShowModalEdit }, new GridButtonParams { ButtonClass = "btn btn-sm btn-primary", ButtonText = "Edit" });
So, what happens?
When the user clicks a row, then the OnRowClick event is fired, this calls an 'async void' event handler that makes an awaited call to the Service. The Service returns full details of the item and displays it in a component to the right of the grid. Very similar to an example in the help docs provided.
Now, if the user clicks the [Edit] button on the row, then this opens a Modal window which is a Razor component, which has access to the same service. It makes an awaited call to the same service to pull the item to be edited.
So the problem is that clicking on the button also selects the row, so I'm making 2 calls through to the same Service and then I get an error from the Entity Framework complaining that two threads are accessing the Context at the same time:
I can see various ways to fix this (have separate contexts, add locking around the call, etc, etc). What's considered the "best practice"?
The text was updated successfully, but these errors were encountered: