9

I'm working on a project and my task is to add an advanced search and filtering option which allows the users to query desired results from a list of Windows events by specifying as many conditions as they want.

The idea is each Windows event log has several properties such as LogName, Source, CreatedDate, Message, Number, etc. (part of the FieldItem enum). In total, there are four possbile data types: String, DateTime, Integral (Int/Long), and EventEntryType. Each of these four data types has its own collection of selector operands (part of the SelectorOperator enum). Here is a picture to give you a better idea of how the overall structure looks like:

My initial implementation of this idea is this:

 public static class SearchProvider
{
    public static List<EventLogItem> SearchInLogs(List<EventLogItem> currentLogs, SearchQuery query)
    {
        switch (query.JoinType)
        {
            case ConditionJoinType.All:
                return SearchAll(currentLogs, query);
            case ConditionJoinType.Any:
                return SearchAny(currentLogs, query);
            default:
                return null;
        }
    }

    private static List<EventLogItem> SearchAll(List<EventLogItem> currentLogs, SearchQuery query)
    {
        foreach (SearchCondition condition in query.Conditions)
        {
            switch (condition.FieldName)
            {
                case FieldItem.Category:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Contains:
                            currentLogs = currentLogs.Where(item => item.Category.ToLower().Contains(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.EndsWith:
                            currentLogs = currentLogs.Where(item => item.Category.ToLower().EndsWith(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.Is:
                            currentLogs = currentLogs.Where(item => string.Equals(item.Category, condition.FieldValue as string, StringComparison.OrdinalIgnoreCase)).ToList();
                            break;
                        case SelectorOperator.StartsWith:
                            currentLogs = currentLogs.Where(item => item.Category.ToLower().StartsWith(condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
                case FieldItem.InstanceID:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Equals:
                            currentLogs = currentLogs.Where(item => item.InstanceID == long.Parse(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.IsGreaterThan:
                            currentLogs = currentLogs.Where(item => item.InstanceID > long.Parse(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.IsLessThan:
                            currentLogs = currentLogs.Where(item => item.InstanceID < long.Parse(condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
                case FieldItem.LogName:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Contains:
                            currentLogs = currentLogs.Where(item => item.LogName.ToLower().Contains(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.EndsWith:
                            currentLogs = currentLogs.Where(item => item.LogName.ToLower().EndsWith(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.Is:
                            currentLogs = currentLogs.Where(item => string.Equals(item.LogName, condition.FieldValue as string, StringComparison.OrdinalIgnoreCase)).ToList();
                            break;
                        case SelectorOperator.StartsWith:
                            currentLogs = currentLogs.Where(item => item.LogName.ToLower().StartsWith(condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
                case FieldItem.Message:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Contains:
                            currentLogs = currentLogs.Where(item => item.Message.ToLower().Contains(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.EndsWith:
                            currentLogs = currentLogs.Where(item => item.Message.ToLower().EndsWith(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.Is:
                            currentLogs = currentLogs.Where(item => string.Equals(item.Message, condition.FieldValue as string, StringComparison.OrdinalIgnoreCase)).ToList();
                            break;
                        case SelectorOperator.StartsWith:
                            currentLogs = currentLogs.Where(item => item.Message.ToLower().StartsWith(condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
                case FieldItem.Number:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Equals:
                            currentLogs = currentLogs.Where(item => item.Number == int.Parse(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.IsGreaterThan:
                            currentLogs = currentLogs.Where(item => item.Number > int.Parse(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.IsLessThan:
                            currentLogs = currentLogs.Where(item => item.Number < int.Parse(condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
                case FieldItem.Source:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Contains:
                            currentLogs = currentLogs.Where(item => item.Source.ToLower().Contains(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.EndsWith:
                            currentLogs = currentLogs.Where(item => item.Source.ToLower().EndsWith(condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.Is:
                            currentLogs = currentLogs.Where(item => string.Equals(item.Source, condition.FieldValue as string, StringComparison.OrdinalIgnoreCase)).ToList();
                            break;
                        case SelectorOperator.StartsWith:
                            currentLogs = currentLogs.Where(item => item.Source.ToLower().StartsWith(condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
                case FieldItem.Type:
                    switch (condition.SelectorOperator)
                    {
                        case SelectorOperator.Is:
                            currentLogs = currentLogs.Where(item => item.Type == (EventLogEntryType)Enum.Parse(typeof(EventLogEntryType), condition.FieldValue as string)).ToList();
                            break;
                        case SelectorOperator.IsNot:
                            currentLogs = currentLogs.Where(item => item.Type != (EventLogEntryType)Enum.Parse(typeof(EventLogEntryType), condition.FieldValue as string)).ToList();
                            break;
                    }
                    break;
            }
        }

        return currentLogs;
    }

A sample query might look like this:

Condition Selector:

All of the conditions are met

Conditions:

LogName Is "Application"
Message Contains "error"
Type IsNot "Information"
InstanceID IsLessThan 1934

As you can see, the SearchAll() method is quite long and not very maintainable due to the nested switch statements. The code works, however, I feel like this is not the most elegant way to implement this design. Is there a better way to approach this problem? Maybe by figuring out a way to reduce the complexity of the switch hierarchy OR by making the code more generic? Any help/suggestion is appreciated.

4

4 回答 4

2

处理此类任务的标准方法是创建自定义 IQueryable 提供程序并仅使用 LINQ。从字面上看,您正在寻找的每个操作都有一个通过 LINQ 表达式的标准可扩展性机制。基本思想是您将拥有ExpressionVisitor应用每个重写规则的实现,而不是拥有巨大的 switch 语句。由于您可以使用任意数量的表达式访问者,因此您的维护和可扩展性成本会大大降低。

如果您想采用这种方法,我强烈建议您查看IQToolkit和 Matt Warren 的构建 IQueryable博客系列。

于 2013-05-13T22:06:44.223 回答
2

避免嵌套和相关重复的一种方法是将提取值的代码部分与对其执行操作的代码部分分开。这是一个应该说明该技术的小示例:

Func<EventLogEntry,string> getString = null;
Func<EventLogEntry,int> getInt32 = null;
...
switch (condition.FieldName) {
    case FieldItem.Category: getString = e => e.Category; break;
    case FieldItem.Message:  getString = e => e.Message;  break;
    case FieldItem.Number:   getInt32  = e => e.Number;   break;
    default:                 throw new ApplicationException("Unsupported field");
}
switch (condition.SelectorOperator) {
   case SelectorOperator.Contains:
        currentLogs = currentLogs.Where(item => getString(item).ToLower().Contains(condition.FieldValue as string)).ToList();
   break;
   case SelectorOperator.EndsWith:
       currentLogs = currentLogs.Where(item => getString(item).ToLower().EndsWith(condition.FieldValue as string)).ToList();
   break;
   case SelectorOperator.Is:
       currentLogs = currentLogs.Where(item => string.Equals(getString(item), condition.FieldValue as string, StringComparison.OrdinalIgnoreCase)).ToList();
   break;
   case SelectorOperator.StartsWith:
       currentLogs = currentLogs.Where(item => getString(item).ToLower().StartsWith(condition.FieldValue as string)).ToList();
   break;
   case SelectorOperator.Equals:
       currentLogs = currentLogs.Where(item => getInt32(item) == int.Parse(condition.FieldValue as string)).ToList();
   break;
   case SelectorOperator.IsGreaterThan:
       currentLogs = currentLogs.Where(item => getInt32(item) > int.Parse(condition.FieldValue as string)).ToList();
   break;
   case SelectorOperator.IsLessThan:
       currentLogs = currentLogs.Where(item => getInt32(item) < int.Parse(condition.FieldValue as string)).ToList();
   break;
}

添加一个新的源字段现在需要case在第一个字段中添加另一个switch;向类型添加新操作只需要case在第二个中添加一个新操作switch,从而减少代码中“维护点”的数量。

于 2013-05-13T22:36:04.350 回答
1

我不明白为什么人们建议使用 IQueryable 方法。我一直认为 IQueryable 用于将 C# 查询转换为其他技术中的查询,例如 SQL(SELECT 语句)或 XML(XQuery),因此它可以在适当的位置执行,而无需了解所使用的技术的任何细节。查询被转换为(由您作为开发人员/程序员或您的代码 - 与该技术没有紧密耦合)。

由于您的查询是在 C#/.NET 代码中执行的,因此不需要 IQueryable。

例如,如果您将使用 EventLog 服务的本机查询功能,那么最好实现 IQueryable 以将 C# LINQ 转换为查询字符串或 EventLog 服务可以理解和执行的其他形式。

对我来说,这个问题看起来像是一个通过链接谓词来创建复合谓词的问题,因此复合谓词可以在 LINQ Where 语句中使用。

这取决于您希望解决方案的通用性,但这是一种可能的实现,它大量使用类型推断和 lambda 闭包来创建复合谓词:

class Predicate<T>
{
    public static Func<T, bool> Or(params Func<T, bool>[] predicates)
    {
        return item => predicates.Any(p => p(item));
    }

    public static Func<T, bool> And(params Func<T, bool>[] predicates)
    {
        return item => predicates.All(p => p(item));
    }

    #region Generic predicates

    public static Func<T, bool> Is<TValue>(Func<T, TValue> selector, string value) where TValue : IEquatable<TValue>
    {
        return item => GetEqualityComparer<TValue>().Equals(selector(item), Parse<TValue>(value));
    }

    public static Func<T, bool> IsNot<TValue>(Func<T, TValue> selector, string value) where TValue : IEquatable<TValue>
    {
        return item => !Is(selector, value)(item);
    }

    public static Func<T, bool> IsLessThan<TValue>(Func<T, TValue> selector, string value) where TValue : IComparable<TValue>
    {
        return item => GetComparer<TValue>().Compare(selector(item), Parse<TValue>(value)) < 0;
    }

    public static Func<T, bool> IsLessThanOrEqualTo<TValue>(Func<T, TValue> selector, string value) where TValue : IComparable<TValue>
    {
        return item => GetComparer<TValue>().Compare(selector(item), Parse<TValue>(value)) <= 0;
    }

    public static Func<T, bool> IsGreaterThan<TValue>(Func<T, TValue> selector, string value) where TValue : IComparable<TValue>
    {
        return item => !IsLessThanOrEqualTo(selector, value)(item);
    }

    public static Func<T, bool> IsGreaterThanOrEqualTo<TValue>(Func<T, TValue> selector, string value) where TValue : IComparable<TValue>
    {
        return item => !IsLessThan(selector, value)(item);
    }

    public static Func<T, bool> IsBetween<TValue>(Func<T, TValue> selector, string lower, string higher) where TValue : IComparable<TValue>
    {
        return item => IsGreaterThan(selector, lower)(item) && IsLessThan(selector, higher)(item);
    }

    #endregion

    #region String specialized predicates

    public static Func<T, bool> Contains(Func<T, string> selector, string value)
    {
        return item => selector(item).IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
    }

    public static Func<T, bool> StartsWith(Func<T, string> selector, string value)
    {
        return item => selector(item).StartsWith(value, StringComparison.OrdinalIgnoreCase);
    }

    public static Func<T, bool> EndsWith(Func<T, string> selector, string value)
    {
        return item => selector(item).EndsWith(value, StringComparison.OrdinalIgnoreCase);
    }

    #endregion

    private static IEqualityComparer<TValue> GetEqualityComparer<TValue>()
    {
        // If value type is string, use OrdinalIgnoreCase equality comparer.
        return typeof(TValue) == typeof(string) ? (IEqualityComparer<TValue>)StringComparer.OrdinalIgnoreCase : EqualityComparer<TValue>.Default;
    }

    private static IComparer<TValue> GetComparer<TValue>()
    {
        // If value type is string, use OrdinalIgnoreCase comparer.
        return typeof(TValue) == typeof(string) ? (IComparer<TValue>)StringComparer.OrdinalIgnoreCase : Comparer<TValue>.Default;
    }

    private static TValue Parse<TValue>(string value)
    {
        // We need special handling for Enum type since, unfortunately, System.String doesn't handle conversion to Enum type in its IConvertible.ToType implementation.
        // All other used types (string, DateTime, int, long) are supported by Convert class.
        return (TValue)(typeof(TValue).IsEnum ? Enum.Parse(typeof(TValue), value) : Convert.ChangeType(value, typeof(TValue), CultureInfo.InvariantCulture));
    }
}

// For easier typing, no need to explicitly specify type.
class EventLogPredicate : Predicate<EventLogItem>
{
}

以下是如何使用它:

var items = new List<EventLogItem>()
{
    new EventLogItem() { LogName = "First" },
    new EventLogItem() { LogName = "Second bla", Number = 100 },
    new EventLogItem() { LogName = "Third bla", Number = 25 },
    new EventLogItem() { LogName = "Fourth", Number = 25 }
};

var predicate = EventLogPredicate.And(EventLogPredicate.Contains(item => item.LogName, "bla"), EventLogPredicate.IsLessThan(item => item.Number, "50"));

var filteredItems = items.Where(predicate).ToArray();
于 2013-05-14T08:08:52.300 回答
1

我认为您确实需要两个 switch 语句,但它们不需要嵌套。您可以分离出操作以对任何类型的对象进行通用处理,然后在运行时传入您正在搜索的对象。

public static class SearchProvider
{
    static Func<object, bool> GetSearchMethod(SelectorOperator selectorOperator, string conditionFieldValue)
    {
        switch (selectorOperator)
        {
            //strings
            case SelectorOperator.Contains:
                return new Func<object, bool>(s => s.ToString().ToLower().Contains(conditionFieldValue));
            case SelectorOperator.StartsWith:
                return new Func<object, bool>(s => s.ToString().ToLower().StartsWith(conditionFieldValue));
            case SelectorOperator.EndsWith:
                return new Func<object, bool>(s => s.ToString().ToLower().EndsWith(conditionFieldValue));
            case SelectorOperator.Is:
                return new Func<object, bool>(s => string.Equals(s.ToString(), conditionFieldValue, StringComparison.OrdinalIgnoreCase));

            //numbers
            case SelectorOperator.Equals:
                return new Func<object, bool>(n => (long)n == long.Parse(conditionFieldValue));
            case SelectorOperator.IsGreaterThan:
                return new Func<object, bool>(n => (long)n > long.Parse(conditionFieldValue));
            case SelectorOperator.IsLessThan:
                return new Func<object, bool>(n => (long)n < long.Parse(conditionFieldValue));

            //type
            case SelectorOperator.TypeIs:
                return new Func<object, bool>(t => (EventLogEntryType)t == (EventLogEntryType)Enum.Parse(typeof(EventLogEntryType), conditionFieldValue));
            case SelectorOperator.TypeIsNot:
                return new Func<object, bool>(t => (EventLogEntryType)t != (EventLogEntryType)Enum.Parse(typeof(EventLogEntryType), conditionFieldValue));

            default:
                throw new Exception("Unknown selector operator");
        }
    }

    private static List<EventLogItem> SearchAll(List<EventLogItem> currentLogs, SearchQuery query)
    {
        foreach (SearchCondition condition in query.Conditions)
        {
            var search = GetSearchMethod(condition.SelectorOperator, condition.FieldValue as string);
            switch (condition.FieldName)
            {
                case FieldItem.Category:
                    currentLogs = currentLogs.Where(item => search(item.Category)).ToList();
                    break;
                case FieldItem.InstanceID:
                    currentLogs = currentLogs.Where(item => search(item.InstanceID)).ToList();
                    break;
                case FieldItem.LogName:
                    currentLogs = currentLogs.Where(item => search(item.LogName)).ToList();
                    break;
                case FieldItem.Message:
                    currentLogs = currentLogs.Where(item => search(item.Message)).ToList();
                    break;
                case FieldItem.Number:
                    currentLogs = currentLogs.Where(item => search(item.Number)).ToList();
                    break;
                case FieldItem.Source:
                    currentLogs = currentLogs.Where(item => search(item.Source)).ToList();
                    break;
                case FieldItem.Type:
                    currentLogs = currentLogs.Where(item => search(item.Type)).ToList();
                    break;
            }
        }
        return currentLogs;
    }
}

请注意,我之所以发布这么晚是因为 SO 服务器崩溃了,然后我就去睡觉了 :(
因此它类似于 @dasblinkenlight 的答案。

于 2013-05-14T06:37:44.183 回答