Reputation: 535
I want to create a generic function to insert or update a record in Entity Framework. The problem is the Id property is not in the base class but in each of the specific types. I had the idea to create a function that will return the Expression to check for that Id.
Example:
public void InsertOrUpdateRecord<T>(T record) where T : ModelBase
{
var record = sourceContext.Set<T>().FirstOrDefault(GetIdQuery(record));
if(record == null)
{
//insert
}
else
{
//update
}
}
private Expression<Func<T, bool>> GetIdQuery<T>(T record) where T : ModelBase
{
if (typeof(T) == typeof(PoiModel))
{
//here is the problem
}
}
private Expression<Func<PoiModel, bool>> GetIdQuery(PoiModel record)
{
return p => p.PoiId == record.PoiId;
}
How do I return an expression that checks the Id for that specific type? Can I convert? Also tried to do with methods with overload parameters but, as far as I know, if it's generic the compiler will always go for the generic function.
Upvotes: 0
Views: 1125
Reputation: 236228
You can create generic Upsert
extension which will look for entity in database by entity key value and then add entity or update it:
public static class DbSetExtensions
{
private static Dictionary<Type, PropertyInfo> keys = new Dictionary<Type, PropertyInfo>();
public static T Upsert<T>(this DbSet<T> set, T entity)
where T : class
{
DbContext db = set.GetContext();
Type entityType = typeof(T);
PropertyInfo keyProperty;
if (!keys.TryGetValue(entityType, out keyProperty))
{
keyProperty = entityType.GetProperty(GetKeyName<T>(db));
keys.Add(entityType, keyProperty);
}
T entityFromDb = set.Find(keyProperty.GetValue(entity));
if (entityFromDb == null)
return set.Add(entity);
db.Entry(entityFromDb).State = EntityState.Detached;
db.Entry(entity).State = EntityState.Modified;
return entity;
}
// other methods explained below
}
This method uses entity set metadata to get key property name. You can use any type of configuration here - xml, attributes or fluent API. After set is loaded into memory Entity Framework knows which propery is a key. Of course there could be composite keys, but current implementation do not support this case. You can extend it:
private static string GetKeyName<T>(DbContext db)
where T : class
{
ObjectContext objectContext = ((IObjectContextAdapter)db).ObjectContext;
ObjectSet<T> objectSet = objectContext.CreateObjectSet<T>();
var keyNames = objectSet.EntitySet.ElementType.KeyProperties
.Select(p => p.Name).ToArray();
if (keyNames.Length > 1)
throw new NotSupportedException("Composite keys not supported");
return keyNames[0];
}
To avoid this metadata search you can use caching in keys
Dictionary. Thus each entity type will be examined only once.
Unfortunately EF 6 do not expose context via DbSet
. Which is not very convenient. But you can use reflection to get context instance:
public static DbContext GetContext<TEntity>(this DbSet<TEntity> set)
where TEntity : class
{
object internalSet = set.GetType()
.GetField("_internalSet", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(set);
object internalContext = internalSet.GetType().BaseType
.GetField("_internalContext", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(internalSet);
return (DbContext)internalContext.GetType()
.GetProperty("Owner", BindingFlags.Instance | BindingFlags.Public)
.GetValue(internalContext, null);
}
Usage is pretty simple:
var db = new AmazonContext();
var john = new Customer {
SSN = "123121234", // configured as modelBuilder.Entity<Customer>().HasKey(c => c.SSN)
FirstName = "John",
LastName = "Snow"
};
db.Customers.Upsert(john);
db.SaveChanges();
Further optimization: you can avoid reflecting DbContext if you'll create Upsert
method as member of your context class. Usage will look like
db.Upsert(john)
Upvotes: 0
Reputation: 37770
Well, you can write such method, but it will be rather complex in the general case.
The concept is:
Note, that there are at least two pitfalls, which could affect code:
Here's the sample for entity types, whose primary key consists from single property, and these types are roots of hierarchy (that is, they are not derived from another entity type):
static class MyContextExtensions
{
public static bool Exists<T>(this DbContext context, T entity)
where T : class
{
// we need underlying object context to access EF model metadata
var objContext = ((IObjectContextAdapter)context).ObjectContext;
// this is the model metadata container
var workspace = objContext.MetadataWorkspace;
// this is metadata of particular CLR entity type
var edmType = workspace.GetType(typeof(T).Name, typeof(T).Namespace, DataSpace.OSpace);
// this is primary key metadata;
// we need them to get primary key properties
var primaryKey = (ReadOnlyMetadataCollection<EdmMember>)edmType.MetadataProperties.Single(_ => _.Name == "KeyMembers").Value;
// let's build expression, that checks primary key value;
// this is _CLR_ metatadata of primary key (don't confuse with EF metadata)
var primaryKeyProperty = typeof(T).GetProperty(primaryKey[0].Name);
// then, we need to get primary key value for passed entity
var primaryKeyValue = primaryKeyProperty.GetValue(entity);
// the expression:
var parameter = Expression.Parameter(typeof(T));
var expression = Expression.Lambda<Func<T, bool>>(Expression.Equal(Expression.MakeMemberAccess(parameter, primaryKeyProperty), Expression.Constant(primaryKeyValue)), parameter);
return context.Set<T>().Any(expression);
}
}
Of course, some intermediate results in this code could be cached to improve performance.
P.S. Are you sure, that you don't want to re-design your model? :)
Upvotes: 0
Reputation: 63732
I've found that using dynamic
for dynamic overload resolution like this is immensely useful:
void Main()
{
InsertOrUpdateRecord(new PoiModel()); // Prints p => p.PoiId == record.PoiId
InsertOrUpdateRecord(new AnotherModel()); // Prints a => a.AnotherId == record.AnotherId
InsertOrUpdateRecord("Hi!"); // throws NotSupportedException
}
class PoiModel { public int PoiId; }
class AnotherModel { public int AnotherId; }
public void InsertOrUpdateRecord<T>(T record)
{
GetIdQuery(record).Dump(); // Print out the expression
}
private Expression<Func<T, bool>> GetIdQuery<T>(T record)
{
return GetIdQueryInternal((dynamic)record);
}
private Expression<Func<PoiModel, bool>> GetIdQueryInternal(PoiModel record)
{
return p => p.PoiId == record.PoiId;
}
private Expression<Func<AnotherModel, bool>> GetIdQueryInternal(AnotherModel record)
{
return a => a.AnotherId == record.AnotherId;
}
private Expression<Func<T, bool>> GetIdQueryInternal<T>(T record)
{
// Return whatever fallback, or throw an exception, whatever suits you
throw new NotSupportedException();
}
You can add as many GetIdQueryInternal
methods as you like. The dynamic overload resolution will always try to find the most specific arguments possible, so in this case, PoiModel
drops to the PoiModel
overload, while "Hi!"
drops to the fallback, and throws an exception.
Upvotes: 1