Erstellen einer Predicate Builder-Erweiterungsmethode

8

Ich habe ein Kendo UI-Raster, das derzeit das Filtern nach mehreren Spalten zulässt. Ich frage mich, ob es einen alternativen Ansatz gibt, um die äußere switch-Anweisung zu entfernen.

Grundsätzlich möchte ich in der Lage sein, eine Erweiterungsmethode zu erstellen, damit ich nach a filtern kann, IQueryable<T> und ich möchte die äußere case-Anweisung löschen, damit ich die Spaltennamen nicht wechseln muss.

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

Einige zusätzliche Informationen, ich verwende NHibernate Linq. Ein weiteres Problem ist, dass die Spalte "Name" in meinem Raster tatsächlich "Vorname" + "" + "Nachname" in meiner Kontaktentität ist. Wir können auch davon ausgehen, dass alle filterbaren Spalten Zeichenfolgen sind.

BEARBEITEN Denken Sie daran, dass dies mit NHibernate Linq und AST funktionieren muss .

Rippo
quelle
2
Haben Sie Predicate Builder gesehen ?
Robert Harvey
@RobertHarvey - Ja, aber ich habe versucht, die Namen mehrerer Spalten aufzulösen.
Rippo

Antworten:

8

Beantwortung Ihrer spezifischen Frage ,

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

Im Fall von "Name" nennen Sie es als;

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

Sie sollten eine Überladung hinzufügen als,

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

Sie können es also so für das Feld "Firma" nennen.

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

Dies verhindert, dass der Aufrufer gezwungen wird, ein Array zu erstellen, wenn er nur ein Feld / eine Eigenschaft auswählen möchte.

Was Sie wahrscheinlich suchen, ist wie folgt

Um diese Logik vollständig um die Definition des zu entfernen selectorund predicatebenötigen Sie weitere Informationen darüber, wie der Filter aufgebaut ist. Wenn möglich, sollte der Filter die Eigenschaften selectorund predicateals haben, damit FilterContactList verwendet wird, die automatisch erstellt werden.

Erweitern Sie das ein wenig,

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

Deiner FilterContactListwürde dann werden

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
quelle
@Rippo Code aktualisiert, Sie brauchen offensichtlich den Wert, den wir suchen!
M Afifi
Interessant, sieht so aus, als würde es keinen Ball spielen ... Ausdruck 'Invoke (Wert (System.Func 2[Domain.Model.Entities.Contact,System.Collections.Generic.IEnumerable1 [System.String]]), Kontakt) konnte nicht analysiert werden. Any (Wert (System.Func`2 [System.String, System .Boolean])) ': Objekt vom Typ' System.Linq.Expressions.ConstantExpression 'kann nicht in den Typ' System.Linq.Expressions.LambdaExpression 'konvertiert werden. Wenn Sie versucht haben, einen Delegaten anstelle eines LambdaExpression zu übergeben, wird dies nicht unterstützt, da Delegaten keine analysierbaren Ausdrücke sind.
Rippo
@Rippo Kannst du bitte den Code hinter FilterDescriptor und den Stack-Trace einfügen?
M Afifi
Der Filterdeskriptor
Rippo
Vollständiger Stapel und Aufrufcode
Rippo
1

Ich denke, ein einfacher Weg, dies zu tun, wäre, eine Karte mit Eigenschaftsnamen für Func zu erstellen:

z.B

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

Und ändern Sie Ihren Code in:

 var propertyFunc = propertyLookup(filter.Member);

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

Sie können den Schalter auch ganz entfernen, indem Sie eine Suche nach der passenden Funktion erstellen:

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

Also, um alles zusammenzufassen:

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 - Ist es nicht redundant, c.FirstName zu überprüfen, wenn Sie auch prüfen (c.FirstName + "" c.LastName)?

Brian Flynn
quelle
Beim erneuten Lesen der Antwort von @ MAfifi ist die Methode ähnlich - nur mithilfe von Lambdas mit Lookups anstelle von Klassen und switch-Anweisungen implementiert. Der Hauptvorteil des Lookup-Ansatzes gegenüber dem Switch besteht darin, dass das Hinzufügen neuer Funktionen oder Spalten eine einfachere Codeänderung erfordert - und auch erweiterbarer ist (es müssen nicht alle in einer Klasse definiert werden).
Brian Flynn
Danke dafür, ich habe es versucht, aber ich bin auf den folgenden Fehler System.InvalidCastException Unable to cast object of type 'NHibernate.Hql.Ast.HqlParameter' to type 'NHibernate.Hql.Ast.HqlBooleanExpression'.
gestoßen
Ich bin mit NHibernate nicht sehr vertraut, aber es sieht so aus, als hätte es Schwierigkeiten, mit der komplexeren where-Klausel umzugehen. Sie können versuchen, die Abfrage zu ändern in: contactList.Select (c => new {Contact = c, Values ​​= propertyLookup (c)}) .Where (cv => cv.Values.Any (v => matchFunc (v, filter))) .Wert) .Wählen Sie (cv => cv.Contact);
Brian Flynn
Entschuldigung Tippfehler in dieser Abfrage: contactList.Select (c => new {Contact = c, Werte = propertyLookup (c)}) .Where (cv => cv.Values.Any (v => matchFunc (v, filter.Value)) )). Wählen Sie (cv => cv.Contact);
Brian Flynn