VI Teil 4 – Die Applikation
14.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.DataAnnotationsSystem.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.
14.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:
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.
14.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.
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.
14.10 Umgang mit DataAnnotations
14.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 elegt den Namen eines Elements bzw. einer Reihe (in Datenbankterminologie) fest. -
in datawählt den abzufragenden Aufzählungstyp -
where e < 5legt die Bedingung fest -
orderby elegt fest, nach welchem Wert die optionale Sortierung erfolgen soll -
select ewä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.
| 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
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
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).
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.
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.
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.
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.
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.
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 }
15. Repository
Das Repository ist ein Entwurfsmuster (Pattern), das den Datenzugriff regelt. Es dient der Geschäftslogik als Zugriffsschicht auf Daten. Dabei wird die eigentliche Datenquelle idealerweise vollständig abstrahiert. Das bedeutet für den Entwickler der Geschäftslogik, das er keine Kenntnisse über die Datenbank – oder ob überhaupt eine solche benutzt wird – oder die konkreten Befehle zur Steuerung der Persistenz hat.
15.1 Alternative Entwurfsmuster
Das Repository ist das häufigste und einfachste Muster, aber nicht das einzige. Bei größeren Projekten kann es unzureichend sein. Vor der Vorstellung eines konkreten Repositories in diesem Kapitel soll deshalb eine kurze Diskussion alternativer Modell erfolgen. Typisch für Server-Anwendungen sind:
- UoW: Unit Of Work
- CQRS: Command Query Responsiblity Segregation
Beide ergänzen die vorgestellte Gesamtarchitektur in hervorragender Weise.