14. Einführung

Wie bereits im Einleitungstext zum Buch beschrieben, sind die Ansprüche an moderne Serveranwendungen sehr hoch. Alles soll gleichzeitig geliefert werden:

  • Skalierbarkeit: Einer oder 1 Million Benutzer gleichzeitig ist egal – Google kann das ja auch
  • Performance: Immer schnell und immer responsiv, da gibt es keine Kompromisse
  • Zuverlässigkeit: 24/7 ist Pflicht und sogenannte Wartungsfenster undenkbar; eine Website ist immer online
  • Sicherheit: Anmeldung mit allen gängigen Verfahren, natürlich immer HTTPS, und tokenbasiert sowieso
  • Cloud-fähig: Am Ende alles einfach nach Azure oder AWS verteilen sollte kein Problem sein
  • Wartbarkeit: Änderungen, Erweiterungen, Sonderwünsche sind alles kein Problem, natürlich bei laufendem Betrieb
  • Teamfähigkeit: Große, verteilte Teams entwickeln die Software
  • Testbarkeit: Umfangreiche Unit-Tests zu jeder Zeit und an jedem Platz
  • Paketierung: Wiederholt eingesetzter Code wird in Paketen bereitgestellt

Und, diese Liste ließe sich sicher weiter fortsetzen. Eine große Herausforderung, die heute als “Stand der Technik” angesehen wird. Das heißt auch, dass dies erwartet wird, auch wenn der Kunde es nicht ausdrücklich fordert.

14.1 Domain-Driven-Design

Eine wichtige Grundlage für solche Projekte ist richtige Herangehensweise. Bewährt hat sich das Domain-Driven-Design. Dahinter steckt eine bestimmte Art und Weise, die Aufgabenstellung zu abstrahieren. Statt eines rein technischen Ansatzes wird eine fachliche Denkweise und fachliche Logik als primäres Modell gewählt. Das Domänenmodell ist ein Modell der fachlichen Domäne.

Die Beispielanwendung verwaltet Maschinen in einer Fabrik. Die fachliche Domäne ist deshalb der Betrieb einer Fabrikanlage.

Teile des Domänenmodells sind:

  • Entitäten: Das sind Container für abstrahierte Objekte
  • Wertobjekte: Das sind losgelöste, unveränderliche Einheiten ohne fachliche Bezug
  • Aggregate: Das sind zusammengefasste Einheiten aus Entitäten und Wertobjekten
  • Assoziationen: Das sind die Beziehungen zwischen den Entitäten
  • Serviceobjekte: Darstellung der Fachlichkeit durch einen zustandslosen Dienst oder eine Dienstschnittstelle
  • Domänenereignisse: Dynamische Aktionen, die durch Anfragen getriggert werden
  • Module: Rein fachliche Repräsentanz, meist entkoppelt und isoliert

Das ist sicher bis hier her sehr theoretisch. Die konkreten Umsetzung soll in den folgenden Abschnitten kurz gezeigt werden.

Neben der rein fachlichen Sicht werden häufig hilfsweise technische Aspekte benötigt, wozu Repositories und Fabriken (Factories) gehören. Dem Repository wird hier ein eigenes Kapitel gewidmet.

Entitäten

Entitäten sind Container für abstrahierte Objekte. Bei dem Projekt zur Fabrikationssteuerung sind dies:

  • Maschinen
  • Geräte, aus denen die Maschinen bestehen
  • Werte, die die Geräte erfasst haben

Beispielsweise ist der “Industrierobotor IR-200” (Maschine) mit dem Gerät “Spannungsmessung AM-35” ausgerüstet. Damit wird die Spannung während eines Vorgangs gemessen. Der Wert wird in der Entität “Werte” erfasst.

Wertobjekte

Wertobjekte sind losgelöste, unveränderliche Einheiten ohne fachliche Bezug. Im konkreten Projekt sind dies:

  • Einheiten, die die Werte der Geräte näher beschreiben

Die Spannungsmessung im letzten Abschnitt erfolgte abstrakt. Konkret ist es sicher hilfreich zu wissen, dass die Einheit “Volt” benutzt wird und die Anzeige das Formelzeichen “V” benutzt. Ein Wertobjekt beschreibt dies. Es ist unveränderlich, das sich physikalische Einheiten eher selten ändern.

Aggregate

Aggregate sind zusammengefasste Einheiten aus Entitäten und Wertobjekten. Konkret enthält eine Maschine eine Vielzahl von Geräten, die wiederum eine Vielzahl von Messwerten produzieren.

Assoziationen

Assoziationen sind die Beziehungen zwischen den Entitäten. Konkret wird hiermit festgelegt, welche Geräte in welcher Maschine stecken und welche Geräte welche Messwerte erfasst haben.

Serviceobjekte

Serviceobjekte dienen der Darstellung der Fachlichkeit durch einen zustandslosen Dienst oder eine Dienstschnittstelle. Es gibt eine Dienst, der der Verwaltung der Maschinen dient (da kann die Geräte implizieren). Ein weiterer Dienst dient dem Abruf der Messwerte.

Domänenereignisse

Domänenereignisse sind dynamische Aktionen, die durch Anfragen getriggert werden. Wenn ein neues Gerät hinzugefügt wird, könnte es einen inizialen Zyklus durchlaufen. Messwerte außerhalb des erwarteten Bereichs triggern einen Alarm, der höher priorisiert verarbeitet wird.

Module

Module sind eine rein fachliche Repräsentanz, meist entkoppelt und isoliert vom Rest der Applikation. Die grafische Darstellung der Anlage ist eine solche vollkommen isolierte Aufgabe, die gut als Modul abgebildet werden kann.

14.2 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 im Domänenmodell
  • 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 Segragation) eingesetzt werden. Mehr dazu finden Sie im Abschnitt Repository.

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.

14.3 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.

14.4 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.

14.5 Umgang mit DataAnnotations

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