Thomas Kramer

IT-COW | August 2010

"LINQ und Konsorten" Teil 3

By Administrator at August 30, 2010 13:08
Filed Under: 3.5, C#, Webcast-Reviews

Gerade arbeite ich den dritten Teil der Webcast-Reihe LINQ und Konsorten von Dariusz Parys ab, mein Review zum vorangegangenen Teil der Reihe ist hier zu finden.

 

In diesem Teil geht es speziell um die Handhabung von LINQ to Objects und LINQ to DataSets. Herr Parys bevorzugt Konsolenanwendungen zum Erklären, diese sind auch besser zu dem Zweck geeignet. Er hatte verschiedene Beispiele gebracht, ich führe diese hier der Reihe nach einmal auf.

 

Als erstes wurde eine Liste von Personen, initialisiert über Object Initializers, erstellt. Daraus wurden dann über eine LINQ-Abfrage die Personen herausgesucht die über 20 sind und ausgegeben:

 

class Program
{
    static void Main(string[] args)
    {
        List<Person> listOfPersons = new List<Person>()

        {new Person { Email = "a@b.com", Age = 20 },       
         new Person { Email = "b@c.com", Age = 22 },       
         new Person { Email = "c@d.com", Age = 30 }};       
         
         var query = from p in listOfPersons
                          where p.Age > 20
                          select p;
        foreach (var item in query)
         {
             Console.WriteLine(item.Email);
         }
       
        Console.ReadKey();      
    }
}

public class Person
{
    public string Email
    {
        get;
        set;
    }
    public int Age
    {
        get;
        set;
    }
}

 

Die Klasse Person benutzt hier Auto-Implemented Properties. Als zweites Beispiel hatte er dann gezeigt wie LINQ auch auf einen einzelnen String angewandt werden kann:

 

class Program
{
    static void Main(string[] args)
    {
        string bestellnummer = "firma1234-auf1283-to29";

        var query = from c in bestellnummer
                         where Char.IsDigit(c)
                         select c;

        Console.Write("Ziffern in Bestellnummer: ");

        foreach (var item in query)
        {
            Console.Write(item);
        }

        Console.WriteLine();
        Console.ReadLine();
    }
}

 

Die Ausgabe hier ist einfach 1234128329, es werden also mit der Funktion (where) Char.IsDigit nur Zahlen aus einem String übernommen - die foreach-Schleife geht dann jede einzelne Ziffer durch und gibt sie nebeneinander aus. Die umgekehrte Funktion nennt sich übrigens Char.IsLetter(c). Das dritte Beispiel sah nun so aus:

 

class Program
{
    static void Main(string[] args)
    {
        string bestellnummer = "firma1234-auf1283-to29";

        var query = bestellnummer.TakeWhile(c => c != '-');

        foreach (var item in query)
        {
            Console.Write(item);
        }
          
        Console.ReadKey();
    }
}

 

Das ist wieder eine Lambda-Expression. In dem Fall das ungleich (!=) in der Lambda-Expression beachten, es werden alle Zeichen des Strings bis zum ersten Auftauchen des Zeichens - übernommen.

 

Zu der Online-Erklärung für die Funktion TakeWhile: Gibt Elemente aus einer Sequenz zurück, solange eine angegebene Bedingung true ist.

 

Viertes Beispiel, welches über die Funktion .Except des String-Arrays "vorgeschlageneAngestellte" nur die Teilnehmer übernimmt, die nicht bereits in dem String-Array "teilnehmerLetzterVeranstaltung" enthalten sind (würde ich aber schon fast nicht mehr zu LINQ zählen, weder typische LINQ-Abfrage noch Lambda-Expression...):

 

static void Main(string[] args)
{
   string[] vorgeschlageneAngestellte = new string[] { "Dariusz", "Frank", "Daniel" };
   string[] teilnehmerLetzterVeranstaltung = new string[] { "Dariusz", "Daniel" };

   var query = vorgeschlageneAngestellte.Except(teilnehmerLetzterVeranstaltung);

   foreach (var item in query)
   {
      Console.WriteLine("Approved: {0}", item);
   }
            
   Console.ReadKey();
}

 

