简体   繁体   English

如何使通用方法根据类型返回表达式?

[英]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!" 动态重载解决方案将始终尝试找到可能的最具体参数,因此在这种情况下, PoiModelPoiModel重载,而"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: 这个概念是:

  • get EDM metadata for given entity type; 获取给定实体类型的EDM元数据;
  • detect primary key properties for this type; 检测此类型的主键属性;
  • get current values for the primary key properties; 获取主键属性的当前值;
  • build an expression to check primary key existence in database; 建立一个表达式来检查数据库中主键的存在;
  • run appropriate extension method, using that expression. 使用该表达式运行适当的扩展方法。

Note, that there are at least two pitfalls, which could affect code: 请注意,至少有两个陷阱,它们可能会影响代码:

  • entity type could have composite primary key; 实体类型可以具有复合主键;
  • entity types could participate in some inheritance hierarchy. 实体类型可以参与某些继承层次结构。

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM