1

I have implemented Specific Pattern following vkhorikov/SpecificationPattern

For example, i have a product table and it has many to many relation with WarehouseProducts table. I need to find all products for a given list of wearhouses. So, this is what i have kind of

public class Products
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<WarehouseProduct> Warehouses { get; set; }
}

public class WarehouseProduct
{
    [Key]
    public int Id { get; set; }
    public int WarehouseId { get; set; }
    [ForeignKey("ProductId")]
    public int ProductId { get; set; }
}
public class WarehouseProductSpecification : Specification<Products>
{
    private readonly List<int> _ids;    
    public WarehouseProductSpecification(IEnumerable<int> warehouseIds)
    {
        _ids = warehouseIds.ToList();
    }    
    public override Expression<Func<Products, bool>> ToExpression()
    {
        Expression<Func<WarehouseProduct, bool>> expr =
            (w) => _ids.Contains(w.WarehouseId);

        return
            q => !_ids.Any()
                 || (_ids.Any() && q.Warehouses != null && q.Warehouses.Any(expr.Compile()));
    }
}

But, when i execute I got the following error

System.NotSupportedException Cannot compare elements of type 'System.Collections.Generic.ICollection`1[[Data.TableObjects.WarehouseProduct, Data, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]'. Only primitive types, enumeration types and entity types are supported.

I am really struggling to create the specification for a ICollection. Is there any way to achieve that? FYI, I am using EF6 to connect to a SQLServer database.

Updated

// To resond to the first comment..

I have use the spec on the repostiory so the following code gets error

   var products = _context.Products
                .Include("WarehouseProducts")
                .Where(warehouseProductSpec.ToExpression())
                .ToList();

so the to list gets the error

Update 2

I tried to use the code added by @Evk

if (_ids.Count == 0)
    return x => true;
return q => q.Warehouses.Any(w => _ids.Contains(w.WarehouseId));

I got the following error while trying your code

    Test [0:10.297] Failed: System.ArgumentException: Property 'System.String WearhouseId' is not defined for type 'Data.TableObjects.Products'
System.ArgumentException
Property 'System.String WearhouseId' is not defined for type 'Data.TableObjects.Products'
   at System.Linq.Expressions.Expression.Property(Expression expression, PropertyInfo property)
   at System.Linq.Expressions.ExpressionVisitor.VisitArguments(IArgumentProvider nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
   at System.Linq.Expressions.ExpressionVisitor.VisitLambda[T](Expression`1 node)
   at System.Linq.Expressions.ExpressionVisitor.VisitArguments(IArgumentProvider nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
   at System.Linq.Expressions.ExpressionVisitor.VisitBinary(BinaryExpression node)
   at Infrastructure.Expressions.AndSpecification`1.ToExpression() in C:\..\Expressions\AndSpecification
MJK
  • 3,434
  • 3
  • 32
  • 55
  • On which line do you get that error? – FaizanHussainRabbani Mar 04 '18 at 14:06
  • I have updated the question @FaizanRabbani – MJK Mar 04 '18 at 14:11
  • 1
    `ParameterReplacer` implementation from the sample you are following is so naïve, and apparently incorrect. – Ivan Stoev Mar 04 '18 at 17:00
  • @IvanStoev can you point me to any good example please – MJK Mar 04 '18 at 17:02
  • See this for instance https://stackoverflow.com/questions/36650934/combine-two-lambda-expressions-with-inner-expression/36651409#36651409 – Ivan Stoev Mar 04 '18 at 17:04
  • 1
    `AndSpecification` implementation using the above: `public override Expression> ToExpression() { var left = _left.ToExpression(); var right = _right.ToExpression(); return Expression.Lambda>( Expression.AndAlso(left.Body, right.Body.ReplaceParameter(right.Parameters[0], left.Parameters[0])), left.Parameters[0] ); }` Hope that helps. – Ivan Stoev Mar 04 '18 at 17:14
  • @IvanStoev thanks for the code, it works now – MJK Mar 04 '18 at 17:51

1 Answers1

2

You have to always remember that your expression is converted to SQL query by entity framework. For example, think about how this

q.Warehouses.Any(expr.Compile())

can be translated to SQL? It cannot, because result of expr.Compile() is basically .NET code - it's not an expression tree any more. You can use third party libraries, like LinqKit, to be able to integrate one expression into another, but without that it will not just work. In your specific case that is not needed though.

First you need to clean up your expression. If list of _ids is empty - you don't need to filter anything. So return expression which just returns true (matches all):

if (_ids.Count == 0)
   return x => true;

Now, after if, we know that there are ids in a list, so we can skip all checks related to that. Then, you don't need to check Warehouses for null. This check is what causes exception you observe. It doesn't make sense given that this expression will be converted to SQL query, so this check can be removed. Code inside expression will never be executed directly (at least in EF6), so no null reference exceptions are possible.

That leaves us only with one expression, which actually does useful work, so final ToExpression will be:

if (_ids.Count == 0)
    return x => true;
return q => q.Warehouses.Any(w => _ids.Contains(w.WarehouseId));
Evk
  • 98,527
  • 8
  • 141
  • 191
  • I have added the error I get when i tried your code. It seems the AndSpecification cannot handle this. can you please point me some direction – MJK Mar 04 '18 at 16:19
  • @MJK that is because AndSpecification is implemented incorrectly by your link (specifically ParameterReplacer). As I understand, that code is just for illustration purposes, it is not production ready. I'd suggest using stable library, like LinqKit mentioned above, for combining expressions with And and Or. – Evk Mar 04 '18 at 17:16