Crear un método de extensión Predicate Builder

8

Tengo una cuadrícula de interfaz de usuario de Kendo que actualmente estoy permitiendo filtrar en varias columnas. Me pregunto si hay un enfoque alternativo para eliminar la declaración del interruptor externo.

Básicamente, quiero poder crear un método de extensión para poder filtrar IQueryable<T> y quiero eliminar la declaración de mayúsculas y minúsculas para no tener que cambiar los nombres de columna.

    private static IQueryable<Contact> FilterContactList(FilterDescriptor filter, IQueryable<Contact> contactList)
    {
        switch (filter.Member)
        {
            case "Name":
                switch (filter.Operator)
                {
                    case FilterOperator.StartsWith:
                        contactList = contactList.Where(w => w.Firstname.StartsWith(filter.Value.ToString()) || w.Lastname.StartsWith(filter.Value.ToString()) || (w.Firstname + " " + w.Lastname).StartsWith(filter.Value.ToString()));
                        break;
                    case FilterOperator.Contains:
                        contactList = contactList.Where(w => w.Firstname.Contains(filter.Value.ToString()) || w.Lastname.Contains(filter.Value.ToString()) || (w.Firstname + " " + w.Lastname).Contains( filter.Value.ToString()));
                        break;
                    case FilterOperator.IsEqualTo:
                        contactList = contactList.Where(w => w.Firstname == filter.Value.ToString() || w.Lastname == filter.Value.ToString() || (w.Firstname + " " + w.Lastname) == filter.Value.ToString());
                        break;
                }
                break;
            case "Company":
                switch (filter.Operator)
                {
                    case FilterOperator.StartsWith:
                        contactList = contactList.Where(w => w.Company.StartsWith(filter.Value.ToString()));
                        break;
                    case FilterOperator.Contains:
                        contactList = contactList.Where(w => w.Company.Contains(filter.Value.ToString()));
                        break;
                    case FilterOperator.IsEqualTo:
                        contactList = contactList.Where(w => w.Company == filter.Value.ToString());
                        break;
                }

                break;
        }
        return contactList;
    }

Alguna información adicional, estoy usando NHibernate Linq. También otro problema es que la columna "Nombre" en mi cuadrícula es en realidad "Nombre" + "" + "Apellido" en mi entidad de contacto. También podemos suponer que todas las columnas filtrables serán cadenas.

EDITAR Recuerde que esto debe funcionar con NHibernate Linq y AST.

Rippo
fuente
2
¿Has visto Predicate Builder ?
Robert Harvey
@RobertHarvey: sí, pero me colgué tratando de resolver los nombres de varias columnas.
Rippo

Respuestas:

8

Respondiendo tu pregunta específica ,

private static IQueryable<Contact> FilterContactList(
    FilterDescriptor filter,
    IQueryable<Contact> contactList,
    Func<Contact, IEnumerable<string>> selector,
    Predicate<string> predicate)
{
    return from contact in contactList
           where selector(contract).Any(predicate)
           select contact;
}

En el caso de "Nombre", lo llamas como;

FilterContactList(
    filter,
    contactList,
    (contact) => new []
        {
            contact.FirstName,
            contact.LastName,
            contact.FirstName + " " + contact.LastName
        },
    string.StartWith);

Debe agregar una sobrecarga como,

private static IQueryable<Contact> FilterContactList(
    FilterDescriptor filter,
    IQueryable<Contact> contactList,
    Func<Contact, string> selector,
    Predicate<string> predicate)
{
    return from contact in contactList
           where predicate(selector(contract))
           select contact;
}

Entonces puede llamarlo así para el campo "Compañía".

FilterContactList(
    filter,
    contactList,
    (contact) => contact.Company,
    string.StartWith);

Esto evita la sobrecarga de obligar a la persona que llama a crear una matriz cuando solo tiene la intención de seleccionar un Campo / Propiedad.

Lo que probablemente buscas es algo como sigue

Para eliminar esa lógica por completo alrededor de la definición selectory predicatenecesita más información sobre cómo se construye el filtro. Si es posible, el filtro debe tener las propiedades selectory predicatecomo para que FilterContactList use que se construyen automáticamente.

Ampliando eso un poco,

public class FilterDescriptor
{
    public FilterDescriptor(
        string columnName,
        FilterOperator filterOperator,
        string value)
    {
        switch (columnName)
        {
            case "Name":
                Selector = contact => new []
                               {
                                   contact.FirstName,
                                   contact.LastName,
                                   contact.FirstName + " " + contact.LastName
                               };
                break;
            default :
                // some code that uses reflection, avoids having
                // a case for every column name

                // Retrieve the public instance property of a matching name
                // (case sensetive) and its type is string.
                var property = typeof(Contact)
                    .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                    .FirstOrDefault(prop =>
                        string.Equals(prop.Name, columnName) &&
                        prop.PropertyType == typeof(string));

                if (property == null)
                {
                    throw new InvalidOperationException(
                        "Column name does not exist");
                }

                Selector = contact => new[]
                {
                    (string)property.GetValue(contact, null)
                };
                break;
        }

        switch (filterOperator)
        {
            case FilterOperator.StartsWith:
                Predicate = s => s.StartsWith(filter.Value);
                break;
            case FilterOperator.Contains:
                Predicate = s => s.Contains(filter.Value);
                break;
            case FilterOperator.IsEqualTo:
                Predicate = s => s.Equals(filter.Value);
                break;
        }
    }

