For a given Entity Framework data set, I have written several adapters to import data into my entity models. Rather than manually write mapping code for each entity model, I tried for a generalized approach using reflection.
My requirements are:
- The updater must create the entity if it does not exist, or update the existing entity with any changed properties or relationships.
- External data are often subsets; therefore, do not delete entities if they are not in external data
- If external data reference other entities which do not exist, eg tags, the updater must create them and ensure the references are still valid, while respecting key constraints.
My entities implement an interface, IEntity:
public interface IEntity
{
int Id { get; set; }
T CreateEmpty<T>() where T:class,IEntity;
bool EntityEquals(IEntity entity);
}
EntityEquals compares entities based on their natural keys. An implementation might look like:
public class Tag : IEntity
{
public int Id { get; set; }
// Name is a natural key
public string Name { get; set; }
public T CreateEmpty<T>() where T : class, IEntity
{
return new Tag() as T;
}
public bool EntityEquals(IEntity entity)
{
var e = entity as Tag;
return e != null && Name==e.Name;
}
}
I invoke this like
EntityDataUpdater.Update(context, externalEntitiesList);
And here's the implementation. Comments and feedback greatly appreciated!
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using Project.Models;
namespace Project.Models.Adapters
{
public static class EntityDataUpdater
{
public static void Update(DbContext context, IEnumerable<IEntity> entities)
{
if (!entities.Any()) return;
var set = context.Set(entities.First().GetType());
set.Load(); // prefetch data to memory
foreach (var entity in entities)
{
var tEntity = entity.ImportOrUpdate(context);
// if newly imported, add it
if (tEntity.Id == 0) set.Add(tEntity);
// otherwise, indicate that it is updated
else context.Entry(tEntity).State = EntityState.Modified;
}
context.SaveChanges();
}
}
static class EntityCopyExtensions
{
private static bool TryFindIn<T>(this T entity, DbContext context, out T o) where T : class, IEntity
{
var set = context.Set(entity.GetType());
o = set.Local.Cast<IEntity>()
.FirstOrDefault(item => item.EntityEquals(entity)) as T // check local first
?? ((IEnumerable)set).Cast<IEntity>()
.FirstOrDefault(item => item.EntityEquals(entity)) as T; // then check DB
return o != null;
}
private static IEntity FindOrCreateIn(this IEntity entityIn, DbContext context)
{
// if item exists, return it; otherwise create it
IEntity entityOut;
return !entityIn.TryFindIn(context, out entityOut) ? entityIn.ImportOrUpdate(context) : entityOut;
}
public static T ImportOrUpdate<T>(this T entityIn, DbContext context) where T : class, IEntity
{
var entityT = entityIn.GetType();
T entityOut;
if(!entityIn.TryFindIn(context, out entityOut))
{
entityOut = entityIn.CreateEmpty<T>();
}
// copy properties
foreach (var property in entityT.GetProperties()
.Where(p => p.Name != "Id" &&
p.CanWrite && (
p.PropertyType == typeof(string) ||
p.PropertyType == typeof(int) ||
p.PropertyType == typeof(DateTime)
))){
var value = property.GetValue(entityIn, null);
if (value != null) property.SetValue(entityOut, value, null);
}
// copy direct references
foreach (var reference in entityT.GetProperties()
.Where(r => r.PropertyType.IsImplementationOf(typeof(IEntity)))
){
var origValue = (IEntity) reference.GetValue(entityIn, null);
reference.SetValue(entityOut,origValue.FindOrCreateIn(context),null);
}
// copy collections of references
foreach (var reference in entityT.GetProperties()
.Where(r=>
r.PropertyType.IsImplementationOf(typeof(IEnumerable)) &&
r.PropertyType.IsGenericType &&
r.PropertyType.GetGenericArguments().First().IsImplementationOf(typeof(IEntity))
)){
var itemsIn = (IEnumerable) (reference.GetValue(entityIn, null));
// itemsOut begins with all items from entityOut
var itemsOut = ((IEnumerable) (reference.GetValue(entityOut, null))).Cast<IEntity>().ToList();
// then adds any new items from entityIn
foreach (IEntity item in itemsIn)
{
var itemOut = item.FindOrCreateIn(context);
//only add new items
if (!itemsOut.Any(i => i.Id == itemOut.Id) || item.Id==0)
{
itemsOut.Add(item.FindOrCreateIn(context));
}
}
reference.SetValue(entityOut,itemsOut.CastTo(reference.PropertyType),null);
}
return entityOut;
}
/// <summary>
/// Cast each element in <paramref name="list"/> to type of the first object,
/// or null
/// </summary>
private static IEnumerable CastTo(this IList list, Type targetT)
{
if (!(targetT.IsImplementationOf(typeof(IEnumerable)) && targetT.IsGenericType))
throw new ArgumentException("targetT must be an IEnumerable of T.");
var itemT = targetT.GetGenericArguments().First();
var ret = (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(new[] {itemT}));
foreach (var i in list)
ret.Add(i);
return ret;
}
/// <summary>
/// Returns true if type <paramref name="t"/> implements interface <paramref name="i"/>
/// </summary>
private static bool IsImplementationOf(this Type t, Type i)
{
if (!i.IsInterface) throw new ApplicationException("Method must be of type Interface");
return t.GetInterfaces().Contains(i);
}
/// <summary>
/// Returns true if type <paramref name="t"/> implements interface <paramref name="i"/>
/// </summary>
private static bool IsImplementationOf(this Type t, string i)
{
if (!Type.GetType(i, true).IsInterface) throw new ApplicationException("Method must be of type Interface");
return t.GetInterface(i) != null;
}
}
}