Fünftes Beispiel, welches die Teilnehmer die in einer Textdatei vermerkt sind (Aufbau: Nachname, Vorname) in ein String-Array einliest, anhand der enthaltenen Kommas in die einzelnen Bestandteile separiert und gruppiert ausgibt:

 

using System.IO;
class Program
{
    static void Main(string[] args)
    {
       string[] teilnehmer = File.ReadAllLines( "teilnehmer.txt" );          


       var query = from attendee in teilnehmer
                        let nachname = attendee.Split(',')
                        group nachname by nachname[0][0] into g
                        orderby g.Key
                        select g;

       foreach(var item in query)
       {
          Console.WriteLine(item.Key);
          foreach (var item2 in item)
          {
             Console.WriteLine("Vorname {0}, Nachname {1}", item2[1], item2[0]);
          }
       }
            
       Console.ReadKey();
    }
}

 

Let behält temporär den Nachnamen zwischen, auf den im group-Befehl zugegriffen wird - nach dem ersten Buchstaben der Nachnamen wird gruppiert. Das Ergebnis kommt in die Menge g, danach wird über g.Key (Key enthält immer das Gruppierungsfeld) sortiert und das Ergebnis kommt in die Query. Danach wird das Ergebnis ausgegeben...

 

Interessant ist hierbei, das anschließend die Ergebnismenge query nicht nur die Liste der ersten Buchstaben des jeweiligen Nachnamens enthält, sondern auch wiederum die enthaltenen Datensätze pro Gruppierung - jeweils wieder über das Komma in Einzelbestandteile separiert (in ein Array). Der Befehl WriteLine in der zweiten foreach-Schleife gibt hier also den jeweiligen Vor- und Nachnamen aus. Das muss ich mir mal merken...

 

Anschließend hatte er ein interessantes Beispiel gebracht wie man alle laufenden Prozesse ausgeben kann die mit 'a' beginnen - inklusive Anzahl der jeweiligen Handles:

 

using System.Diagnostics;

class Program
{
    static void Main(string[] args)
    {
       var query = from p in Process.GetProcesses()
                        where p.ProcessName.StartsWith("a")
                        orderby p.HandleCount descending
                        select new
                        {
                            Process = p.ProcessName,
                            Handle = p.HandleCount
                        };

       foreach (var item in query)
       {
          Console.WriteLine("Process: {0} ({1})", item.Process, item.Handle);
       }

       Console.ReadKey();
    }
}

 

Dann kam ein Beispiel um über eine LINQ-Abfrage alle Typen einer Assembly, mit ihren jeweiligen Methoden, auszugeben:

 

using System.Reflection;

static void Main(string[] args)
{
   Assembly assembly = Assembly.Load("System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");

   var query = from type in assembly.GetTypes()
                    where type.IsPublic
                    from method in type.GetMethods()
                    where method.ReturnType.IsArray==true
                    || (method.ReturnType.GetInterface(typeof(System.Collections.Generic.IEnumerable<>).FullName) != null && method.ReturnType.FullName != "System.String")
                    group method.ToString() by type.ToString();

                    foreach( var item in query)
                    {
                       Console.WriteLine("Type: {0}",item.Key);
                       foreach (var m in item)
                       {
                          Console.WriteLine("\t{0}", m);
                       }  
                    Console.ReadKey();
                   }

   Console.ReadKey();
}

 

Interessant ist hier das auf eine erste from-Abfrage eine zweite folgt, also eine Unterabfrage: nimm zuerst alle Typen der Assembly und von denen alle Methoden die ein Array zurückgeben - oder das Interface IEnumerable implementieren und keinen String zurückgeben.

 

Das Ergebnis gruppiere dann nach dem Typ...  MSDN-Link zur Assembly.Load-Methode: Link, \t im String generiert einen Zeilenvorschub...

 

Danach dann noch ein Beispiel wie man eine LINQ-Abfrage auf ein DataSet ausführt (wieder auf die Northwind-Datenbank):

 

