My Base repository Class
public class Repository : IRepository where TEntity : clas
Update:
Some additional questions arose from the comments. What to do with reference navigation properties and what to do if the related entities do not implement such generic interface, also the inability of compiler inferring the generic type arguments when using such generic method signature.
After thinking a bit I came to conclusion that no base class/interface (and even generic entity type) is needed at all, since EF Core metadata contains all information needed to work with PK (which is used by Find
/ FindAsync
methods and change tracker for instance).
Following is a method which recursively applies disconnected entity graph modifications using only the EF Core metadata information/services. If needed, it could be modified to receive "exclusion" filter in case some entities/collections should be skipped:
public static class EntityGraphUpdateHelper
{
public static async ValueTask<object> UpdateGraphAsync(this DbContext context, object entity) =>
await context.UpdateGraphAsync(await context.FindEntityAsync(entity), entity, new HashSet<IForeignKey>());
private static async ValueTask<object> UpdateGraphAsync(this DbContext context, object dbEntity, object entity, HashSet<IForeignKey> visited)
{
bool isNew = dbEntity == null;
if (isNew) dbEntity = entity;
var dbEntry = context.Entry(dbEntity);
if (isNew)
dbEntry.State = EntityState.Added;
else
{
// ensure is attached (tracked)
if (dbEntry.State == EntityState.Detached)
dbEntry.State = EntityState.Unchanged;
// update primitive values
dbEntry.CurrentValues.SetValues(entity);
}
// process navigations
foreach (var navEntry in dbEntry.Navigations)
{
if (!visited.Add(navEntry.Metadata.ForeignKey)) continue; // already processed
await navEntry.LoadAsync();
if (!navEntry.Metadata.IsCollection())
{
// reference type navigation property
var refValue = navEntry.Metadata.GetGetter().GetClrValue(entity);
navEntry.CurrentValue = refValue == null ? null :
await context.UpdateGraphAsync(navEntry.CurrentValue, refValue, visited);
}
else
{
// collection type navigation property
var accessor = navEntry.Metadata.GetCollectionAccessor();
var items = (IEnumerable<object>)accessor.GetOrCreate(entity, false);
var dbItems = (IEnumerable<object>)accessor.GetOrCreate(dbEntity, false);
var itemType = navEntry.Metadata.GetTargetType();
var keyProperties = itemType.FindPrimaryKey().Properties
.Select((p, i) => (Index: i, Getter: p.GetGetter(), Comparer: p.GetKeyValueComparer()))
.ToList();
var keyValues = new object[keyProperties.Count];
void GetKeyValues(object sourceItem)
{
foreach (var p in keyProperties)
keyValues[p.Index] = p.Getter.GetClrValue(sourceItem);
}
object FindItem(IEnumerable<object> targetCollection, object sourceItem)
{
GetKeyValues(sourceItem);
foreach (var targetItem in targetCollection)
{
bool keyMatch = true;
foreach (var p in keyProperties)
{
(var keyA, var keyB) = (p.Getter.GetClrValue(targetItem), keyValues[p.Index]);
keyMatch = p.Comparer?.Equals(keyA, keyB) ?? object.Equals(keyA, keyB);
if (!keyMatch) break;
}
if (keyMatch) return targetItem;
}
return null;
}
// Remove db items missing in the current list
foreach (var dbItem in dbItems.ToList())
if (FindItem(items, dbItem) == null) accessor.Remove(dbEntity, dbItem);
// Add current items missing in the db list, update others
var existingItems = dbItems.ToList();
foreach (var item in items)
{
var dbItem = FindItem(existingItems, item);
if (dbItem == null)
accessor.Add(dbEntity, item, false);
await context.UpdateGraphAsync(dbItem, item, visited);
}
}
}
return dbEntity;
}
public static ValueTask<object> FindEntityAsync(this DbContext context, object entity)
{
var entityType = context.Model.FindRuntimeEntityType(entity.GetType());
var keyProperties = entityType.FindPrimaryKey().Properties;
var keyValues = new object[keyProperties.Count];
for (int i = 0; i < keyValues.Length; i++)
keyValues[i] = keyProperties[i].GetGetter().GetClrValue(entity);
return context.FindAsync(entityType.ClrType, keyValues);
}
}
Please note that similar to EF Core methods, SaveChangesAsync
call is not part of the above method, and it should be called separately afterwards.
Original:
Handling collections of entities implementing such generic interface requires slightly different approach, since there is no non generic base class / interface to be used for extracting the Id
.
One possible solution is to move the collection handling code to a separate generic method and call it dynamically or via reflection.
For instance (use the VS to determine the necessary using
s):
public static class EntityUpdateHelper
{
public static async Task UpdateEntityAsync<TEntity, TId>(this DbContext context, TEntity entity, params Expression<Func<TEntity, object>>[] navigations)
where TEntity : class, IEntity<TId>
{
var dbEntity = await context.FindAsync<TEntity>(entity.Id);
var dbEntry = context.Entry(dbEntity);
dbEntry.CurrentValues.SetValues(entity);
foreach (var property in navigations)
{
var propertyName = property.GetPropertyAccess().Name;
var dbItemsEntry = dbEntry.Collection(propertyName);
var dbItems = dbItemsEntry.CurrentValue;
var items = dbItemsEntry.Metadata.GetGetter().GetClrValue(entity);
// Determine TEntity and TId, and call UpdateCollection<TEntity, TId>
// via reflection
var itemType = dbItemsEntry.Metadata.GetTargetType().ClrType;
var idType = itemType.GetInterfaces()
.Single(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntity<>))
.GetGenericArguments().Single();
var updateMethod = typeof(EntityUpdateHelper).GetMethod(nameof(UpdateCollection))
.MakeGenericMethod(itemType, idType);
updateMethod.Invoke(null, new[] { dbItems, items });
}
await context.SaveChangesAsync();
}
public static void UpdateCollection<TEntity, TId>(this DbContext context, ICollection<TEntity> dbItems, ICollection<TEntity> items)
where TEntity : class, IEntity<TId>
{
var dbItemsMap = dbItems.ToDictionary(e => e.Id);
foreach (var item in items)
{
if (!dbItemsMap.TryGetValue(item.Id, out var oldItem))
dbItems.Add(item);
else
{
context.Entry(oldItem).CurrentValues.SetValues(item);
dbItemsMap.Remove(item.Id);
}
}
foreach (var oldItem in dbItemsMap.Values)
dbItems.Remove(oldItem);
}
}
and call it from Customer
repository:
return await _context.UpdateEntityAsync(entity, e => e.Addresses);
In case of generic repository (no navigations argument), and all child collection entities implementing that interface, simple iterate the dbEntry.Collections
property, e.g.
//foreach (var property in navigations)
foreach (var dbItemsEntry in dbEntry.Collections)
{
//var propertyName = property.GetPropertyAccess().Name;
//var dbItemsEntry = dbEntry.Collection(propertyName);
var dbItems = dbItemsEntry.CurrentValue;
var items = dbItemsEntry.Metadata.GetGetter().GetClrValue(entity);
// ...
}