SfGrid Edit->Update is causing EF Core tracking exceptions when DataSource is having a complex object

Hello,

I'm using:
      .NET Core 5.0.0
     Syncfusion.Blazor.Grid 18.4.0.47
     EF Core 5.0.4

These are the classes used
    public class Computer
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public string InventoryNumber { get; set; }
        public Guarantee Guarantee { get; set; }
        public string Comments { get; set; }
    }
    public class Guarantee
    {
        public int Id { get; set; }
        public string DocumentNumber { get; set; }
        public DateTime IssueDate { get; set; }
        public DateTime ExpirationDate { get; set; }
        public string ImagePath { get; set; }
    }

This is the way grid is being displayed

. . .
@inject ApplicationDbContext dbContext

<h3>Computers</h3>

<SfGrid @ref="gridComputersList" DataSource="computersList" AllowFiltering="true" AllowPaging="true" AllowSorting="true"
        Toolbar="@(new List<string>() { "Add", "Edit", "Delete", "Cancel", "Update", "Search" })">
    <GridFilterSettings Type="Syncfusion.Blazor.Grids.FilterType.Menu"></GridFilterSettings>
    <GridEditSettings AllowAdding="true" AllowDeleting="true" AllowEditing="true" AllowEditOnDblClick="true" Mode="EditMode.Dialog" Dialog="DialogParams">
    <Template>
        @{
            var computer = (context as Computer);
            <div>
                <div class="form-row">
                    <div class="form-group col-md-12">
                        <label>Computer Name</label>
                        <SfTextBox FloatLabelType="FloatLabelType.Always" @bind-Value="@(computer.Name)" />
                    </div>
                    <div class="form-group col-md-12">
                        <label>Inventory Number</label>
                        <SfTextBox FloatLabelType="FloatLabelType.Always" @bind-Value="@(computer.InventoryNumber)" />
                    </div>                    
                    <div class="form-group col-md-12">
                        <label>Comments</label>
                        <SfTextBox FloatLabelType="FloatLabelType.Always" @bind-Value="@(computer.Comments)" />
                    </div>
                    <div class="form-group col-md-12">
                        <label>Document Number</label>
                        <SfTextBox FloatLabelType="FloatLabelType.Always" @bind-Value="@(computer.Guarantee.DocumentNumber)" />
                    </div>
                    <div class="form-group col-md-12">
                        <label>Issue Date</label>
                        <SfDatePicker FloatLabelType="FloatLabelType.Always" @bind-Value="@(computer.Guarantee.IssueDate)"></SfDatePicker>
                    </div>
                    <div class="form-group col-md-12">
                        <label>Expiration Date</label>
                        <SfDatePicker FloatLabelType="FloatLabelType.Always" @bind-Value="@(computer.Guarantee.ExpirationDate)"></SfDatePicker>
                    </div>
                </div>
            </div>
        }
    </Template>
    </GridEditSettings>
    <GridEvents OnActionComplete="OnGridActionCompliete" TValue="Computer"></GridEvents>
    <GridColumns>
        <GridColumn Field="@nameof(Computer.ID)" IsIdentity="true" IsPrimaryKey="true" Visible="false"></GridColumn>
        <GridColumn Field="@nameof(Computer.Name)"></GridColumn>
        <GridColumn Field="@nameof(Computer.InventoryNumber)"></GridColumn>
        <GridColumn Field="@nameof(Computer.Comments)"></GridColumn>
        <GridColumn Field="Guarantee.DocumentNumber"></GridColumn>
        <GridColumn Field="Guarantee.IssueDate" Format="d" Type="ColumnType.Date" DefaultValue="DateTime.Now"></GridColumn>
        <GridColumn Field="Guarantee.ExpirationDate" Format="d" Type="ColumnType.Date" DefaultValue="DateTime.Now.AddYears(1)"></GridColumn>
        <GridColumn HeaderText="Image" AllowEditing="false">
            <Template>
                @{
                    var guarantee = (context as Computer).Guarantee;
                    <div class="align-items-center d-flex justify-content-center">
                        @if (string.IsNullOrEmpty(guarantee.ImagePath) || !File.Exists("wwwroot" + guarantee.ImagePath))
                        {
                            <div>
                                <button class="btn" @onclick="@(e => OpenPopUp(guarantee.Id))"><span class="iconify" data-icon="grommet-icons:document-upload" data-inline="false"></span></button>
                            </div>
                        }
                        else
                        {
                            <div>
                                <button class="btn" @onclick="@(e=>ShowImageAsync(guarantee.ImagePath))"><span class="iconify" data-icon="bi:image" data-inline="false"></span></button>
                                <button class="btn" @onclick="@(e => DeleteImage(guarantee.Id))"><span class="iconify" data-icon="fluent:delete-20-regular" data-inline="false"></span></button>
                            </div>
                        }
                    </div>
                }
            </Template>
        </GridColumn>
    </GridColumns>
</SfGrid>

. . .
@code {
    List<Computer> computersList = new List<Computer>();
    private SfGrid<Computer> gridComputersList;
    private DialogSettings DialogParams = new DialogSettings { MinHeight = "400px", Width = "450px" };
    bool UploadIsVisible = false;

    private int dialogItemId = 0;

    protected override async Task OnInitializedAsync()
    {
        computersList = await dbContext.Computers.Include(x => x.Guarantee).ToListAsync();
    }

    private void OnGridActionCompliete(ActionEventArgs<Computer> args)
    {
        var x = args;
        switch (args.RequestType)
        {
            case Syncfusion.Blazor.Grids.Action.Save:
                if (args.Data.ID == 0)
                {
                    dbContext.Computers.Add(args.Data);
                }

                dbContext.SaveChanges(); //Exception
                gridComputersList.Refresh();
                break;
            case Syncfusion.Blazor.Grids.Action.Delete:
                dbContext.Computers.Remove(args.Data);
                dbContext.SaveChanges();
                break;
            default:
                break;
        }
    }
. . .
}


Add and delete functions work without any issues, but when I try to edit any value of an existing item in the Grid I'm getting an exception on dbContext.SaveChanges() line e.g.

System.InvalidOperationException
  HResult=0x80131509
  Message=The instance of entity type 'Guarantee' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
 

And this seems to happen only when I'm passing a list where entry has some navigation property. For example, if I hide columns where Field has Guranatee and dont do .Include(x => x.Guarantees) then edit is working as expected.

How to resolve this?

Thanks!




3 Replies 1 reply marked as answer

RN Rahul Narayanasamy Syncfusion Team March 29, 2021 01:05 PM UTC

Hi Jan, 

Greetings from Syncfusion. 

Query: The instance of entity type 'Guarantee' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked.  

We have validated your query with the provided error details and we might suspect that this was an general issue related to EF. We suggest you to check the below links at your end. Please find the below links for your reference. 

Reference 

Could you please check these reference and ensure the problem at your end. Please let us know if you have any concerns. 

Regards, 
Rahul 
 


Marked as answer

PA Patrick replied to Rahul Narayanasamy July 7, 2022 01:03 PM UTC

I do have the same Issue, but "NoTracking" as described in the articles do not solve the problem and I dbContext is injected scoped.


If run an update from a classic HTML Table everything runs smooth, but when I use SFGrid I get the Error " The instance of entity type 'Guarantee' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked".


This works fine:

<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>
<button class="btn btn-primary" @onclick="@(args => { Update(args, trigger); })">Update</button>
</td>
</tr>
}
</table>
</div>


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

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

if (getResult.IsSuccess)
{
Triggers = getResult.Result;
}

}

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



This is raising the exception:

@page "/ManageTriggers"
@using MigraineDiary_Common
@using Action = Syncfusion.Blazor.Grids.Action
@inject TriggerRepository TriggerRepository
@inject ToastService ToastService
@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>
</SfGrid>

@code {

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

protected override async Task OnInitializedAsync()
{
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);
}
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();
}
}

}

The Data Handler looks like this:
(None of the Get Methods do help for the issue)
public class TriggerRepository
{
IDbContextFactory<ApplicationDbContext> _dbContextFactory;
ApplicationDbContext Db { get; }

public TriggerRepository(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_dbContextFactory = contextFactory;
Db = contextFactory.CreateDbContext();
}
public async Task<OperationResult<ICollection<Trigger>>> GetAll()
{
try
{
var getResult = Db.Triggers.ToList();

return OperationResult<ICollection<Trigger>>.Success(getResult);
}
catch (Exception e)
{
return OperationResult<ICollection<Trigger>>.Failure(ResultType.BadRequest, e.Message);
}

}

public IQueryable<Trigger> GetAllAsQueryable()
{
try
{
return Db.Triggers.AsQueryable();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}


public async Task<Trigger> Update(Trigger trigger)
{
try
{
Db.Entry(trigger).State = EntityState.Detached;
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.AsNoTracking();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}

public async Task Update(IEnumerable<Trigger> triggers)
{
try
{
Db.UpdateRange(triggers);
await Db.SaveChangesAsync();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}

public async Task Add(Trigger trigger)
{
try
{
var addResult = Db.Triggers.Add(trigger);
await Db.SaveChangesAsync();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}

public async Task Delete(Trigger trigger)
{
try
{
var removeResult = Db.Triggers.Remove(trigger);
await Db.SaveChangesAsync();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}


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

Hi Patrick,


We suggest you to follow up on this thread for future updates on this query.


Regards,

Renjith R


Loader.
Up arrow icon