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.
Nested switch structures are difficult to understand because you can easily confuse the cases of an inner switch as belonging to an outer statement. Therefore nested switch statements should be avoided.
It is possible to have a switch as part of the statement sequence of an outer switch. Even if the case constants of the inner and outer switch contain common values, no conflicts will arise. C++ specifies that at least 256 levels of nesting be allowed for switch statements.
The biggest problem with switch statements, in general, is that they can be a code smell. Switch overuse might be a sign that you're failing to properly employ polymorphism in your code.
4) The break statement is used inside the switch to terminate a statement sequence. When a break statement is reached, the switch terminates, and the flow of control jumps to the next line following the switch statement. 5) The break statement is optional. If omitted, execution will continue on into the next case.
The standard way to handle this kind of task would be to create a custom IQueryable provider and just use LINQ. Literally, every operation that you are looking for has a standard extensibility mechanism through LINQ expressions. The basic idea is that you would have ExpressionVisitor
implementations applying each rewrite rule instead of having a giant switch statement. Since you can use as many expression visitors as you want, your maintenance and extensibility costs go way down.
I highly recommend looking at IQToolkit and Matt Warren's Building an IQueryable blog series if you want to take this approach.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With