I discovered one problem while creating my project. If someone refer to the issue I will be grateful. In my project I use a layered model. The repository layer (data access layer) that communicates with the database (DB) and the service layer (business logic layer) in which services and objects are implemented (data transfer object).
As a result, there is a problem with the dbSet.Update method. When object (obj) comes into Update method as parameter, during the method call of the _db.Entry(dbSet.Local.FirstOrDefault(i => i.Id == obj.Id) ?? obj).State = EntityState.Modified or _db.Update(dbSet.Local.FirstOrDefault(i => i.Id == obj.Id) ?? obj) in the case of the first user's update (like from "view.xaml") obj is update and changes saving in the database (because dbSet.Local.FirstOrDefault(i => i.Id == obj.Id) returns null and obviously my obj gets into the _db.Update method). In case of a repeated user's update (in the "view.xaml" view) object obj -- when it gets into the _db.Update(dbSet.Local.FirstOrDefault(i => i.Id == obj.Id) ?? obj) method, it isn't take account, as the data context already tracks it and in the _db.Update method gets an object from dbSet.Local.FirstOrDefault(i => i.Id == obj.Id). Everything would be fine if this object in dbSet.Local was updated according to the type that comes from the user. However, this is not the case, it is tracked but not changed when the user edits its properties. It is not tracked properly rather due to the fact that I use services and, accordingly, data transfer object entities.
In view of the foregoing, I have a question. How to make to update entity (by a new modified object) that are tracked, or how to manually assign the necessary object in dbSet.Local to replace the one stored there? or how to make Microsoft.EntityFrameworkCore.ChangeTracking not track changes to my objects in any way?
In order to make changes not tracked I used the QueryTrackingBehavior.NoTracking parameter for the DbContextOptionsBuilder entity, but this only helps on the first load, and tracking is still used when the data is updated further. I also used the dbSet.Local.Clear() methods, but this is a bad idea, as data updating due to the deletion of the previous data from the database (like delete 20 rows from table and add one updated).
public abstract class GenericRepository<T, TKey> : IGenericRepository<T, TKey> where T : class, IEntity, new()
{
protected DbContext context;
protected DbSet<T> dbSet;
private readonly object _lock = new object();
public GenericRepository(DbContext context)
{
this.context = context;
this.dbSet = context.Set<T>();
}
public virtual IQueryable<T> GetAll()
{
lock (_lock)
return dbSet;
}
public virtual async Task<T> GetAsync(TKey id)
{
try
{
return await dbSet.FindAsync(id);
}
catch (Exception exc)
{
throw exc;
}
}
public async Task AddAsync(T obj)
{
try
{
//dbSet.Local.Clear();
await dbSet.AddAsync(obj);
//context.Entry(obj).State = EntityState.Added;
}
catch (Exception exc)
{
throw exc;
}
}
public async void Delete(TKey id)
{
//context.Entry(obj).State = EntityState.Deleted;
//context.Remove(obj);
try
{
T obj = await GetAsync(id);
dbSet.Remove(obj);
//dbSet.Remove(dbSet.Find(1504));
}
catch (Exception exc)
{
throw exc;
}
}
public void Update(T obj)
{
try
{
context.Entry(dbSet.Local.FirstOrDefault(i => i.Id == obj.Id) ?? obj).State = EntityState.Modified;
//dbSet.Update(dbSet.Local.FirstOrDefault(i => i.Id == obj.Id) ?? obj);
//dbSet.Update(obj).State = EntityState.Modified;
//dbSet.Update(obj);
//dbSet.Local.FirstOrDefault(i => i.Id == obj.Id)
}
catch (Exception exc) { throw exc; }
}
public async Task SaveAsync()
{
try
{
await context.SaveChangesAsync();
}
catch (Exception exc)
{
throw exc;
}
}
public virtual IQueryable<T> Where(Expression<Func<T, bool>> predicate)
{
lock (_lock)
return dbSet.Where(predicate);
}
//public virtual IQueryable<T> FindBy(Expression<Func<T, bool>> predicate)
//{
// return dbSet.Where(predicate);
//}
}
}
Data saving method occurs at a higher level (service level). In the View user updates data (for example, adjusts the property one of the rows of the DataGrid), then in a context binding to this View, specifically in the ViewModel, calls the binding property (by INotifyPropertyChanged), which initiates call of the method that is responsible for the CRUD operation, then this method (On_bt_CategoryUpdate_Command) calling method from private field of genericService which is responsible for updating data in the database.
public class MainViewModel : ViewModel
{
#region Fields
private IGenericService<ContractDTO, int> _contractService;
#endregion
#region Commands
#endregion
#region Properties
private object GetList;
public object _GetList { get => GetList; private set => Set(ref GetList, value); } //Property to initialize DataGrid
private ContractDTO _contractDTO;
public ContractDTO SelectedItem { get => _contractDTO; set => Set(ref _contractDTO, value); } //Property with the data of the selected row in the DataGrid
#endregion
public SpesTechViewModel(IGenericService<ContractDTO, int> contractService)
{
_contractService = contractService;
}
#region Commands function
private bool Can_bt_CategoryUpdate_Command() => true;
private void On_bt_CategoryUpdate_Command()
{
try
{
_contractService.UpdateAsync(SelectedItem); //Method that updates and saves the data in the database
}
catch (Exception exc) { MessageBox.Show(exc.Message); }
}
#endregion
#region Function
void GetByFilter<T>(IFilterModel<T> filter, IGenericService<T, int> service) where T : class, new()
{
lock (_lock)
_GetList = service.Where(filter.Predicate()).Result.ToObservableCollection();
}
#endregion
}
_contractService.UpdateAsync(SelectedItem):
public async Task<DbObjectDTO> UpdateAsync(DbObjectDTO obj)
{
try
{
DbObject dbObject = mapper.Map<DbObject>(obj);
repository.Update(dbObject);
await repository.SaveAsync();
return mapper.Map<DbObjectDTO>(dbObject);
}
catch (Exception exc) { throw exc; }
}
the first thing in this method: using AutoMapper, the DTO object is converted into a DAL object; second: data is update using the repository layer (code above, public void Update(T obj) method); third: saving to the database; fourth: reverse mapping.
If I'm trying to do smth like this in the Update(T obj) method of GenericRepository<T, TKey>:
public void Update(T obj)
{
try
{
dbSet.Update(obj);
//dbSet.Local.FirstOrDefault(i => i.Id == obj.Id)
}
catch (Exception exc) { throw exc; }
}
On the second update of the same Entity in dbSet with a new changes of T obj I get an exception:
{"The instance of entity type 'Contract' 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."}
CodePudding user response:
It's unusual to be so heavily coupled to the local cache. The normal pattern is to call DbSet.Find(id)
, and update the properties of the entity that it returns.
var entity = await dbSet.FindAsync(new object[] { id }, cancellationToken);
var entry = context.Entry(entity);
entry.CurrentValues.SetValues(obj);
await context.SaveChangesAsync(cancellationToken);
This pattern has a few benefits compared to what you are doing:
- If no entity exists with that id, you have a chance to inform the caller
- Entity Framework handles the caching
- Entity Framework only updates the properties that have actually changed
If the entity is not in the cache, then Entity Framework does have to go to the database. In certain scenarios, e.g. if your entity has a large binary property and you plan to overwrite it with a new value, you might want to avoid loading the existing values from the database and just skip to the update part.
var local = dbSet.Local.FirstOrDefault(i => i.Id == obj.Id);
if (local != null)
{
var entry = context.Entry(local);
entry.CurrentValues.SetValues(obj);
}
else
{
var entry = dbSet.Attach(obj);
entry.State = EntityState.Modified;
}
await context.SaveChangesAsync(cancellationToken);
This is an exception to the general use case, I recommend you follow the normal pattern in most cases and only use this if performance dictates it.