Skip to content
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

Closed
DrGriff opened this issue Nov 1, 2019 · 14 comments

Comments

@DrGriff
Copy link

DrGriff commented Nov 1, 2019

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:

public class MyService
{
    public MyService(MyContext context)
    {
        this.Context = context;
    }

    private MyContext Context { get; }

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:

A second operation started on this context before a previous operation completed

I can see various ways to fix this (have separate contexts, add locking around the call, etc, etc). What's considered the "best practice"?

@gustavnavar
Copy link
Owner

gustavnavar commented Nov 2, 2019

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:
dotnet/aspnetcore#5545

We should have to wait until Preview 2 release to verify that it works as expected.

@gustavnavar
Copy link
Owner

.NET Core 3.1 Preview 2 has been released:
https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-core-3-1-preview-2/

Look at the "Stop event propagation in Blazor apps" section

@DrGriff
Copy link
Author

DrGriff commented Nov 4, 2019

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).

@gustavnavar
Copy link
Owner

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.

@DrGriff
Copy link
Author

DrGriff commented Nov 8, 2019

I tried with Transient, but that resulted in an error, so using

services.AddDbContext<MyContext>();
services.AddScoped<MyService>();

@gustavnavar
Copy link
Owner

gustavnavar commented Nov 8, 2019

Have you tried this?

services.AddDbContext<MyContext>(ServiceLifetime.Transient);
services.AddTransient<MyService>();

@DrGriff
Copy link
Author

DrGriff commented Nov 8, 2019

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?
(Thanks once again Gustav)

@DrGriff
Copy link
Author

DrGriff commented Nov 12, 2019

As an FYI: an alternative to:

Razor Page
@inject MyService Service
Startup
services.AddDbContext<MyContext>(ServiceLifetime.Transient);
services.AddTransient<MyService>();

Is to use

Razor Page
@inherits OwningComponentBase<Data.MyService>
Startup
services.AddDbContext<MyContext>();
services.AddScoped<MyService>();

See Using Entity Framework Core with Blazor

@DrGriff DrGriff closed this as completed Nov 12, 2019
@gustavnavar
Copy link
Owner

gustavnavar commented Nov 14, 2019

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))
{ }

@DrGriff
Copy link
Author

DrGriff commented Nov 14, 2019

Oh, very interesting... When you say:

neither [...] work as expected on Blazor Server App projects.

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 @inherits OwningComponentBase<Data.MyService> in the razor page, unless anyone can think of a reason not to), so thanks!

@gustavnavar
Copy link
Owner

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:

  • grid
  • create
  • read
  • update
  • delete

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:

  • If you move from the grid to update a record and you change a value and then go back to the grid without saving, the grid shows the changed value. The only way to get the correct value is to refresh the browser
  • If you move from the grid to update a record and you change a value and then go back to the grid saving the changes, the grid shows the old value.
  • If you move from the grid to update a record and you change a value of a child field (e.g. c.Costumer.CompanyName) and then go back to the grid saving the changes, this change is applied to the Customer entity. I know that this one is just a matter of not adding this column, burt I wanted to check all possible combinations.

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:
9628d5b
you will see these behaviors on the CRUD page of the GridBlazorServerSide project.

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:
39c5faf

@robalexclark
Copy link

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.

@tecxx
Copy link

tecxx commented Mar 13, 2020

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.
the last section here: https://docs.microsoft.com/en-us/aspnet/core/blazor/dependency-injection?view=aspnetcore-3.1 is the most helpful finding so far.

@DrGriff
Copy link
Author

DrGriff commented Mar 16, 2020

So the pattern that seems to work for us is to instantiate the Context manually, as follows:

Startup

services
                .AddDbContext<MyContext>(options => options.UseSqlServer(connectionString))
                .AddScoped<MyService>();

MyContext

public class MyContext : DbContext
{
    public MyContext(DbContextOptions<MyContext> options)
        : base(options)
    {
    }

MyService

public class MyService : EciService
{
    private readonly DbContextOptions<MyContext> options;

    public MyService (DbContextOptions<MyContext> options)
    {
        this.options = options;
    }

public ItemsDTO<MyEntity> GetTheGridRows(Action<IGridColumnCollection<MyEntity>> columns, QueryDictionary<StringValues> query)
{
    using var context = new MyContext(this.options);
    IQueryable<MyEntity> channels = context.MyTable
        .AsNoTracking()
        .OrderBy(d => d.Something);


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants