【问题标题】:How do I make a generic method to return an Expression according to the type?如何根据类型创建通用方法来返回表达式?
【发布时间】:2015-10-22 07:36:37
【问题描述】:

我想创建一个通用函数来插入或更新实体框架中的记录。问题是 Id 属性不在基类中,而是在每个特定类型中。我有想法创建一个函数,该函数将返回表达式以检查该 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? 我可以转换吗?还尝试使用具有重载参数的方法,但据我所知,如果它是泛型的,编译器将始终使用泛型函数。

【问题讨论】:

  • 你能添加一个接口到特定类型的id吗?
  • 每种类型都有不同的属性作为id
  • 你不能为所欲为。如果你得到这个表达式,它不会帮助你,因为这个表达式不能转换为 sql 代码。
  • 为什么要在泛型函数中编写非泛型代码。也许您应该只编写一个非泛型函数或将委托传递给您的泛型函数。
  • 考虑使用 GenericRepository 和 UnitOfWork 模式,而不是自己制作一些东西。检查是否应该添加或更新不是存储库逻辑而是应用程序逻辑,并且应该与存储库无关。

标签: c# .net entity-framework generics


【解决方案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!" 会下降到后备并引发异常。

【讨论】:

  • 最后我决定为每种类型创建特定的方法,因为数据模型很小,但我会认为这个答案是正确的
【解决方案2】:

嗯,你可以写这样的方法,但是一般情况下会比较复杂。

概念是:

  • 获取给定实体类型的 EDM 元数据;
  • 检测此类型的主键属性;
  • 获取主键属性的当前值;
  • 构建一个表达式来检查数据库中是否存在主键;
  • 使用该表达式运行适当的扩展方法。

请注意,至少有两个陷阱可能会影响代码:

  • 实体类型可以有复合主键;
  • 实体类型可以参与某些继承层次结构。

这是实体类型的示例,其主键由单个属性组成,这些类型是层次结构的根(即,它们不是从另一个实体类型派生的):

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);
    }
}

当然,可以缓存此代码中的一些中间结果以提高性能。

附:您确定不想重新设计模型吗? :)

【讨论】:

    【解决方案3】:

    您可以创建通用的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、属性或 fluent 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 Dictionary 中使用缓存。因此每个实体类型只会被检查一次。

    不幸的是,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)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-04-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-04
      • 2010-10-01
      相关资源
      最近更新 更多