2013-05-13 6 views
9

Ich arbeite an einem Projekt und meine Aufgabe besteht darin, eine erweiterte Such-und Filteroption hinzuzufügen, die die Benutzer gewünschte Ergebnisse aus einer Liste von Windows-Ereignissen durch die Angabe so viele Bedingungen abfragen können wie sie wollen.Verschachtelte Switch-Anweisungen: Architectural Design Problem

Die Idee ist, jedes Windows-Ereignisprotokoll mehrere Eigenschaften wie LogName, Source, CreatedDate, Message, Number usw. (Teil des FieldItem Enum) hat. Insgesamt gibt es vier mögliche Datentypen: String, DateTime, Integral (Int/Long) und EventEntryType. Jeder dieser vier Datentypen hat seine eigene Sammlung von Selektoroperanden (Teil der SelectorOperator enum). Hier ist ein Bild, das Ihnen eine bessere Vorstellung davon zu geben, wie die Gesamtstruktur wie folgt aussieht:

Meine erste Umsetzung dieser Idee ist folgende:

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

Eine Beispielabfrage könnte wie folgt aussehen :

Zustand Selector:

All of the conditions are met 

Bedingungen:

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

Wie Sie sehen können, die SearchAll() Methode ist ziemlich lang und nicht sehr wartbar aufgrund der verschachtelten switch Aussagen. Der Code funktioniert, aber ich denke, das ist nicht die eleganteste Art, dieses Design zu implementieren. Gibt es einen besseren Weg, dieses Problem anzugehen? Vielleicht, indem Sie einen Weg finden, die Komplexität der switch Hierarchie ODER den Code generischer zu machen? Jede Hilfe/Anregung wird geschätzt.

+0

IMO, das sieht aus wie ein Kandidat für mehrere Versand (Besucher Muster). Ich überlasse die Antworten für die Menschen schlauer als ich :) –

+1

Was ist Ihre Datenzugriffsstrategie? Wenn Sie LINQ to SQL oder LINQ to Entities verwenden, kann dies sehr einfach (und auch elegant) werden, wenn Sie die Schnittstelle 'IQueryable ' verwenden. – Yuck

+0

@Yuck Die Ereignisprotokolle werden von 'System.Diagnostics.EventLog.GetEventLogs()' zurückgegeben, und daraus wird eine Liste von 'EventLogItem' (benutzerdefinierter Typ) erstellt und schließlich an ein' ListView'-Steuerelement gebunden. 'IQueryable' ist das, worüber ich eine Ahnung hatte, aber ich war nicht sicher, wie ich es implementieren/verwenden sollte. – PoweredByOrange

Antwort

1

Ich denke, Sie brauchen zwei Switch-Anweisungen, aber sie müssen nicht geschachtelt werden. Sie können die Operationen trennen, um generisch für jedes Objekt zu arbeiten, und dann das Objekt übergeben, das Sie zur Laufzeit durchsuchen.

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

