简体   繁体   中英

System.InvalidOperationException: when saving data to my database

ASP.Net core blazor application. When the page first opens it pulls a list of repair orders from the database and displays them on the page. This works great.

The table of data has a column with an 'Edit' button. When you edit the data a modal pops up and displays the RO information for someone to edit.

My problem is when I click 'Save', I get the following error.

Error Code:

System.InvalidOperationException: 'A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext, however instance members are not guaranteed to be thread safe. This could also be caused by a nested query being evaluated on the client, if this is the case rewrite the query avoiding nested invocations.'

It is worth noting this feature worked great and I have no clue what I changed to make it fail. OnInitAsync() method:

protected override async Task OnInitAsync()
    {
        await LoadData();        
    }

Method to load all the data:

protected async Task LoadData()
    {
        _repairOrders = await Task.Run(() => RepairOrderService.GetAllRepairOrders().ToArray());
        _vehicleLocations = await Task.Run(() => VehicleLocationService.GetAllLocations().ToArray());
        _repaitStages = await Task.Run(() => RepairStageService.GetAllRepairStages().ToArray());
        _employees = await Task.Run(() => EmployeeService.GetAllEmployees().ToArray());
    }

Method called to save data.

protected async Task SaveRepairOrder()
    {
        if (ro.Id != 0)
        {
            await Task.Run(() =>
            {
                RepairOrderService.EditRepairOrder(ro);
            });
        }
        else
        {
            await Task.Run(() =>
            {
                RepairOrderService.CreateRepairOrder(ro);
            });
        }
        this.isAdd = false;
        await LoadData();
    }

This is the method in my Data access layer class that is throwing the error.

//Get all repair order details as a list
        public List<RepairOrder> GetAllRepairOrders()
        {
            try
            {
                return db.RepairOrder.ToList();
            }
            catch
            {
                throw;               
            }
        }

I made the LoadData() method async after the issue started. Can anyone see what I am missing?

**** Update ******** I changed "services.AddSingleton();" to "services.AddTransient();" and it seems to be working. I need to dig in and try to determine if this was the way I should have done this in the beginning!

As the error mentions what is happening here is that some other thread is already using the context as your method is trying to use it.

Changing the registration to Transient works because the instance is no longer shared... each time you get it, it will be new one... this has some drawbacks, for instance, if you try to "join" two "IQueriable"s it will fail... memory consumption should also go UP...

Now, to your problem, you have to debug the code flow, because apparently, a previously started operation is running on the background AS YOUR CODE ENTERS the LoadData method! So the "Task.Run" doesnt actually have nothing to do with the error... if you remove it you should get the same error...

Something you can do to verify this hypothesis is adding a delay (await Task.Delay(2000);) of about a second or two before loading data... this should be enough to allow the other operation to finish, releasing the context for your method

There is more than one issue here.

First, the use of async. You are doing await Task.Run() everywhere, that is only limited useful. Much better to await an async Db operation (FindAsync, ToListAsync) without Task.Run().

Second, the lifetime management of Services. Blazor doesn't do AddScoped yet (except maybe for some startup/identity items).

So be aware that when you inject that db the normal way (eg like in a MVC application) it will not get Disposed. Each of your clients has its own DbContext. For the length of their session.

Which does not have to be a problem but you should pay extra care in avoiding Changetracking. And maybe manage the Db context yourself.

I would write:

public async Task<List<RepairOrder>> GetAllRepairOrders()
{
     return await db.RepairOrder
       .AsNoTracking()   // tracking is expensive, only do it whne needed
       .ToListAsync();   // or maybe ToArrayAsync
}

and then, in LoadData()

_repairOrders = await RepairOrderService.GetAllRepairOrders();

This is a Blazor server-side, right ?

In server-side Blazor all the objects added to the DI container should be scoped " Scoped ". The life-time of an object is spanned to that of the connection. When you add an object as a Singleton, its life-time is the duration of the application. Which is why none of your objects should be added as Singleton.

I'd advice you against using Task.Run. Your app is very basic, and it should work if you follow the above.

I'd advise you to use Lists instead of Arrays to hold your data.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM