angularasp.net-coreasp.net-web-apibreeze

Breeze "IN" operator causes server side error


I am writing a web app using angular for the front end, Breeze for the data management/middle layer, and ASP.net core and EFCore on the server side.

The problem I am encountering is that it seems that creating a query using the "IN" operator on an integer field, just doesn't seem to work.

I have simplified the code to just what is needed to reproduce the problem.

On the client side I have a breeze query that looks like this:

import { Injectable } from '@angular/core';
import { EntityManager, EntityQuery, FilterQueryOp, Predicate } from 'breeze-client';
import { Observable, of, from } from 'rxjs';
import { EntityManagerProvider } from './entity-manager-provider';
import { Verb } from './model/verb';

@Injectable({providedIn: 'root'})
export class VerbService {

  manager: EntityManager    

  constructor(private entityManagerProvider: EntityManagerProvider) {
    this.manager = entityManagerProvider.newManager();    
  }

  getVerbs(): Observable<Verb[]> {

    const query = new EntityQuery('Verbs')
      .where("verbID", FilterQueryOp.In, [1, 2, 3]);
    return from(
      this.manager.executeQuery(query)
        .then(data => { return <Verb[]>data.results }));
    }
}

Which produces the following get request:

https://localhost:44356/api/breeze/Verbs?{"where":{"VerbID":{"in":[1,2,3]}}}

which as far as I can tell is correct.

The problem is that this causes the following error:

ArgumentException: Method 'Boolean Contains(System.String)'
declared on type 'System.Collections.Generic.List`1[System.String]'
cannot be called with instance of type 'System.Collections.Generic.List`1[System.Int32]'

Just to be clear nothing involved in this is a string or a List<string>. VerbID is an Int and I'm passing in an array of Ints.

If I change the client-side query to this:

const query = new EntityQuery('Verbs')
      .where("verbID", FilterQueryOp.Equals, 1)
      .orderBy("name");

which produces a request that looks like this:

https://localhost:44356/api/breeze/Verbs?{"where":{"VerbID":{"eq":1}}}

It works fine, so do all other types of queries I have tried that don't use the "in" operator.

For the sake of clarity, the requests are properly URL escaped I have just unescaped them to make them a bit easier to read.

I am just posting this to see if anyone can advise me if I am doing something wrong before I start debugging the Breeze source code to see if it is a bug.

Here is the server-side code for reference, which as you can see I am not really doing anything other than wrapping the breeze ASP.net Core/EF Core server-side stuff.

public class Verb
{
    public int VerbID { get; set; }
    public string Name { get; set; }
    public string Translation { get; set; }
    public string VerbTypeDescription { get; set; }
}

public class VerbsContext : DbContext
{
    public VerbsContext(DbContextOptions<VerbsContext> options) : base(options)
    {
    }

    public DbSet<Verb> Verbs { get; set; }
        
}
[Route("api/[controller]/[action]")]
[BreezeQueryFilter]
public class BreezeController : ControllerBase
{
    private readonly VerbsPersistenceManager _persistenceManager;

    public BreezeController(VerbsContext context)
    {
        _persistenceManager = new VerbsPersistenceManager(context);
    }

    public IQueryable<Verb> Verbs()
    {
        return _persistenceManager.Context.Verbs;
    }        
}

public class VerbsPersistenceManager : EFPersistenceManager<VerbsContext>
{
    public VerbsPersistenceManager(VerbsContext context) : base(context) { }
}

Solution

  • I have done a bit of digging and it's definitely a bug, or more specifically the "IN" operator seems to be incomplete and at the moment is just a bit of a hack that only works on strings.

    This is the offending line from the Breeze.Core source code:

    else if (op == BinaryOperator.In) 
    {
        // TODO: need to generalize this past just 'string'
        var mi = TypeFns.GetMethodByExample((List<String> list) => List.Contains("abc"), expr1.Type);
        return Expression.Call(expr2, mi, expr1);
    }