VIII Teil 4 – Die Applikation

15.7 Aufbau einer Anwendung

In diesem Abschnitt sollen die Elemente einer einfachen Anwendung vorgestellt werden. Die benötigten Bausteine sind:

  • Der Kontext – dieser Teil regelt den Zugriff auf eine Datenbank
  • Die Datenmodelle – dies ist die logische Repräsentation der Entitäten
  • Das Mapping – Anweisungen zum Abbilden des logischen Objektmodells auf das relationale Modell

Optional kann einiges im Zugriffsweg vereinfacht werden, wenn Entwurfsmuster wie Repository oder CQRS (Command Query Responsibility Segration) eingesetzt werden.

Der Kontext

Der Kontext wurde bereist kurz erwähnt. Es handelt sich hier um den eigentlichen Zugriffsweg – also die Repräsentanz der Datenbank im Objektraum.

 1 using Microsoft.Data.Entity;
 2 using System.Collections.Generic;
 3 
 4 namespace JoergIsAGeek.DatabaseLayer
 5 {
 6   public class EventContext : DbContext
 7   {
 8     public DbSet<CompanyEvent> CompanyEvents { get; set; }
 9     public DbSet<Attendee> Attendees { get; set; }
10 
11     protected override void OnModelCreating(ModelBuilder modelBuilder)
12     {
13         // Eine Eigenschaft zum Pflichtfeld machen
14         modelBuilder.Entity<CompanyEvent>()
15             .Property(b => b.Title)
16             .IsRequired();
17     }
18   }
19 }

Die Methode OnModelCreating dient dazu, das Speichermodell zu erstellen. Soweit dies durch die Struktur der Objekte und Attribute aus den folgenden Namensräumen bereits erfolgt, muss hier nichts hingeschrieben werden:

  • System.ComponentModel.DataAnnotations
  • System.ComponentModel.DataAnnotations.Schema

Die Festlegung der Eigenschaft IsRequired könnte jedoch auch durch das Attribut [Required] im Model (hier: Event) erreicht werden:

1 [Required]
2 public string Title { get; set; }

Die Benutzung der Methode OnModelCreating erlaubt es weiterhin, komplexere und gegebenenfalls dynamische Mappings zwischen Speichermodell und Objektmodell zu erstellen. Typische Beispiele sind unter anderem:

  • 1:1-Beziehungen
  • Benennung von Kreuztabellen bei n:n-Beziehungen
  • Festlegen von Kaskadierendem Löschen (cascading delete)

Mit dynamisch ist hier die Möglichkeit gemeint, die Einstellungen zur Laufzeit zu verändern. Der ModelBuilder wird einmalig beim Start der Applikation aufgerufen und hinterlegt das Modell dann im Speicher der Anwendung. Bei nächsten Start der Applikation werden die Änderungen wirksam. Ein erneutes Übersetzen ist niciht notwendig.

Den Kontext mit Dependency Injection registrieren

Das Konzept Dependency Injection ist zentral in ASP.NET Core 1.0. Deshalb wird auch der Datenbankkontext, wie jeder andere Dienst, zentral registriert und bei Bedard abgerufen.

Die Registrierung des Kontexts erfolgt in der Datei Startup.cs. Öffnen Sie diese und fügen Sie folgende Zeilen am Anfang hinzu (das ist der Namensraum des Kontexts und Entity Framework selbst):

1 using DemoWeb.AspNet5.Models;
2 using Microsoft.Data.Entity;

Jetzt wird die Methode AddDbContext benutzt, um den Dienst zu registrieren.

1 public void ConfigureServices(IServiceCollection services)
2 {
3     var connection = ConfigurationManager.ConnectionStrings["EventContext"\
4 ];
5     services.AddEntityFramework()
6      .AddSqlServer()
7      .AddDbContext<EventContext>(options => options.UseSqlServer(connectio\
8 n));

Der Aufruf erfolgt implizit durch die Laufzeitumgebung. Der entscheidende Vorgang hier erfolgt in Zeile 6. Damit das so funktioniert, muss in den Projekteinstellungen die Verbindungszeichenfolge unter dem Namen EventContext abgelegt werden.

15.8 Erzeugen einer Datenbank

Die Migration auf ein Datenbankschema ist seit Langem Teil des Entity Frameworks. Mit dem Entity Framework Core 1.0 wurde die Vorgehensweise komplett überarbeitet und viele Elemente befinden sich noch in Entwicklung. Wenn Sie eine ältere Version dieses Buches haben beschaffen Sie sich das aktuellste Update.

Die Datenmodelle

Zur Vereinfachung der Datenmodell sollten Sie immer auf eine Basisklasse setzen. Die Basisklasse enthält:

  • Den Primärschlüssel
  • Felder für die Zugriffsgeschichte
  • Optional universelle Datenfelder (property bag)

In komplexeren Szenerien kommen weitere Basisklassen hinzu, die spezielle Ausprägungen auf abstrakter Ebene – also unabhängig vom fachlichen Modell – steuern. Dazu gehören beispielsweise:

  • Lokalisierungsinformationen
  • Selbstreferenzierende Hierarchien
  • Globale Eigenschaften

Beachten Sie bei der Nutzung von Klassenhierarchien, dass die Spalten der vererbten Klassen jeder Tabelle in der Hierarchie hinzugefügt werden. Durch diese Technik entstehen keine Relationen. Flache Speichermodelle entsprechen zwar nicht immer den Normalisierungsregeln, sind aber effizient in Bezug auf den Umgang mit großen Datenmengen bei Abfragen.

 1 public abstract class EntityBase {
 2 
 3   [Key]
 4   [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
 5   [Column(Order = 1)]
 6   public int Id { get; set; }
 7 
 8   [Column(TypeName = "datetime2", Order = 2)]
 9   public DateTime CreatedAt { get; set; }
10 
11   [Column(TypeName = "datetime2", Order = 3)]
12   public DateTime ModifiedAt { get; set; }
13 
14   [StringLength(80)]
15   [Column(Order = 4)]
16   public string CreatedBy { get; set; }
17 
18   [StringLength(80)]
19   [Column(Order = 5)]
20   public string ModifiedBy { get; set; }
21 
22 }

Dieses Modell stellt sicher, dass jede Tabelle über Spalten wie den Primärschlüssel, das Datum der Erstellung, der letzten Änderung, und Benutzerinformationen zum ersten und zum letzten Zugriff verfügt. Die Basisklasse ist aber darüber hinaus auch sinnvoll, um eine gemeinsame Grundlage aller Modellklassen zu haben. So kann das allgemeine “Entitätsobjekt” des Kontexts leicht auf EntityBase gecastet werden, beispielsweise beim Zugriff auf den Change Tracker (base.ChangeTracker.Entities<EntityBase>()).

Jede Model-Klasse erbt nun von EntityBase.

 1 [Table("CompanyEvents")]
 2 public class CompanyEvent : EntityBase {
 3 
 4   [Required]
 5   [StringLength(100)]
 6   public string Titel { get; set; }
 7 
 8   [Required]
 9   public DateTime StartsAt { get; set; }
10 
11   [Required]
12   public DateTime EndsAt { get; set; }
13 
14   public ICollection<Attendee> Attendees { get; set; }
15 
16 }
1 [Table("Attendees")]
2 public class Attendee : EntityBase
3 {
4   [Required]
5   public string Name { get; set; }
6 
7   public ICollection<CompanyEvent> CompanyEvents { get; set; }
8 
9 }

Sie können nun Veranstaltungen ablegen und diesen Teilnehmer zuordnen. Die Datenbank hat folgendes Aussehen:

Abbildung: Ansicht der Datenbank im SQL Server
Abbildung: Ansicht der Datenbank im SQL Server

Konkreter Kontext

In den vorangegangenen Abschnitten wurden bereits Fragmente des Kontexts gezeigt. Mit den Modellen ist es nun an der Zeit, den Kontext vollständig aufzubauen und damit den Zugriff auf die Datenbank mit allen Funktionen zu ermöglichen.

Der Kontext umfasst die beiden Verwaltungstabellen, aber auch die komplette Struktur für die Authentifizierung gegenüber der Datenbank mit ASP.NET. Dies wird durch Verarbung von IdentityDbContext erreicht. Die generischen Parameter bestimmen die Klassen, die die Bausteine der Authentifizierung liefern.

 1 public class CompanyEventContext : 
 2        IdentityDbContext<EventUser, 
 3                          EventRole, 
 4                          string, 
 5                          EventUserLogon, 
 6                          EventUserRole,
 7                          EventUserClaim> {
 8 
 9   public CompanyEventContext()
10     : base("CompanyEventContext") {
11   }
12 
13   public DbSet<CompanyEvent> CompanyEvents { get; set; }
14 
15   public DbSet<Attendee> Attendees { get; set; }
16 
17   public override int SaveChanges() {
18     var dn = DateTime.Now;
19     var identity = Thread.CurrentPrincipal.Identity as ClaimsIdentity;
20     foreach (var entry in ChangeTracker.Entries<EntityBase>()) {
21       if (entry.State == EntityState.Added)
22       {
23         entry.Entity.CreatedAt = dn;
24         entry.Entity.ModifiedAt = dn;
25         entry.Entity.CreatedBy = identity.Name;
26       }
27       if (entry.State == EntityState.Modified) {
28         entry.Entity.ModifiedAt = dn;
29         entry.Entity.ModifiedBy = identity.Name;          
30       }
31     }
32 
33     return base.SaveChanges();
34   }
35 
36 
37   protected override void OnModelCreating(DbModelBuilder modelBuilder) {
38     // first to avoid internal Identity names
39     base.OnModelCreating(modelBuilder);
40 
41     modelBuilder.Entity<EventUser>().ToTable("User");
42     modelBuilder.Entity<EventRole>().ToTable("Roles");
43     modelBuilder.Entity<EventUserRole>().ToTable("User_x_Roles");
44     modelBuilder.Entity<EventUserLogon>().ToTable("Logons");
45     modelBuilder.Entity<EventUserClaim>().ToTable("User_x_Claims");
46   }
47 }

Der Aufruf von OnModelCreating erfolgt hier, um die Namen der Tabellen selbst bestimmen zu können. Sie müssen dies nicht tun, wenn es keine Vorgaben bezüglich der Namenswahl gibt.

Falls eigene Modelle für die Identitäten benutzt werden, wie im Kontext bereits gezeigt, müssen auch die passenden Klassen erstellt werden. Beachten Sie im folgenden Code-Block die Basisklassen, die mit Identity beginnen. Diese werden durch die Identity Foundation geliefert. Die Anpassungen beziehen sich auf die Kompatibilität mit EntityBase. Da Sie nicht von zwei Basisklassen erben dürfen, wird hier auf die gemeinsame Schnittstelle IEntityBaseausgewichen.

 1 namespace JoergIsAGeek.Workshop.DataModels.Identity {
 2 
 3   [Table("EventUser")]
 4   public class EventUser : IdentityUser<int, EventUserLogon, EventUserRole\
 5 , EventUserClaim>, IEntityBase {
 6     
 7     [Column(TypeName = "datetime2")]
 8     public DateTime CreatedAt { get; set; }
 9 
10     [Column(TypeName = "datetime2")]
11     public DateTime ModifiedAt { get; set; }
12     
13     [StringLength(200)]
14     public string ModifiedBy { get; set; }
15 
16     [StringLength(200)]
17     public string CreatedBy { get; set; }
18 
19   }
20 
21 
22   [Table("Logins")]
23   public class EventUserLogon : IdentityUserLogin<int>, IEntityBase {
24     [Column(TypeName = "datetime2")]
25     public DateTime CreatedAt { get; set; }
26 
27     [Column(TypeName = "datetime2")]
28     public DateTime ModifiedAt { get; set; }
29 
30     [StringLength(200)]
31     public string ModifiedBy { get; set; }
32 
33     [StringLength(200)]
34     public string CreatedBy { get; set; }
35 
36     [NotMapped]
37     int IEntityBase.Id { get; set; }
38 
39   }
40 
41   [Table("Claims")]
42   public class EventUserClaim : IdentityUserClaim<int>, IEntityBase {
43     [Column(TypeName = "datetime2")]
44     public DateTime CreatedAt { get; set; }
45 
46     [Column(TypeName = "datetime2")]
47     public DateTime ModifiedAt { get; set; }
48 
49     [StringLength(200)]
50     public string ModifiedBy { get; set; }
51 
52     [StringLength(200)]
53     public string CreatedBy { get; set; }
54 
55   }
56 
57   [Table("UserRoles")]
58   public class EventUserRole : IdentityUserRole<int>, IEntityBase {
59 
60     [Column(TypeName = "datetime2")]
61     public DateTime CreatedAt { get; set; }
62 
63     [Column(TypeName = "datetime2")]
64     public DateTime ModifiedAt { get; set; }
65 
66     [StringLength(200)]
67     public string ModifiedBy { get; set; }
68 
69     [StringLength(200)]
70     public string CreatedBy { get; set; }
71 
72     [NotMapped]
73     int IEntityBase.Id { get; set; }
74   }
75 
76   [Table("Roles")]
77   public class EventRole : IdentityRole<int, EventUserRole>, IEntityBase {
78     [Column(TypeName = "datetime2")]
79     public DateTime CreatedAt { get; set; }
80 
81     [Column(TypeName = "datetime2")]
82     public DateTime ModifiedAt { get; set; }
83 
84     [StringLength(200)]
85     public string ModifiedBy { get; set; }
86 
87     [StringLength(200)]
88     public string CreatedBy { get; set; }
89 
90   }
91 
92 }

Das Mapping

Das Mapping bildet das objektorientierte Modell auf das Speichermodell ab. Dies kann sehr einfach gehalten werden, wenn primär mit den Klassen gearbeitet wird. Entity Framework kümmert sich dann mit vielen Automatismen um das korrekte Verhalten. Wird eine vorhandene Datenbank benutzt, kann der Aufwand erheblich höher sein.

15.9 Zugriff in der Anwendung

Von der Anwendung aus besteht Zugriff über die Controller. Es ist allerdings nicht empfehlenswert, die Abfragelogik direkt im Controller zu platzieren. Es wird in der Praxis oft vorkommen, dass ähnliche oder gleiche Abfragen an unterschiedlichen Stellen der Applikation benötigt werden. In solchen Fällen würde Sie schnell Code-Teile wiederholen. Im sinne des DRY-Prinzips (don’t repeat yourself) werden dehalb Abfragen ausgelagert. Dafür ist eine Geschäftslogikschicht zuständig – der Business Logic Layer.

Auch in der Geschäftslogikschicht werden Sie allerdings feststellen, dass sich ähnliche Zugriffe wiederholen. Deshalb ist es auch hier sinnvoll eine weitere Abstraktion vorzunehmen. Dies erfolgt im einfachsten Fall mit Hilfe eines Repositories. Ein Repository stellt elementare Operationen bereit, um den Zugriff auf das Entity Framework zu vereinfachen. In diesem Abschnitt wird zuerst das Repository und dann die Konstruktion der Geschäftslogikschicht diskutiert.

Das Repository

Ein einfaches Repository regelt elementare CRUD-Operationen:

 1 using DataAccessLayer;
 2 using System;
 3 using System.Collections.Generic;
 4 using System.Data.Entity;
 5 using System.Linq;
 6 using System.Text;
 7 using System.Threading.Tasks;
 8 
 9 namespace Repository
10 {
11     public class DataRepository<T> where T : EntityBase
12     {
13         VeranstaltungsContext ctx = new VeranstaltungsContext();
14 
15         public IQueryable<T> SelectAll()
16         {
17             return ctx.Set<T>();
18         }
19 
20         public IEnumerable<T> Query(Func<T, bool> predicate)
21         {
22             return ctx.Set<T>().Where(predicate).ToList();
23         }
24 
25         public void InsertOrUpdate(T model)
26         {
27             ctx.Entry(model).State = model.Id == 0 ? EntityState.Added : E\
28 ntityState.Modified;
29             ctx.SaveChanges();
30         }
31 
32         public void Delete(T model)
33         {
34             ctx.Entry(model).State = EntityState.Deleted;
35             ctx.SaveChanges();
36         }
37 
38 
39     }
40 }

Die Geschäftslogikschicht

Die Geschäftslogikschicht soll einfach im Zugriff sein und komplexe Operationen ermöglichen. Es ist deshalb sinnvoll, hier ein Singleton-Pattern zu benutzen und eine Aufteilung der Instanzen in Einheiten, die der fachlichen Logik folgen. Da mehrere Klassen erstellt werden, ist eine gemeinsame abstrakte Basisklasse sinnvoll.

Listing: Basis
 1 namespace BusinessLayer
 2 {
 3   public static class Singleton<T> where T: Manager<T>, new()
 4   {
 5     private static volatile T _instance;
 6     private static readonly object Lock = new object();
 7 
 8     public static T Instance
 9     {
10         get
11         {
12             if (_instance != null) return (T) _instance;
13             lock (Lock)
14             {
15                 if (_instance == null)
16                 {
17                     _instance = new T();
18                 }
19             }
20             return _instance;
21         }
22     } 
23 
24   }
25 }

Nun wird eine Basisklasse erstellt, die den Zugriff auf die generischen Repositories vereinfacht:

 1 namespace BusinessLayer
 2 {
 3     public abstract class Manager<T> : Singleton<T> 
 4         where T : Manager<T>, new() 
 5     {
 6 
 7        protected GenericRepository<Event> repEvents = new GenericRepositor\
 8 y<Event>();
 9        protected GenericRepository<User> repUsers = new GenericRepository<\
10 User>();
11     }
12 }

Die eigentliche Implementierung erfolgt nun in der Klasse EventManager:

namespace BusinessLayer {

 1 public class EventsManager : Manager<EventsManager>
 2 {
 3 
 4     public IEnumerable<Event> GetNewEvents()
 5     {
 6         var now = DateTime.Now;
 7         return repEvents.SelectAll().Where(ver => ver.StartsAt > now).ToLi\
 8 st();
 9     }
10 
11     public User GetUser()
12     {
13         return repUsers.SelectAll().First();
14     }
15 
16 }

}

Die Controller

Es folgt nun die Erstellung der Controller. Da die Geschäftslogikschicht

 1 using EFGetStarted.AspNet5.NewDb.Models;
 2 using Microsoft.AspNet.Mvc;
 3 using System.Linq;
 4 
 5 namespace EFGetStarted.AspNet5.NewDb.Controllers
 6 {
 7     public class EventsController : Controller
 8     {
 9 
10         public IActionResult Index()
11         {
12             return View(EventsManager.Instance.GetNewEvents());
13         }
14 
15         public IActionResult Create()
16         {
17             return View();
18         }
19 
20         [HttpPost]
21         [ValidateAntiForgeryToken]
22         public IActionResult Create(Event event)
23         {
24             if (ModelState.IsValid)
25             {
26                 EventsManager.Instance.AddEvent(event);
27                 return RedirectToAction("Index");
28             }
29 
30             return View(event);
31         }
32 
33     }
34 }

Der Controller ist sehr einfach. Sie profitieren hier erheblich von der Vorarbeit.

Die Views

 1 @model IEnumerable<Event>
 2 
 3 @{
 4     ViewBag.Title = "Events";
 5 }
 6 
 7 <h2>Veranstaltungen</h2>
 8 
 9 <p>
10     <a asp-controller="Events" asp-action="Create">Neu</a>
11 </p>
12 
13 <table class="table">
14     <tr>
15         <th>Id</th>
16         <th>Url</th>
17     </tr>
18 
19     @foreach (var item in Model)
20     {
21         <tr>
22             <td>
23                 @Html.DisplayFor(modelItem => item.BlogId)
24             </td>
25             <td>
26                 @Html.DisplayFor(modelItem => item.Url)
27             </td>
28         </tr>
29     }
30 </table>

Für das Erzeugen neuer Datensätze wird ebenfalls eine View erstellt. Dies sieht dann folgendermaßen aus:

 1 @model Event
 2 
 3 @{
 4     ViewBag.Title = "Neue Veranstaltung";
 5 }
 6 
 7 <h2>@ViewData["Title"]</h2>
 8 
 9 <form asp-controller="Blogs" asp-action="Create" method="post" class="form\
10 -horizontal" role="form">
11     <div class="form-horizontal">
12         <div asp-validation-summary="ValidationSummary.All" class="text-da\
13 nger"></div>
14         <div class="form-group">
15             <label asp-for="Url" class="col-md-2 control-label"></label>
16             <div class="col-md-10">
17                 <input asp-for="Url" class="form-control" />
18                 <span asp-validation-for="Url" class="text-danger"></span>
19             </div>
20         </div>
21         <div class="form-group">
22             <div class="col-md-offset-2 col-md-10">
23                 <input type="submit" value="Create" class="btn btn-default\
24 " />
25             </div>
26         </div>
27     </div>
28 </form>

Starten der Anwendung

Die Anwendung kann nun gestartet werden. In Visual Studio nutzen Sie einfach F5.

15.10 Umgang mit DataAnnotations

15.11 Die Abfragesprache LINQ

Praktisch führt der einzige Weg zu brauchbaren Abfragen über LINQ. LINQ steht für Language Integrated Query. Mit LINQ wurde, wie der Name schon andeutet, eine Abfragesprache in .NET-Programmiersprachen integriert. Dies ist keineswegs neu und steht bereits seit einigen Jahren zur Verfügung. Es bestand jedoch vor der Nutzung des Entity Framework kein Zwang, LINQ zu benutzen. Dies ist mit der aktuellen Generation von Datenzugriffstechniken anders. Heute führt kein Weg an LINQ vorbei. Deshalb soll an dieser Stelle eine sehr kurze und kompakte Einführung das Thema ergänzen.

Sprachliche Grundlagen

Diese Erweiterung erfolgte nicht als spezielles fest eingebautes Feature, sondern es wurde vielmehr ein Erweiterungsframework geschaffen, mit dessen Hilfe sich sowohl LINQ als auch andere eigene .NET Erweiterungen entwickeln lassen.

Seit der Entwicklung des .NET Frameworks gibt es eine klaffende Lücke zwischen der Welt der Daten und der Welt der Programme. LINQ ist der Versuch, diese Lücke zu verkleinern. Der Entwickler soll auf möglichst einfache Weise in die Lage versetzt werden, unter Verwendung einer von der Datenquelle unabhängigen API (Programmschnittstelle) mit Daten umzugehen. LINQ besteht nur aus Erweiterungen und Features im Compiler, es wurden keine zusätzlichen Befehle in die CLR eingefügt. So ist es möglich, den Code von LINQ auf der .NET-Laufzeitumgebung unter Verwendung der entsprechenden Funktionsbibliotheken auszuführen.

Dabei wird auf folgende Technologien zurückgegriffen:

  • Type Inference: Implizite Ableitung des Typs durch den Compiler
  • Anonymous Types: Implizite Typen ohne explizite Klassendefinition
  • Object Initializer: Initialisierung von Eigenschaften im new-Konstrukt
  • Extension Methods: Erweiterung bestehender Klassen durch externe Methoden
  • Lambda Expressions: Implizite generische Ausdrücke
  • Expression Trees: Hierarchische Strukturen von komplexen Ausdrücken
  • Generic Delegates: Angabe von generischen Typen für die Argumente
  • Nullables: Nullbare Werttypen

Wenn Sie damit nicht vertraut sind, schauen Sie sich die entsprechende Dokumentation zu C# einmal etwas genauer an.

Architektur und Funktionsweise

LINQ kann mit verschiedenen Datenquellen verwendet werden. Dazu wurde für jede Datenquelle eine entsprechende Bibliothek implementiert. Die folgende Abbildung erläutert dies.

![Abbildung: Architekturüberblick über LINQ]{images/linq.png}

Unter Verwendung einer gemeinsamen Schnittstelle – in diesem Fall kann man von einer API sprechen – stehen dem Entwickler Standardabfrageoperatoren innerhalb der .NET-Bibliothek zur Verfügung. Diese API besteht aus einem festen Satz entsprechender Erweiterungsfunktionen.

Schlüsselworte

Unabhängig davon, mit welcher Datenquelle LINQ verwendet wird, unterstützt der C#-Compiler Schlüsselwörter, die es dem Entwickler einfacher machen, eine Abfrage im Programmcode zu schreiben. Dies ist freilich nur eine Abbildung der entsprechenden Methoden. Die Methoden sind umfassender als die Schlüsselwörter, sodass es manchmal sinnvoller ist, gleich die Methodenschreibeweise zu benutzen, um ein einheitlicheres Codebild zu erhalten.

Das folgende Beispiel zeigt eine einfache Abfrage mittels LINQ-To-Object, die Datenbank ist hier noch nicht involviert.

1 var data = new [] {0,1,2,3,4,5,6,7,8,9,10}; 
2 var res = from e in data
3           where e < 5
4           orderby e 
5           select e;

Aus dem Array data werden alle Zahlen, welche kleiner als 5 sind, in die Ergebnisaufzählung res übernommen. An dieser Stelle soll nicht der Inhalt der Abfrage, sondern die interne Umsetzung einer Abfrage im Vordergrund stehen.

  • from e legt den Namen eines Elements bzw. einer Reihe (in Datenbankterminologie) fest.
  • in data wählt den abzufragenden Aufzählungstyp
  • where e < 5 legt die Bedingung fest
  • orderby e legt fest, nach welchem Wert die optionale Sortierung erfolgen soll
  • select e wählt das zurückzugebende Element bzw. mehrere Elemente

Dabei ist die Reihenfolge (relativ) egal, solange from und in am Anfang der Abfrage stehen. Der Compiler setzt dieses Beispiel in eine Aufrufkette von Erweiterungsfunktionen mit Lambda-Ausdrücken um. Das sieht dann folgendermaßen aus:

1 IEnumerable<int> res = data.Where(e => e < 5)
2                            .Orderby(e => e)
3                            .Select(e => e);

Im nächsten Schritt werden aus den Lambda-Ausdrücken anonyme Methoden erzeugt und die Abfrage wird in IL-Code übersetzt.

Jede der Erweiterungsmethoden gibt mindestens die generische Schnittstelle IEnumerable<T> zurück. Viele der Erweiterungsmethoden geben außerdem die davon abgeleitete Schnittstelle IQueryable<T> zurück. Das hat den Vorteil, dass hier ein Aufzählungstyp zurückgegeben wird, welcher erst eine Aktion ausführt, wenn der Zugriff erfolgt. So wird beispielsweise die SQL-Datenbank erst abgefragt, wenn ein Zugriff auf die Elemente des Aufzählungstyps erfolgt.

Vereinfacht gesagt, erfolgt der Zugriff auf die Datenquelle erst mit dem foreach auf dem Ergebnis der Abfrage. Selbst wenn weitere Abfragen oder Erweiterungsmethoden auf das Ergebnis einer Abfrage angewendet werden, erfolgt der Zugriff erst mit dem Zugriff auf die Elemente des Ergebnisses.

Die Abfrageoperatoren

Die folgende Tabelle fast alle verfügbaren Abfrageoperatoren zusammen.

Tabelle: Abfrageoperatoren
Abfrageoperator LINQ-Ausdruck in C#
GroupBy group … by … group … by … into …
GroupJoin join … in … on … equals … into …
Join join … in … on … equals …
OrderBy orderby …
OrderByDescending orderby … descending
Select select
SelectMany from … in … from … in …
ThenBy orderby … , …
ThenByDescending orderby … , … descending
Where where …

Im Folgenden sollen die häufigsten Arten von Abfragen kurz vorgestellt werden. Dabei ist es nicht das Ziel, auf jede Kombination sowie Erweiterung einzeln einzugehen, da dies dem Konzept diese Bändchens widerspricht. Für eine detaillierte Betrachtung von LINQ ist weiteres Bändchen dieser Reihe geplant.

Einfache Abfragen

In den folgenden Beispielen sollen ein paar typische Abfragen auf Listen und Arrays im Speicher vorgestellt werden.

Where
Listing: Erstes Where-Beispiel
1 int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
2 
3 var res = from n in numbers
4           where n > 5
5           select n;

Diese einfache Abfrage zeigt, das Prinzip aller Abfragen mit LINQ. From Variable wählt den Namen der Variablen innerhalb von LINQ aus, where legt die Bedingung unter Verwendung der internen Variablen fest. in legt die Datenquelle fest, welche mindestens die Schnittstelle IEnumerable<T> implementieren muss.

OrderBy

Mit ein wenig mehr Aufwand können die Elemente der Liste neu sortiert werden. Dazu wird der OrderBy-Operator in die Abfrage eingefügt.

1 var res = from n in numbers
2           where n > 5
3           orderby n
4           select n;

Alternativ kann die Erweiterungsmethode auch außerhalb der LINQ-Abfrage verwendet werden.

1 var res = (from n in numbers
2            where n > 5
3            select n).OrderBy(x => x);

Es ist auch möglich, eine Gruppierung der Daten vorzunehmen. Hierfür ist der GroupBy-Operator zu verwenden.

GroupBy
Listing: Einfache Gruppierung
1 var res = from n in numbers
2           orderby n
3           group n by n > 5 into g
4           select new 
5           { 
6              GreaterFive = g.Key, 
7              Numbers = g
8           };

Im Beispiel werden zwei Gruppen gebildet, eine für alle Nummern, welche kleiner 5 sind, und eine Gruppe für den Rest. Mit Hilfe der Projektion in der Select-Klausel wird ein anonymer Typ erzeugt, welcher die Eigenschaft GreaterFive enthält, mit deren Hilfe zu erkennen ist, um welche der beiden Gruppen es sich handelt, und die Eigenschaft Numbers, welche eine Aufzählung der Nummern enthält.

Eine andere Möglichkeit wäre die Gruppierung in Restklassen. Dabei werden alle Zahlen, welche durch eine Konstante geteilt den gleichen Rest ergeben, in eine Gruppe zusammengefasst.

1 var res = from n in numbers
2           group n by n % 3 into g
3           select new 
4           { 
5              Class = g.Key, 
6              Numbers = g
7           };

Im Beispiel wurden die Zahlen in Restklassen, bezogen auf die Konstante 3, gruppiert. Etwas umfangreicher wird es im nächsten Beispiel. Hier wird ein anonymer Typ erzeugt, um zusätzliche Informationen mit der Abfrage zurückzugeben.

Select-Projektion

var people = new [] { new {Name = “Fischer”, GivenName = “Matthias”, Autor=true}, new {Name = “Krause”, GivenName = “Jörg”, Autor=true}, new {Name = “Mustermann”, GivenName = “Peter”, Autor=false} };

var autoren = from person in people where person.Autor select new { Vorname = person.GivenName, Name = person.Name }; ~~~

Aus einer Liste von Personen, wird eine Aufzählung mit einem anonymen Typ erstellt, welcher alle Personen enthält, deren Eigenschaft Autor auf true gesetzt ist. Der anonyme Typ enthält denn die Eigenschaften Vorname und Name. Mit Hilfe des Aggregations-Operators First kann das erste Element der Liste ausgewählt werden. Auf die Eigenschaften kann dann wie bei bekannten Typen zugegriffen werden.

1 string VornameErsterAutor = autoren.First().Vorname;
SelectMany

Das folgende Beispiel zeigt, wie ein SelectMany umgesetzt werden kann. Dabei sollen aus einer Liste von Büchern mit ihren jeweiligen Autoren eine Liste von „Buch, Autor“-Kombinationen erzeugt werden.

var books = new[] { new { Title = “ASP.NET Profiewissen”, Authors = new[] { new { Name = “Fischer” }, new { Name = “Krause” } } }, new { Title = “Windows Communication Foundation (WCF)”, Authors = new[] { new { Name = “Fischer” }, new { Name = “Krause” } } }, new { Title = “.NET 3.5”, Authors = new[] { new { Name = “Fischer” }, new { Name = “Krause” } } }, };

var publications = from book in books where book.Authors.Count() > 0 from author in book.Authors select new { book.Title, author.Name }; ~~~

Eine andere Variante besteht darin, ein SelectMany über zwei Aufzählungstypen auszuführen und jedes Element mit jedem Element zu verwenden (Pivot-Tabelle, Kreuzprodukt).

Listing: Beispiel für SelectMany (Teil 2)
1 int[] i = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
2 int[] k = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
3 
4 var cross = from x in k
5             where x > 2
6             from y in i
7             where y > 3
8             select new {x, y, product = x*y};

Als Ergebnis wird eine Liste von allen Produkten aus x und y zurückgegeben, wobei x größer 2 und y größer 3 sein muss.

Join

Im Zusammenhang mit Daten wird oft auch eine komplexere Möglichkeit gebraucht. Im dem nachfolgenden Beispiel gibt es zwei Aufzählungen von Nachnamen und Vornamen, mit jeweils einer id, welche zusammengeführt werden sollen.

Listing: Beispiel für Join mit LINQ
 1 var names = new[] 
 2             {
 3                new 
 4                { Name = "Fischer", id = 1 }, 
 5                new 
 6                { Name = "Krause", id = 2 }
 7              };
 8 
 9 var givennames = new[] 
10                  {
11                     new { GivenName = "Jörg", id = 2 }, 
12                     new { GivenName = "Matthias", id = 1}
13                  };
14 
15 var persons = from name in names
16               join givenname in givennames
17               on name.id equals givenname.id
18               select new 
19               {
20                  givenname.GivenName, 
21                  name.Name
22               };

Aggregatoren

Aggregatoren sind Funktionen, welche die Aufzählung auf einen Typen reduzieren. Einer der bekanntesten Aggregatoren ist Count<T>().

Count
1 int[] i = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
2 int anzahl = i.Count();

Weitere Aggregatoren wie Sum<T>(), Min<T>(), Max<T>() und Average<T>() existieren in der LINQ-Bibliothek. Diese können jedoch nur auf primitive Wertetypen angewendet werden. Um Eigenschaften komplexer Objekte direkt summieren zu können, verfügen diese Erweiterungsmethoden über entsprechenden Überladungen, die mit Hilfe eines Lambda-Ausdrucks einen primitiven Typ aus einer Eigenschaft eines Objektes erzeugen.

Sum
1 var objects = new [] 
2               {
3                  new {number = 0}, 
4                  new {number = 1},
5                  new {number = 2}, 
6                  new {number = 3}, 
7                  new {number = 4}
8               };
9 int summe = objects.Sum(x => x.number);

Selektoren

First und FirstOrDefault

Eine der am häufigsten gebrauchten Auswahlfunktionen ist die Funktion First<T>(), welche aus einer Aufzählung von T die erste Instanz zurück gibt. Alternativ kann auch FirstOrDefault<T>() verwendet werden, welche alternativ default(T) zurückgibt, wenn die Aufzählung kein Element enthält.

1 int[] a = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 }.First();

Das Beispiel gibt 5 zurück. In einem etwas komplexeren Beispiel wird die erste gerade Zahl in einer Liste gesucht, welche größer 3 ist.

1 int[] a = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
2 int res = (from n in a 
3            where (n > 3 && (n & 1) == 1)
4            orderby n select n).FirstOrDefault();

Alternativ kann eine überladene Methode von First verwendet werden, welche einen Lambda-Ausdruck annimmt. Die Abfrage könnte dann wie folgt aussehen:

1 int res = (a.OrderBy(n => n))
2   .FirstOrDefault(n => (n > 3 && (n & 1) == 1));
Last und LastOrDefault

Das gleiche wie für First<T>() gilt auch für Last<T> (), sowie LastOrDefault<T>(). Mit Hilfe dieser Auswahlfunktion kann das letzte Element aus einer Liste zurückgegeben werden. Eine entsprechend überladene Methode, welche einen Lambda-Ausdruck entgegennimmt, ist auch hier vorhanden.

Take, Skip und TakeWhile, SkipWhile

Die Erweiterungsfunktionen Take<T>(int n) und Skip<T>(int n) helfen beim Auswählen der ersten n Elemente, wobei mit Skip n Elemente übersprungen werden können. SkipWhile<T>(Func<T,T,bool>) und TakeWhile<T>(Func<T,T,bool>) nehmen jeweils einen Lambda-Ausdruck entgegen, mit dessen Hilfe die zu überspringenden oder die auszuwählenden Elemente angegeben werden können.

1 int[] i = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
2 var r1 = i.Skip(5); // 5 Elemente überspringen
3 var r2 = i.Take(6); // 6 Elemente übernehmen
4 var r3 = i.SkipWhile(x => x < 6); // alle x kleiner 6 überspringen
5 var r4 = i.TakeWhile(x => x > 6); // alle x größer 6 übernehmen
Where

Where dient dem Filtern von Daten. Eine Aufzählung von Daten wird durchlaufen und mit Hilfe eines Lambda-Ausdruckes verarbeitet. Ist der Lambda-Ausdruck WAHR, wird das Element in die Ergebnisaufzählung übernommen, andernfalls wird das Element übersprungen.

Listing: Signatur der Where Klausel
1 public static IEnumerable<T> Where<T>(
2     this IEnumerable<T> source,
3     Func<T, bool> predicate);

Diese Erweiterungsfunktion verwendet, wie (fast) alle anderen Erweiterungen für LINQ, ein yield return. Auf diese Wiese wird sichergestellt, dass nur dann ein weiteres Ergebnis zurückgegeben wird, wenn das vorherige Ergebnis abgearbeitet wurde.

Prinzipiell werden in den LINQ-Erweiterungen alle Elemente mit einer foreach-Schleife durchlaufen, der Lambda-Ausdruck wird angewendet und das Ergebnis wird zurückgegeben.

Listing: Funktionsprinzip der Where-Klausel
 1 public static IEnumerable<T> Where<T>(
 2     this IEnumerable<T> source,
 3     Func<T, bool> predicate);
 4 {
 5    foreach(T element in source)
 6    {
 7       if (predicate(element))
 8       {
 9           yield return element;
10       }
11    }
12 }

Die Implementierung im Framework hat zusätzlich noch entsprechende Parameterüberprüfungen, die sicherstellen, dass nur gültige Parameter übergeben werden:

1 int[] numbers = new int[] = {0,1,2,3,4,5,6,7,8,9,10};
2 var res = numbers.where(x => x % 3 == 0);

Das Anwendungsbeispiel zeigt ein Array von Zahlen. Darin werden alle Elemente gesucht, welche ohne Rest durch 3 teilbar sind.

Select

Select dient der Auswahl der zurückzugebenden Eigenschaften eines Objektes. Manchmal wird ein komplexer zusammengesetzter Datentyp in einer Abfrage verwendet, jedoch sollen nur einzelne Eigenschaften weiter verarbeitet werden.

Die Klasse Person hat die Eigenschaften Name, Givenname, Birthday, Street, City. Nach einer Abfrage sollen jedoch nur der Name und der Givenname weiter verarbeitet werden. Um die Daten zu reduzieren, welche weiter verarbeitet werden, können diese mit der Hilfe des Select-Operators und den anonymen Klassen zu einem neuen anonymen Aufzählungstyp zusammengefasst werden.

Der Select-Operator ist wie folgt deklariert.

Listing: Funktionsprinzip der Select-Klausel
1 public static IEnumerable<S> Select<T, S>(
2     this IEnumerable<T> source,    
3     Func<T, S> selector)
4 {
5    foreach(T element in source)
6    {
7       yield return selector(element);
8    }
9 } 

Ähnlich dem Where-Operator werden alle Elemente des Aufzählungstypen durchlaufen, jedoch wird hier das Ergebnis des Lambda-Ausdruckes zurückgegeben.

Listing: Beispiel für Select-Operator
 1 public class Person
 2 {
 3    public string Name;
 4    public string Givenname;
 5    public DateTime Birthday;
 6    public string Street;
 7    public string City;
 8 }
 9 
10 class Program 
11 {
12    static void Main(string[] args) 
13    {
14       Person[] data = new Person[] 
15       {
16         new Person() { 
17                       Name="Fischer", 
18                       Givenname="Matthias", 
19                       City="Rathenow"
20                      },
21         new Person() {  
22                       Name="Krause", 
23                       Givenname="Jörg",
24                       City="Berlin"
25                      }
26       };
27 
28       var res = data.Select(p => new { Vorname = p.Givenname, 
29                                        Nachname = p.Birthday, 
30                                        p.City });
31       foreach (var elm in res) 
32       {
33         Console.WriteLine("Vorname = {0}, Nachname = {1}, Stadt = {2}"
34            , elm.Vorname, elm.Nachname, elm.City);
35       }
36    }
37 }