Sto cercando di estendere la mia routine di seed personalizzata (so che EF Core 2.1 supporta seeding nativamente, ma ho un blocco nella conversione) per applicare le eliminazioni. Se esiste un record nel database, ma non più nei dati seme, voglio eliminarlo. Piuttosto che scrivere una routine di eliminazione personalizzata per ogni DbSet
, sto cercando di implementare con i generici (e possibilmente la riflessione se necessario).
Il mio primo tentativo:
private static void Delete<TEntity>(DbContext dbContext, IEnumerable<TEntity> seedRows) where TEntity : class, IBaseEntity
{
var toRemove = dbContext.Set<TEntity>().Except(seedRows);
dbContext.RemoveRange(toRemove);
dbContext.SaveChanges();
}
Tuttavia, poiché TEntity
contiene alcune proprietà che sono nulle nei dati seme (come i timestamp generati sull'aggiunta), non posso confrontare l'intera entità nella chiamata Except()
(con il comparatore di uguaglianza predefinito, comunque). Mi interessa davvero solo paragonare la chiave primaria.
I miei lavori in corso per affrontare questo problema sono di seguito. TEntity
potrebbe avere una chiave primaria di una semplice colonna Id
, oppure potrebbe essere una mappatura molti-a-molti con una chiave primaria complessa di due <EntityName>Id
. IBaseEntity
momento, IBaseEntity
non ha alcuna informazione su Id
/ chiave primaria poiché è implementata sia da entità di base che da entità molti-a-molti / junction.
private static void Delete<TEntity>(DbContext dbContext, IEnumerable<TEntity> seedRows) where TEntity : class, IBaseEntity
{
var idProperties = typeof(TEntity).GetProperties().Where(p => p.Name.Contains("Id"));
var toRemove = dbContext.Set<TEntity>().Select(s => idProperties).Except(seedRows.Select(s => idProperties));
dbContext.RemoveRange(toRemove);
dbContext.SaveChanges();
}
Le due istanze di .Select(s => idProperties)
, tuttavia, ovviamente non funzionano. C'è un modo per selezionare le proprietà Id
(o, in alternativa, la chiave primaria) di un DbSet<T>
da utilizzare nel confronto Except()
? Sono aperto ad un approccio completamente diverso, poiché mi sento come se fossi fuori dalle erbacce.
I metadati di EF Core forniscono tutte le informazioni necessarie necessarie.
Invece di riflettere, puoi usare la classe Expression
per creare dinamicamente criteri come questo (pseudocodice):
(seedRows1.Key1 == e.Key1 && seedRows1.Key2 == e.Key2 ... && seeedRows1.KeyM == e.KeyM)
||
(seedRows2.Key1 == e.Key1 && seedRows2.Key2 == e.Key2 ... && seeedRows2.KeyM == e.KeyM)
...
||
(seedRowsN.Key1 == e.Key1 && seedRowsN.Key2 == e.Key2 ... && seeedRowsN.KeyM == e.KeyM);
che restituirebbe gli elementi corrispondenti dal db. Per ottenere gli elementi non corrispondenti, i criteri possono essere semplicemente invertiti e utilizzati come predicati per l'eliminazione. Si noti che per il singolo PK ciò si tradurrebbe in criteri SQL NOT IN (...)
.
Mettendolo in azione:
private static void Delete<TEntity>(DbContext dbContext, IEnumerable<TEntity> seedRows)
where TEntity : class//, IBaseEntity
{
var entityType = dbContext.Model.FindEntityType(typeof(TEntity));
var entityPK = entityType.FindPrimaryKey();
var dbEntity = Expression.Parameter(entityType.ClrType, "e");
Expression matchAny = null;
foreach (var entity in seedRows)
{
var match = entityPK.Properties
.Select(p => Expression.Equal(
Expression.Property(dbEntity, p.PropertyInfo),
Expression.Property(Expression.Constant(entity), p.PropertyInfo)))
.Aggregate(Expression.AndAlso);
matchAny = matchAny != null ? Expression.OrElse(matchAny, match) : match;
}
var dbQuery = dbContext.Set<TEntity>().AsQueryable();
if (matchAny != null)
{
var predicate = Expression.Lambda<Func<TEntity, bool>>(Expression.Not(matchAny), dbEntity);
dbQuery = dbQuery.Where(predicate);
}
var dbEntities = dbQuery.ToList();
if (dbEntities.Count == 0) return;
dbContext.RemoveRange(dbEntities);
dbContext.SaveChanges();
}