Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EF object comparison with generic types

I have the following problem: I have a generic EF repository using DbContext designed for Int or Guid entity keys, so i have a base entity class:

public class EntityBase<TKey> where TKey : struct, IComparable
{
    public virtual TKey Id { get; set; }
}
  • TKey will be provided as Int or Guid in derived classes.

When i run the code

public virtual void LoadEntity()
{
    TEntity entity = Repository.Get<TEntity, TKey>(e => object.Equals(e.Id, EntityId));
}

or

public virtual void LoadEntity()
{
    TEntity entity = Repository.Get<TEntity, TKey>(e => e.Id.CompareTo(EntityId) == 0);
}

where Entity is of type TKey and is set in derived classes as int, for example, I get the following error:

Unable to cast the type 'System.Int32' to type 'System.Object'. LINQ to Entities only supports casting Entity Data Model primitive types.

Repository.Get just pass predicate parameter as a filter for a Where call for DbSet repository;

I understand the error - EF tries to translate to SQL statement and does not know how to treat the object comparison. But I don't know how to rewrite base class and/or LoadEntity() function to allow EF to operate with primitive types. Any ideas?

like image 249
Lixi Avatar asked Dec 16 '22 00:12

Lixi


1 Answers

I think there is an easy way around it but it is a hack. Let me stress it again - it really is a hack. You can try this:

Repository.Get<TEntity, TKey>(e => (object)e.Id == (object)EntityId);

The code above in general should not work. In the CLR world the values would be boxed and will be compared by references. Even if the boxed values were the same the references would be different and therfore the result will be false. However EF queries are not executed by the CLR but translated to SQL. As a result the query will be translated to something like: WHERE Id = {EntityId} which is what you need. Again, using this requires understanding how and why this stuff works and is probably a bit risky. However since there is a hack there should be a cleaner solution. In fact the clean (and not easy solution here) is to build the above expression manually. Here is an example (Sorry I am not using exactly your entities):

    private static TEntity GetEntity<TEntity, TKey>(Expression<Func<TEntity, TKey>> property, TKey keyValue)
        where TKey : struct
        where TEntity : BaseEntity<TKey>
    {
        using (var ctx = new Context2())
        {
            var query = Filter(ctx.Set<TEntity>(), property, keyValue);
            return query.First();
        }
    }


    private static IQueryable<TEntity> Filter<TEntity, TProperty>(IQueryable<TEntity> dbSet,
                                                                  Expression<Func<TEntity, TProperty>> property,
                                                                  TProperty value)
        where TProperty : struct
    {

        var memberExpression = property.Body as MemberExpression;
        if (memberExpression == null || !(memberExpression.Member is PropertyInfo))
        {
            throw new ArgumentException("Property expected", "property");
        }

        Expression left = property.Body;
        Expression right = Expression.Constant(value, typeof (TProperty));

        Expression searchExpression = Expression.Equal(left, right);
        var lambda = Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(left, right),
                                                            new ParameterExpression[] {property.Parameters.Single()});

        return dbSet.Where(lambda);
    }

Note that in the Filter method I build a filter expression that I can compose on. In this example the effective query looks something like this DbSet().Where(e => e.Id == idValue).First() (looks similar to the hack above) but you can use other linq operators on top of this query (including invoking Filter method on the result of the Filter method to filter by multiple criteria)

I defined the entities and the context as follows:

public class BaseEntity<TKey> where TKey : struct
{
    public TKey Id { get; set; }
}

public class EntityWithIntKey : BaseEntity<int>
{
    public string Name { get; set; }
}

public class EntityWithGuidKey : BaseEntity<Guid>
{
    public string Name { get; set; }
}

public class Context2 : DbContext
{
    public DbSet<EntityWithIntKey> EntitiesWithIntKey { get; set; }

    public DbSet<EntityWithGuidKey> EntitiesWithGuidKey { get; set; }
}

You invoke the GetEntity method like this: var e2 = GetEntity(e => e.Id, guidKey);

like image 110
Pawel Avatar answered Jan 02 '23 06:01

Pawel