EF Tracking Error, only in SfGrid

When I use SfGrid to update I get the following Error from EF Core. This happens onlye when I use SfGrid, it does not happen if I use the same mechanic, if I use my own custom Table.


I don't understand what I have to to differently in sfgrid.


Error: System.InvalidOperationException: The instance of entity type 'Trigger' cannot be tracked because another instance with the key value '{TriggerId: 6727b31f-9c74-4394-9723-ce785b93850f}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.


Using SFGrid code

@page "/ManageTriggers"
@inherits UserInfoComponent
@using Action = Syncfusion.Blazor.Grids.Action
@inject TriggerRepository TriggerRepository
@inject ToastService ToastService
@inject UserService UserService
@attribute [Authorize(Roles = Sd.AdminRole)]

<h3>ManageTriggers</h3>

<SfGrid TValue="Trigger" DataSource="@Triggers"
Toolbar="@(new List<string>() { "Add", "Edit", "Delete", "Cancel", "Update" })">
<GridEvents OnActionBegin="ActionBegin" OnActionComplete="ActionComplete" TValue="Trigger"></GridEvents>
<GridColumns>
<GridColumn Field="@nameof(Trigger.TriggerId)"></GridColumn>
<GridColumn Field="@nameof(Trigger.TriggerTitle)"></GridColumn>
<GridColumn Field="@nameof(Trigger.IsPublic)" Type="ColumnType.Boolean"></GridColumn>
<GridColumn>
<Template>
@{ var trigger = (context as Trigger);}
<p>@trigger.Owner.Email</p>
</Template>
</GridColumn>

</GridColumns>
<GridEditSettings
AllowAdding="true"
AllowDeleting="true"
AllowEditing="true">
</GridEditSettings>
</SfGrid>

@code {

IEnumerable<Trigger> Triggers { get; set; } = new List<Trigger>();
IEnumerable<ApplicationUser> Users { get; set; } = new List<ApplicationUser>();

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
Users = await UserService.GetAllUsers();
await RefreshTriggers();
}

async Task RefreshTriggers()
{
var getAllResult = TriggerRepository.GetAllAsNoTracking();

Triggers = getAllResult;
}


async Task ActionBegin(Syncfusion.Blazor.Grids.ActionEventArgs<Trigger> arg)
{
if (arg.RequestType == Action.Save)
{
if (arg.Action == "Add")
{
await TriggerRepository.Add(arg.Data.TriggerTitle, (await GetUserAsync()).Email);
}
else // Update
{
await TriggerRepository.Update(arg.Data);

}
}
if (arg.RequestType == Action.Delete)
{
await TriggerRepository.Delete(arg.Data);
}
}

async Task ActionComplete(Syncfusion.Blazor.Grids.ActionEventArgs<Trigger> arg)
{
if (arg.RequestType.Equals(Syncfusion.Blazor.Grids.Action.Save))
{
await RefreshTriggers();
}
}

}


Using normal html Table, works fine:

@page "/ManageTriggersClassic"
@using MigraineDiary_Common
@inject TriggerRepository TriggerRepository
@attribute [Authorize(Roles = Sd.AdminRole)]

<h3>ManageTriggersClassic</h3>

<div class="container">
<table class="table table-bordered">
<tr>
<th>ID</th>
<th>Title</th>
<th>Is Public</th>
<th>Owner</th>
<th>Category</th>
</tr>
@foreach (var trigger in Triggers)
{
<tr>
<td>@trigger.TriggerId.ToString()</td>
<td>
<SfTextBox @bind-Value="@trigger.TriggerTitle"></SfTextBox>
</td>
<td>
<SfCheckBox @bind-Checked="@trigger.IsPublic"></SfCheckBox>
</td>
<td>
@trigger.Owner.UserName
</td>
<td>
<SfComboBox Placeholder="Select Category"
@bind-Value="@trigger.TriggerCategory" DataSource="TriggerCategories"
AllowCustom="false"
AllowFiltering="false">
<ComboBoxFieldSettings Value="Id" Text="Title"></ComboBoxFieldSettings>
</SfComboBox>
</td>
<td>
<button class="btn btn-primary" @onclick="@(args => { Update(args, trigger); })">Update</button>
</td>
</tr>
}
</table>
</div>


@code {

IEnumerable<Trigger> Triggers { get; set; } = new List<Trigger>();

IEnumerable<TriggerCategory> TriggerCategories { get; set; } = new List<TriggerCategory>();

protected override async Task OnInitializedAsync()
{
TriggerCategories = TriggerRepository.GetTriggerCategories();
var getResult = await TriggerRepository.GetAll();

Triggers = getResult;
}

async void Update(MouseEventArgs obj, Trigger trigger)
{
await TriggerRepository.Update(trigger);
}

}

5 Replies

RS Renjith Singh Rajendran Syncfusion Team July 11, 2022 12:40 PM UTC

Hi Patrick,


Greetings from Syncfusion support.


We have analyzed the shared codes. We could see that you have not defined IsPrimaryKey for any of the columns in grid. When performing CRUD in grid, it is must to define a unique valued column as primary key column in Grid. So we suggest you to ensure to add IsPrimaryKey for a unique valued column in grid.

https://blazor.syncfusion.com/documentation/datagrid/editing


And also, we could see that you have re-fetched the data from your service and assigned to grid’s DataSource property based on Action as Save inside OnActionComplete. We are suspecting that this might have caused the reported problem. Grid will internally handle the refresh when perform CRUD actions in grid. So we suggest you to check this by removing the below highlighted codes(in red) in your application.


 

    async Task ActionComplete(Syncfusion.Blazor.Grids.ActionEventArgs<Trigger> arg)

    {

        if (arg.RequestType.Equals(Syncfusion.Blazor.Grids.Action.Save))

        {

            await RefreshTriggers();

        }

    }

 


Please check this using the above suggestions and if you are still facing difficulties then kindly get back to us with the entire stacktrace of the exception you are facing.


Regards,

Renjith R




PA Patrick July 11, 2022 01:50 PM UTC

Hi Renijith


Thanks for your reply. I tried your suggestions, but I still get the same error.


Here the adjusted code:

@page "/ManageTriggers"
@inherits UserInfoComponent
@using Action = Syncfusion.Blazor.Grids.Action
@inject TriggerRepository TriggerRepository
@inject ToastService ToastService
@inject UserService UserService
@attribute [Authorize(Roles = Sd.AdminRole)]

<h3>ManageTriggers</h3>

<SfGrid TValue="Trigger" DataSource="@Triggers"
Toolbar="@(new List<string>() { "Add", "Edit", "Delete", "Cancel", "Update" })">
<GridEvents OnActionBegin="ActionBegin" OnActionComplete="ActionComplete" TValue="Trigger"></GridEvents>
<GridEditSettings
AllowAdding="true"
AllowDeleting="true"
AllowEditing="true">
</GridEditSettings>
<GridColumns>
<GridColumn Field="@nameof(Trigger.TriggerId)" IsPrimaryKey="true"></GridColumn>
<GridColumn Field="@nameof(Trigger.TriggerTitle)"></GridColumn>
<GridColumn Field="@nameof(Trigger.IsPublic)" Type="ColumnType.Boolean"></GridColumn>
@*<GridColumn>
<Template>
@{ var trigger = (context as Trigger);}
<p>@trigger.Owner.Email</p>
</Template>
</GridColumn>*@
</GridColumns>

</SfGrid>

@code {

IEnumerable<Trigger> Triggers { get; set; } = new List<Trigger>();
IEnumerable<ApplicationUser> Users { get; set; } = new List<ApplicationUser>();

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
Users = await UserService.GetAllUsers();
await RefreshTriggers();
}

async Task RefreshTriggers()
{
var getAllResult = TriggerRepository.GetAllAsNoTracking();

Triggers = getAllResult;
}


async Task ActionBegin(Syncfusion.Blazor.Grids.ActionEventArgs<Trigger> arg)
{
if (arg.RequestType == Action.Save)
{
if (arg.Action == "Add")
{
await TriggerRepository.Add(arg.Data.TriggerTitle, (await GetUserAsync()).Email);
}
else // Update
{
await TriggerRepository.Update(arg.Data);

}
}
if (arg.RequestType == Action.Delete)
{
await TriggerRepository.Delete(arg.Data);
}
}

async Task ActionComplete(Syncfusion.Blazor.Grids.ActionEventArgs<Trigger> arg)
{
if (arg.RequestType.Equals(Syncfusion.Blazor.Grids.Action.Save))
{
//await RefreshTriggers();
}
}

}


These are the repo methods:

using Microsoft.EntityFrameworkCore;
using MigraineDiary_Common;
using MigraineDiary_DataAccess.Data;
using MigraineDiary_Models;

namespace MigraineDiary_Business.Repository;

public class TriggerRepository
{
IDbContextFactory<ApplicationDbContext> _dbContextFactory;
ApplicationDbContext Db { get; }

public TriggerRepository(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_dbContextFactory = contextFactory;
Db = contextFactory.CreateDbContext();
}

public async Task<Trigger> Update(Trigger trigger)
{
try
{
Db.Update(trigger);
var saveResult = await Db.SaveChangesAsync();

return trigger;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}

public IEnumerable<Trigger> GetAllAsNoTracking()
{
try
{
return Db.Triggers.Include(trigger => trigger.Owner).AsNoTracking();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}


public IEnumerable<TriggerCategory> GetTriggerCategories()
{
try
{
return Db.TriggerCategories;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}





SP Sarveswaran Palani Syncfusion Team July 12, 2022 03:47 PM UTC

Hi Patrick,


Greetings from Syncfusion support.


We have analyzed your query and fixed this issue by detaching the local entry and attach the updated entry before saving (like below code snippet). We suggest you to use the same way to resolve the issue in your project.


//To Update the records of a particluar Order   

        public void UpdateOrder(Order Order)

        {

            try

            {

                var local = db.Set<Order>().Local.FirstOrDefault(entry => entry.OrderID.Equals(Order.OrderID));

                // check if local is not null

                if (local != null)

                {

                    // detach

                    db.Entry(local).State = EntityState.Detached;

                }

                db.Entry(Order).State = EntityState.Modified;

                db.SaveChanges();

            }

            catch

            {

                throw;

            }

        }


Please get back to us if you are facing any further issues


Regards,

Sarveswaran PK



PA Patrick replied to Sarveswaran Palani July 13, 2022 02:41 PM UTC

Dear Sarvesan


Thank you this solution does work so far.


But I still do not understand, why this happens only with SfGrid. Is this the preferred way to use SfGrid with DbContext or is there a cleaner solution you would suggest?


Also what would be the solution in a DbContext, connected scenario?


Br Patrick



SP Sarveswaran Palani Syncfusion Team July 14, 2022 03:43 PM UTC

Hi Patrick,


From your code example we found that you have tried to add the record to database when RequestType is Add. We suggest you insert the record when RequestType is Save. For both Insert and Update action, RequestType will be Save only. We can differentiate the two action using Action argument.


Refer the below general link to know more details about this issue.


https://stackoverflow.com/a/42475617/9634972


Please get back to us if you have further queries.


Regards,

Sarveswaran PK


Loader.
Up arrow icon