Archiv der Kategorie: VB.NET

LINQ Gruppierung und Sortierung mit VB.NET

Wer einmal vor der Aufgabe stehen sollte, dass er in einer Tabelle nach einem Kriterium gruppieren und nach der Anzahl der Vorkommen in der Gruppe sortieren muss, dem wird folgender Code helfen:

   1: Using dataContext = _dataContextInitializer.GetDataContext(True)

   2:     Dim states = From pj In dataContext.PrintJobs

   3:         Where (pj.CreateDate > _backwardTimeSpan)

   4:         Group pj By pj.StatusId Into Group

   5:         Order By Group.Count Descending

   6:         Select New With {.statusId = StatusId, Group.Count}

   7: End Using

Was wird genau gemacht? Ich hole mir alle Druckaufträge, deren Erstelldatum größer einem von mir definierten Datum sind. Danach gruppiere ich anhand der StatusId (z.B. fertig, zum Drucker gesendet, etc.) und zu guter Letzt sortiere ich die StatusIds anhand der Häufigkeit ihres Vorkommens.

Liste in DataTable konvertieren

Wer aus welchem Grund auch immer einmal vor dem Problem steht eine Liste in ein DataTable konvertieren zu müssen, kann folgendes Code Snippet verwenden:

1: Public Function ConvertListToDataTable(objects As System.Collections.IList)

As DataTable Implements cwContracts.IConverter.ConvertListToDataTable

   2:     Dim dataTable As New DataTable

   3:  

   4:     If objects IsNot Nothing AndAlso objects.Count > 0 Then

   5:         'Erzeuge Spalten aus den Properties des Objekts

   6:         Dim objectProperties = objects(0).GetType.GetProperties()

   7:         For Each prop In objectProperties

   8:             Try

   9:                 dataTable.Columns.Add(prop.Name, prop.PropertyType)

  10:             Catch ex As NotSupportedException

  11:                 dataTable.Columns.Add(prop.Name)

  12:             End Try

  13:         Next

  14:  

  15:  

16: 'Erzeuge die DataRows und fülle die passenden Objektwerte in die

passenden Spalten

  17:         Dim newRow = dataTable.NewRow

  18:  

  19:         For Each item In objects

  20:             newRow = dataTable.NewRow()

  21:  

  22:             For Each prop In item.GetType.GetProperties()

  23:                 newRow(prop.Name) = prop.GetValue(item, Nothing)

  24:             Next

  25:  

  26:             dataTable.Rows.Add(newRow)

  27:         Next

  28:     End If

  29:  

  30:     Return dataTable

  31: End Function

Mit einem Converter könnt ihr das auch in C# Code konvertieren.

Beachtet, dass ihr das try-catch in Zeile 8-12 nur verwenden müsst, wenn in der Liste Nullable Values sein können. Das wäre beispielsweise der Fall, wenn ihr mit dem Entity Framework bzw. LINQ auf eine Datenbanktabelle zugegriffen habt, die Felder beinhaltet, die null sein dürfen. Wenn es euch nicht wichtig ist, dass die Columns im DataSet typisiert sind, könnt ihr auch einfach nur Zeile 11 verwenden.

Ähnliche LINQ Abfragen durch IQueryable kapseln

Wer öfters ähnliche LINQ Abfrage durchführen muss und sich dabei an das Clean Code Prinzip Don’t Repeat Yourself (DRY) halten will, der kann dies über das Interface IQueryable machen. Um das zu verdeutlichen, soll folgendes Beispiel dienen:

Stellen wir uns vor, wir haben einer Klasse mit 2 Datenbank-Abfragen, die zum Teil die gleichen Einschränkungen haben:

  • gemeinsame Anforderung: Hole alle Artikel aus dem Artikelstamm, die nicht auf gelöscht gesetzt sind und deren Erstelldatum innerhalb der letzten 12 Monate liegt
  • Abfrage 1: filtere zusätzlich auf die Einheit ‘Stück’
  • Abfrage 2: filtere alle, die als Lagerartikel markiert sind

Natürlich könnte man jetzt 2 gänzlich separate Abfragen machen. Das Problem ist, dass wenn man die 12 Monate auf 24 Monate ausdehnen will, dies an zwei Stellen erledigen muss. Das riecht (bad smells).

Eine Lösung wäre nun, dass ihr eine dritte Methode definiert, die ungefähr so aussehen könnte:

   1: Private Function GetStockArticles(ByRef vContext As cwDB.comWorkEntities) As IQueryable(Of cwDB.Article)

   2:     If vContext Is Nothing Then

   3:         Throw New ApplicationException("Kein Zugriff auf die Datenbank. DataContext war leer.")

   4:     Else

   5:         Dim lArticles As IQueryable(Of cwDB.Article) = _

   6:                     From X In vContext.Articles

   7:                      Where _

   8:                          X.IsStockArticle = True AndAlso _

   9:                          X.IsDeleted = False

  10:                      Select X

  11:  

  12:         If lArticles Is Nothing OrElse lArticles.Count < 1 Then

  13:             Throw New ApplicationException("Keine Lagerartikel gefunden")

  14:         End If

  15:  

  16:         Return lArticles

  17:     End If

  18: End Function

 