Hinweis gepostet ich so spät, weil der SO-Server abgestürzt ist, dann ich ins Bett ging :(
Daher ist es ähnlich wie @ dasblinkenlight Antwort.

2

Der Standard Weg, um diese Art von Aufgabe zu behandeln wäre, einen benutzerdefinierten IQueryable Provider zu erstellen und nur LINQ zu verwenden. Buchstäblich jede Operation, nach der Sie suchen, verfügt über einen Standard-Erweiterungsmechanismus über LINQ-Ausdrücke. Die Grundidee ist, dass Sie Implementierungen haben, die jede Rewrite-Regel anwenden, anstatt eine riesige Switch-Anweisung zu haben. Da Sie so viele Expression-Besucher verwenden können, wie Sie möchten, gehen Ihre Wartungs- und Erweiterungskosten erheblich zurück.

Ich empfehle dringend, Blick auf IQToolkit und Matt Warren Building an IQueryable Blog-Serie, wenn Sie diesen Ansatz nehmen möchten.

+0

Das Tutorial ist gründlich, aber ein bisschen zu kompliziert für mich, weißt du, ob es irgendwo ein einfacheres (aber gleichzeitig praktisches) Tutorial gibt? Ich konnte keine auf Google finden. Vielen Dank. – PoweredByOrange

2

Eine Möglichkeit, Verschachtelung und die damit verbundene Duplizierung zu vermeiden, besteht darin, die Teile des Codes, die den Wert extrahieren, von Teilen des Codes zu trennen, die Operationen darauf ausführen. Hier ist ein kleines Beispiel, das die Technik veranschaulichen soll:

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

Hinzufügen einer neuen Quellfeld bringt nun einen weiteren case zum Hinzufügen der ersten switch; Das Hinzufügen einer neuen Operation zu einem Typ würde nur eine neue case in der zweiten switch erfordern, wodurch die Anzahl der "Wartungspunkte" in Ihrem Code reduziert wird.

+0

Dies ist definitiv eine bessere Lösung als meine ursprüngliche Implementierung und erhöht die Wartbarkeit des Programms erheblich. Nachdem ich aber gesehen habe, wie andere mit "IQueryable" und "ExpressionVisitor" arbeiten, versuche ich herauszufinden, wie ich diese auf mein Problem anwenden kann. Ihre Antwort ist definitiv ein guter Ausgangspunkt. Vielen Dank! – PoweredByOrange

+2

@ programmer93 Ein Ansatz basierend auf ['IQueryProvider'] (http://msdn.microsoft.com/en-us/library/system.linq.iqueryprovider.aspx) würde eine bessere Integration mit der .NET-Umgebung bieten, aber es ist wesentlich komplexer (ich habe ein paar von ihnen implementiert, und es hat viel gelernt - sogar zusätzlich zu den LINQ-Ausdrücken). – dasblinkenlight

+0

Hmm ... Nun, ich hätte nichts dagegen, es zu lernen, ich bin nicht sehr gut informiert, wenn es um LINQ geht, aber ich fühle, dass sie extrem nützlich sein können, um bessere Programme zu schreiben, so dass ich sie irgendwann lernen muss.Aber auf der anderen Seite haben Sie wahrscheinlich recht, ich muss das nicht komplizierter machen als das, was es derzeit ist, wenn man bedenkt, dass Windows EventLogs Eigenschaften nicht regelmäßig geändert werden :) Danke für Ihren Rat, ich Ich werde darüber nachdenken. – PoweredByOrange

1

Ich verstehe nicht, warum die Leute IQueryable Ansatz vorschlagen Ich dachte immer, dass IQueryable verwendet wird, um C# -Abfrage in eine Abfrage in einer anderen Technologie wie SQL (SELECT-Anweisung) oder XML (XQuery) umzuwandeln, so dass es an der entsprechenden Stelle ausgeführt werden kann, ohne irgendwelche Besonderheiten der Technologie kennen zu müssen Die Abfrage wird transformiert in (von Ihnen als Entwickler/Programmierer oder durch Ihren Code - keine enge Kopplung an diese te Chnologie).

Da Ihre Abfrage in C# /. NET-Code ausgeführt wird, ist IQueryable nicht erforderlich. Wenn Sie beispielsweise die systemeigenen Abfragefunktionen des EventLog-Dienstes verwenden, wäre es großartig, IQueryable zu implementieren, um C# LINQ in eine Abfragezeichenfolge oder eine andere Form zu transformieren, die der EventLog-Dienst versteht und ausführt.

Für mich sieht dieses Problem wie ein Problem der Erstellung von Verbundprädikaten durch Verkettung von Prädikaten aus, sodass das Verbundprädikat in der LINQ Where-Anweisung verwendet werden kann.

Es hängt davon ab, wie generic Sie Ihre Lösung sein wollen, aber hier ist eine mögliche Implementierung, die Typinferenz und Lambda-Verschlüsse stark verwendet Verbund Prädikate zu erstellen:

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

Und hier ist, wie Sie es verwenden können:

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(); 
+0

Ich mag diesen Ansatz, aber die Bedingungen sind benutzerdefinierte Typen, und ich muss sie durchlaufen, also bin ich nicht sicher, wie dies mit 'var Prädikat = EventLogPredicate.And (EventLogPredicate.Contains (Element => item.LogName , "bla"), EventLogPredicate.IsLessThan (item => item.Number, "50")); 'da ich die Bedingungen nicht ermitteln kann, ohne sie vorher durchlaufen zu müssen. – PoweredByOrange