Dire che ho una classe di Sale
:
public class Sale : BaseEntity //BaseEntity only has an Id
{
public ICollection<Item> Items { get; set; }
}
E una classe di Item
:
public class Item : BaseEntity //BaseEntity only has an Id
{
public int SaleId { get; set; }
public Sale Sale { get; set; }
}
E un repository generico (metodo di aggiornamento):
public async Task<int> UpdateAsync<T>(T entity, params Expression<Func<T, object>>[] navigations) where T : BaseEntity
{
var dbEntity = _dbContext.Set<T>().Find(entity.Id);
var dbEntry = _dbContext.Entry(dbEntity);
dbEntry.CurrentValues.SetValues(entity);
foreach (var property in navigations)
{
var propertyName = property.GetPropertyAccess().Name;
await dbEntry.Collection(propertyName).LoadAsync();
List<BaseEntity> dbChilds = dbEntry.Collection(propertyName).CurrentValue.Cast<BaseEntity>().ToList();
foreach (BaseEntity child in dbChilds)
{
if (child.Id == 0)
{
_dbContext.Entry(child).State = EntityState.Added;
}
else
{
_dbContext.Entry(child).State = EntityState.Modified;
}
}
}
return await _dbContext.SaveChangesAsync();
}
Sto avendo difficoltà per aggiornare la Item
raccolta sul Sale
di classe. Con questo codice sono riuscito ad add
o modify
un Item
. Ma, quando delete
alcuni elementi sul livello UI
, nulla viene eliminato.
EF Core
ha qualcosa a che fare con questa situazione, mentre utilizza un modello di archivio generico?
AGGIORNARE
Sembra che il tracciamento degli Items
sia perso. Ecco il mio metodo di recupero generico con include.
public async Task<T> GetByIdAsync<T>(int id, params Expression<Func<T, object>>[] includes) where T : BaseEntity
{
var query = _dbContext.Set<T>().AsQueryable();
if (includes != null)
{
query = includes.Aggregate(query,
(current, include) => current.Include(include));
}
return await query.SingleOrDefaultAsync(e => e.Id == id);
}
Apparentemente la domanda è per applicare le modifiche di entità disconnessa (altrimenti non dovrai fare altro che chiamare SaveChanges
) contenente le proprietà di navigazione della raccolta che devono riflettere gli elementi aggiunti / rimossi / aggiornati dall'oggetto passato.
EF Core non offre tale funzionalità pronta all'uso. Supporta upsert semplice (inserimento o aggiornamento) tramite il metodo di Update
per entità con chiavi generate automaticamente, ma non rileva ed elimina gli elementi rimossi.
Quindi devi fare quel rilevamento da solo. Il caricamento degli elementi esistenti è un passo nella giusta direzione. Il problema con il tuo codice è che non tiene conto dei nuovi elementi, ma invece sta facendo una manipolazione dello stato inutile degli elementi esistenti recuperati dal database.
Di seguito è la corretta attuazione della stessa idea. Utilizza alcuni interni EF Core ( IClrCollectionAccessor
restituiti dal metodo GetCollectionAccessor()
- entrambi richiedono l' using Microsoft.EntityFrameworkCore.Metadata.Internal;
) per manipolare la raccolta, ma il codice utilizza già il metodo GetPropertyAccess()
interno, quindi suppongo che non dovrebbe essere un problema: nel caso in cui qualcosa venga cambiato in una futura versione di EF Core, il codice dovrebbe essere aggiornato di conseguenza. L'accessorio di raccolta è necessario perché mentre IEnumerable<BaseEntity>
può essere utilizzato per accedere genericamente alle raccolte a causa della covarianza, lo stesso non si può dire su ICollection<BaseEntity>
perché è invariante e abbiamo bisogno di un modo per accedere ai metodi Add
/ Remove
. L'accessorio interno fornisce tale capacità e un modo per recuperare genericamente il valore della proprietà dall'entità passata. Aggiornamento: a partire da EF Core 3.0, GetCollectionAccessor e IClrCollectionAccessor fanno parte dell'API pubblica.
Ecco il codice:
public async Task<int> UpdateAsync<T>(T entity, params Expression<Func<T, object>>[] navigations) where T : BaseEntity
{
var dbEntity = await _dbContext.FindAsync<T>(entity.Id);
var dbEntry = _dbContext.Entry(dbEntity);
dbEntry.CurrentValues.SetValues(entity);
foreach (var property in navigations)
{
var propertyName = property.GetPropertyAccess().Name;
var dbItemsEntry = dbEntry.Collection(propertyName);
var accessor = dbItemsEntry.Metadata.GetCollectionAccessor();
await dbItemsEntry.LoadAsync();
var dbItemsMap = ((IEnumerable<BaseEntity>)dbItemsEntry.CurrentValue)
.ToDictionary(e => e.Id);
var items = (IEnumerable<BaseEntity>)accessor.GetOrCreate(entity);
foreach (var item in items)
{
if (!dbItemsMap.TryGetValue(item.Id, out var oldItem))
accessor.Add(dbEntity, item);
else
{
_dbContext.Entry(oldItem).CurrentValues.SetValues(item);
dbItemsMap.Remove(item.Id);
}
}
foreach (var oldItem in dbItemsMap.Values)
accessor.Remove(dbEntity, oldItem);
}
return await _dbContext.SaveChangesAsync();
}
L'algoritmo è piuttosto standard. Dopo aver caricato la raccolta dal database, creiamo un dizionario contenente gli elementi esistenti codificati da Id (per una ricerca rapida). Quindi eseguiamo un singolo passaggio sui nuovi elementi. Usiamo il dizionario per trovare l'elemento esistente corrispondente. Se non viene trovata alcuna corrispondenza, l'elemento viene considerato nuovo e viene semplicemente aggiunto alla raccolta target (tracciata). In caso contrario, l'elemento trovato viene aggiornato dalla fonte e rimosso dal dizionario. In questo modo, dopo aver terminato il ciclo, il dizionario contiene gli elementi che devono essere eliminati, quindi tutto ciò di cui abbiamo bisogno è rimuoverli dalla raccolta (tracciata) di destinazione.
E questo è tutto. Il resto del lavoro verrà svolto dal tracker modifiche EF Core: gli elementi aggiunti alla raccolta di destinazione verranno contrassegnati come Added
, aggiornati - Unchanged
o Modified
e gli elementi rimossi, a seconda del comportamento di eliminazione in cascata, saranno essere contrassegnato per la cancellazione o l'aggiornamento (dissociarsi dal genitore). Se si desidera forzare la cancellazione, è sufficiente sostituire
accessor.Remove(dbEntity, oldItem);
con
_dbContext.Remove(oldItem);
@craigmoliver Ecco la mia soluzione. Non è il massimo, lo so - se trovi un modo più elegante, per favore condividi.
repository:
public async Task<TEntity> UpdateAsync<TEntity, TId>(TEntity entity, bool save = true, params Expression<Func<TEntity, object>>[] navigations)
where TEntity : class, IIdEntity<TId>
{
TEntity dbEntity = await _context.FindAsync<TEntity>(entity.Id);
EntityEntry<TEntity> dbEntry = _context.Entry(dbEntity);
dbEntry.CurrentValues.SetValues(entity);
foreach (Expression<Func<TEntity, object>> property in navigations)
{
var propertyName = property.GetPropertyAccess().Name;
CollectionEntry dbItemsEntry = dbEntry.Collection(propertyName);
IClrCollectionAccessor accessor = dbItemsEntry.Metadata.GetCollectionAccessor();
await dbItemsEntry.LoadAsync();
var dbItemsMap = ((IEnumerable<object>)dbItemsEntry.CurrentValue)
.ToDictionary(e => string.Join('|', _context.FindPrimaryKeyValues(e)));
foreach (var item in (IEnumerable)accessor.GetOrCreate(entity))
{
if (!dbItemsMap.TryGetValue(string.Join('|', _context.FindPrimaryKeyValues(item)), out object oldItem))
{
accessor.Add(dbEntity, item);
}
else
{
_context.Entry(oldItem).CurrentValues.SetValues(item);
dbItemsMap.Remove(string.Join('|', _context.FindPrimaryKeyValues(item)));
}
}
foreach (var oldItem in dbItemsMap.Values)
{
accessor.Remove(dbEntity, oldItem);
await DeleteAsync(oldItem as IEntity, false);
}
}
if (save)
{
await SaveChangesAsync();
}
return entity;
}
Contesto:
public IReadOnlyList<IProperty> FindPrimaryKeyProperties<T>(T entity)
{
return Model.FindEntityType(entity.GetType()).FindPrimaryKey().Properties;
}
public IEnumerable<object> FindPrimaryKeyValues<TEntity>(TEntity entity) where TEntity : class
{
return from p in FindPrimaryKeyProperties(entity)
select entity.GetPropertyValue(p.Name);
}