Das Ergebnis könnt ihr in den zwei Abfragen dann weiterverwenden. Zwei Anmerkungen dazu:

  • Ihr müsst den Context an die Methode übergeben, sonst könnt ihr später nicht auf das Ergebnis zugreifen.
  • Wenn ihr eine Liste zurückgeben würdet, hättet ihr den Nachteil, dass die Abfrage bereits durchgeführt wurde, sprich ihr hättet schon einen Datenbankzugriff gemacht. Durch IQueryable erhaltet ihr lediglich den ExpressionTree zurück, auf dem ihr die Abfrage verfeinern und dann feuern könnt.

Parameter ByVal oder ByRef übergeben

Ich will kurz erklären, wie ich auf diesen Blogeintrag gekommen bin: Wir haben vor kurzem unsere internen Namenskonventionen überarbeitet. In diesem Kontext kam die Frage auf, ob man lokale Variablen von Parametern anhand der Schreibweise unterscheiden können sollte. Das macht natürlich dann Sinn, wenn sich eine lokale Variable anders verhält als eine, die per Parameter in die Methode injiziert wurde. Wann ist das der Fall? Nun, hier gilt es zu unterscheiden, ob die Variable ByValue (in C# gibt man hier nichts an) oder ByRef (in C# verwendet man das Schlüsselwort ref) übergeben wird.

Wo liegen die Unterschiede:

Handelt es sich bei dem Parameter um einen Wertetyp, so muss man eine Unterscheidung machen, da sich die Variable unterschiedlich verhält. Hierzu verweise ich auf diesen Link, der besagt, dass bei Werttypen, die per Referenz als Parameter an eine Subroutine übergeben werden, Änderungen sich nicht auf die Subroutine beschränken, sondern sich auf die aufrufende Routine auswirken. Wird die Variable hingegen per ByVal übergeben, so haben Änderungen daran nur lokale Auswirkungen (d.h. im Kontext der Subroutine).

Anders sieht es bei Parametern aus, die einen Referenzdatentyp abbilden. Hier spielt es keine Rolle, wie der Parameter an die Subroutine übergeben wird. Da in beiden Fällen immer nur der Zeiger auf das Objekt übergeben wird, haben Änderungen immer globale Auswirkungen. Vergleicht dazu auch diesen Artikel hier (Kommentare beachten).

Als ich dazu etwas programmiert habe, bin ich aber noch auf folgende Entdeckung gestoßen, die mir doch Rätsel aufgibt:

    Sub Main()
        Dim i = 15
        Console.WriteLine(Object.ReferenceEquals(i, i))
        ReferenceTestWithValueTypes(i, i)
        Console.ReadLine()
    End Sub

    Public Sub ReferenceTestWithValueTypes _ (ByRef object1 As Int32, ByRef object2 As Int32)

        Console.WriteLine(Object.ReferenceEquals(object1, object2))
        object2 = 20
        Console.WriteLine(object1)
        Console.WriteLine(object2)
        Console.WriteLine(Object.ReferenceEquals(object1, object2))
    End Sub

Die Ausgabe produziert folgendes Ergebnis:

clip_image002

Vielleicht kann ein Leser dies auflösen?!

Data Transfer Objects verwalten

Wer in seinen Programmen auch mit DTOs (Data Transfer Objects) arbeitet, der steht vor dem Problem sich um deren Persistenz kümmern zu müssen. Der Namensraum System.Xml.Serialization bietet hierfür alles nötige. Und da wir gemäß dem DRY-Prinzip (Don’t Repeat Yourself) nicht für jedes DTO eine entsprechende Persistenzverwaltung schreiben möchten, machen wir das generisch:

VB.NET Code:

Imports System.Xml
Imports System.IO

Public Class ObjectPersistenceStore(Of T)
    “‘ <summary>
    “‘ Stellt den Zustand des Objekts anhand der übergebenen XML Daten wiederher
    “‘ </summary>
    “‘ <param name=“state“>Der Zustand des Objekts in Form eines XML Strings</param>
    “‘ <returns>Eine Instanz des DTOs</returns>
    “‘ <remarks></remarks>
    Public Function LoadObjectState(ByVal state As String) As T
        Dim result As T = Nothing

        Try
            Dim reader As New Serialization.XmlSerializer(GetType(T))
            result = CType(reader.Deserialize(New StringReader(state)), T)
        Catch ex As InvalidOperationException
            Console.WriteLine(„Die XML Daten in der Datei sind fehlerhaft: “ + ex.Message)
            Throw
        Catch ex As FileNotFoundException
            Console.WriteLine(„Datei nicht gefunden: “ + ex.Message)
            Throw
        Catch ex As NotSupportedException
            Console.WriteLine(„Objekt „)
            Throw
        Catch ex As Exception
            Console.WriteLine(ex.Message)
            Throw ex
        End Try

        Return result
    End Function

    “‘ <summary>
    “‘ Liefert den Zustand eines DTO zurück
    “‘ </summary>
    “‘ <param name=“dataTransferObject“>Das zu serialisierende Objekt</param>
    “‘ <returns>Ein XML String</returns>
    “‘ <remarks></remarks>
    Public Function GetObjectState(ByVal dataTransferObject As T) As String
        Dim result = String.Empty

        Try
            Dim xmlSerializer = New Serialization.XmlSerializer(GetType(T))
            Dim stringWriter = New System.IO.StringWriter
            xmlSerializer.Serialize(stringWriter, dataTransferObject)

            result = stringWriter.ToString
        Catch ex As InvalidOperationException
            ‚das Objekt muss einen leeren Konstruktor haben
            Console.WriteLine(„Das Objekt kann nicht serialisiert werden: “ + ex.Message)
            Throw
        Catch ex As NotSupportedException
            ‚Interfaces können nicht serialisiert werden
            Console.WriteLine(„Es wurde kein gültiges Objekt zum Serialisieren übergeben: “ + ex.Message)
            Throw
        Catch ex As Exception
            Console.WriteLine(ex.Message)
            Throw
        End Try

        Return result
    End Function
End Class

 

C# Code:

using System;
using System.IO;

    public class ObjectPersistenceStore<T>
    {
        /// <summary>
        /// Stellt den Zustand des Objekts anhand der übergebenen XML Daten wiederher
        /// </summary>
        /// <param name=“state“>Der Zustand des Objekts in Form eines XML Strings</param>
        /// <returns>Eine Instanz des DTOs</returns>
        /// <remarks></remarks>
        public T LoadObjectState(string state)
        {
            T result = default(T);

            try
            {
                var reader = new System.Xml.Serialization.XmlSerializer(typeof(T));
                result = (T)reader.Deserialize(new StringReader(state));
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(„Die XML Daten in der Datei sind fehlerhaft: “ + ex.Message);
                throw;
            }
            catch (FileNotFoundException ex)
            {
                Console.WriteLine(„Datei nicht gefunden: “ + ex.Message);
                throw;
            }
            catch (NotSupportedException ex)
            {
                Console.WriteLine(„Objekt „);
                throw;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }

            return result;
        }

        /// <summary>
        /// Liefert den Zustand eines DTO zurück
        /// </summary>
        /// <param name=“dataTransferObject“>Das zu serialisierende Objekt</param>
        /// <returns>Ein XML String</returns>
        /// <remarks></remarks>
        public string GetObjectState(T dataTransferObject)
        {
            var result = string.Empty;

            try
            {
                var xmlSerializer = new System.Xml.Serialization.XmlSerializer(typeof(T));
                var stringWriter = new System.IO.StringWriter();
                xmlSerializer.Serialize(stringWriter, dataTransferObject);

                result = stringWriter.ToString();
            }
            catch (InvalidOperationException ex)
            {
                //das Objekt muss einen leeren Konstruktor haben
                Console.WriteLine(„Das Objekt kann nicht serialisiert werden: “ + ex.Message);
                throw;
            }
            catch (NotSupportedException ex)
            {
                //Interfaces können nicht serialisiert werden
                Console.WriteLine(„Es wurde kein gültiges Objekt zum Serialisieren übergeben: “ + ex.Message);
                throw;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }

            return result;
        }
    }

 

Bei der Verwendung ist zu beachten, dass Objekte, die deserialisiert werden sollen, einen leeren Konstruktur benötigen. Interfaces lassen sich per se nicht serialisiert.

Wenn man die oben geschilderte Logik z.B. in einem Interfaces deklariert und mit Dependency Injection bei dem Serializer arbeitet, dann kann man für beliebige DTOs verschiedene Persistenz-Manager implementieren (z.B. einen für Daten aus Dateien, einen anderen für Daten aus Datenbanken), die noch dazu unterschiedliche Serialisierungsformate verwenden. Und wenn man das ganze noch einen Schritt weiter treibt, kann man die Logik auch in einer ExtensionMethod kapseln und jedes DTO um diese erweitern.

Die Einsatzmöglichkeiten sind vielfältig, z.B. könnte man die Implementierung für eine Sitzungsverwaltung im eigenen ERP System verwenden. Ich verwende das Konzept für das Konfigurationsmanagement.

%d Bloggern gefällt das: