-1

TL;DR:

//Works
public async Task<Article> GetAsync(int id)
{
    return (await GetAsync(entity => entity.Id.Equals(id))).SingleOrDefault();
}

//TId : struct
//Exception "Unable to create a constant value of type 'System.Object'. Only primitive types or enumeration types are supported in this context."
public async Task<TEntity> GetAsync(TId id)
{
    //Same error:
    //return await Set.Where(Predicate).SingleOrDefaultAsync(entity => entity.Id.Equals(id));
    return (await GetAsync(entity => entity.Id.Equals(id))).SingleOrDefault();
}

Original:

When I'm using a generic method I get an exception but the same method works with non generic implementation. Why is this? Generic repository:

public class EntityRepository<TEntity, TId, TContext> : IEntityRepository<TEntity, TId>
    where TEntity : class, IEntity<TId>
    where TId : struct
    where TContext : BaseIdentityDbContext
{
    protected TContext Context { get; }
    protected IDbSet<TEntity> Set { get; }

    /// <summary>
    /// Use to always filter entities based on a predicate
    /// </summary>
    protected virtual Expression<Func<TEntity, bool>> Predicate => entity => true;


    protected EntityRepository(TContext context, IDbSet<TEntity> set)
    {
        Context = context;
        Set = set;
    }

    /// <summary>
    /// Returns the entity that corresponds to the given id
    /// </summary>
    /// <returns>The found entity, or null</returns>
    public virtual async Task<TEntity> GetAsync(TId id)
    {
        return (await GetAsync(entity => entity.Id.Equals(id))).SingleOrDefault();
    }

    /// <summary>
    /// Returns the entities that corresponds to the given predicate
    /// </summary>
    public virtual async Task<IReadOnlyCollection<TEntity>> GetAsync(Expression<Func<TEntity, bool>> predicate)
    {
        return await Set
            .Where(Predicate)
            .Where(predicate)
            .ToListAsync();
    }
}

By hiding/override the original method I can get it to work:

public class ArticleRepository : EntityRepository<Article, int, MyDbContext >, IArticleRepository
{
    public ArticleRepository(MyDbContext context) : base(context, context.Articles)
    {
    }

    public async new Task<Article> GetAsync(int id)
    {
        return (await GetAsync(entity => entity.Id.Equals(id))).SingleOrDefault();
    }
}

I have also tried EqualityComparer but it does not work, same error for non generic implementation.

//Exception: Unable to create a constant value of type 'System.Collections.Generic.EqualityComparer`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]'. Only primitive types or enumeration types are supported in this context.
public async Task<TEntity> GetAsync(TId id)
{
    return (await GetAsync(entity => EqualityComparer<TId>.Default.Equals(entity.Id, id))).SingleOrDefault();
}
Ogglas
  • 62,132
  • 37
  • 328
  • 418

2 Answers2

2

TL; DR: EntityFramework doesn't know how to do that.

In more detail, you need to understand that the code in your lambda doesn't execute as any other line of C# code. It is transformed into an expression tree, which is inspected by EntityFramework and transformed into the appropriate SQL code which is actually executed on the database. The C# code corresponding to the lambda never really runs. That's also why using EqualityComparer cannot help you - it would need to be understood by EF, and EF only understands a couple of operators, built-in functions etc. It only supports a fraction of C#/.NET, and it changes the meaning of some of the expressions. Supporting EqualityComparer would require EF to be able to translate arbitrary IL code to equivalent SQL - as good as impossible in practice.

Given that Equals is a virtual method and TId isn't constrained any more than object, it will be equivalent to calling object.Equals. object.Equals takes an argument of type object (it's not generic). object is not a primitive type - and as the error message is trying to tell you, only primitive types and enums are supported. In contrast, when you actually use e.g. int instead of a generic type argument, overload resolution will pick the int.Equals(int) overload, which is easily understood by EF as "compare two integers" (and could more nicely be written as simply entity.Id == id).

Luaan
  • 62,244
  • 7
  • 97
  • 116
  • Thanks. Is there anyway to work around this or should I skip this generic approach all together you think? – Ogglas Jun 29 '17 at 13:54
  • @Ogglas No simple way around it I know of; there's some tricks you can do with `dynamic`, but I wouldn't consider that better than simply having a separate method for each possible ID type. – Luaan Jun 29 '17 at 13:59
  • @Ogglas The funny thing is what you're trying to do is exceedingly simple in SQL, but isn't easy to represent in C#; in my own LINQ data provider, I have many ways around these limitations, but as far as I know, the best thing you can do with EF is use custom SQL expression instead of a C# lambda. – Luaan Jun 29 '17 at 14:01
0

I think you should try something this:

Add an interface for entity with Id:

public interface IEntity<TId> where TId : struct
{
    TId Id { get; set; }
}

And add a restriction:

public async Task<TEntity> GetAsync(TId id) where TEntity: IEntity<TId> {}
Roman Koliada
  • 4,286
  • 2
  • 30
  • 59