我想创建一个通用函数来插入或更新Entity Framework中的记录。问题是Id属性不在基类中,而是在每个特定类型中。我有想法创建一个函数,它将返回Expression以检查该Id。
示例:
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;
}
如何返回检查该特定类型的Id的表达式? 我可以转换吗?还尝试使用带有重载参数的方法,但据我所知,如果它是通用的,编译器将始终使用泛型函数。
答案 0 :(得分:1)
我发现使用dynamic
进行动态重载解析非常有用:
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();
}
您可以根据需要添加任意数量的GetIdQueryInternal
方法。动态重载解析将始终尝试找到可能的最具体的参数,因此在这种情况下,PoiModel
会降至PoiModel
重载,而"Hi!"
会降至回退,并引发异常
答案 1 :(得分:0)
嗯,你可以编写这样的方法,但在一般情况下它会相当复杂。
概念是:
请注意,至少存在两个可能影响代码的陷阱:
以下是实体类型的示例,其主键由单个属性组成,这些类型是层次结构的根(即,它们不是从另一个实体类型派生的):
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);
}
}
当然,可以缓存此代码中的一些中间结果以提高性能。
P.S。您确定,您不想重新设计您的模型吗? :)
答案 2 :(得分:0)
您可以创建通用Upsert
扩展,它将按实体键值在数据库中查找实体,然后添加实体或更新它:
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
}
此方法使用实体集元数据来获取密钥属性名称。您可以在此处使用任何类型的配置 - xml,属性或流畅的API。将set加载到内存后,Entity Framework知道哪个属性是关键。当然可能有复合键,但当前的实现不支持这种情况。你可以扩展它:
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];
}
要避免此元数据搜索,您可以在keys
字典中使用缓存。因此,每种实体类型只会被检查一次。
不幸的是,EF 6不会通过DbSet
公开上下文。哪个不太方便。但是你可以使用反射来获取上下文实例:
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);
}
用法非常简单:
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();
进一步优化:如果您将Upsert
方法创建为上下文类的成员,则可以避免反映DbContext。用法看起来像
db.Upsert(john)