Futher into my studies on MVVM I found an issue I can't understand, and I couldn't find (rather, I think I couldn't word my Google searches correctly) information specific to this situation:
I have the following entities:
public class Sale : EntityBase
{
public ICollection<SaleItem> SaleItems {get;set;}
}
public class SaleItem : EntityBase
{
public Sale Sale {get;set;}
public Stock Stock {get;set;}
public TaxType TaxType {get;set;}
}
public class Stock : EntityBase
{
public TaxType TaxType {get;set;}
public ICollection<Sale> SaleStocks {get;set;}
}
public class EntityBase
{
[Key]
public int ID {get;set;}
}
public TaxType
{
[Key]
public int TaxCode {get;set;}
public string Description {get;set;
}
TaxType
is seeded by the database migrations. I'm using MySQL.
From what I've read on what is the Alternate for AddorUpdate method in EF Core?, to record a new entry on my database, I should just call _context.Update(Sale)
and _context.SaveChangesAsync()
.
However I still can't understand what am I doing wrong on a simple CRUD:
User is directed to a viewModel:
SaleViewModel.cs
private SaleItemStore _saleItemStore;
public Sale Sale {get;set;} = new();
public ObservableCollection<SaleItem> SaleItems {get;set;} = new();
public SaleViewModel(IServiceProvider serviceProvider)
{
_saleItemStore = serviceProvider.GetRequiredService<SaleItemStore>();
SaveSale = new SaveSaleCommand(this, serviceProvider);
}
public class SaveSaleCommand()
{
public SaveSaleCommand(SaleViewModel saleViewModel, IServiceProvidere serviceProvider)
{
_parentViewModel = saleViewModel;
_saleDataService = serviceProvider.GetRequiredService<SalaDataService>();
}
public Execute()
{
foreach (SaleItem saleItem in SaleItems)
{
Sale.SaleItems.Add(saleItem);
}
_saleDataService.AddOrUpdate(Sale sale);
}
}
On a different viewModel, the user can select, among other properties, the TaxType
, from a dropdown combobox. The combobox's ItemsSource is bound to TaxTypesList
like:
SelectStockToSaleItemViewModel
private TaxTypeDataService _taxTypeDataService;
public SaleItem SaleItem {get;set;} = new();
public TaxType SelectedTaxType {get;set;}
public ObservableCollection<TaxType> TaxTypesList {get;} = new();
public SelectStockToSaleItemViewModel(IServiceProvider serviceProvider)
{
_taxTypeDataService = serviceProvider.GetRequiredService<TaxTypeDataService>();
SendSaleItemBack = new SendSaleItemBackCommand(this, serviceProvider);
Task.Run(FillLists);
}
public async Task FillLists()
{
foreach (TaxType taxType in await _taxTypeDataService.GetAllAsNoTracking())
{
TaxTypesList.Add(taxType);
}
}
public SendSaleItemBackCommand()
{
private SaleItemStore _saleItemStore;
public SendSaleItemBackCommand(SelectStockToSaleItemViewModel selectStockToSaleItemViewModel, IServiceProvider serviceProvider)
{
_parentViewModel = selectStockToSaleItemViewModel;
_saleItemStore = serviceProvider.GetRequiredService<SaleItemStore>();
}
public Execute()
{
_saleItemStore.Message = new SaleItem()
{
TaxType = _parentViewModel.SelectedTaxType
}
//Takes user back to previous ViewModel;
}
}
So, my idea is that on SelectStockToSaleItemViewModel
the user selects a TaxType
from a combobox filled with _taxTypeDataService.GetAllAsNoTracking()
, and when they execute SendSaleItemBackCommand
, SaleItemStore
has its property set to a SaleItem
with the TaxType
not null;
Afterwards, should the user add more SaleItems
to Sale
, they'll have to open SelectStockToSaleItemViewModel
again, and do the process for each extra SaleItem
they wand to add to Sale
.
Finally, they will have to execute SaveSaleCommand
, and I'd end up with a Sale
and its associated SaleItems
on my database.
My dataServices are:
public class SaleDataService
{
private MyDbContext _context;
public SaleDataService(IServiceProvider serviceProvider)
{
_context = serviceProvider.GetRequiredService<MyDbContext>();
}
public async Task<Sale> AddOrUpdate(Sale sale)
{
_context.Update(sale);
await _context.SaveChangesAsync();
return sale;
}
}
public class TaxTypeDataService
{
private MyDbContext _contex;
public TaxTypeDataService(IServiceProvider serviceProvider0
{
_context = serviceProvider.GetRequiredService<MyDbContext>();
}
public async Task<List<TaxType>> GetAllAsNoTracking()
{
return _context.Set<TaxType>().AsNoTracking().ToListAsync();
}
}
public class Messaging<TObject> : IMessaging<TObject>
{
public TObject Message { get; set; }
}
SaleItemStore
is singleton. SelectStockToSaleItemViewModel
, and SelectStockToSaleItemViewModel
are transient; TaxTypeDataService
and SaleDataService
are scoped; As for the context,
public static IHostBuilder AddDbContext(this IHostBuilder host)
{
host.ConfigureServices((_, services) =>
{
string connString = $"MyConnString";
ServerVersion version = new MySqlServerVersion(new Version(8, 0, 23));
Action <DbContextOptionsBuilder> configureDbContext = c =>
{
c.UseMySql(connString, version, x =>
{
x.CommandTimeout(600);
x.EnableRetryOnFailure(3);
});
c.EnableSensitiveDataLogging();
};
services.AddSingleton(new AmbiStoreDbContextFactory(configureDbContext));
services.AddDbContext<AmbiStoreDbContext>(configureDbContext);
});
return host;
}
The issue happens when the user tries to save a Sale
with two or more SaleItems
using the same TaxType
, as EF Core throws an "another instance of TaxType is already being tracked". I understand the TaxType
starts being tracked when the first Sale.SaleItem
is updated, but how do I deal with this issue?
According to The instance of entity type 'Product' cannot be tracked because another instance with the same key value is already being tracked and The instance of entity type cannot be tracked because another instance of this type with the same key is already being tracked I did fill the combobox with TaxType
using an AsNoTracking
call, but I don't think it applies to my situation. Also, they say to "flush" the tracked entries before updating by setting the context's tracked entries' states to Detached
, but, again, TaxType
starts being tracked while being saved. I've checked the context's tracked entries collection, and there is no mention of TaxType
being tracked at all before Update(Sale)
is called.
The only way I can think of as a workaround is using Fluent API and setting the foreign key, rather than the property itself (i.e. TaxTypeId = SelectedTaxType.ID
), which doesn't look like the best way to deal with this.