    public Func<Contact, IEnumerable<string>> Selector { get; private set; }
    public Func<string, bool> Predicate { get; private set; }
}

Tu FilterContactListentonces se convertiría

private static IQueryable<Contact> FilterContactList(
    FilterDescriptor filter,
    IQueryable<Contact> contactList)
{
    return from contact in contactList
           where filter.Selector(contract).Any(filter.Predicate)
           select contact;
}
M Afifi
fuente
Código actualizado de @Rippo, ¡obviamente necesita el valor que estamos buscando!
M Afifi
Interesante, parece que no está jugando a la pelota ... No se pudo analizar la expresión 'Invocar (valor (System.Func 2[Domain.Model.Entities.Contact,System.Collections.Generic.IEnumerable1 [System.String]]), contact) .Any (value (System.Func`2 [System.String, System .Boolean])) ': El objeto del tipo' System.Linq.Expressions.ConstantExpression 'no se puede convertir al tipo' System.Linq.Expressions.LambdaExpression '. Si intentó pasar un delegado en lugar de una LambdaExpression, esto no es compatible porque los delegados no son expresiones analizables.
Rippo
@Rippo, ¿puede incluir el código detrás de FilterDescriptor, por favor, y el seguimiento de la pila?
M Afifi
El descriptor del filtro es de Kendo docs.kendoui.com/api/wrappers/aspnet-mvc/Kendo.Mvc/…
Rippo
Pila completa y código de llamada: gist.github.com/4181453
Rippo
1

Creo que una manera simple de hacer esto sería crear un mapa de nombres de propiedades para Func:

p.ej

private static Dictionary<string, Func<Contact, IEnumerable<string>>> propertyLookup = new Dictionary<string, Func<Contact, IEnumerable<string>>>();

static ClassName() 
{
   propertyLookup["Name"] = c => new [] { c.FirstName, c.LastName, c.FirstName + " " c.LastName };
   propertyLookup["Company"] = c => new [] { c.Company }; 
}

Y cambie su código a:

 var propertyFunc = propertyLookup(filter.Member);

 case FilterOperator.StartsWith:
          contactList = contactList.Where(c => propertyFunc(c).Any(s => s.StartsWith(filter.Value));

También puede eliminar el interruptor por completo creando una búsqueda para la función de coincidencia:

matchFuncLookup[FilterOperator.StartsWith] = (c, f) => c.StartsWith(f);
matchFuncLookup[FilterOperator.Contains] = (c, f) => c.Contains(f);

var matchFunc = matchFuncLookup[filter.Operator];

contactList = contactList.Where(c => propertyFunc(c).Any(s => matchFunc(s, filter.Value));

Entonces, para poner todo junto:

public class ClassName
{
    private static readonly Dictionary<string, Func<Contact, IEnumerable<string>>> PropertyLookup
        = new Dictionary<string, Func<Contact, IEnumerable<string>>>();
    private static readonly Dictionary<FilterOperator, Func<string, string, bool>> MatchFuncLookup
        = new Dictionary<FilterOperator, Func<string, string, bool>>();

    static ClassName()
    {
        PropertyLookup["Name"] = c => new[] { c.FirstName, c.LastName, c.FirstName + " " + c.LastName };
        PropertyLookup["Company"] = c => new[] { c.Company };
        MatchFuncLookup[FilterOperator.StartsWith] = (c, f) => c.StartsWith(f);
        MatchFuncLookup[FilterOperator.Contains] = (c, f) => c.Contains(f);
        MatchFuncLookup[FilterOperator.IsEqualTo] = (c, f) => c == f;
    }

    private static IQueryable<Contact> FilterContactList(FilterDescriptor filter, IQueryable<Contact> contactList)
    {
        var propertyLookup = PropertyLookup[filter.Member];
        var matchFunc = MatchFuncLookup[filter.Operator];
        return contactList.Where(c => propertyLookup(c).Any(v => matchFunc(v, filter.Value)));
    }
} 

NB: ¿No es redundante verificar c.Primer Nombre si también está verificando (c.Primer Nombre + "" c.LastName)?

Brian Flynn
fuente
Al releer la respuesta de @ MAfifi, el método es similar: solo se implementó usando lambda con búsquedas en lugar de clases y declaraciones de cambio. La ventaja clave del enfoque de búsqueda sobre el interruptor es que agregar nuevas funciones o columnas requiere un cambio de código más fácil, y también es más extensible (no tiene que definirse en una sola clase).
Brian Flynn
Gracias por esto, he intentado esto PERO me he encontrado con el siguiente error: System.InvalidCastException Unable to cast object of type 'NHibernate.Hql.Ast.HqlParameter' to type 'NHibernate.Hql.Ast.HqlBooleanExpression'.
Rippo
No estoy muy familiarizado con NHibernate, pero parece que tiene dificultades para tratar con la cláusula where más compleja. Puede intentar modificar la consulta a: contactList.Select (c => new {Contact = c, Values ​​= propertyLookup (c)}) .Where (cv => cv.Values.Any (v => matchFunc (v, filter .Valor) .Seleccione (cv => cv.Contact);
Brian Flynn
lo siento error tipográfico en esa consulta: contactList.Select (c => new {Contact = c, Values ​​= propertyLookup (c)}) .Where (cv => cv.Values.Any (v => matchFunc (v, filter.Value) )). Seleccione (cv => cv.Contact);
Brian Flynn el