static void Main(string[] args)
{
   using (SqlConnection conn = new SqlConnection(@"server=(local)\sqlexpress;integrated security=true;database=northwind"))
   {
      NorthwindData.ProductsDataTable table = new NorthwindData.ProductsDataTable();

      ProductsTableAdapter adapter = new ProductsTableAdapter();

      adapter.Fill(table);

      // table.WriteXml(Console.Out); --> gibt die Datensätze in XML-Form aus

      var query = from p in table
                       where p.CategoryID == 2
                       select p;

      foreach (var item in query)
      {
         Console.WriteLine(item.ProductName);
      }
           
   }

   Console.ReadKey();
}

 

Zuletzt wollte er dann noch ein Beispiel bringen wie man auf zwei Datenquellen gleichzeitig zugreift, dafür wollte er parallel zur Northwind-Datenbank noch die Adventureworks-Datenbank heranziehen, die ich erst nachinstallieren musste... falls es jemanden interessiert, hier ist die Installationsanleitung und hier der Download-Link.

 

Die Datenbank wird wieder in einem Installer geliefert und ist 82 MB groß (2008er-Version)... leider lässt sie sich bei mir nicht installieren (Vista64), folgende Fehlermeldung erscheint immer: A fatal error occurred during installation. Details: Fehler bei Objektinitialisierung [...]. Eigentlich wollte ich das heute abschließen, aber damit werde ich dann bis morgen pausieren.

 

Update 31.08.2010: die Fehlermeldung des Installers besagte das die Datei databaseselection.xaml fehlerhaft ist. Installationsprobleme mit der Datenbank scheinen verbreitet zu sein: 1 2... letztendlich habe ich dann die Vorgänger-Version der Datenbank für SQL Server 2005 heruntergeladen und installiert, diese Version ist mit 28 MB auch nicht ganz so groß. 

 

Das Beispiel sah dann folgendermaßen aus, er hatte dazu die Produkt-Tabellen beider Datenbanken dem DataSet hinzugefügt:
      

static void Main(string[] args)
       {
           NorthwindData.ProductsDataTable northwindProducts = new NorthwindData.ProductsDataTable();
           NorthwindData.ProductDataTable awProducts = new NorthwindData.ProductDataTable();

           using (SqlConnection conn = new SqlConnection(@"server=(local)\sqlexpress;integrated security=true;database=northwind"))
           {
               ProductsTableAdapter adapter = new ProductsTableAdapter();
               adapter.Fill(northwindProducts);
           }

           using (SqlConnection conn = new SqlConnection(@"server=(local)\sqlexpress;integrated security=true;database=adventureworks"))
           {
               ProductTableAdapter adapter = new ProductTableAdapter();
               adapter.Fill(awProducts);
           }

           var query = from prod in
                          ( from np in northwindProducts
                            select np.ProductName ).Union(
                          from ap in awProducts
                            select ap.Name)
                          orderby prod ascending
                          select prod;
              
            foreach (var item in query)
            {
                Console.WriteLine(item);
            }

            Console.ReadKey();
        }

    }

 

Also wieder einfach zu verstehen - die zwei from-Abfragen müssen über Klammern () in IENumerable-Objekte separiert werden und werden anschließend mit dem SQL-typischen UNION miteinander verbunden.

 

Gerade eben noch festgestellt: die using-Befehle werden hier eigentlich nicht benötigt weil die Datenverbindung bereits im Designer im TableAdapter gespeichert wird, mit der Fill-Methode des Adapters wird automatisch die Datenverbindung geöffnet. Allerdings muss dann natürlich die Variable adapter beim zweiten Auftauchen umbenannt werden, weil sie dann nicht mehr in der Klammer ist.

 

Prinzipiell können also LINQ-Abfragen auf alle möglichen Objekte - generische Listen, Arrays, DataSets, Datenbank-Tabellen... - angewandt werden.

 

Damit bin ich dann auch mit diesem Webcast durch, insgesamt einige interessante Beispiele. Ich will auf jeden Fall die ganze Webcast-Reihe zu dem Thema durcharbeiten.

 

Fortsetzung: Link.

 

Weitere Links


  • Tanz den => Lambda mit mir... Link
  • Visual LINQ Query Builder: Link

 

Monats-Liste