Criando um método de extensão do Predicate Builder

8

Tenho uma grade de interface do usuário do Kendo que atualmente estou permitindo a filtragem em várias colunas. Gostaria de saber se existe uma abordagem alternativa para remover a instrução switch externa?

Basicamente, quero criar um método de extensão para poder filtrar um IQueryable<T> e quero descartar a instrução de caso externo para não precisar alterar os nomes das colunas.

    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;
    }

Algumas informações adicionais, estou usando o NHibernate Linq. Outro problema é que a coluna "Nome" na minha grade é na verdade "Nome" + "" + "Sobrenome" na minha entidade de contato. Também podemos assumir que todas as colunas filtráveis ​​serão cadeias de caracteres.

EDIT Lembre-se de que isso precisa funcionar com o NHibernate Linq e o AST.

Rippo
fonte
2
Você já viu o Predicate Builder ?
31812 Robert Harvey
@ RobertHarvey - sim, mas fiquei desligado tentando resolver os vários nomes de colunas.
Rippo

Respostas:

8

Respondendo a sua pergunta 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;
}

No caso de "Nome", você o chama como;

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

Você deve adicionar uma 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;
}

Assim, você pode chamar assim no campo "Empresa".

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

Isso evita a sobrecarga de forçar o chamador a criar uma matriz quando ele apenas pretende selecionar um campo / propriedade.

O que você provavelmente está procurando é o seguinte

Para remover completamente essa lógica ao definir selectore predicateprecisar de mais informações sobre como o filtro é construído. Se possível, o filtro deve ter as propriedades selectore predicatecomo para o FilterContactList usar, que são construídas automaticamente.

Expandindo um pouco isso,

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; }
}

O seu FilterContactListse tornaria

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
fonte
@Rippo código atualizado, você obviamente precisa do valor que estamos procurando!
M Afifi
Interessante, parece que não está jogando bola ... Não foi possível analisar a expressão 'Invoke (valor (System.Func 2[Domain.Model.Entities.Contact,System.Collections.Generic.IEnumerable1 [System.String]]), contato). Qualquer valor (System.Func`2 [System.String, System .Boolean])) ': O objeto do tipo' System.Linq.Expressions.ConstantExpression 'não pode ser convertido no tipo' System.Linq.Expressions.LambdaExpression '. Se você tentou passar um delegado em vez de um LambdaExpression, isso não é suportado porque os delegados não são expressões analisáveis.
Rippo
@Rippo, você pode incluir o código por trás do FilterDescriptor e o rastreamento da pilha?
M Afifi
O descritor de filtro é de Kendo docs.kendoui.com/api/wrappers/aspnet-mvc/Kendo.Mvc/…
Rippo
Pilha completa e código de chamada: gist.github.com/4181453
Rippo
1

Eu acho que uma maneira simples de fazer isso seria criar um mapa de nomes de propriedades para o Func:

por exemplo

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 }; 
}

E mude seu código para:

 var propertyFunc = propertyLookup(filter.Member);

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

Você também pode eliminar completamente a opção criando uma pesquisa para a função correspondente:

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));

Então, para juntar tudo:

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 - Não é redundante verificar c.FirstName se você também está verificando (c.FirstName + "" c.LastName)?

Brian Flynn
fonte
Relendo a resposta do @ MAfifi, o método é semelhante - implementado usando lambda com pesquisas, em vez de classes e instruções de troca. A principal vantagem da abordagem de pesquisa sobre o switch é que a adição de novas funções ou colunas exige uma alteração mais fácil do código - e também é mais extensível (nem tudo precisa ser definido na classe).
Brian Flynn
Obrigado por isso, eu tentei isso, mas foram executados o seguinte erro: System.InvalidCastException Unable to cast object of type 'NHibernate.Hql.Ast.HqlParameter' to type 'NHibernate.Hql.Ast.HqlBooleanExpression'.
Rippo
Não estou muito familiarizado com o NHibernate, mas parece que está tendo dificuldades para lidar com a cláusula where mais complexa. Você pode tentar modificar a consulta para: contactList.Select (c => new {Contact = c, Valores = propertyLookup (c)}) .Where (cv => cv.Values.Any (v => matchFunc (v, filter .value) .Select (CV => cv.Contact);
Brian Flynn
desculpe erro de digitação nessa consulta: contactList.Select (c => new {Contact = c, Valores = propertyLookup (c)}) .Where (cv => cv.Values.Any (v => matchFunc (v, filter.Value)) )) Selecione (cv => cv.Contact);
Brian Flynn