述語ビルダー拡張メソッドの作成


8

現在、複数の列でのフィルタリングを許可している剣道UIグリッドがあります。外側のswitchステートメントを削除する代替アプローチがあるかどうか疑問に思っていますか?

基本的には、拡張メソッドを作成して、フィルターを適用できるようIQueryable<T> にしたいのですが、外側のcaseステートメントを削除して、列名を切り替える必要がないようにします。

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

追加情報として、NHibernate Linqを使用しています。また、グリッドの「名前」列が実際には連絡先エンティティの「Firstname」+「」+「LastName」であるという問題もあります。また、すべてのフィルター可能な列は文字列であると想定できます。

編集これはNHibernate LinqとASTで動作する必要があることを覚えておいてください。


2
述語ビルダーを見たことがありますか?
Robert Harvey、

@RobertHarvey-はい、しかし複数の列名を解決しようとしてハングアップしました。
Rippo 2012年

回答:


8

特定の質問答える

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

「名前」の場合は、次のように呼びます。

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

あなたはとしてオーバーロードを追加する必要があります、

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

したがって、「会社」フィールドでは、このように呼び出すことができます。

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

これにより、呼び出し元が1つのフィールド/プロパティのみを選択する場合に配列を作成するように強制するオーバーヘッドが回避されます。

あなたはおそらく次のようなものです

そのロジックを削除するには、完全に周りの定義selectorpredicateフィルタが構築される方法についての詳細情報を必要とします。可能であれば、フィルターにはFilterContactListのselectorand predicateプロパティが必要であり、それを使用して自動的に構築されます。

少し拡大して、

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

あなたFilterContactListはそれから

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

@Rippoはコードを更新しました。明らかに、私たちが探している値が必要です。
M Afifi 2012年

興味深い、ボールをプレーしていないようです...式 'Invoke(value(System.Func 2[Domain.Model.Entities.Contact,System.Collections.Generic.IEnumerable1 [System.String]])、contact).Any(value(System.Func`2 [System.String、System .Boolean])) ':タイプ「System.Linq.Expressions.ConstantExpression」のオブジェクトは、タイプ「System.Linq.Expressions.LambdaExpression」に変換できません。LambdaExpressionの代わりにデリゲートを渡そうとした場合、デリゲートは解析可能な式ではないため、これはサポートされません。
Rippo

@Rippoには、FilterDescriptorの背後にあるコードとスタックトレースを含めることができますか?
M Afifi

フィルタ記述子は剣道からですdocs.kendoui.c​​om/api/wrappers/aspnet-mvc/Kendo.Mvc/...
Rippo

完全なスタックと呼び出しコード:gist.github.com/4181453
Rippo

1

これを行う簡単な方法は、プロパティ名とFuncのマップを作成することだと思います。

例えば

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

コードを次のように変更します。

 var propertyFunc = propertyLookup(filter.Member);

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

一致する関数のルックアップを作成することで、スイッチを完全に削除することもできます。

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

したがって、すべてをまとめるには:

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

注意-(c.FirstName + "" c.LastName)もチェックしている場合、c.FirstNameをチェックすることは冗長ではありませんか?


@MAfifiの答えを読み直すと、メソッドは似ています-クラスとスイッチステートメントの代わりにルックアップ付きのラムダを使用して実装されます。スイッチに対するルックアップアプローチの主な利点は、新しい関数または列を追加するためにコードを簡単に変更する必要があることです。また、コードを拡張することもできます(1つのクラスですべてを定義する必要はありません)。
ブライアンフリン

このおかげで、私はこれを試してみましたが、次のエラーにSystem.InvalidCastException Unable to cast object of type 'NHibernate.Hql.Ast.HqlParameter' to type 'NHibernate.Hql.Ast.HqlBooleanExpression'.
遭遇し

私はNHibernateにあまり詳しくありませんが、より複雑なwhere句を処理するのが難しいようです。クエリを次のように変更してみてください:contactList.Select(c => new {Contact = c、Values = propertyLookup(c)}).Where(cv => cv.Values.Any(v => matchFunc(v、filter .Value).Select(cv => cv.Contact);
Brian Flynn

そのクエリで申し訳ありませんタイプミス:contactList.Select(c => new {Contact = c、Values = propertyLookup(c)}).Where(cv => cv.Values.Any(v => matchFunc(v、filter.Value) ))。Select(cv => cv.Contact);
ブライアンフリン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.