0

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 SelectStockToSaleItemViewModelthe 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 TaxTypebeing 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.

Artur S.
  • 185
  • 3
  • 15

1 Answers1

0

Alright, I read some more about EF Core and I think I understood "the recommended way".

Whenever working with data services and dependency injection, the entities must be loaded by the same context that will be using them to add or update the context's database.

My mistake was loading the collections used by the interface (comboboxes, drop downs and checkboxes) using AsNoTracking. What happened was that whenever I tried to add or update two entities that used the same AsNoTracking-obtained entity, EF Core (as it should) tried to add the entity to the context's change tracker. So when the second entity tried to add/update, it threw a The instance of entity type cannot be tracked because another instance of this type with the same key is already being tracked exception.

So the "correct flow" is to load a collection/list using the same context that will be using them. Either using a singleton dbContext for the whole application and not using AsNoTracking, which is not recommended due to the possibility of ending up with concurrent operations; or using scoped dbContext for each "unit-of-work", i.e. a single dialog to add a new record, or an updating window with a record pulled from the same instance of the context that will be saving it, and disposing of the context (and it's change tracker) after whatever you wanted to do has been dealt with.

One of the big mistakes I was making when using scoped contexts was creating one context to load a datagrid with entries, which, when double-clicked, would open a dialog (with a new scoped context) to edit it without releasing that record from the previous context, throwing the already being tracked exception.

Artur S.
  • 185
  • 3
  • 15