Muaddib
Muaddib

Reputation: 535

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. The problem is the Id property is not in the base class but in each of the specific types. I had the idea to create a function that will return the Expression to check for that 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? 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.

Upvotes: 0

Views: 1125

Answers (3)

Sergey Berezovskiy
Sergey Berezovskiy

Reputation: 236228

You can create generic Upsert extension which will look for entity in database by entity key value and then add entity or update it:

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. After set is loaded into memory Entity Framework knows which propery is a key. 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. Thus each entity type will be examined only once.

Unfortunately EF 6 do not expose context via 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. Usage will look like

db.Upsert(john)

Upvotes: 0

Dennis
Dennis

Reputation: 37770

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;
  • 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.

P.S. Are you sure, that you don't want to re-design your model? :)

Upvotes: 0

Luaan
Luaan

Reputation: 63732

I've found that using dynamic for dynamic overload resolution like this is immensely useful:

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. 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!" drops to the fallback, and throws an exception.

Upvotes: 1

Related Questions