簡體   English   中英

如何使通用方法根據類型返回表達式?

[英]How do I make a generic method to return an Expression according to the type?

我想創建一個通用函數來插入或更新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的表達式? 我可以轉換嗎? 我也嘗試過使用帶有重載參數的方法,但是據我所知,如果它是通用的,編譯器將始終使用通用函數。

我發現將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多個GetIdQueryInternal方法。 動態重載解決方案將始終嘗試找到可能的最具體參數,因此在這種情況下, PoiModelPoiModel重載,而"Hi!" 下降到后備狀態,並引發異常。

好了,您可以編寫這樣的方法,但是在一般情況下它將相當復雜。

這個概念是:

  • 獲取給定實體類型的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);
    }
}

當然,可以緩存此代碼中的某些中間結果以提高性能。

PS您確定不想重新設計模型嗎? :)

您可以創建通用的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。 在將集合加載到內存中之后,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)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM