C
C#3mo ago
UntoldTitan

EFCore Collection Trying to save a duplicate.

I'm building a dotnet maui app, and when I try to save a Rental with a ICollection<Equipment> to my database, it errors saying that the equipment already exists. Here's the models:
public class Rental
{
public int Id { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public ICollection<Equipment> Equipment { get; } = new List<Equipment>();
public DateTime RentalDate { get; set; }
public DateTime ReturnDate { get; set; }
[JsonIgnore]
public float TotalCost => Equipment.Sum(p => p.Cost);
}
public class Rental
{
public int Id { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public ICollection<Equipment> Equipment { get; } = new List<Equipment>();
public DateTime RentalDate { get; set; }
public DateTime ReturnDate { get; set; }
[JsonIgnore]
public float TotalCost => Equipment.Sum(p => p.Cost);
}
Equipment:
public class Equipment
{
public int Id { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
[JsonIgnore]
public string CategoryName => Category.Name;
public string Name { get; set; }
public string Description { get; set; }
public float Cost { get; set; }
}
public class Equipment
{
public int Id { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
[JsonIgnore]
public string CategoryName => Category.Name;
public string Name { get; set; }
public string Description { get; set; }
public float Cost { get; set; }
}
Here's the code that saves it:
Rental createdRental = (Rental)result.Data;
if (createdRental == null)
{
return;
}
db.Update(createdRental);
await db.SaveChangesAsync();
StateHasChanged();
Rental createdRental = (Rental)result.Data;
if (createdRental == null)
{
return;
}
db.Update(createdRental);
await db.SaveChangesAsync();
StateHasChanged();
At a break point at the createdRental == null line, it shows that there are two equipments in the List. It then throws an error on the db.SaveChangesAsync() line. I think I just need a way to tell EFCore to not save the elements in the list, but to just create a link to them from the Rental im trying to save.
No description
No description
45 Replies
Kringe
Kringe3mo ago
Are you trying to create or update a rental?
UntoldTitan
UntoldTitan3mo ago
Only create
Kringe
Kringe3mo ago
should the syntax then not be db.rentals.add()
UntoldTitan
UntoldTitan3mo ago
Same error if it's add()
Kringe
Kringe3mo ago
what is the error because in the screenshot i see instance already tracked but i cant fully read it
UntoldTitan
UntoldTitan3mo ago
The full error says that the Equipment.id already exists in the database, which is true, it does already exist
Kringe
Kringe3mo ago
you want to add the equiment when adding the rental i assume
UntoldTitan
UntoldTitan3mo ago
No, I just want the rental to link to existing equipment. The equipments are already made and created, and then the rental should just contain existing equipments
Kringe
Kringe3mo ago
ye I understand I recommend retrieving the equipment form the db and then adding it i you give me a sec I can make a query
Keswiik
Keswiik3mo ago
That error isn't telling you it already exists. That error is telling you that another DbContext has loaded that specific Equipment entity and is already tracking it. An entity can only be tracked by a single instance of DbContext at a time.
Kringe
Kringe3mo ago
ye i also saw that if that persists you can call changetracker.clear()
Keswiik
Keswiik3mo ago
Could also mean they aren't properly disposing of their DbContexts or have queries tracking entites that should not be.
UntoldTitan
UntoldTitan3mo ago
Oh ok (I'm definitely not disposing properly I'll have to do that) thanks
Kringe
Kringe3mo ago
true youre probably doing something wrong if you randomly get that
Keswiik
Keswiik3mo ago
What do your EF queries look like?
UntoldTitan
UntoldTitan3mo ago
yep
Keswiik
Keswiik3mo ago
i mean show code of your EF queries, lol as you are probably setting every query to track entities and if you have read-only queries, you should be using AsNoTracking()
UntoldTitan
UntoldTitan3mo ago
I'll send em here in a minute But i def have multiple DbContexts, how do i dispose of those
Keswiik
Keswiik3mo ago
show your controller / service code first need to see how you're using everything
Kringe
Kringe3mo ago
this i what i do to get the related entities
UntoldTitan
UntoldTitan3mo ago
Sure When I'm grabbing entities I don't need em sorted or anything I just do db.Equipments.ToList();
Keswiik
Keswiik3mo ago
Or you use EF as intended and do .Include(cr => cr.Equipment) as part of your query and it'll populate it automatically. that means you are loading and tracking every single Equipment instance, which is bad.
UntoldTitan
UntoldTitan3mo ago
oh ok Here, lemme show you This is my Rental razor page.
@using AnalysisPrototype.DataManagers;
@using AnalysisPrototype.Data;
@using Microsoft.EntityFrameworkCore;
@using Microsoft.FluentUI.AspNetCore.Components;
@using AnalysisPrototype.Components.Dialogs;
@inject IDialogService DialogService;
@page "/rentals"


<h1>Rentals</h1>
<FluentButton OnClick="@OpenCreationDialog" IconStart="@(new Icons.Regular.Size20.Add())">Add</FluentButton>
<table>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Rented Equipment</th>
<th>Rent Date</th>
<th>Return Date</th>
<th>Total Cost</th>
</tr>
@foreach (var rental in db.Rentals.Include(p => p.Customer).Include(p => p.Equipment).ToList())
{
<tr>
<td>@rental.Customer.FirstName</td>
<td>@rental.Customer.LastName</td>
<td>@string.Join(",",rental.Equipment.Select(p => p.Name).ToList())</td>
<td>@rental.RentalDate</td>
<td>@rental.ReturnDate</td>
<td>@rental.TotalCost</td>
<td><FluentButton OnClick="@(() => DeleteRental(rental.Id))">Delete</FluentButton></td>
</tr>
}
</table>

@code {
DataContext db = new DataContext();

private bool _trapFocus = true;
private bool _modal = true;

public void DeleteRental(int id)
{
db.Rentals.Remove(db.Rentals.First(p => p.Id == id));
db.SaveChanges();
StateHasChanged();
}

public async void OpenCreationDialog()
{
DialogParameters parameters = new()
{
Title = $"New Rental",
Width = "500px",
TrapFocus = _trapFocus,
Modal = _modal,
PreventScroll = true
};

IDialogReference dialog = await DialogService.ShowDialogAsync<RentalCreateDialog>(parameters);
DialogResult? result = await dialog.Result;
Rental createdRental = (Rental)result.Data;
if (createdRental == null)
{
return;
}
db.Update(createdRental);
await db.SaveChangesAsync();
StateHasChanged();
}
}
@using AnalysisPrototype.DataManagers;
@using AnalysisPrototype.Data;
@using Microsoft.EntityFrameworkCore;
@using Microsoft.FluentUI.AspNetCore.Components;
@using AnalysisPrototype.Components.Dialogs;
@inject IDialogService DialogService;
@page "/rentals"


<h1>Rentals</h1>
<FluentButton OnClick="@OpenCreationDialog" IconStart="@(new Icons.Regular.Size20.Add())">Add</FluentButton>
<table>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Rented Equipment</th>
<th>Rent Date</th>
<th>Return Date</th>
<th>Total Cost</th>
</tr>
@foreach (var rental in db.Rentals.Include(p => p.Customer).Include(p => p.Equipment).ToList())
{
<tr>
<td>@rental.Customer.FirstName</td>
<td>@rental.Customer.LastName</td>
<td>@string.Join(",",rental.Equipment.Select(p => p.Name).ToList())</td>
<td>@rental.RentalDate</td>
<td>@rental.ReturnDate</td>
<td>@rental.TotalCost</td>
<td><FluentButton OnClick="@(() => DeleteRental(rental.Id))">Delete</FluentButton></td>
</tr>
}
</table>

@code {
DataContext db = new DataContext();

private bool _trapFocus = true;
private bool _modal = true;

public void DeleteRental(int id)
{
db.Rentals.Remove(db.Rentals.First(p => p.Id == id));
db.SaveChanges();
StateHasChanged();
}

public async void OpenCreationDialog()
{
DialogParameters parameters = new()
{
Title = $"New Rental",
Width = "500px",
TrapFocus = _trapFocus,
Modal = _modal,
PreventScroll = true
};

IDialogReference dialog = await DialogService.ShowDialogAsync<RentalCreateDialog>(parameters);
DialogResult? result = await dialog.Result;
Rental createdRental = (Rental)result.Data;
if (createdRental == null)
{
return;
}
db.Update(createdRental);
await db.SaveChangesAsync();
StateHasChanged();
}
}
All the other pages are similar to this @ded
Keswiik
Keswiik3mo ago
first thing that jumps out at me is you're not using DI for any of this since you're using razor pages I assume this is MAUI with Blazor?
UntoldTitan
UntoldTitan3mo ago
yep
Keswiik
Keswiik3mo ago
then my first suggestion is to look through this razor tutorial on how to set up your pages and use codebehind https://learn.microsoft.com/en-us/aspnet/core/data/ef-rp/intro?view=aspnetcore-8.0&tabs=visual-studio
Razor Pages with Entity Framework Core in ASP.NET Core - Tutorial 1...
Shows how to create a Razor Pages app using Entity Framework Core
Keswiik
Keswiik3mo ago
not sure how much is applicable since I don't use maui or blazor myself
UntoldTitan
UntoldTitan3mo ago
ok, thanks no worries
Keswiik
Keswiik3mo ago
as for your queries and code, I wouldn't make a single datacontext like this
@code {
DataContext db = new DataContext();
@code {
DataContext db = new DataContext();
instead I would do something like
public void DeleteRental(int id)
{
using var db = new DataContext();
db.Rentals.Remove(db.Rentals.First(p => p.Id == id));
db.SaveChanges();
StateHasChanged();
}
public void DeleteRental(int id)
{
using var db = new DataContext();
db.Rentals.Remove(db.Rentals.First(p => p.Id == id));
db.SaveChanges();
StateHasChanged();
}
using will implicitly dispose of something once whenever the enclosing scope exits, so in this case it will dispose of the context when DeleteRental exits
UntoldTitan
UntoldTitan3mo ago
Ah ok, that makes more sense What would be a better way of grabbing all of the data?
Keswiik
Keswiik3mo ago
you should also be able to replace db.Rentals.Remove(db.Rentals.First(p => p.Id == id)); with db.Rentals.Remove(new Rental { Id = id }), that way you can delete without loading entities from the database
UntoldTitan
UntoldTitan3mo ago
Ok
Keswiik
Keswiik3mo ago
can you show the page that query is in?
UntoldTitan
UntoldTitan3mo ago
Its there, in the foreach, not in the code
Keswiik
Keswiik3mo ago
oh, didn't see that
UntoldTitan
UntoldTitan3mo ago
lol
Keswiik
Keswiik3mo ago
ok yeah that's gonna track every customer and equipment that is rented out
UntoldTitan
UntoldTitan3mo ago
which is not something i need to do, i just wanna grab em all
Keswiik
Keswiik3mo ago
you can add .AsNoTracking() at the beginning of your query
UntoldTitan
UntoldTitan3mo ago
Ok cool So what im understanding is that each DataContext should really only be used for like one or two operations and then disposed of?
Keswiik
Keswiik3mo ago
generally speaking, yes, a DbContext should be considered a single unit of work
UntoldTitan
UntoldTitan3mo ago
Ok cool
Keswiik
Keswiik3mo ago
if you want to only load the data you need, you can do that by using Select within your query but properly tracking / not tracking your entities is a good place to start, should help get rid of your errors
UntoldTitan
UntoldTitan3mo ago
Just finished changing everything over, lemme try it rq Works now lmao thanks i have learned more about efcore from this, very epic 👍
Keswiik
Keswiik3mo ago
:PepoSalute: