[英]How do I make a generic method to return an Expression according to the type?
I want to create a generic function to insert or update a record in Entity Framework. 我想创建一个通用函数来插入或更新Entity Framework中的记录。 The problem is the Id property is not in the base class but in each of the specific types.
问题是Id属性不在基类中,而是在每种特定类型中。 I had the idea to create a function that will return the Expression to check for that Id.
我的想法是创建一个函数,该函数将返回Expression以检查该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? 如何返回检查该特定类型的ID的表达式? 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.
我也尝试过使用带有重载参数的方法,但是据我所知,如果它是通用的,编译器将始终使用通用函数。
I've found that using dynamic
for dynamic overload resolution like this is immensely useful: 我发现将
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();
}
You can add as many GetIdQueryInternal
methods as you like. 您可以根据需要添加
GetIdQueryInternal
多个GetIdQueryInternal
方法。 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!"
动态重载解决方案将始终尝试找到可能的最具体参数,因此在这种情况下,
PoiModel
会PoiModel
重载,而"Hi!"
drops to the fallback, and throws an exception. 下降到后备状态,并引发异常。
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. 当然,可以缓存此代码中的某些中间结果以提高性能。
PS Are you sure, that you don't want to re-design your model? PS您确定不想重新设计模型吗? :)
:)
You can create generic Upsert
extension which will look for entity in database by entity key value and then add entity or update it: 您可以创建通用的
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
}
This method uses entity set metadata to get key property name. 此方法使用实体集元数据获取密钥属性名称。 You can use any type of configuration here - xml, attributes or fluent API.
您可以在此处使用任何类型的配置-xml,属性或流畅的API。 After set is loaded into memory Entity Framework knows which propery is a key.
在将集合加载到内存中之后,Entity Framework知道哪个属性是关键。 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. 为了避免这种元数据搜索,您可以在
keys
Dictionary中使用缓存。 Thus each entity type will be examined only once. 因此,每个实体类型将仅被检查一次。
Unfortunately EF 6 do not expose context via DbSet
. 不幸的是,EF 6没有通过
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. 进一步的优化:如果将
Upsert
方法创建为上下文类的成员,则可以避免反映DbContext。 Usage will look like 用法看起来像
db.Upsert(john)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.