Professionelle Web-Applikationen
Professionelle Web-Applikationen
Jörg Krause
Buy on Leanpub

Einleitung

ASP.NET geht nach 15 Jahren in der bekannten Form in Rente und steht sogleich neu wieder auf. Basierend auf einem neuen Kern, komplett überarbeitet und bereit für die moderne Welt der clientgetriebenen Webanwendungen, ist lediglich der Name noch eine Referenz an die Vergangenheit. ASP.NET Core 1.0 (Codename ASP.NET vNext) bringt eine runde, zuverlässige und vor allem schnelle Plattform für die Entwicklung von Serveranwendungen.

Zusammen mit der Paketverwaltung Node Package Manager (npm) entstand eine leistungsfähige Umgebung zur plattformunabhängigen Programmierung. ASP.NET Core läuft auch auf Linux und bietet dem Entwickler vergleichbare Perspektiven wie node.js. Allerdings bleibt C# die bevorzugte Programmiersprache und Visual Studio die ideale Entwicklungsumgebung.

Dieses Werk geht auf ASP.NET Core in seiner elementarsten Form ein, zeigt die Programmierung einer einfachen Applikationsstruktur für eine Webanwendung und stellt die wichtigsten Bausteine vor – Programmierung mit MVC und Entity Framework Core 1.0 (vormals EF 7).

Dieses Buch ist aktuell zur Version Core 1.0 Final, veröffentlicht im Juni 2016. Es wurde zuletzt an den Stand November 2016 angepasst.

Dieses Werk

Alle Codes sind außerdem zum schnellen Testen und Ausprobieren online auf Github verfügbar.

Der gesamte Text enthält auch einige Teile die originalen Dokumentationen, teilweise umfassend erweitert. Im Gegensatz zu anderen Werken, die nur auf die Webseiten der Anbieter verweisen, soll damit zum einen eine deutsche Beschreibung bereitstehen, zum anderen aber auch das mühevolle Zusammensuchen von diversen Seiten an einer Stelle konzentriert werden.

Das Werk nutzt eine bestimmte Kombination aus Versionen der benutzten Bausteine. Wird eine neue Kombination aktuell, wird es komplett aktualisiert. So wird sichergestellt, dass die Beispiele immer funktionieren und es keine Kompatibilitätsprobleme gibt.

Die Zielgruppe

Wer sollte dieses Werk lesen? Dieses Werk wendet sich an Leser, die aktiv verteilte webbasierte Applikationen entwickeln, mit Webseiten oder Webdiensten arbeiten, unabhängig von der verwendeten Plattform.

Das Buch führt schnell, direkt und kompakt in das Thema JavaScript und Node ein. Die Lernkurve ist steil und die Menge des vermittelten Wissens enorm. Sie sollten in der Lage sein, parallel andere Quellen zurate zu ziehen oder wenigstens im Zugriff haben.

Um den besten Nutzen aus dem Buch ziehen zu können, sollten Sie:

  • ein Softwareentwickler, -architekt oder -designer sein, der aktiv Anwendungen entwirft,
  • mit JavaScript vertraut sein und problemlos mit objektorientierter Programmierung arbeiten können,
  • ein prinzipielles Verständnis für Webdienste und Webanwendungen,
  • eine Entwicklungsumgebung wie Visual Studio oder Visual Studio Code kennen und beherrschen, einschließlich elementarer Fertigkeiten beim Debuggen, Verteilen und Installieren von Applikationen, und
  • in der Lage sein, grundsätzliche Installationen unter Ihrem Betriebssystem vornehmen zu können. Dies umfasst die Fähigkeit Programme mit Administrator- bzw. Root-Rechten zu installieren.

Sehr hilfreich sind immer typische Entwicklerkenntnisse der Web-Welt:

  • HTML und CSS
  • Protokolle wie HTTP, SMTP, TCP, IP usw.
  • Programmiergrundlagen wie SQL und idealerweise LINQ

Mit diesen Voraussetzungen werden Sie optimalen und unmittelbaren Nutzen aus dem Buch ziehen. Wenn Sie hier Defizite haben, sollten Online-Quellen oder andere Bücher zumindest in Reichweite sein.

Anmerkungen

Dieses Buch entstand neben der laufenden Arbeit und parallel zu Kundenprojekten. Wie jedes Werk dieser Art fehlt es vor allem an Zeit, die Dinge so genau und umfassend zu recherchieren und zu testen, dass es letztlich perfekt wird. Es gibt vermutlich ebenso wenig perfekte Bücher, wie es perfekte Software gibt. Es gibt aber gute Software, und wir hoffen sehr, ein gutes und praktisches Buch geschrieben zu haben.

Sollte es dennoch Anlass zur Kritik, Wünsche oder Hinweise geben, schreiben Sie uns bitte eine E-Mail an joerg@krause.de, damit ich dies bei künftigen Auflagen berücksichtigen kann.

Bitte haben Sie Verständnis dafür, dass im Fall von Fragen zur Umsetzung von Projekten, allgemeinen Problemen bei der Installation, Nutzung und Verteilung, bei Schwierigkeiten mit Editoren und damit einhergehenden Fragen die beste und schnellste Hilfe in den einschlägigen Foren zu finden ist. Ich kann hier beim besten Willen keine Unterstützung bieten, da die Zahl der Fragen regelmäßig jeden verfügbaren Zeitraum füllen würde.

Konventionen

In diesem Werk werden einige Darstellung benutzt, die Struktur geben und die Lesbarkeit erleichtern.

Dieses Buch enthält keine Fragen und Übungen. Es ist ein Nachschlage- und Informationswerk. Lernen kann man programmieren am Besten am konkreten Projekt, nicht mit abstrakten Idealbeispielen. Die Informationen dienen als Grundlage für eigene Versuche und als Muster, nicht als “Nachklick-Anleitung”. Solche Anleitungen sind als Video verfügbar und auf meinen Youtube-Channel zu finden.

Werkzeuge

Softwareentwicklung ist eine Mischung aus Kunst und Handwerk. Gemäß dem Spruch “Kunst kommt von Können” und der bekannten Weißheit, dass ein Handwerker im Wesentlichen an der Qualität seiner Werkzeuge bemessen wird, kommt der Auswahl der Tools eine besondere Bedeutung zu. Schaut man sich bei den Profis der Zunft an, was auf dem Bildschirm passiert, so wird schnell deutlich, dass die Zeit der grafischen Oberflächen und komplexen Designer vorbei zu sein scheint. Auch Visual Studio ist bereits geraume Zeit nicht mehr so “visual” wie es am Anfang mal war.

Der Vorteil dieser Vorgehensweise ist ein hohes Maß an Kontrolle über den Quellcode und des Ergebnis. Der Nachteil ist eine erhebliche Lernkurve am Anfang. Die Geschwindigkeit und Präzision bei der Entwicklung ist die Mühe aber wert. Die Auswahl der Werkzeuge beschränkt sich deshalb mehr auf einen geeigneten Texteditor.

Zur Auswahl stehen einfache Editoren wie Notepad++ oder Sublime. Will man auch aus dem Editor heraus debuggen ist Visual Studio Community Edition das große Schiff, das .NET-Entwickler natürlich kennen. Vorteil ist die Projektverwaltung und die vielen Editorfunktionen für alle Arten von Code-Varianten, einschließlich Markdown und SASS. Als weiterer Editor für alle Plattformen kommt Visual Studio Code in Frage. In diesem Buch wird Visual Studio 2015 Update 3 benutzt. Dazu kommt das separate Installationspaket .NET Core for Visual Studio.

Abbildung: Installation des Core-Frameworks
Abbildung: Installation des Core-Frameworks

Nach erfolgter Installation können Sie einen Blitzstart versuchen:

  • Öffnen Sie Visual Studio 2015
  • Wählen Sie File > New > Project…
  • Wählen Sie aus den Vorlagen Templates > Visual C# > Web
  • Wählen Sie ASP.NET Core Web Application (.NET Core) als Vorlage
  • Vergeben Sie einen Namen und klicken Sie OK

Starten Sie ohne Authentifizierung. Dann können Sie Entity Framework Core 1.0 installieren. Das Paket kann von Nuget installiert werden. In der Console des Paket-Managers geben Sie folgendes ein:

Install-Package Microsoft.EntityFrameworkCore.SqlServer

Über den Autor

Ich arbeite als freier Trainer, Berater und Softwareentwickler für große Unternehmen weltweit. Bauen Sie auf die Erfahrung aus 25 Jahren Arbeit mit Web-Umgebungen und vielen, vielen großen und kleinen Projekten.

Mir sind vor allem solide Grundlagen wichtig. Statt immer dem neuesten Framework hinterher zu rennen wären viele Entwickler besser beraten, sich eine robuste Grundlage zu schaffen. Wer dies kompakt und schnell lernen will ist hier richtig. Auf meiner Website www.joergkrause.de sind viele weitere Informationen zu finden.

Ich hat über 40 Titel bei renommierten Fachverlagen in Deutsch und Englisch verfasst, darunter einige Bestseller. Aktuell erscheinen regelmäßig Titel im Eigenverlag texxtoor. Diese sind über Amazon gedruckt oder auf Kindle zu beziehen.

Lernen Sie mich über meine Seiten im Web kennen. Die Einstiegspunkte sind:

  • Web: http//www.joergkrause.de
  • Twitter: @joergisageek

Neben der Website können Sie auch direkten Kontakt über www.IT-Visions.de aufnehmen. Wenn Sie für Ihr Unternehmen eine professionelle Beratung zu Web-Themen oder eine Weiterbildungsveranstaltung für Softwareentwickler planen, kontaktieren Sie mich über meine Website oder buchen Sie direkt über http://www.IT-Visions.de.

1. ASP.NET auf einen Blick

ASP.NET Core 1.0 ist eine umfassende Neuentwicklung der Programmierumgebung für Webanwendung aus dem Hause Microsoft. Es gibt eine ganze Reihe von neuen Konzepten und Prinzipien. Neu ist auch der gesamte Kern – die Ausführungsumgebung. Bewährte Konzepte und Ideen lassen sich allerdings an vielen Stelln wiederfinden.

1.1 Was ist ASP.NET Core 1.0?

Neu ist vor allem, dass die gesamte Umgebung nun Open Source ist. Alle Teile sind im Quelltext auf Github verfügbar. Neu ist auch eine sehr starke Modularisierung. So lassen sich Funktionen sehr feingranular nachladen. Dies beschleunigt den Ablauf, denn oft werden weniger DLLs geladen als zuvor oder webb es mehr sind dann sind diese deutlich kleiner und schlanker.

Die Applikationsentwicklung ist jetzt plattformübergreifend möglich. Was zuvor nur mit Java und seit einiger Zeit mit Node möglich war, ist nun auch Standard bei ASP.NET Core 1.0. Sie können die finalen Webseiten auf Windows, MacOS und Linux ablaufen lassen – ohne Anpassungen. Typisch für Linux-Umgebungen sind Kommandozeilenwerkzeuge, die nun auch für ASP.NET angeboten werden. Mit Visual Studio geht es bequem und im bekannten Rahmen, ohne geht es auch und in der für Linux bekannten Art und Weise. Möglich wurde dies durch das neue .NET-Core Framework, ein kleiner und kompakter .NET-Kern, der komplett portabel ist.

Umsteiger werden sich von einigen Dingen verabschieden müssen. So gibt es keine System.Web.dll mehr. Damit einher gehen eine Reihe von statischen Klassen verloren, die Entwickler häufig eingesetzt haben. Der Lernaufwand ist hier nicht unerheblich. Aber es war genau diese DLL, die wegen ihrer Abhängigkeiten einer weiteren Steigerung der Performance im Weg stand. Stattdessen werden fehlende Funktionen nun über Nuget nachgeladen – kleine schlanke Module für spezielle Einsatzgebiete.

Bislang gab es drei Konzepte für die serverseitige Entwicklung – WebForms, MVC und WebAPI. Dies fließt zu einem Modell zusammen. Es gibt wieder Erweiterungen der HTML-Syntax (asp--Attribute statt <asp:>-Elemente), ein verbessertes MVC-Konzept, das weiter auf Razor aufsetzt und eine weiterentwickelte WebAPI, die statt eigener Controller vollständig mit MVC verschmilzt. Das einzige Entwurfsmuster, dass nunmehr zum Einsatz kommt, ist MVC – Modell, View, Controller. WebForms gibt es nicht mehr.

Es gibt eine explizite Unterstützung für Dependency Injection und damit ein starkes Erweiterungsmodell.

Die Konfiguration ist abhängig von der Produktionsplattform, sodass der Transport vom Entwicklungs- zum Testsystem und weiter zum Webserver oder einer Cloud-Umgebung besonders einfach ist.

Neben den IIS (Internet Information Services) wird nun auch ein Self Host angeboten (ähnlich wie bei WCF – der Windows Communication Foundation). Dieser hat eine schlanke und schnell Pipeline für die Anforderungsverarbeitung.

1.2 Wie es funktioniert

ASP.NET Core-Applikationen werden für das .NET Execution Environment (DNX) gebaut und unter diesem ausgeführt. Mehr zu DNX finden Sie im nächsten Kapitel. Die Integration mit DNX erfolgt über des Paket ASP.NET Application Hosting. Der Einstiegspunkt in die Applikation ist die Klasse Startup.

 1 public class Startup
 2 {
 3   public void ConfigureServices(IServiceCollection services)
 4   {
 5   }
 6 
 7   public void Configure(IApplicationBuilder app)
 8   {
 9   }
10 }

Die Methode ConfigureServices (Zeile 3) definiert die Dienste, die die Applikation verwendet. Die Methode Configure (Zeile 7) definiert die Middleware (Dienstschicht), die die Anforderungen in der Pipeline verarbeitet.

Dienste

Ein Dienst (in ASP.NET Core 1.0) ist ein Baustein der einer Anwendung für allgemeine Aufgaben zu Verfügung steht. Er wird über Dependency Injection bereitgestellt.

In ASP.NET Core 1.0 gibt es einen einfachen IoC-Container, der den Aufruf über einen Konstruktor unterstützt. Reicht dieser nicht aus, kann stattdessen eine andere DI-Bibliothek benutzt werden.

Dienste gibt es in drei Arten: singleton, scoped und transient. Transiente Dienste werden bei jeder Anforderung neu erstellt. Scoped Dienste werden per Scope (Sichtbereich) erstellt und dann vorgehalten. Bei einer Web-Applikation ist der Scope der Kontext der Anforderung, als die Lebensdauer eines Request-Response-Ablaufs. Singleton Dienste werden nur einmal erstellt und dann unabhängig vom Aufrufer immer wieder benutzt.

Middleware

Die Abtrennung der Middleware aus der Applikation ist ein wesentlicher Vorteil für die Architektur einer komplexen Webanwendung. Typische Beispiele sind anwendungstransparente Elemente wie Authentifizierung oder Protokollierung. Die Middleware arbeitet auf dem HttpContext und erledigt alle Aufgaben, für die in vorherigen Versionen auf diese Klasse zugegriffen wurde. Middleware-Komponenten werden sequenziell abgearbeitet. Die Benutzung erfolgt zwar indirekt, lässt sich aber gut steuern. Datz wird eine Erweiterungsmethode der Schnittstelle IApplicationBuilder in der Methode Configure benutzt.

Abbildung: Das Middleware-Konzept
Abbildung: Das Middleware-Konzept

Einige eingebaute Middleware-Komponenten erleichtern den Start:

  • Arbeit mit statischen Dateien (JavaScript, Bilder)
  • Routing
  • Diagnose
  • Authentifizierung
  • Konfiguration für CORS (Cross-Origin Resource Sharing)

OWIN-basierte Middleware lässt sich direkt nutzen. Mehr dazu finden Sie im Kapitel zu OWIN.

Server

Die Hosting-Umgebung des ASP.NET Application Hosting Modells hört nicht direkt auf Requests. Es gibt eine HTTP-Server Implementierung, die dafür verantwortlich ist. Diese Umgebung stellt die Anforderung als HttpContext bereit. Die Komponente ist austauschbar. Standardmäßig wird jedoch ein Art Aufsatz auf den IIS benutzt. Alternativ ist ein Self Host möglich. Werden die IIS nicht benutzt, Kommt der WebListener-Server zum Einsatz, der unter Windows wie auch alle anderen Webserver auf den Systemtreiber http.sys aufsetzt. Läuft ASP.NET Core 1.0 auf einer anderen Plattform, steht weder http.sys noch die IIS zur Verfügung. In solchen Fällen eignet sich die plattformunabhängige Implementierung eines Webserver, die unter dem Namen Kestrel bereitgestellt wird.

Das Wurzelverzeichnis

Das Wurzelverzeichnis der Anwendung ist die Stelle, ab der statische Dateien ausgeliefert werden. Der Pfad zu dieser Position ist konfigurierbar und wird in der project.json mit dem Wert “webroot” eingestellt:

1 {
2   "webroot": "wwwroot",
3   "directory": "bower_components",
4   "userSecretsId": "aspnet5-WebApplication-6a607857-a6aa-4091-bce3-0\
5 7a185bf55dd",
6   "version": "1.0.0-*"
7   ...

Der Standardwert ist “wwwroot”. Alle statischen Dateien müssen hierhin kopiert werden. Am einfachsten erfolgt dies mit einem passenden Gulp-Task beim Bauen der Applikation. Beachten Sie, dass die Projektkonfiguration nun eine json-Datei ist und nicht mehr die bekannte web.config. Falls als Webserver die IIS benutzt werden – was nun eine Option ist – muss für die Konfiguration der IIS im Ausführungsverzeichnis (z.B. “wwwroot”) dennoch die web.config vorhanden sein.

Konfiguration

ASP.NET Core 1.0 benutzt kein neues Konfigurationsmodell.

// anpassen auf neue Web Config

JSON

JSON steht für JavaScript Object Notation. Es ist das Format, indem in ASP.NET vNext die Konfiguration des Projekts und der Applikation erfolgt. Dabei spielt es keine Rolle, ob der Code auf JavaScript basiert, primär mit C# gearbeitet wird oder ob JSON sonstwo im Projekt vorkommt.

JSON wird zur Übertragung und zum Speichern von strukturierten Daten benutzt. Es handelt sich um eine Form der Serialisierung. Die Daten können beliebig verschachtelt werden, beispielsweise ist ein Array von Objekten möglich. Zur Kodierung der Zeichen wird standardmäßig UTF-8 eingesetzt; alternatic sind UTF-16 und UTF-32 möglich.

JSON unterstützt die folgenden Datentypen:

  • Nullwert: wird durch das Schlüsselwort null dargestellt.
  • Boolescher Wert: wird durch die Schlüsselwörter true und false dargestellt. Da es sich um Schlüsselwörter handelt werden sie nicht in Anführungszeichen gesetzt.
  • Zahl: Eine Folge der Ziffern 0–9 mit oder ohne negativem Vorzeichen und mit oder ohne Dezimalpunkt. Die Zahl kann durch die Angabe eines Exponenten e oder E ergänzt werden, dem ein Vorzeichen und eine Folge der Ziffern 0–9 folgt. Beachten Sie dass JavaScript nur Gleitkommazahlen unterstützt, eine explizite Serialisierung für Ganzzahlen gibt es nicht.
  • Zeichenkette: Dies beginnt und endet mit doppelten geraden Anführungszeichen (“). Sie kann Unicode-Zeichen und Escape-Sequenzen enthalten.
  • Array: Wird mit den Literalen [ und ] erstellt. Das Array enthält eine durch Kommata geteilte, geordnete Liste von Werten gleichen oder verschiedenen Typs. Leere Arrays sind zulässig. Arrays können verschachtelt werden.
  • Objekt: Dieses besteht aus den Literalen { und }. Es enthält eine durch Kommata geteilte, ungeordnete Liste von Eigenschaften. Objekte ohne Eigenschaften sind zulässig. Bei der Deserialisierung wird ein leeres Objekt im Sinne von new Object() erstellt.
  • Eigenschaft: Diese besteht aus einem Schlüssel und einem Wert, getrennt durch einen Doppelpunkt (Schlüssel:Wert). Jeder Schlüssel darf in einem Objekt nur einmal enthalten sein. Der Schlüssel ist immer eine Zeichenkette. Der Wert ist ein Objekt, ein Array, eine Zeichenkette, eine Zahl oder einer der Ausdrücke true, false oder null.

Es ist erlaubt, außerhalb dieser Struktur Leerzeichen zu benutzen, beispielsweise um Klammern einzurücken, damit die Lesbarkeit erhöht wird.

JSON unterstützt nicht alle von JavaScript unterstützten Datentypen. Folgende Datentypen sind nicht erlaubt:

  • NaN: Wird nur null konvertiert
  • Infinity: Wird nur null konvertiert
  • -Infinity: Wird nur null konvertiert
  • Date: Wird als String in das ISO-8601-Format konvertiert
  • Function-, RegExp- und Error-Objekte: Werden komplette verworfen

Ein typisches Beispiel zeigt das folgende Listing:

 1 {
 2   "Name": "Joerg",
 3   "Telefon": "12345678",
 4   "Waehrung": "EURO",
 5   "Frei": true
 6   "Anschrift": {
 7     "Strasse": "Musterweg",
 8     "Ort": "Maxhausen",
 9     "Postleitzahl": 98765,
10   },
11   "Technologien": [".NET", "JavaScript", "PHP"]
12 }

2. .NET Core

Der .NET-Kern (Core) 1.0 ist eine kleine, optimierte Laufzeitumgebung. Sie ergänzt das .NET-Framework und wird immer dann eingesetzt, wenn die gesamte Funktionsbreite des Frameworks nicht benötigt wird und stattdessen die Vorteile bei der Performance und Plattformunabhängigkeit relevant sind.

Der Core ist eine modulare Laufzeitumgebung für Windows, Linux und MacOS. Er besteht aus mehreren Bibliotheken, .NET Core genannt, sowie einer Laufzeit, Core CLR genannt. Der Core ist quelloffen (Open Source) und ist auf GitHub verfügbar:

  • .NET Core Libraries:
    • https://github.com/dotnet/corefx
  • .NET Core Common Language Runtime (CoreCLR):
    • https://github.com/dotnet/coreclr

Ergänzt wird dies durch Kommandzeilenwerkzeuge, die .NET Core CLI (Command Line Interface). In früheren Versionen wurde dies DNX genannt.

Die Bereitstellung erfolgt allerdings bevorzugt über Nuget (nuget.org). Die CoreFX-Bibliotheken sind einzeln verfügbar, um den Speicherverbrauch der finalen Anwendung so klein wie möglich zu halten. Der Stammnamensraum ist System. Jede Bibliothek ordnet sich darunter an: System.<LibName>. Die Idee dahinter ist die Auflösung der Abhängigkeit von Installationsvoraussetzungen. Die Bibliotheken werden immer mit der Anwendung ausgeliefert und auf dem Zielsystem muss nichts vorhanden sein. Mit der Anwendung wird sogar die Laufzeit CoreCLR verteilt, sodass alles was die Anwendung braucht verteilt wird. Verschiedene Versionen laufen Seite an Seite und völlig unabhängig. Einen Global Assembly Cache gibt es nicht mehr.

Die Standardbibliotheken sind beispielsweise: Collections, Console, Diagnostics, IO, LINQ, JSON und XML.

2.1 Motivation

Das.NET-Framework war nie das alleinige, große, umfassende Rahmenwerk, das alle Entwickler zufriedenstellt. Angefangen mit dem Compact Framework bis zu Silverlight und WinRT gab es viele Varianten, Derivate und Sonderfälle. Auf anderen Plattformen setzte sich das mit Mono oder Xamarin fort. Dies alles soll mit CoreFX vereinheitlicht werden. CoreFX wird nicht für alle Anwendungsfälle .NET ersetzen, aber in vielen Fällen reicht es aus und dann steht eine neue, einheitlich Basis für einfachere Umgebungen zur Verfügung.

CoreFX wird, wenn es irgendwann vollständig implementiert ist, nicht zwingend kleiner als .NET sein. Das ist nicht das Ziel. Die Modularisierung erlaubt es aber, dass Entwickler sich Teile herauslösen und nur das benutzen, was wirklich gebraucht wird. Dadurch ist die finale Anwendung in der Tat kleiner.

Der Host der Laufzeitumgebung – quasi die Startanwendung – ist das .NET Execution Environment. Dieses kann sowohl CoreFX als auch .NET ausführen. DNX-Projekt können für beide Laufzeitumgebungen lauffähig gemacht werden. Deshalb hat die Projektvorlage für ASP.NET Core 1.0 beide Referenzen:

1 "frameworks": {
2     "net461": { },
3     "netstandard1.5": { }
4 },

Dabei steht net461 für .NET 4.6.1 und netstandard für die .NET Core-Umgebung. netstandard1.5 dient dem Erstellen von portierbaren Assemblies. Wählen Sie stattdessen netcoreapp1.0, um eine eigenständige Applikation zu erstellen. Dies ist der Fall, wenn Sie eine ASP.NET Core 1.0 Web-Applikation bauen.

Abbildung: Projektreferenzen für .NET und CoreFX
Abbildung: Projektreferenzen für .NET und CoreFX

Sollte im Code eine Abhängigkeit auftreten, so können Sie mit einer Compiler-Direktive darauf reagieren:

1 #if NET461
2   // Code der nur mit .NET-Framework läuft
3 #endif

Soll konsequent nur mit .NET Core gearbeitet werden, so kann die Referenz dnx461 entfernt werden.

Wechseln des Frameworks

Zum Wechseln des Frameworks ändern Sie den Eintrag in der Datei project.json. Nach dem Speichern fängt Visual Studio sofort an, das Paket herunterzuladen und bereitzustellen. Der Vorgang kann einen Moment dauern. Beobachten Sie die Statusleiste oder das Ausgabefenster, um den Vorgang zu überwachen.

Microsoft empfiehlt, immer beide Versionen des Frameworks zu referenzieren – .NET Core und .NET 4.x. Natürlich können Sie die beschränken, in dem die eine oder die andere Referenz entfernt wird. Sollten Sie mit Techniken aus ASP.NET 4.6 oder früheren Versionen arbeiten, oder Teile des Projekts darin entwickeln oder Teile migrieren, ist .NET 4.x zwingend erforderlich. .NET Core hat dafür keine Unterstützung.

.NET Core und NuGet

NuGet ist das Repository, über das weitere Funktionsbausteine für .NET Core bereitgestellt werden. Stellen Sie sich das als Ersatz für lokale Ordner mit Assemblies oder auch als Ersatz für den Global Assembly Cache vor. Es wird also keine explizite Trennung mehr erkennbar sein zwischen .NET-Bausteinen und Angeboten anderer Anbieter (third party). Solche fremden Angebote können direkt auf .NET Core-Assemblies referenzieren, was die Distribution stabiler Pakete vereinfacht. Vor allem aber werden alle Abhängigkeiten mitgeführt. Das heißt, dass auf dem Zielsystem keine Voraussetzungen wie beispielsweise eine bestimmte .NET-Version herrschen müssen. Was benötigt wird, kommt mit der Applikation mit. Darüberhinaus benötigt kaum eine Anwendung alles. Das resultierende Paket ist deshalb in aller Regel kleiner als die klassische Kombination aus Framework und eigenen Assemblies.

.NET Core ist eine modulare Untermenge des .NET Frameworks. Es steht als quelloffen (Open Source) zur Verfügung und adressiert mehrere Plattformen. Es kann mit klassischen Framework-Anwendungen koexistieren.

2.2 Die Ausführungsumgebung

Die Ausführungsumgebung (.NET Execution Environment) besteht aus dem Entwickler-Kit (SDK) und der Laufzeitumgebung. Sie ost für Windows, Mac und Linux verfügbar. Sie bietet einen Host-Prozess, unter dem die Anwendung abläuft, eine Steuerlogik für die Laufzeit und einen definierten Einsprungpunkt zum Start der Applikation. Primär ist diese Umgebung auf ASP.NET optimiert, kann aber auch andere Programmarten ausführen, wie beispielsweise Konsolenapplikationen.

Der Vorteil der Ausführungsumgebung ist die plattformunabhängigkeit. Auch wenn die Entwicklung auf Windows stattfindet, kann die Produktionsumgebung Linux sein. Plattformspezifische Besonderheiten werden weitgehend ausgeblendet und müssen nur selten berücksichtigt werden. Die Konfigurationsumgebung basiert auf JSON (JavaScript Object Notation) und hat keinen Bezug zu bestimmten PLattformfunktionen. Die Integration mit anderen Paket-Managern, speziell NPM (Node Package Manager) und Bower, ist besonders einfach, weil diese ebenso auf JSON setzen. Pakete lassen sich global auf einer Maschine bereitstellen, um die fortlaufende Arbeit am Projekt zu vereinfachen.

Projekte

Ein DNX-Projekt ist ein Ordner mit einer Konfigurationsdatei. Diese Datei hat immer den Namen project.json. Der Name des Projekts ist der Name des Ordnerns. Die Konfigurationsdatei hat folgenden Aufbau:

 1 {
 2   "version": "1.0.0-*",
 3   "description": "Mein erstes Projekt",
 4   "authors": [ "joergisageek" ],
 5   "tags": [ "" ],
 6   "projectUrl": "",
 7   "licenseUrl": "",
 8 
 9   "frameworks": {
10     "net461": { },
11     "netstandard1.5": {
12       "dependencies": {
13         "Microsoft.CSharp": "4.0.1-beta-23516",
14         "System.Collections": "4.0.11-beta-23516",
15         "System.Linq": "4.0.1-beta-23516",
16         "System.Runtime": "4.0.21-beta-23516",
17         "System.Threading": "4.0.11-beta-23516"
18       }
19     }
20   }
21 }

Alle Dateien im Ordner sind automatisch Teil des Projekts. In package.json können Dateien explizit ausgeschlossen werden. Ein expliziter Einschluss wie in traditionellen Visual Studio-Projektformaten ist nicht erforderlich. Dies vereinfacht und beschleunigt den Ladevorgang. Um Vorgänge für den Entwickler zu automatisieren, werden Kommandos benutzt. Diese sind ebenso Teil des Projekts. Mehr dazu finden Sie im Abschnitt “Kommandos”.

Die Eigenschaft “frameworks” (Zeile 9) bestimmt, welche Frameworks benutzt bzw. unterstützt werden sollen. Derart erstellte Projekte dienen auch dazu, neue Nuget-Pakete zu erstellen (anlog zu npm, wo dieselbe Umgebung Pakete konsumiert und produziert). Zum Erstellen von Paketen dient das Werkzeug DNU (.NET Development Utility). Der Erstellungsvorgang erstellt die binäre Ausgabe des Projekts.

Umgang mit Abhängigkeiten

Abhängigkeiten werden durch einen Namen und eine Versionsnummer definiert. Versionen benutzen ein semantisches Versionierungsverfahren. Abhängigkeiten können auf Pakete auf Nuget, lokale Nuget-Pakete oder andere DNX-Projekte verweisen. Auf der Ebene der Applikation (Solution Level) kann mit einer Datei global.json ein Verweis auf andere lokale Projekte definiert werden:

1 {
2   "projects": [ "src", "test" ],
3   "sdk": {
4     "version": "1.0.0-preview2-003118"
5   }
6 }

Mit global.json wird auch die minimale Laufzeit festgelegt (SDK-Version).

Abhängigkeiten werden transparent vererbt. Sie müssen also nur die Abhängigkeiten der letzten Ebene festlegen, deren Abhängigkeiten werden automatisch nachgezogen. Die Bereitstellung der abhängigen Pakete erfolgt automatisch. Wenn diese binär vorliege, werden sie direkt gelinkt. Wenn sie als Quellcode vorliegen, werden sie im Speicher übersetzt und angebunden.

Die Werkzeuge

Paketabhängigkeiten können nur aufgelöst werden, wenn das Basispaket installiert wurde. Um Pakete manuell nachinstallieren zu können, wird das Kommandzeilenwerkzeug. Die nötigen Pakete werden zuerst in der Datei project.json festgelegt. Dann werden sie mittels dotnet wieder hergestellt:

dotnet restore

Falls eigene Pakte erstellt werden – also Applikationsbausteine zur Nutzung durch andere Applikationen – müssen diese zuerst veröffentlicht werden. Dies erfolgt im ersten Schritt lokal mit folgendem Kommando:

dotnet publish

Die Struktur die daraus entsteht hat einen festen Aufbau:

1 output/
2       /packages
3       /appName
4       /commandName.cmd

Der Ordner output enthält die Paketdateien. Kommandos, die urspünglich definiert wurden, werden in die Batch-Datei commandName.cmd gepackt.

Der eigentliche Erstellungsvorgang für Pakete wird folgendermaßen ausgelöst:

dotnet pack

Falls Assemblies erstellt werden sollen, wird dieses Kommando benutzt:

dotnet build

2.3 Kommandos

Ein Kommando ist ein Ausführungspunkt, der über einen bestimmten Namen erreicht werden kann. Kommandos werden in der Datei project.json definiert:

1 "commands": {
2   "web": "Microsoft.AspNet.Server.Kestrel",
3   "ef": "EntityFramework.Commands"
4 },

Das Werkzeug zum Ausführen ist dotnet – Die Ausführungsumgebung. Starten Sie bei der gezeigten Definition den integrierten Webserver wie folgt:

dotnet run

Sollte der Code nicht kompiliert worden sein, wird implizit dotnet build aufgerufen.

Falls dnx nicht im aktuellen Projektverzeichnis ausgeführt wird, kann der Pfad zur package.json angegeben werden:

dotnet <Pfad/nach/project.json> <Kommando>

Kommandos können auch als Nuget-Paket bereitgestellt werden. Die Ausführung eines Kommandos in Form eines Assemblynamens mutet etwas seltsam an. Tatsächlich muss die angegebene Assembly einen Einsprungpunkt bieten. Dazu wird die Schnittstelle ILoader implementiert, nachdem die Ausführungsumgebung sucht. Der ILoader lädt die eigentliche Assembly anhand ihres Namens. Er ruft dann den Einsprungpunkt aus. Dies ist normaler, verwalteter Code.

2.4 Der Applikations-Host

Der Applikations-Host ist der erste verwaltete Einsprungpunkt. Er behandelt Abhängigkeiten, verarbeitet die Dateiproject.json und bietet verschiedene Dienste an. Letztlich ist der Host eine Alternative zum direkten Aufrufen des Einsprungpunkts einer Assembly mittels dotnet run. In der Praxis kann man auch davon ausgehen, dass die Benutzung eines Hosts der bevorzugte Weg ist. Selten wird eine Applikation ihre Startumgebung vollständig selbst mitbringen. Speziell bei ASP.NET ist der Host der Webserver, der hier immer benötigt wird.

Der Host liefert verschiedene Dienste. Mittels Dependency Injection sind die Dienstinstanzen flexibel austauschbar. Typische Dienste sind:

  • IServiceProvider
  • IApplicationEnvironment
  • ILoggerFactory

Die Dienste werden im Konstruktor der Klasse, die den Einsprungpunkt bietet, mit konkreten Instanzen bestückt (“injiziert”).

3. Struktur und Konfiguration

Der hauptsächliche Nutznießer der Laufzeitumgebung DNX ist derzeit ASP.NET. Außer dem Namen hat dies nur noch wenig mit dem bisherigen ASP.NET zu tun, auch wenn einige Konzepte übernommen wurden. Es ist einfacher “das neue ASP.NET Core” zu lernen, wenn man erstmal möglichst viel von der alten Welt ausblendet. Zuerst werden einige fundamentale Konzepte vorgestellt, die unbedingt notwendig sind, um den Rest des Buches zu verstehen.

3.1 Die Projektstruktur

Die Projektstruktur für vNext-Projekte ist anders als bei bisherigen ASP.NET-Projekten. Die Projektstruktur ensteht durch die Standardvorlage in Visual Studio mit dem Namen Web Application. Statt weniger komplexer Dateien gibt es jetzt mehr Dateien, diese sind jedoch kompakter und spezialisierter.

Ein Projekt erstellen

Um Sie zu erreichen, gehen Sie wie üblich über die Programmiersprache, beispielsweise C# auf den Ordner Web und dann auf ASP.NET Core Templates – Web Application. Die Auswahl des Frameworks 4.6 auf der vorherigen Seite spielt für die Wahl der DNX-Laufzeitumgebung hier keine Rolle.

Abbildung: Neues Projekt erstellen
Abbildung: Neues Projekt erstellen

Wie bei den vorherigen Projektformen kann die Authentifizierungsform gewählt werden. Die Auswahl entspricht den bisherigen Möglichkeiten:

  • Keine Authentifizierung
  • Authentifizierung mit Benutzerkonten – dies nutzt OAuth und erlaubt sowohl eine lokale Datenbank als auch Authentifizierungsprovider
  • Work and School basiert auf einem bestehenden Active Directory, lokal oder in der Azure Cloud
  • Windows Authentifizierung nutzt NTLM im lokalen Intranet
Abbildung: Authentifizierung festlegen
Abbildung: Authentifizierung festlegen

Für erste Experimente empfiehlt es sich, auf die Authentifizierung zu verzichten und diese Funktionen erst später hinzuzufügen. Das Projekt erscheint sonst auf den ersten Blick unnütz komplex.

Nach der Auswahl ensteht die Struktur. Alle benötigten Pakete werden nun heruntergeladen – bis sich das Projekt nutzen lässt kann je nach Internet-Verbindung einige Zeit vergehen. Solange dieser Vorgang läuft, steht Restoring Packages im Solution Explorer.

Abbildung: Das Projekt
Abbildung: Das Projekt

Folgende Bausteine liefert diese Projektvorlage:

  • Der Ordner Properties: Enthält die Eigenschaften des Projekts in der Datei launchSettings.json
  • Der Ordner References: Die Referenzierung der Laufzeitumgebungen DNX 4.6.1 und .NET Core 1.0 (wenn beide benutzt werden)
  • Der Ordner Dependencies: Die Abhängigkeiten der Client-Bausteine bzw. der JavaScript-Umgebung. Standardmäßig verweist dieser Teil auf die Repositories Bower und npm (Node Package Manager).
  • Der Ordner wwwroot: Der Stammordner des Zielprojekts, also quasi die Abbildung der Produktionsumgebung. Hier sind die final benutzten Client-Skripte, Bilder, CSS-Dateien usw. enthalten. Ergänzt wird die Umgebung durch die .NET-Assemblies, die im übergeordneten Projekt erzeugt werden. Der Gulp-Task kopiert aus den JavaScript-Quelldateien die im Client tatsächlich benutzten Versionen nach wwwroot bzw. einen der Unterordner.
  • Der Ordner Controllers: Die Controller für die Web- bzw. API-Umgebung. Eine Unterscheidung zwischen API-Controller und der Verarbeitung von Razor-Views gibt es nicht mehr. Die neue Basisklasse Controller kann beides.
  • Der Ordner Views: Wird mit Razor-Views gearbeitet, liegen diese wie bisher hier.
  • Die Dateien:
    • appsettings.json: Allgemeine Einstellungen zur Applikation.
    • gulpfile.js: Die Steuerdatei für den clientseitigen Erstellungsvorgang, basierend auf JavaScript-Skripten.
    • project.json: Die Abhängigkeiten, die durch Zugriff auf Nuget aufgelöst werden.
    • Project_Readme.html: Eine statische Info-Datei. Diese können Sie bedenkenlos löschen.
    • Startup.cs: Steuerung des Startverhaltens des Projekts. Dies ersetzt die Aufrufe in der früher benutzten global.asax. Insbesondere werden hier die durch Dependency Injection nun dynamisch linkbaren Module vereinbart.

Die scheinbare Flut von neuen Dateitypen soll dazu dienen, die Aufgabe jeder einzelnen Datei zu beschränken. So ist der Zweck und damit der Umfang jeder Datei reduziert und damit die konkrete Aufgabe einfacher.

Auf der Ebene der Solution gibt es noch die Datei global.json und den Ordner artifacts. In global.json wird das SDK (Zielplattform) festgelegt. Kompilierte Assemblies sind in artifacts zu finden.

Die Client-Repositories

Der Umgang mit speziellen Client-Repositories ist ein Bruch mit der Fokussierung auf Nuget. Nuget ist nur noch für .NET und CoreFX zuständig. Es ist zwar absehbar, dass viele Client-Pakete (also solche, die auf JavaScript und CSS basieren), weiter über Nuget gepflegt werden. Die Ausrichtung der JavaScript-Szene auf Bowser & Co. führt aber dazu, dass kleinere Projekte entweder überhaupt nicht mehr oder mit Verzögerung bereitgestellt werden.

Bower

Bower liefert clientseitige Projekte wie beispielsweise jQuery oder AngularJS. Bower selbst verwaltet nur Pakete und deren Abhängigkeiten, die eigentlichen Codes werden jedoch von Github abgerufen. Es werden also in den meisten Fällen Quellprojekte geliefert. Statt einer Datei udn eventuell noch der minimierten Version kann dies dazu führen, dass Dutzende JavaScript-Dateien auf der Festplatte landen oder auch mal statt CSS die Präkompiler-Versionen in der Sprache LESS oder SASS auftauchen.

Sie müssen also, wenn Sie mit aktuellen Paketen arbeiten wollen, hier die entsprechenden Werkzeuge einsetzen, um mit diesen Quellpaketen umgehen zu können. Dazu dient der Task Runner – eine auf node.js basierende Ausführungsumgebung für entwicklerseitiges JavaScript. Der JavaScript-Code wird dabei nicht im Browser, sondern auf der Kommandozeile ausgeführt. Dies schließt volle Rechte beim Zugriff auf das Dateisystem mit ein.

NPM

Derartige entwicklerseitige Vorgänge basieren auf auch Paketen. Node.js bringt neben der Laufzeitumgebung für JavaScript auch eine eigene Paketverwaltung mit den Node Package Manager npm. Damit lassen sich Werkzeuge beschaffen, die man bei der Entwicklung einsetzen kann, unter anderem:

  • Kopieren von Dateien
  • Ordner aufräumen
  • Minimieren von JavaScript- und CSS-Dateien
  • Übersetzen von TypeScript nach JavaScript
  • Übersetzen von LESS oder SASS nach CSS
  • Unit Tests für JavaScript

Die ausführende Umgebung ist node.js, als Steuerungsinstrument wird jedoch ein darauf aufbauendes Werkzeug benutzt: Gulp. In Visual Studio gibt es die Funktion Task Runner, die Gulp-Skripte ausführen kann.

Konfigurationen

ASP.NET Core 1.0 kann mehrere Frameworks adressieren, damit verschiedene Laufzeitumgebungen bedient werden können. Standardmäßig wird das normale .NET-Framework benutzt. Alternativ kann .NET Core benutzt werden. .NET Core kommt in Frage, wenn das Hosting später auf Linuy erfolgen soll. Wenn ältere Bausteine integriert werden sollen, wird diese Option meist nicht zur Verfügung stehen, da Referenzen auf das .NET-Framework existieren. Die zur Verfügung stehenden Abhängigkeiten finden Sie in der Projektstruktur unter References. Die Einstellung selbst erfolgt in der Datei project.json unter dem Abschnitt targets.

Die Einstellungen selbst müssen nicht zwingend in den Dateien vorgenommen werden. Wie üblich unterstützt Visual Studio die Einrichtung durch zahlreiche grafische Dialoge. Die Grundkonfiguration ist im Kontextmenü des Projekst unter Properties (Alt-Enter) verfügbar.

Abbildung: Projekteigenschaften der Applikation
Abbildung: Projekteigenschaften der Applikation

Die Einstellungen zum Erstellen des Projekts (Build) umfassen folgende Optionen:

  • Produce outputs on build: Wird diese Option aktiviert, werden die Pakete und Assemblies erstellt und im Ordner artifacts abgelegt. Assemblies entstehen für jedes aktivierte Framework inklusive der üblichen PDB-Dateien (in der Debug-Konfiguration) und der XML-Dokumentationsdateien. Außerdem wird eine Paket-Datei für Nuget mit der Erweiterung .nupkg erstellt und die Beschreibung der App aus der project.json kopiert.
  • Compile TypeScript on build: TypeScript-Dateien (mit der Erweiterung *.ts) werden in JavaScript kompiliert. Die Datei wird unmittelbar neben die Quelle platziert.
Abbildung: Projekteigenschaften der Applikation
Abbildung: Projekteigenschaften der Applikation

Im Debug-Tab finden Sie Optionen zum Einstellen des Hosts, der zum Debuggen benutzt wird. Die Optionen lassen sich unter Profilen zusammenstellen und verwalten. Standardmäßig stehen zwei Profile zur Verfügung:

  • IIS Express
  • web

Mit web ist hier das Kommando gemeint, dass den Kestrel-Webserver startet.

Abbildung: Festlegung des Kommandos in project.json
Abbildung: Festlegung des Kommandos in project.json

Mit dem IIS Express können Sie Startadresse, Port, SSL und Authentifizierungsverfahren wählen. Bei Kestrel kann nur die Startadresse festgelegt werden. Beide Profile erlauben die Auswahl der Laufzeitumgebung, des Frameworks und der Architektur.

Abbildung: Projekteigenschaften der Applikation
Abbildung: Projekteigenschaften der Applikation
Die Datei project.json

Die Datei project.json ist neu und konfiguriert das Projekt zum Erstellungszeitpunkt. Hier werden die serverseitige Abhängigkeiten festgelegt und die elementaren Pfade. Die wichtigsten Optionen sind:

  • “version”: Version des Projekts im Semver-Format
  • “authors”: Array mit den Autoren
  • “description”: Beschreibungstext
  • “dependencies”: Objekt mit den Abhängigkeiten (“Name”: “Version”)
  • “frameworks”: Unterstützte Frameworks (dnx461 und netstandard)
  • “scripts”: Array von Kommandos (auf der Kommandzeile), die automatisch ausgeführt werden:
    • “prebuild”: Vor dem Erstellen des Codes
    • “postbuild”: Nach dem Erstellen des Codes
    • “prepack”: Vor dem Erstellen des Pakets
    • “postpack”: Nach dem Erstellen des Pakets
    • “prerestore”: Vor dem Wiederherstellen von Paketen
    • “postrestore”: Nach dem Wiederherstellen von Paketen
  • “compilationOptions”: Einstellungen für den Erstellungsvorgang, die an den Compiler weitergegeben werden:
    • “define”: Array mit Konstanten, die als Compiler-Symbole benutzt werden
    • “allowUnsafe”: true oder false
    • “warningsAsErrors” : true oder false
  • “configurations”: Objekt mit Debug- und Release-Einstellungen
  • “webroot”: Der Ordner für die Client-Dateien (standardmäßig wwwroot)
  • “exclude” und “publishExclude”: Array mit Dateien (mit Platzhaltern), die nicht beachtet bzw. nicht mit veröffentlicht werden.
 1 {
 2   "version": "1.0.0-*",
 3   "compilationOptions": {
 4     "emitEntryPoint": true
 5   },
 6 
 7   "dependencies": {
 8     "Microsoft.AspNet.Diagnostics": "1.0.0-rc1-final",
 9     "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
10     "Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
11     "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-final",
12     "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
13     "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final",
14     "Microsoft.AspNet.Tooling.Razor": "1.0.0-rc1-final",
15     "Microsoft.Extensions.Configuration.FileProviderExtensions" : "1\
16 .0.0-rc1-final",
17     "Microsoft.Extensions.Configuration.Json": "1.0.0-rc1-final",
18     "Microsoft.Extensions.Logging": "1.0.0-rc1-final",
19     "Microsoft.Extensions.Logging.Console": "1.0.0-rc1-final",
20     "Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-final",
21     "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-rc1-fin\
22 al"
23   },
24 
25   "commands": {
26     "web": "Microsoft.AspNet.Server.Kestrel"
27   },
28 
29   "frameworks": {
30     "dnx461": { },
31     "netstandard1.0": { }
32   },
33 
34   "exclude": [
35     "wwwroot",
36     "node_modules"
37   ],
38   "publishExclude": [
39     "**.user",
40     "**.vspscc"
41   ],
42   "scripts": {
43     "prepublish": [ "npm install", "bower install", "gulp clean", "g\
44 ulp min" ]
45   }
46 }
Die Datei global.json

Die Datei global.json konfiguriert die Solution als Ganzes.

1 {
2   "projects": [ "src", "test" ],
3   "sdk": {
4     "version": "1.0.0-rc2-final"
5   }
6 }

The projects property designates which folders contain source code for the solution. By default the project structure places source files in a src folder, allowing build artifacts to be placed in a sibling folder, making it easier to exclude such things from source control.

../_images/solution-files.png The sdk property specifies the version of the DNX (.Net Execution Environment) that Visual Studio will use when opening the solution. It’s set here, rather than in project.json, to avoid scenarios where different projects within a solution are targeting different versions of the SDK.

wwwroot

Der Ordner wwwroot hat eine ganz zentrale Funktion: Er trennt die Projektstruktur während der Entwicklung von der Struktur der finalen Website. Bisher war dies zusammengefasst und das Ergebnis nicht immer optimal für den Entwickler oder den Webserver. wwwroot ist quasi eine Unterstruktur, die die Website abbildet. Während das Routing von ASP.NET MVC einige Probleme bei der Pfadzuordnung lösen konnte, waren statische Dateien immer genau dort abzulegen, wo die Pfade der Webseiten es verlangten. Bei komplexeren Applikationen, wie einer Single Page App mit AngularJS, herrschte dann schnell Chaos, weil die Übersicht im Projekt verloren ging.

Darüberhinaus waren projektspzifische Dateien (wie web.config oder global.asax) Teil der Distribution. Sie mussten daher explizit geschützt werden. Im Grunde ist dies jedoch eine unnütze Maßnahme. Besser wäre es, solche Dateien überhaupt nicht zu verteilen. Dies wird durch wwwroot erreicht.

Clientseitige Dateistrukturen sind eine weitere Fehlerquelle. Neben den mittels bower abgerufenen Quelldateien sind es auch eigene “Hilfsdateien”, die nicht verteilt werden sollten, beim Entwickeln aber notwendig sind. Dazu gehören Codes in TypeScript, LESS oder SASS. Hier werden im Client nur die resultierenden Produkte in JavaScript und CSS benötigt. Nicht mit verteilt werden auch Unit-Tests in JavaScript und Dateien, die mit Debug-Code angereichert sind.

Abbildung: Aufbau des Ordners wwwroot
Abbildung: Aufbau des Ordners wwwroot

Die Entwicklung der serverseitigen Elemente, beispielsweise der API-Controller in C#, findet ohnehin nicht in wwwroot statt. Unterliegen Sie jedoch nicht der Versuchung, der Einfachheit halber JavaScript direkt dort zu platzieren. Gehen Sie stattdessen folgendermaßen vor:

  • Erstellen Sie im Hauptprojekt einen Ordner Client
  • Platzieren Sie die Skripte (TypeScript/JavaScript) passend zu Struktur der Controller und Views
  • Erstellen Sie passende Unit-Tests
  • Erweitern Sie das gulpfile.js (oder gruntfile, falls Grunt benutzt wird) um passende Aufgaben:
    • Minimieren der Datei
    • Zusammenfassen (bundle)
    • kopieren der Dateien an die passende Stelle nach wwwroot

3.2 Startverhalten konfigurieren

Das Startverhalten wird in der Datei Startup.cs beschrieben. Der Konstruktor ist der Startpunkt der Applikation. Dem Konstruktor werden die Umgebungen übergeben, sodass im Code drauf Bezug genommen werden kann:

 1 public Startup(IHostingEnvironment env)
 2 {
 3   var builder = new ConfigurationBuilder()
 4           .SetBasePath(appEnv.ApplicationBasePath)
 5           .AddJsonFile("appsettings.json")
 6           .AddEnvironmentVariables();
 7   Configuration = builder.Build();
 8 }
 9 
10 public IConfigurationRoot Configuration { get; set; }

Zwei weitere Methoden werden implizit (durch die Laufzeit) benutzt: ConfigureServices zum Festlegen der intern verfügbaren Dienste und Configure, um diese Dienste zu konfigurieren.

1 public void ConfigureServices(IServiceCollection services)
2 {
3     services.AddMvc();
4 }

Im Beispiel wurde der Dienst “Mvc” hinzugefügt. Dafür gibt es eine explizite Methode, da es sich um einen Standarddienst handelt. Auf diesen Aufruf stützt sich die Konfiguration der Routen, im folgenden Abschnitt ab Zeile 21:

 1 public void Configure(IApplicationBuilder app, 
 2                       IHostingEnvironment env, 
 3                       ILoggerFactory loggerFactory)
 4 {
 5     loggerFactory.AddConsole(Configuration.GetSection("Logging"));
 6     loggerFactory.AddDebug();
 7 
 8     if (env.IsDevelopment())
 9     {
10         app.UseBrowserLink();
11         app.UseDeveloperExceptionPage();
12     }
13     else
14     {
15         app.UseExceptionHandler("/Home/Error");
16     }
17 
18     app.UseIISPlatformHandler();
19 
20     app.UseStaticFiles();
21 
22     app.UseMvc(routes =>
23     {
24         routes.MapRoute(
25             name: "default",
26             template: "{controller=Home}/{action=Index}/{id?}");

Alles was zur Verarbeitung von Anforderungen notwendig ist, ist an dieser einen Stelle zusammengefasst. Weitere Konfigurations-Dateien oder web-config-Einstellungen sind nicht erforderlich. Applikationsspezifische Einstellungen – also der private Teil – sind in der Datei appsettings.json zu finden.

Konfiguration der Applikation

Eigene Konfigurationen wurden bislang als XML abgelegt. Diese Aufgabe übernimmt jetzt die Datei appsettings.json. Der Name der Datei ist frei wählbar, die Einstellung erfolgt in der Startumgebung Startup.cs (Zeile 3, appsettings.json ist der Standard):

1 public Startup(IHostingEnvironment env)
2 {
3     var builder = new ConfigurationBuilder()
4         .AddJsonFile("appsettings.json")
5         .AddEnvironmentVariables();
6     Configuration = builder.Build();
7 }

Hier ein Beispiel für eine appsettings.json-Datei:

 1 {
 2   "AppSettings": {
 3     "HomeTitle": "Hallo ASP.NET Core"
 4   },
 5   "Logging": {
 6     "IncludeScopes": false,
 7     "LogLevel": {
 8       "Default": "Verbose",
 9       "System": "Information",
10       "Microsoft": "Information"
11     }
12   },
13   "Data": {
14     "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=as\
15 pnetCore-Intro"
16   },
17 
18   "Entity": {
19      "ApplicationDbContext": {
20         "ConnectionString": "Data:DefaultConnection:ConnectionString"
21      }
22   }
23 }

Gegenüber den bisherigen “AppSettings” können hier nicht nur Schlüssel-/Werte-Paare platziert werden, sondern auch komplexe Objekte. Objekte können sich auch gegenseitig referenzieren, wie ín Zeile 18 zu sehen ist.

Um die Datei zu lesen wird eine Schnittstelle IConfiguration injiziert. Die Implementierung Configuration liefert die Elemente über Indexer. Diese steht in der Startup.cs standardmäßig zur Verfügung. Ergänzen Sie zum Zugriff den Code in Zeile 4:

1 public void ConfigureServices(IServiceCollection services)
2 {
3    services.AddMvc();
4    services.AddInstance(typeof (IConfiguration), Configuration);
5 }

Im Code (dies wird meist ein Controller sein), wird dann auf die Schnittstelle IConfiguration verwiesen:

 1 using Microsoft.Extensions.Configuration;
 2 
 3 public class HomeController : Controller
 4 {
 5   private readonly IConfiguration _config;
 6 
 7   public HomeController(IConfiguration config){
 8      _config = config;
 9   }
10 
11   public IActionResult About()
12   {
13      string appName = _config["AppSettings:HomeTitle"];
14      ViewData["Message"] = "Titel: " + appName;
15      return View();
16   }
17 
18   public IActionResult Index(){
19      return View();
20   }
21 }

3.3 Bundling und Optimierung

Wie bereits kurz gezeigt, gibt es für all diese Verfahren ein komplett auf JavaScript basierendes Ökosystem. Die Grundlage ist Node und damit der Zugriff über den Node Package Manager npm.

In MVC6 liefert Microsoft keine eigene Werkzeuge mehr, sondern erweitert Visual Studio um Funktionen, die dieses JavaScript-Ökosystem nutzen. Standardmäßig wird Gulp eingesetzt.

Grunt oder Gulp?

Grunt ist nach eigener Darstellung auf der Website ein „JavaScript based Task Runner“. Er dient der Automation von Entwicklungsvorgängen. Bei Grunt geht Konfiguration vor Programmierung. Ausgangspunkt ist das Gruntfile, mit dem die Aufgaben automatisiert werden. Grunt ist Plug-In-orientiert, d.h. es werden für viele typische Vorgänge fertige Module angeboten, die in das Gruntfile eingebunden werden.

Gulp ist ein Workflow-basiertes System. Es setzt auf das Streaming-Modul von node auf. Abgewickelt werden auch hier Aufgaben. Die elegante Umsetzung verspricht weniger Konfiguration und mehr Flexibilität. Hier geht Programmierung vor Konfiguration. Auch für Gulp gibt es fertige Module, die Standardaufgaben erledigen.

Die Unterschiede

Es geht hier um Konfiguration versus Programmierung. Grunt gibt einen klaren Weg vor – Konfiguration von fertigen Aufgaben. Gulp ist sehr offen – primitive eingebauten Aufgaben, Mini-Aufgaben aus der Community und ein flexibler Weg, diese zu kombinieren.

Grunt lebt also vor allem von seinem Repository an Modulen. Diese sind via npm von Github beschaffbar und die Liste ist beeindruckend lang. Gulp ist jünger und hat weniger zu bieten, benötigt aber auch weniger, weil sich durch wenige Code-Elemente vieles in JavaScript erledigen lässt, wofür man in Grunt erst ein Plug-In bauen müsste.

Das Gruntfile nutzt ein JSON-Objekt zur Konfiguration und ist sehr primitiv aufgebaut:

 1 grunt.initConfig({
 2   clean: {
 3     src: ['build/app.js', 'build/vendor.js']
 4   },
 5   copy: {
 6     files: [{
 7        src: 'build/app.js',
 8        dest: 'build/dist/app.js'
 9     }]
10   },
11   concat: {
12     'build/app.js': ['build/vendors.js', 'build/app.js']
13   }
14 // ... other task configurations ...
15 });
16 grunt.registerTask('build', ['clean', 'bower', 'browserify', 'concat\
17 ', 'copy']); 

Am Ende dieses Skripts organisiert der ‚build‘-Task die zuvor konfigurierten Elemente. Jeder Konfigurationsschritt ist unabhängig von anderen. Die Aufgaben werden sequenziell ausgeführt. Da in den meisten Fällen mit Dateien operiert wird, ist bei jeder Aufgabe eine Quelle und ein Ziel anzugeben. Grunt parst diese Angaben und führt die Kopiervorgänge dann aus.

Gulp ist dagegen reines JavaScript:

 1 var gulp = require('gulp');
 2 var sass = require('gulp-sass');
 3 var minifyCss = require('gulp-minify-css');
 4 var rename = require('gulp-rename');
 5 
 6 //declare the task
 7 gulp.task('sass', function(done) {
 8   gulp.src('./scss/ionic.app.scss')
 9     .pipe(sass())
10     .pipe(gulp.dest('./www/css/'))
11     .pipe(minifyCss({
12       keepSpecialComments: 0
13     }))
14     .pipe(rename({ extname: '.min.css' }))
15     .pipe(gulp.dest('./www/css/'))
16     .on('end', done);
17 });

Hier werden die Dateioperationen zusammengefasst und dann über das Node-Modul Streams ausgeführt. Die Quellangabe wird an alle folgenden Module weitergereicht und dies ermöglicht sehr kompakte Angaben, auch wenn die Aufgaben komplexer werden. Im Hintergrund nutzt Gulp die Bibliothek Vinyl, eine Art virtuelles Dateisystem.

Arbeit mit dem Dateisystem

Beide Werkzeuge nutzen Node und dessen Module zum Arbeiten mit dem Dateisystem. Das ist bei Grunt weitgehend ‚fs‘, bei Gulp eher ’stream‘. Grunt schreibt viele Dateien während des Prozesses und ist strikt dateiorientiert. Streams sind dagegen meist im Speicher und deshalb erfolgen viele Prozesse bei Gulp „in memory“. Nun sind viele Projekte eher klein und der Vorteil bei der Geschwindigkeit kaum messbar. Wer ein Projekt nach Tagen der Arbeit für die Auslieferung fertig macht wird den Unterschied zwischen 400ms und 800ms kaum bemerken (In der Praxis kann das aber auch mal 25ms zu 1200ms sein).

Community

Grunt ist länger am Markt als Gulp und hat eine deutlich größere Community. Allerdings ist die Zunahme bei Gulp erheblich und ein Ausgleich absehbar. Je nach Stärke der nächsten Versionen könnte Grunt den Vorsprung halten oder Gulp könnte auch überholen. Grunt ist derzeit Version 0.4.5 (Stable 02/2016) bzw. 1.0.0 (Dev 02/2016). Die Versionierung ist eher die typische, sehr zurückhaltend und vorsichtige Strategie wie bei vielen Unix-Projekten. Gulp ist dagegen sehr aggressiv und derzeit bereits bei 3.9.1 (Stand 02/2016).

Dokumentation

Die Website und API-Dokumentation zeigt mehr Reife, Umfang und Qualität bei Grunt. Gulp ist eher rudimentär und Quellcode lesen ist an der Tagesordnung. Während es endlose Artikel und Blogs zu beiden gibt, ist eine zentrale Website gerade für Einsteiger ein wichtiger Punkt. Bei Gulp ist davon nicht viel zu sehen.

In Grunt ist schneller konfiguriert – wenige fertige Aufgaben und alles steht bereit. Gulp setzt statt dessen auf verkettete JavaScript-Callbacks. Die Verkettung macht den Code aber auch kompakt und direkt. Grunt-Plug-Ins erscheinen mehr auf ihre Aufgabe zugeschnitten und konkreter an das Buildsystem Grunt angepasst. Gulp ist dagegen clever programmiert und spürbar schneller sowie sehr viel flexibler bei komplexeren Aufgaben. Plug-Ins für Gulp sind eher Node-Stream-Module als explizite Gulp-Plug-Ins. Sie erscheinen weniger stark auf ihren Zweck zugeschnitten als bei Grunt.

Abbildung: [Cheat Sheet für Gulp](© https://github.com/osscafe/gulp-cheatsheet)
Abbildung: [Cheat Sheet für Gulp](© https://github.com/osscafe/gulp-cheatsheet)

Optimierung mit Gulp

Gulp bietet eine ganze Reihe von node-Modulen, die für die Optimierung benutzt werden können. Ein miminales Set besteht aus drei Bereichen:

  1. Minimieren
  2. Zusammenfassen
  3. Löschen und Kopieren

Betrachten Sie zuerst folgendes package.js:

 1 {
 2   "name": "ASP.NET",
 3   "version": "0.0.0",
 4   "devDependencies": {
 5     "gulp": "3.8.11",
 6     "gulp-concat": "2.5.2",
 7     "gulp-cssmin": "0.1.7",
 8     "gulp-uglify": "1.2.0",
 9     "rimraf": "2.2.8"
10   }
11 }
Abbildung: Anzeige der Abhängigkeit, Speicherort und Konfiguration
Abbildung: Anzeige der Abhängigkeit, Speicherort und Konfiguration

Wenn Sie dies so in Visual Studio eingeben, bzw. die vorhandene Datei anpassen, lädt Visual Studio die Module herunter und stellt sie lokal im versteckten Ordner node_modules bereit. Die benutzten Module sind:

  • gulp: Der Task Runner Gulp selbst
  • gulp-concat: Ein Modul zum Verbinden von Dateien
  • gulp-cssmin: Ein Modul zum Minimieren von CSS-Dateien
  • gulp-uglify: Ein Modul zum Minimieren von JS-Dateien
  • rimraf: Ein Modul zum Löschen und kopieren (rimraf ist ein Wortspiel auf “rm” und “rf”, den unter Linux benutzten Kommandzeilenwerkzeugen zum Erzeugen, Umbenennen und zum Löschen von Ordnern).

In gulpfile.js werden die Pakete nun aktiviert. Praktisch sind die Konstruktoraufruf auf die JavaScript-Klassen:

1 var gulp = require("gulp"),
2     minifycss = require("gulp-cssmin"),
3     concat = require("gulp-concat");

Im Skript kann nun auf die Funktionen zugegriffen werden. Gulp nutzt die Stream-Funktionen von node und kann deshalb Dateien sehr schnell asynchron und damit parallel lesen und schreiben. Zum Minimieren des CSS wird eine Kombination aus rimraf zum Entfernen der alten Version und gulp-cssmin zum Erzeugen der neuen Version benutzt:

 1 gulp.task("clean:css", function (cb) {
 2     rimraf(paths.concatCssDest, cb);
 3 });
 4 
 5 gulp.task("min:css", function () {
 6     return gulp.src([paths.css, "!" + paths.minCss])
 7         .pipe(concat(paths.concatCssDest))
 8         .pipe(cssmin())
 9         .pipe(gulp.dest("."));
10 });

Der Bequemlichkeit halber lassen sich die Aufgaben zusammenfassen:

1 gulp.task("min", ["min:js", "min:css"]);

Nun kann im Task Runner der Aufruf erfolgen. Alternativ kann der Auslöser auch an eines der Build-Ereignisse (Clean, Before, After) oder beim Öffnen des Projekts gehängt werden.

Abbildung: Start der Gulp-Aufgaben im Task Runner
Abbildung: Start der Gulp-Aufgaben im Task Runner

4. MVC 6 – Model View Controller

Dieses Kapitel beschreibt die grundlegenden Bausteine einer ASP.NET Core-Applikation, die zur Seitenerstellung auf MVC (Model View Controller) aufsetzt.

4.1 MVC

Das Model View Controller-Konzept (MVC) ist eine etablierte Strategie, Programme mit grafischen Benutzeroberflächen zu entwerfen. MVC ist ein Entwurfsmuster, welches selbst andere Entwurfsmuster verwendet. Es wurde 1978 im XEROX PARC für Smalltalk entwickelt. Die Einführung von MVC war ein großer Schritt in der GUI -Entwicklung. Nach und nach hielt dieses Entwurfsmuster in vielen Desktopanwendungen Einzug. Inzwischen ist es Standard bei der Entwicklung einer Anwendung mit Benutzeroberfläche. Mittlerweile wird es auch im Zusammenhang mit Webanwendungen verwendet.

Der Kerngedanke besteht darin, Daten, Präsentation und Interaktion voneinander zu trennen. Auf diese Weise wird eine Anwendung übersichtlicher und leichter wartbar. Der folgende Abschnitt gibt einen Überblick über die Funktionsteile.

Gibt es Alternativen zum MVC und C#? Nein. Mit MVC wird derzeit ein sehr rigider Bruch mit der Vergangenheit gefahren:

  • WebForms werden nicht mehr unterstützt (kein .aspx mehr)!
  • Visual Basic wird nicht mehr unterstützt (kein .vbhtml mehr)!

Das MVC-Entwurfsmuster

Das Entwurfsmuster Model View Controller besteht im Wesentlichen aus drei Teilen.

Modell (model)

Das Modell stellt einen Zugriff auf die Daten zur Verfügung. Bei den meisten Implementierungen ist das Modell eine Klasse, welche die zu verarbeitenden Daten definiert, beispielsweise eine Person mit Name und Anschrift. In einer Anwendung, die auf einer SQL-Datenbank basiert, würde der Zugriff auf die Daten, sowie die Überprüfung der Konsistenz der Daten, innerhalb des Modells erfolgen.

Es bietet sich an, das Entwurfsmuster “Beobachter” (Observer) für das Modell zu implementieren. Auf diese Weise können andere Schichten, wie beispielsweise die Präsentation, über Änderungen informiert werden.

Präsentation (view)

Die Präsentationsschicht ist nur für die Darstellung von Informationen und gegebenenfalls die Bereitstellung von Bedienelementen zur Benutzerinteraktion zuständig, beispielsweise von Schaltflächen, Eingabefeldern oder Links.

Das Modell sollte die Präsentation über Änderungen informieren, sodass gegebenenfalls neue Daten dargestellt werden können. Ein Modell kann mehr als eine Präsentation haben.

Steuerung (controller)

Alle Aktionen werden in der Steuerungsschicht durchgeführt. Eine Steuerung kann mehrere Präsentationsschichten bedienen. Dabei werden Aktionen der Bedienelemente aus der Präsentationsschicht entgegengenommen, ausgewertet und die Daten im Modell entsprechend verarbeitet oder aktualisiert.

Abbildung: Das Entwurfsmuster "Model View Controller"
Abbildung: Das Entwurfsmuster “Model View Controller”

URL-Routing

Um die Trennung in Modell, Präsentation und Steuerung zu ermöglichen, muss es einen Weg geben, die angeforderte Webadresse auf ein Objekt bzw. die passende Methode (Action) zu verbinden. Dies wird Routing genannt. Das MVC-Framework verfügt über eine extrem leistungsfähige Routing-Komponente, welche bereits einen großen Teil der Vorverarbeitung einer Anforderung übernehmen kann.

4.2 Routing

Das ASP.NET-Routing wird immer dann aktiv, wenn keine konkrete Datei auf der Festplatte adressiert wird. Da die URL keine konkrete Datei betrifft, sind Sie bei der Wahl der Form des URL (fast) völlig frei. Diese Freiheit ermöglicht systematische, beschreibende und damit wartungsfreundliche Formen.

Der übliche Aufbau eines URL ist die folgende Form:

  • http://servername/area/controller/action/parameter

Dabei kann jeder Bestandteil entfallen und die Parameter können beliebig ergänzt werden. Die Abfragezeichenfolgen – der QueryString – kann an diesen Pfad noch angehängt werden. Er kann weitere Parameter enthalten, die mit den im Pfad bereits definierten Paramatern gleichwertig verarbeitet werden. Die Definition des Routing muss all Parametervarianten vordefinieren. Dies ist mit dem QueryString nicht erforderlich. Oft wird deshalb eine Mischung aus beiden benutzt.

Routen

Eine Route ist ein Muster für eine bestimmte Struktur eines URL. Der Route liegt ein Mapping zugrunde, also ein Verfahren zum Verbinden von Pfadangaben zu einem Ziel. Das Ziel ist in ASP.NET ein Handler – eine aktive Komponente, die für die Verarbeitung zuständig ist. Routen können darüber hinaus Namen haben, die der Organisation dienen, nach außen aber nicht in Erscheinung treten. Routen werden in einer Collection gespeichert und in der Reihenfolge verarbeitet, wie sie dort definiert wurden. Es ist deshalb wichtig, das generische Routen mit sehr allgemeiner Struktur am Ende definiert werden. Spezielle Routen würden sonst niemals erreicht werden.

Die Konstruktion der Route basiert auf einem speziellen Trennzeichen, dem Schrägstrich “/”. Dies ist der Standard. Grundsätzlich ist jedes literale Zeichen ohne besondere Bedeutung geeignet, insbesondere “-“, “~”, “_” und “.”. Sonderzeichen in einer URL sind dagegen:

  • ?: Abtrennung des QueryString
  • 5. : Hash für interne Seitensprünge

  • =: Trennzeichen für QueryString-Werte
  • &: Trennzeichen für QueryString-Parameter

Einige Zeichen sind aufgrund der Bedeutung bei Pfadangaben zu vermeiden: ! # $ % ‘ ( ) * + , : ; @ [ ].

Innerhalb der Definition werden Platzhalter für dynamische Teile benutzt, die in geschweiften Klammern stehen. Ein typisches Muster könnte etwa folgendermaßen aussehen (die Sprach- und Landesanteile sind erkennbar):

  • {language}-{country}/{action} : de-DE/show

Die folgende Angabe ist dagegen ungültig (die Sprach- und Landesanteile sind nicht trennbar):

  • {language}{country}/{action} : deDE/show

Hier fehlt das literale Trennzeichen zwischen den ersten beiden Variablen.

5.1 Views

Views dienen der Darstellung. Sie erzeugen das HTML, dass der Browser zur Darstellung benötigt. MVC-Views nutzen als Sprache für interaktive Elemente Razor. Mittels Razor lassen sich C#-Sprachelemene in HTML einbetten. Serverseitig werden diese Elemente dann ausgewertet und in Text übersetzt, der in die HTML-Seite integriert wird. In ASP.NET Core wurde die Notwendigkeit Razor zu benutzen stark zurückgedrängt. So elegant dieses Konzept auf den ersten Blick erscheint, so schwierig erscheint die Darstellung bei komplexen Seiten. Stattdessen werden viele Hilfsfunktionen nun als HTML-Attribute abgebildet.

Razor

Razor ist die Auszeichnungssprache für Views. Sie intergriert C# serverseitig in die HTML-Seite. Die Views heißen folgerichtig .cshtml. Razor-Views sind technisch Klassen, die auf der Basisklasse RazorPage aufsetzen. Das optional für die View vereinbarte View-Model ist ein generischer Parameger der Seite.

Abbildung: Basisklasse der Views
Abbildung: Basisklasse der Views

Die Möglichkeit, Code in die Seite zu schreiben, sollte freilich eine Ausnahme sein bzw. nur dazu dienen, die passenden Übersetzungsanweisungen auszulösen. Um Daten in die View zu bekommen, gibt es spezielle Techniken. Schreiben Sie Code immer in den Controller, nie in die View.

Was ist neu?

Bisher sah eine typische View folgendermaßen aus:

 1 @Html.ValidationSummary(true, "", new { @class = "text-danger" })
 2 <div class="form-group">
 3     @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control\
 4 -label" })
 5     <div class="col-md-10">
 6         @Html.TextBoxFor(m => m.UserName, new { @class = "form-contr\
 7 ol" })
 8         @Html.ValidationMessageFor(m => m.UserName, "", new { @class\
 9  = "text-danger" })
10     </div>
11 </div>

Die Wechsel mit @-Zeichen – typisch für Razor – zerreißen das Bild der Seite. Der Code wirkt funktional, aber nicht ästhetisch. In MVC 6 sieht dies nun folgendermaßen aus:

1 <div asp-validation-summary="ModelOnly" class="text-danger"></div>
2 <div class="form-group">
3     <label asp-for="UserName" class="col-md-2 control-label"></label>
4     <div class="col-md-10">
5         <input asp-for="UserName" class="form-control" />
6         <span asp-validation-for="UserName" class="text-danger"></sp\
7 an>
8     </div>
9 </div>

Der Schlüssel zu dieser Schreibweise sind sogenannte Tag Helper. Entwickler, die clientseitig entwickeln können die wenigen Befehle lernen, ohne zugleich auch noch eine neue Syntax studieren zu müssen.

Tag Helper

Neu in MVC 6 sind Tag Helper. Diese ersetzen die bisherigen Helper und dienen wie diese dazu, den serverseitigen Code zu modularisieren. Die Erstellung ist stärker auf HTML ausgelegt und Markup steht im Vordergrund. Geschrieben werden sie jedoch in reinem C#.

Ein typisches Beispiel für einen Link in MVC 5:

1 @Html.ActionLink("About me", "About", "Home")

Hier wurde eine Hilfsfunktion (Html-Helper) benutzt, um einen Link mittels Parametern zu erzeugen.

In MVC sieht dies folgendermaßen aus:

1 <a asp-controller="Home" asp-action="About">About me</a>

In beiden Fällen entsteht daraus dasselbe HTML:

1 <a href="http://localhost:12345/Home/About">About me</a>
Eingebaute Helper

Tag Helper reagieren in erster Linie auf Tags. Dass sie scheinbar auf Attribute reagieren, liegt daran, dass es viele integrierte Tag Helper gibt, die die Standard-HTML-Elemente behandeln. Zuerst ein einfaches Formular, basierend auf einem View-Model:

 1 @model NextWebApp.Models.About
 2 @{
 3     ViewData["Title"] = "About";
 4 }
 5 <h2>@ViewData["Title"].</h2>
 6 <h3>@ViewData["Message"]</h3>
 7 
 8 <p>Use this area to provide additional information.</p>
 9 
10 <form>
11   <label asp-for="Email"></label>
12   <input asp-for="Email"/>
13 </form>

Das Model enthält eine passende Eigenschaft, Email:

1 namespace NextWebApp.Models {
2   public class About {
3 
4     public string Name { get; set; }
5 
6     public string Email { get; set; }
7 
8   }
9 }

Benutzt werden hier implizit der LabelTagHelper und der InputTagHelper. Das führt zu folgendem HTML im Browser:

1 <form>
2   <label for="Email">Email</label>
3   <input type="text" id="Email" name="Email" 
4          value="joerg@krause.net" />
5 </form>

Dabei sind zwei wichtige Eigenschaften zu sehen:

  1. Metadaten werden gelesen und nach HTML transportiert
  2. Es wird kein statisches HTMl erzeugt, dass die Ausgabe beschränkt

Punkt 1 war bisher auch schon verfügbar. Punkt 2 ist entscheidend, denn bisher wurden einige Attribute fest kodiert. Dies war hinderlich, wenn andere clientseitige Bausteine benutzt werden sollten, also vom Helper vorgesehen.

Direktiven für Tag Helper

Durch Direktiven werden Tag Helper sichtbar oder unsichtbar gemacht. Verfügbar sind folgende Direktiven:

  • @addTagHelper: Macht einen Tag Helper verfügbar
  • @removeTagHelper: Entfernt den Zugriff auf einen Tag Helper
  • ! wird zum Umkehren der Auswahl benutzt
Listing: Views/_ViewImports.cshtml des Beispielprojekts
1 @using NextWebApp
2 @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"

Die Direktive erwartet einen voll qualifizierten Namen. Es kann der Platzhalter “*” benutzt werden. Der erste Teile ist der Namensraum bzw. Klassenname, der zweite Teil die Assembly. Das Beispiel zeigt den Import der eingebauten Tag Helper. Das folgende Listing zeigt die Anpassung derselben Datei mit einer weiteren Direktive, um alle selbst entwickelten Tag Helper zu importieren. Der Namensraum des Beispielprojeckts ist NextWebApp:

Listing: Views/_ViewImports.cshtml des Beispielprojekts für eigene Tag Helper
1 @using NextWebApp
2 @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
3 @addTagHelper "*, NextWebApp"

Der Import kann auf ein Tag Helper oder eine Gruppe beschränkt werden:

1 @using NextWebApp
2 @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
3 @addTagHelper "NextWebApp.TagHelpers.EmailTagHelper, AuthoringTagHel\
4 pers"

Der Platzhalter kann beliebig angeordnert werden:

1 @addTagHelper "NextWebApp.TagHelpers.E*, NextWebApp"
2 @addTagHelper "NextWebApp.TagHelpers.Email*, NextWebApp"

@removeTagHelper entfernt Tag Helper. Dies ist sinnvoll, wenn Sie einen Tag Helper auf einer Seite nicht benutzen möchten, dieser aber in der zentralen Datei _ViewImports.cshtml bereits per Platzhalter inkludiert wurde. Die Parameter entsprechen genau der Direktive @addTagHelper.

Es kann passieren, dass Tag Helper stören. In solchen Fällen kann die Benutzung in einem Tag mit dem “!”-Zeichen unterdrückt werden.

1 <!span asp-validation-for="Email" class="text-danger"></!span>

Beachten Sie hier, dass das “!”-Zeichen auch im schließenden Tag benötigt wird. Der Umgang mit Tags im Editor kann schwierig werden, wenn HTML und Tag Helper stark gemischt werden. Das Verhalten ist zwar besser als im bisher benutzten @Razor-Stil, aber eine explizite Hervorhebung erscheint manchmal sinnvoll. In solchen Fällen kann ein Namensraum-Alias vereinbart werden:

1 @tagHelperPrefix "th:"

Alle Tags, die die Helper nutzen werden dann mit <th:tag></th:tag> bezeichnet. Der Doppelpunkt muss mit angegeben werden. Alle Tags, die auf Helpern basieren, tragen den Alias. Es ist nur ein Alias pro View erlaubt.

1 <th:LargeButton Text="Anzeigen"></th:LargeButton>

Eingebaute Tag Helper

MVC 6 enthält eine ganze Reihe von Tag Helpern, die standardmäßig vorhanden sind. Sie ersetzen die bisher von der Erweiterungsklasse HtmlHelper über die Eigenschaft Html. Sie können aber weiter benutzt werden. Die Tag Helper bieten lediglich eine weitere Option. Folgende Tag Helper stehen zur Verfügung:

  • Anchor (<a/>): erzeugt Hyperlinks
  • Cache (<cache>): Verwaltet partielle Cache-Funktionen
  • Environment (<environment>): Zugriff auf Umgebungsbedingungen und bedingte Ausführung
  • Form (<form/>): Form-Elemente erzeugen
  • Input (<input>): Input-Elemente erzeugen
  • Label (<label/>): Label erstellen
  • Link (<link/>): Link-Elemente erzeugen
  • Select und Option (<select/><option/>): Klappmenüs und deren Optionen
  • Script (<script/>): Für Script-Tags
  • TextArea (<textarea/>): Das Textarea-Element
  • ValidationMessage (asp-validation-for): Ausgabe von Validierungsmeldungen
  • ValidationSummary (asp-validation-summary): Zusammenfassung von Validierungsmeldungen

Diese Tag Helper werden folgendermaßen bekannt gemacht:

1 @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"

Die Tag Helper reagieren auf bestimmte Tags wie in der Liste gezeigt. Manchmal kann der Tag Helper störend sein. Dann kann er temporär mit “!” im Tag unterdrückt werden (auch im schließenden Tag):

1 <!a href="http://www.google.de">Google</!a>
AnchorTagHelper

Der AnchorTagHelper ist eine Alternative zu @Html.ActionLink oder @Url.Action. Unterstützt werden folgende Attribute:

  • asp-controller: Name des Controllers
  • asp-action: Name der Action
  • asp-route-<parameter>: Spezifikation eines Parameters im Platzhalter <parameter>, z.B. asp-route-id="@value"
  • asp-route: Name einer benannten Route
  • asp-protocol: “http” oder “https”
  • asp-host: Name des Servers
  • asp-fragment: Client-Teil des URL nach (und inklusive) des #-Zeichens

Folgende Konstruktion ist damit möglich:

1 <a asp-controller="Product" 
2    asp-action="Display" 
3    asp-route-id="@ViewBag.ProductId">
4    View Details
5 </a>

Bei einer benannten Route wird es noch einfacher. Nehmen Sie zuerst folgende Routendefinition an:

1 routes.MapRoute(
2     name: "login",
3     template: "login",
4     defaults: new { controller = "Account", action = "Login" });

Dann reicht zum Erzeugen des Links folgendes Tag:

1 <a asp-route="login">Login</a>

Der gesamte URL kann folgendermaßen bestimmt werden:

1 <a asp-controller="Account" 
2    asp-action="Register" 
3    asp-protocol="http" 
4    asp-host="meinserver.de" 
5    asp-fragment="logon">Anmelden</a>

Dies erzeugt folgende Ausgabe:

1 <a href="http://meinserver.de/Account/Register#logon">Anmelden</a>
CacheTagHelper

Der CacheTagHelper ermöglicht Cache-Funktionen in Teilen einer Ansicht.

1 <cache expires-after="@TimeSpan.FromMinutes(10)">
2     @Html.Partial("_WhatsNew")
3     *last updated  @DateTime.Now.ToLongTimeString()
4 </cache>

Die möglichen Attribute decken das übliche Spektrum eines Caches ab und entsprechen dem in älteren Versionen bereits verfügbaren [OutputCache]-Attribut auf der Controller-Methode.

Zuerst wird der Verfallsalgorithmus für den Cache festgelegt:

  • expires-after: Nach einer Zeit
  • expires-on: Zu einer Zeit
  • expires-sliding: Nach einem inaktiven Zeitraum

Dann werden Bedingungen festgelegt, wann der Cache ungültig wird:

  • vary-by-user: Neuer Benutzer (Boolesch)
  • vary-by-query: Anderer QueryString (Boolesch)
  • vary-by-cookie: Geändertes Cookie (Name angeben)
  • vary-by-route: Andere Route (Name des Route-Parameters)
  • vary-by-header: Anderes Kopffeld (Name angeben)
  • vary-by: Beliebiger Wert, der direkt angegeben wird

Dazu kommt noch die Priorität:

  • priority: Ein Wert aus der Enumeration CachePreservationPriority
1 <cache vary-by-user="true" vary-by-route="id">
2     <!--View Component or something that gets data from the database\
3 -->
4     *last updated  @DateTime.Now.ToLongTimeString()
5 </cache>
Environment

Mit dem Tag Helper <environment> erhalten Sie Zugriff auf Umgebungsbedingungen und damit eine bedingte Ausführung von Teilen der View.

Form

Das <form>-Tag kennt einige Attribute, die das Senden von Formularen steuern. Es ersetzt den bisherigen Helper Html.BeginForm.

Zur Verfügung stehen folgende Attribute:

  • asp-controller: Name des Controllers
  • asp-action: Name der Action
  • asp-route: Name einer benannten Route
  • asp-route-returnurl: URL zum Zurücksenden
  • method: HTTP-Verb (“GET”, “POST”)
  • asp-anti-forgery: Boolescher Wert für das Anti-Forgery-Token zum Schutz des Formulars. Die Controller-Methode muss zusätzlich das Attribut [ValidateAntiForgeryToken] tragen, damit das Token ausgewertet wird.
1 <form asp-controller="Account" 
2       asp-action="Login" 
3       asp-route-returnurl="@ViewBag.ReturnUrl" 
4       method="post" >
5 </form>
Input

Das Tag <input> erzeugt fast alle Input-Elemente. Die Validierungsinformationen entsprechen der Syntax für jQuery-Validation. Dies war auch bei den bisherigen Versionen der Fall. Wenn Sie AngularJS im Client einsetzen, müssen Sie entweder eigene Tag Helper schreiben oder die Anpassung in AngularJS mittels Direktiven vornehmen.

Zur Verfügung stehen folgende Attribute:

  • asp-for: Bindung an eine Eigenschaft des Models. Damit wird auch das zugehörige for-Attribut in HTML erzeugt, dass auf das korrespondierende Input-Element verweist.
  • asp-format: Eine Formatierungsanweisung im bekannten ToString-Stil, beispielsweise asp-format=”{0:N4}”.

Das type-Attribute ist ein reguläres Attribut in HTML, dass hier zusätzliche Typprüfungen übernimmt. Es wird automatisch erzeugt:

Tabelle: .NET-Typen und das type-Attribut
.NET type=””
String type=”text”
DateTime type=”datetime”
Byte type=”number”
Int16,Int32 type=”number”
Single,Double type=”number”
Boolean type=”checkbox”

Natürlich kann das type-Attribut auch explizit angegeben werden, beispielsweise für type=”radio”.

Der Tag Helper erzeugt Validierungs-Attribute. Stellen Sie sich folgendes Model vor:

1 public class SimpleViewModel
2 {
3   [Required]
4   public string Email { get; set; }
5 }

Im Formular wird folgendes Tag benutzt:

1 <input asp-for="Email" />

Daraus entsteht folgendes HTML:

1 <input type="text" data-val="true" 
2        data-val-required="The Email field is required." 
3        id="Email" 
4        name="Email" 
5        value="" />

Neben der Reaktion auf .NET-Typen reagiert der Helper auch auf Attribute aus dem folgenden Namensraum:

  • System.ComponentModel.DataAnnotations.
Tabelle: .NET-Typen und das type-Attribut
.NET-Attribut type=””
[EmailAddress] type=”email”
[Url] type=”url”
[HiddenInput] type=”hidden”
[Phone] type=”tel”
[DataType(DataType.Password)] type=”password”
[DataType(DataType.Date)] type=”date”
[DataType(DataType.Time)] type=”time”

Beim Zugriff auf Models mit asp-for können komplexe Objekte mit der .-Syntax adressiert werden. Folgendes Model soll als Beispiel dienen:

 1 public class AddressViewModel
 2 {
 3     public string Street { get; set; }
 4 }
 5 
 6 public class RegisterViewModel
 7 {
 8     public string UserName { get; set;}
 9     public AddressViewModel Address { get; set; }
10 }

Die Benutzung erfolgt nun folgendermaßen:

1 <input asp-for="Address.Street" />
Abbildung: Intellisense in Visual Studio
Abbildung: Intellisense in Visual Studio
Label

Mit dem Tag <label> werden Label in Formularen erstellt.

Zur Verfügung stehen folgende Attribute:

  • asp-for: Bindung an eine Eigenschaft des Models
1 <label asp-for="Email"></label>

Je nach Modell – die Daten kommen hier aus den Attributen des Namensraums System.ComponentModel.DataAnnotations – entsteht daraus folgendes HTML:

1 <label for="Email">E-Mail Adresse</label>

Mittels <link> und <script></script> wird auf weitere Ressourcen verwiesen. Der Tag Helper versteht globale Platzhalter, auch auf Verzeichnisebene.

Zur Verfügung stehen folgende Attribute:

  • asp-src-include: Globaler Import
  • asp-src-exclude: Vom globalen Import ausschließen
  • asp-fallback-href: Rückfall, wenn der Zugriff auf den URL in href nicht gelingt
  • asp-fallback-test-class: “hidden”
  • asp-fallback-test-property: “visibility”
  • asp-fallback-test-value: “hidden”
  • asp-append-version: Schaltet den Cache aus, indem ein variabler Wert angehängt wird

Ein Beispiel für CSS zeigt die Benutzung:

1 <link rel="stylesheet" 
2       href="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/css/bootstrap.\
3 min.css"
4       asp-fallback-href="~/lib/bootstrap/css/bootstrap.min.css"
5       asp-fallback-test-class="hidden" 
6       asp-fallback-test-property="visibility" 
7       asp-fallback-test-value="hidden" />

Für die Steuerung der Rückfallebenen wird einiges an Code in der Seite erzeugt:

 1 <link rel="stylesheet" href="//ajax.aspnetcdn.com/ajax/bootstrap/3.0\
 2 .0/css/bootstrap.min.css" />
 3 <meta name="x-stylesheet-fallback-test" class="hidden" />
 4 <script>!function(a,b,c){var d,e=document,f=e.getElementsByTagName("\
 5 SCRIPT"),g=f[f.length-1].previousElementSibling,h=e.defaultView&amp;\
 6 &amp;e.defaultView.getComputedStyle?e.defaultView.getComputedStyle(g\
 7 ):g.currentStyle;if(h&amp;&amp;h[a]!==b)for(d=0;d<c.length;d++)e.wri\
 8 te('<link rel="stylesheet" href="'+c[d]+'"/>')}("visibility","hidden\
 9 ",["\/lib\/bootstrap\/css\/bootstrap.min.css"]);</script>
10 
11 <script src="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/bootstrap.min\
12 .js">
13 </script>
14 <script>(typeof($.fn.modal) === 'undefined'||document.write("<script\
15  src=\"\/lib\/bootstrap\/js\/bootstrap.min.js\"><\/script>"));</scri\
16 pt>

Ein Beispiel für JavaScript zeigt die Benutzung:

1 <script asp-src-include="~/app/**/*.js"></script>

Dieses Tag sucht im Ordner app nach allen Unterordnern und in jedem von diesen nach JavaScript-Dateien. Das führt zu folgender Ausgabe:

1 <script src="/app/controllers/controller1.js"></script>
2 <script src="/app/controllers/controller2.js"></script>
3 <script src="/app/controllers/controller3.js"></script>
4 <script src="/app/controllers/controller4.js"></script>
5 <script src="/app/services/service1.js"></script>
6 <script src="/app/services/service2.js"></script>
Select und Option

Mit den Elementen <select> und <option> werden Klappmenüs und deren Optionen gesteuert.

Zur Verfügung stehen folgende Attribute:

  • asp-for: Bindung an eine Eigenschaft des Models
  • asp-items: Bindung an ein Objekt, das die Optionen erzeugt (was zwangsläufig aufzählbar sein muss)
1 <select asp-for="CountryCode" 
2         asp-items="ViewBag.Countries">
3 </select>
TextArea

Das <textarea>-Tag erzeugt ein mehrzeiliges Text-Eingabefeld. Zur Verfügung stehen folgende Attribute:

  • asp-for: Bindung an eine Eigenschaft des Models

Angenommen Sie haben folgendes Model:

1 public class SimpleViewModel
2 {
3     [Required]
4     [MaxLength(500)]
5     public string Description { get; set; }
6 }

Gestalten Sie nun die View folgendermaßen:

1 <textarea asp-for="Description"></textarea>

Dann wird daraus folgendes HTML:

1 <textarea name="Description" id="Description" 
2           data-val-required="The Description field is required." 
3           data-val-maxlength-max="500" 
4           data-val-maxlength="The field Description must be a string\
5  or array type with a maximum length of '500'." 
6           data-val="true"></textarea>

Die Texte werden durch die Attribute in C# generiert und lassen sich dort vielfältig anpassen und lokalisieren.

TagHelper für die Validierung

Hier werden statt eines Tags zwei Attribute benutzt:

  • asp-validation-for: Ausgabe von Validierungsmeldungen für eine bestimmte Eigenschaft des Models
  • asp-validation-summary: Zusammenfassung von Validierungsmeldungen, angegeben wird ein Wert der Enumeration ValidationSummary. Zulässig Werte sind:
    • All: Alle
    • ModelOnly: Nur für das Model
    • None: Keine

Nutzen Sie folgende Form im HTML:

1 <span asp-validation-for="Email"></span>

Dies erzeugt folgende Ausgabe im HTML:

1 <span class="field-validation-error" 
2       data-valmsg-replace="true" 
3       data-valmsg-for="Email"> 
4    The Email field is required.</span>

Voraussetzung ist, dass die Eigenschaft Email des benutzen Viewmodels das Attribut [Required] trägt.

Sind andere Attribute im HTML im Einsatz, werden diese erhalten.

1 <span asp-validation-for="Email"
2       class="text-danger"></span>

In diesem Beispiel ist zusätzlich eine Klasse angegeben (aus Bootstrap). Das Ergebnis sieht nun folgendermaßen aus:

1 <span class="text-danger field-validation-error" 
2       data-valmsg-replace="true" 
3       data-valmsg-for="Email"> 
4    The Email field is required.</span>

Bei der Zusammenfassung wird ein Tag angegeben, an dessen Stelle die Sammlung aller Fehlermeldungen erscheint.

1 <div asp-validation-summary="ValidationSummary.All"></div>

Das HTML ohne Fehler sieht folgendermaßen aus:

1 <div class="validation-summary-valid" data-valmsg-summary="true">
2   <ul>
3     <li style="display: none;"></li>
4   </ul>
5 </div>

Mit Fehlern ist die Liste gefüllt:

1 <div class="validation-summary-errors" data-valmsg-summary="true">
2   <ul>
3     <li>The Email field is required.</li>
4     <li>The Password field is required.</li>
5   </ul>
6 </div>

Eigene Tag Helper

Eigene Tag Helper ersetzen die bisherigen Helper. Auf den ersten Blick erscheint die Vorgehensweise komplizierter. Allerdings haben sich Helper bisher eher wie speziell angeordnete Teilansichten (partial views) verhalten. Die Beschränkung auf App_Code und die Benennung der Helper nach dem Dateinamen waren ebenso eigenwillige Konzepte. Der neue Ansatz ist hier klarer und direkter.

Ein Tag Helper ist eine Klasse, die die Schnittstelle ITagHelper implementiert. Diese Schnittstelle bietet eine Methode Process, in der die HTML-Ausgabe erzeugt wird. Im einfachsten Fall kann dies direkt erfolgen. Die Klasse kann im Projekt irgendwo platziert sein, der Namensraum sollte gegebenenfalls global bereitgestellt werden. Am einfachsten lässt sich ein Tag Helper erstellen, indem die abstrakte Basisklasse TagHelper benutzt wird.

Um die wiederholte Erstellung von bestimmten Schaltflächen mit Bootstrap-Klassen zu vereinfachen, wäre folgender Tag Helper denkbar:

Listing: Tag Helper für eine Schaltfläche (LargeButtonTagHelper.cs)
 1 [HtmlTargetElement("LargeButton", TagStructure = TagStructure.Normal\
 2 OrSelfClosing)]
 3 public class LargeButtonTagHelper : TagHelper {
 4 
 5   public string Text { get; set; }
 6 
 7   public override void Process(TagHelperContext context, TagHelperOu\
 8 tput output) {
 9     output.TagName = "button";
10     output.Attributes["class"] = "btn btn-large btn-danger";
11     output.Content.SetContent(Text ?? "Senden");
12   }
13 }

Dieser Tag Helper kann nun in Views benutzt werden. Dazu wird die Direktive @addtaghelper benutzt:

1 @addTagHelper "NextWebApp.Helper.*, NextWebApp"
2 
3 <LargeButton Text="Anzeigen"></LargeButton>

Die Direktive verweist auf die Herkunft des Helpers mit einem vollqualifizierten Namen (FQN). Der erste Teil kann dabei zu * verkürzt werden, wenn alle Tag Helpers in einer Assembly importiert werden sollen. Im Beispiel ist NextWebApp.Helper der Namensraum, indem die Klasse LargeButtonTagHelper ist. Alternativ wäre es auch möglich, folgendes zu schreiben: @addTagHelper "*, NextWebApp". Alternativ kann auch eine @using-Direktive benutzt werden:

1 @using NextWebApp.Helper
2 @addTagHelper "*, NextWebApp"
3 
4 <LargeButton Text="Anzeigen"></LargeButton>

Das Attribut Text im Beispiel wird durch die gleichnamige Eigenschaft unterstützt. Groß- und Kleinschreibung wird hier nicht berücksichtigt. Allerdings wird Pascal-Case erkannt (Große Buchstaben relevanter Wortteile) und in HTML als Kebab-Case umgesetzt (mit Trennstrichen).

Komplexere Datentypen

Attribute sind keineswegs auf Zeichenketten beschränkt. Das folgende Beispiel zeigt die Benutzung von Enum-Typen:

Listing: Tag Helper für eine Schaltfläche (SmallButtonTagHelper.cs)
 1 using System.ComponentModel.DataAnnotations;
 2 using System.Reflection;
 3 using Microsoft.AspNet.Razor.TagHelpers;
 4 
 5 namespace NextWebApp.Helper {
 6 
 7   public enum BtnType {
 8     [Display(Name="btn-danger")]
 9     Danger,
10     [Display(Name = "btn-warning")]
11     Warning,
12     [Display(Name = "btn-primary")]
13     Primary,
14     [Display(Name = "btn-info")]
15     Info,
16     [Display(Name = "btn-default")]
17     Default
18   }
19 
20   [HtmlTargetElement("SmallButton", TagStructure = TagStructure.Norm\
21 alOrSelfClosing)]
22   public class SmallButtonTagHelper : TagHelper {
23 
24     public string Text { get; set; }
25 
26     public BtnType ButtonType { get; set; }
27 
28     public override void Process(TagHelperContext context, TagHelper\
29 Output output)
30     {
31       var btClass =
32         ((DisplayAttribute)
33           typeof (BtnType)
34           .GetField(ButtonType.ToString())
35           .GetCustomAttribute(typeof (DisplayAttribute))).Name;
36       output.TagName = "button";
37       output.Attributes["class"] = "btn btn-sm " + btClass;
38       output.Content.SetContent(Text ?? "Senden");
39     }
40   }
41 }

Im Beispiel werden Data Annotations benutzt, um vom Enum-Wert auf die ensprechenden Bootstrap-Klassen für die Schaltfläche umzusetzen. Das Auslesen der Namen erfolgt über Reflection ab Zeile 28 (btClass enthält dann je nach Wert btn-danger usw.). Die Klassen btn und btn-sm erstellen eine typische Schaltfläche im Bootstrap-Stil. Die Anwendung ist sehr einfach:

1 @using NextWebApp.Helper
2 @addTagHelper "NextWebApp.Helper.*, NextWebApp"
3 
4 <SmallButton text="Löschen" button-type="BtnType.Info"></SmallButton>

Beachten Sie hier die Schreibweise des Attributs button-type mit Bindestrich, was zwingend erforderlich ist. Wenn das stört, wird entweder beim Namen der Eigenschaft auf das große “T” verzichtet oder das Attribut HtmlAttributName benutzt.

1 [HtmlAttributeName("buttontype")]
2 public BtnType ButtonType { get; set; }

Mit dieser Änderung sieht der korrekte Aufruf nun folgendermaßen aus (buttontype ohne Bindestrich):

1 @using NextWebApp.Helper
2 @addTagHelper "NextWebApp.Helper.*, NextWebApp"
3 
4 <SmallButton text="Löschen" buttontype="BtnType.Info"></SmallButton>

In allen Fällen muss in der View der Zugriff auf die Namensräume komplexer Klassen möglich sein.

Abbildung: Fehler, wenn ein komplexer Typ nicht gefunden wird
Abbildung: Fehler, wenn ein komplexer Typ nicht gefunden wird
Asynchrone Verarbeitung und Vorlagen

Tag Helper unterstützen neben der synchronen Methode Process auch die asynchrone Variante ProcessAsync. Dies ist sinnvoll, wenn komplexes HTML nicht direkt in der Klasse, sondern als externes Template bereitgestellt wird. Diese Variante ist überdies den bisherigen Helpern sehr viel ähnlicher, weil in den Dateien wieder Razor benutzt werden darf. Zuerst ein Beispiel:

Listing: Tag Helper für eine Schaltfläche (SendButtonTagHelper.cs)
 1 namespace NextWebApp.Helper
 2 {
 3 
 4   [HtmlTargetElement("Send", TagStructure = TagStructure.NormalOrSel\
 5 fClosing)]
 6   public class SendButtonTagHelper : TagHelper
 7     {
 8 
 9     private readonly HtmlHelper _htmlHelper;
10     private readonly IHtmlEncoder _htmlEncoder;
11 
12     public SendButtonTagHelper(IHtmlHelper htmlHelper, IHtmlEncoder \
13 htmlEncoder) {
14       _htmlHelper = htmlHelper as HtmlHelper;
15       _htmlEncoder = htmlEncoder;
16     }
17 
18     [ViewContext]
19     public ViewContext ViewContext
20     {
21       set
22       {
23         _htmlHelper.Contextualize(value);
24       }
25     }
26     
27     public string Value { get; set; }
28 
29     public string Id { get; set; }
30 
31     public override async Task ProcessAsync(TagHelperContext context\
32 , TagHelperOutput output) {
33       output.TagName = null;
34       output.SuppressOutput();
35  
36       var partial = await _htmlHelper.PartialAsync(
37           $"Helpers/Button",
38           new Button {
39             Value = Value,
40             Id = Id
41           }, null);
42 
43       var writer = new StringWriter();
44       partial.WriteTo(writer, _htmlEncoder);
45 
46       output.Content.AppendHtml(writer.ToString());
47 
48       
49     }
50   }
51 
52 }

Ab Zeile 33 ist hier zu sehen, dass auf eine Teilansicht (partial view) zugegriffen wird. Der Ladevorgang soll dabei asynchron erfolgen. Damit hier await benutzt werden kann, muss die Methode mit async dekoriert sein. Gesucht wird die View im Pfad Views/Shared. Dort wird die entsprechende Ansicht platziert:

Listing: Tag Helper für eine Schaltfläche (SendButtonTagHelper.cs)
1 @model NextWebApp.Models.Button
2 <div>
3    <button id="@Model.Id">@Model.Value</button>
4 </div>

Die Übertragung mehrerer Daten gelingt am besten mit einem Objekt:

Listing: Model für den Tag Helper (Button.cs)
1 namespace NextWebApp.Models {
2   public class Button {
3     public string Value { get; set; }
4     public string Id { get; set; }
5   }
6 }

Die Benutzung ist dagegen sehr einfach:

1 @addTagHelper "NextWebApp.Helper.*, NextWebApp"
2 
3 <Send Value="Senden" Id="btn1"></Send>  

5.2 Dienste in Ansichten

Dienste in Ansichten weren in diese injiziert. Der Dienst selbst ist eine Klasse, deren Instanzen eine dedizierte Aufgabe übernehmen.

Listing: ServicesStatisticsService.cs
 1 using System.Linq;
 2 using System.Threading.Tasks;
 3 using TodoList.Models;
 4 
 5 namespace NextWebApp.Services
 6 {
 7   public class StatisticsService
 8   {
 9     private readonly ApplicationDbContext db;
10 
11     public StatisticsService(ApplicationDbContext context)
12     {
13       db = context;
14     }
15 
16     public async Task<int> GetCount()
17     {
18       return await Task.FromResult(db.TodoItems.Count());
19     }
20 
21     public async Task<int> GetCompletedCount()
22     {
23       return await Task.FromResult(
24         db.TodoItems.Count(x => x.IsDone == true));
25     }
26 
27     public async Task<double> GetAveragePriority()
28     {
29       if (db.TodoItems.Count() == 0)
30       {
31         return 0.0;
32       }
33 
34       return await Task.FromResult(
35         db.TodoItems.Average(x =>x.Priority));
36     }
37   }
38 }

In der View wird diese Dienstklasse nun bereitgestellt. Der Controller hat hier keine Funktion.

Listing: Dienst in einer Ansicht
 1 @inject TodoList.Services.StatisticsService Statistics
 2 
 3 
 4 <div>@Html.ActionLink("Create New Todo", "Create", "Todo") </div>
 5 </div>
 6   <div class="col-md-4">
 7     @await Component.InvokeAsync("PriorityList", 4, true)
 8     <h3>Stats</h3>
 9     <ul>
10       <li>Items: @await Statistics.GetCount()</li>
11       <li>Completed: @await Statistics.GetCompletedCount()</li>
12       <li>Average Priority: @await Statistics.GetAveragePriority()</\
13 li>
14     </ul>
15   </div>
16 </div>

Damit das funktioniert, muss die Klasse bekanntgegeben werden. Dies passiert in der Datei Startup.cs:

Listing: Startup.cs
1 public void ConfigureServices(IServiceCollection services)
2 {
3   services.AddMvc();
4   services.AddTransient<NextWebApp.Services.StatisticsService>();
5 }

5.3 Ansichtskomponenten (View Components)

Das Konzept der Ansichtskomponenten ist neu in ASP.NET Core. Es soll die teilweise unübersichtlichen und komplexen Strukturen aus Teilansichten (partial views) vereinfachen.

Übersicht

Ansichtskomponenten sind ähnlich wie Teilansichten. Sie ermöglichen eine Trennung von Zuständigkeiten und sind testbar. Sie können sich eine Ansichtskomponente ähnlich wie einen einfachen, separaten Controller vorstellen, der außerhalb der Grundstruktur existiert und wiederverwendbar ist. Generell erstellen Ansichtskomponenten nur Teil einer Seite. Diese Teile können jedoch in sich komplexer als einfache Teilansichten sein. Typische Beispiele sind:

  • Dynamische Menüs
  • Tag Clouds
  • Anmeldemasken
  • Warenkörbe
  • Nachrichtenlisten

Am Beispiel einer Anmeldemaske ist leicht zu erkennen, dass es hier nicht nur um zwei Textfelder geht. Eine solche Maske hat viele Funktionen:

  • Wenn der Benutzer nicht angemeldet ist, wird die Anmeldemaske angezeigt
  • Wenn der Benutzer angemeldet ist, wird der Anmeldename und ein Link zum abmelden angezeigt
  • Wenn der Benutzer Administrator ist, wird ein Link zur Administration angezeigt

Viele weitere situations- und datenabhängige Funktionen sind leicht vorstellbar. Vor allem im Zusammenhang mit Claims – den Profileigenschaften angemeldeter Benutzer – lassen sich hier umfassende Funktionen abbilden. Die Anwendung ist überall möglich und nicht auf eine bestimmte Controller-Struktur beschränkt.

Ansichtskomponenten bestehen aus zwei Teilen: Einer Klasse, die direkt oder indirekt von ViewComponent abstammt und einer Ansicht im typischen Razor-Stil. Aus Gründen der Testbarkeit komplexer Klassen kann es sinnvoll sein, nicht direkt von ViewComponent abzuleiten. In solchen Fällen wird eine einfache C#-Klasse erstellt und mit dem Attribut [ViewComponent] dekoriert. Die dritte Möglichkeit ist die Benutzung einer Klasse mit dem Suffix ViewComponent. Diese Klasse muss public, darf nicht verschachtelt und nicht abstrakt sein.

Listing: PriorityListViewComponent.cs
 1 using System.Linq;
 2 using Microsoft.AspNet.Mvc;
 3 using NextWebApp.Models;
 4 
 5 namespace NextWebApp.ViewComponents
 6 {
 7   public class PriorityListViewComponent : ViewComponent
 8   {
 9     private readonly ApplicationDbContext db;
10 
11     public PriorityListViewComponent(ApplicationDbContext context)
12     {
13       db = context;
14     }
15 
16     public IViewComponentResult Invoke(int maxPriority)
17     {
18       var items = db.TodoItems.Where(x => x.IsDone == false &&
19           x.Priority <= maxPriority);
20 
21       return View(items);
22     }
23   }
24 }

Die Klasse kann sich irgendwo im Projekt befinden. Der Suffix wird überall erkannt und der Name vor dem Suffix extrahiert. Falls das Attribut benutzt wird, kann der Name explizit festgelegt werden:

1 [ViewComponent(Name = "PriorityList")]
2 public class NameEgal : ViewComponent

Ergänzend zur Klassen ist nun noch der Ansichtscode zu schreiben. Dieser ist nicht frei platzierbar. Da Ansichtskomponenten nicht spezifisch zu einem Controller sind, werden Sie am besten unter View/Shared/Components platziert. Im letzten Beispiel war der Name der Komponente “PriorityList”. Der Name der Ansichtskomponente ist also “PriorityList.cshtml”.

Die Bildung der Instanz erfolgt durch das Dependency Injection Modul und pflanzt die verlangten Dienste in den Konstruktor ein. Dadurch wird in den Beispielen der Datenbankkontext bereitgestellt.

Die Abarbeitung der Komponente erfolgt in der Methode Invoke. Diese gibt es auch als asynchrone Variante InvokeAsync.

Listing: View-Datei der Ansichtskomponente
1 @model IEnumerable<TodoList.Models.TodoItem>
2 
3 <h3>Priority Items</h3>
4 <ul>
5   @foreach (var todo in Model)
6   {
7     <li>@todo.Title</li>
8   }
9 </ul>

Der Aufruf der Komponente erfolgt in jeder regulären Ansicht mit der Hilfsmethode Component.

Listing: Benutzung der Ansichtskomponente
1 @* Markup removed for brevity *@
2 <div>@Html.ActionLink("Create New Todo", "Create", "Todo") </div>
3 <div>
4   <div class="col-md-4">
5     @Component.Invoke("PriorityList", 1)
6   </div>
7 </div>

Die Methode Invoke verarbeitet die Ansichtskomponente synchron. Es kann für die Leistung des Gesamtsystems sinnvoll sein, die Verarbeitung asynchron zu erlauben. Stehen genug Threads zur Verfügung, wird die Verarbeitung parallelisiert. Eine asynchrone Ansichtskomponente könnte etwa folgendermaßen aussehen:

Listing: Asynchrone Ansichtskomponente
 1 using System.Threading.Tasks;
 2 
 3 public class PriorityListViewComponent : ViewComponent
 4 {
 5   private readonly ApplicationDbContext db;
 6 
 7   public PriorityListViewComponent(ApplicationDbContext context)
 8   {
 9     db = context;
10   }
11 
12   // Synchronous Invoke removed.
13 
14   public async Task<IViewComponentResult> InvokeAsync(int maxPriorit\
15 y, bool isDone)
16   {
17     var items = await GetItemsAsync(maxPriority, isDone);
18     return View(items);
19   }
20 
21   private Task<IQueryable<TodoItem>> GetItemsAsync(int maxPriority, \
22 bool isDone)
23   {
24     return Task.FromResult(GetItems(maxPriority, isDone));
25   }
26   private IQueryable<TodoItem> GetItems(int maxPriority, bool isDone)
27   {
28     var items = db.TodoItems.Where(x => x.IsDone == isDone &&
29         x.Priority <= maxPriority);
30 
31     string msg = "Priority <= " + maxPriority.ToString() +
32            " && isDone == " + isDone.ToString();
33     ViewBag.PriorityMessage = msg;
34 
35     return items;
36   }
37 }
1 @model IEnumerable<TodoList.Models.TodoItem>
2 
3 <h4>@ViewBag.PriorityMessage</h4>
4 <ul>
5   @foreach (var todo in Model)
6   {
7     <li>@todo.Title</li>
8   }
9 </ul>

Der Aufruf benötigt die Nutzung de Schlüsselwortes @await:

Listing: Nutzung einer asynchronen Ansichtskomponente
1 <div class="col-md-4">
2     @await Component.InvokeAsync("PriorityList", 2, true)
3 </div>
Umgang mit dem Namen

Specifying a view name

A complex view component might need to specify a non-default view under some conditions. The following shows how to specify the “PVC” view from the InvokeAsync method: Update the InvokeAsync method in the PriorityListViewComponent class.

Listing: View-Datei der Ansichtskomponente
 1 public async Task<IViewComponentResult> InvokeAsync(int maxPriority,\
 2  bool isDone)
 3 {
 4   string MyView = "Default";
 5   // If asking for all completed tasks, render with the "PVC" view.
 6   if (maxPriority > 3 && isDone == true)
 7   {
 8     MyView = "PVC";
 9   }
10   var items = await GetItemsAsync(maxPriority, isDone);
11   return View(MyView, items);
12 }
Listing: View-Datei der Ansichtskomponente
 1 @model IEnumerable<TodoList.Models.TodoItem>
 2 
 3 <h2> PVC Named Priority Component View</h2>
 4 <h4>@ViewBag.PriorityMessage</h4>
 5 <ul>
 6   @foreach (var todo in Model)
 7   {
 8     <li>@todo.Title</li>
 9   }
10 </ul>
Listing: View-Datei der Ansichtskomponente
1 @await Component.InvokeAsync("PriorityList",  4, true)

5.4 Controller

Controller sind der Baustein der Applikation der für die Verarbeitung der Anfragen zuständig ist. Hier werden die Modelle beschafft und die Views aufgerufen. Der Aufruf der Methoden des Controllers – den Action-Methoden – erfolgt über das Routing.

Grundlagen

Wichtig ist es von vornherein, dem Controller die richtigen Aufgaben zu überlassen. Es kann sonst leicht passieren, dass die Struktur des MVC-Entwurfsmusters kaputt geht. Zur Wiederholung sei hier nochmals aufgeführt, wozu MVC dient. MVC trennt Aufgaben in drei Bereiche:

  • Eingabe-Logik
  • Geschäfts-Logik
  • UI-Logik

Controller sind für die Eingabe-Logik zuständig. Die UI-Logik gehört in die View. Die Geschäfts-Logik sollte in einer separaten Logikschicht und in den Datenmodellen platziert werden.

Ein neuer Controller wird im Projekt über Add > New Item > MVC Controller hinzugefügt. Er muss im Ordner Controllers sein und den Suffix`Controller haben.

Listing: Vorlage für einen Controller
1 namespace NextWebApp.Controllers {
2   public class UserController : Controller {
3     // GET: /<controller>/
4     public IActionResult Index() {
5       return View();
6     }
7   }
8 }

Dies unterscheidet sich nur geringfügig von den bisherigen MVC-Versionen. Der Rückgabewert ist eine Schnittstelle mit dem Namen IActionResult. Aufrufbar von außen sind alle Methoden, die folgende Eigenschaften haben:

  • öffentlich
  • nicht abstrakt
  • keine Erweiterungsmethode

Wenn Sie eine solche Methode unzugänglich machen möchten, dann eignet sich dafür das Attribut [NonAction]. Das Attribut hat keine Parameter. Der Aufruf erfolgt zudem über die Methode (auch HTTP-Verb genannt) GET, die standardmäßig benutzt wird. Alle anderen HTTP-Verben müssen explizit angegeben werden.

Das Routing enthält eine Standardroute, die den folgenden Aufbau hat:

  • {controller}/{action}

Dabei ist für den Wert {action} als Standard der Name Index festgelegt. Der Aufruf der Methode Index des Controllers UserControllerist deshalb alleine mit folgender URL möglich (der Port wird in den Projekteigenschaften festgelegt):

  • http://localhost:1234/User

Die Festlegung des Routing erfolgte bereits in der Datei Startup.cs:

1 app.UseMvc(routes =>
2 {
3   routes.MapRoute(
4     name: "default",
5     template: "{controller=Home}/{action=Index}/{id?}");
6 });

Die Segmentierung des URL erfolgt standardmäßig über “/”-Literale.

Die Methode kann sowohl Ansichten als auch einfache Daten zurückgegeben. Wenn Sie einen Client mit JavaScript haben, wäre eine solche Methode denkbar:

1 public IActionResult Index() {
2   return Json(new { Name = "Joerg", City = "Berlin" });
3 }

Parameter

Controller-Methoden sind ziemlich intelligent bei der Übernahme von Parametern. Der Standardparamter, der in der Route vereinbart wurde, ist dabei aber nur eine Option. Generell werden alle Parameter des QueryStrings (bei GET) oder die Formulardaten (via POST) direkt erkannt. Dies gilt auch für Daten, die als JSON angeliefert werden. Sie können einzeln als Parameter der Methode benannt werden.

Freilich ist es besser, die Parameter in der Route zu vereinbaren. So hat die Infrastruktur die Chance, fehlerhafte Daten bereits frühzeitig abzufangen. Darüberhinaus sollten Sie als Zeichenkette (string) angelieferte Daten immer besondres behandeln, um beim Rückliefern an die View das Einschleusen von Code zu verhindern. Dues erfolgt beispielsweise mit folgender Methode:

HtmlEncoder.Default.HtmlEncode

Werden Daten per GET geliefert, kann eine URL nun folgendermaßen aussehen:

  • http://localhost:1234/User/Show?Name=Joerg&City=Berlin

Actions und Rückgabemethoden

In den vorangegangenen Beispielen wurden bereits zwei Methoden eingesetzt: View zum Aufrufen von Ansichten und Json für einfache Nutzlasten. Oft wird jedoch mehr Kontrolle über HTTP erwartet. Vor allem komplexere Client-Applikationen profitieren davon, bestimmte HTTP-Codes zu senden.

Umgang mit View-Daten

Den Views können Daten übergeben werden. Dies erfolgt einmal durch ein sogenanntes Viewmodel, einem Datenmodell, das speziell für eine Ansicht aufbereitet wurde. Das ist der ideale Weg, weil dadurch über die Direktive @model ein typsisierter Zugriff auf die Daten in der Ansicht besteht. Die Direktive leitet den Typ als Generic an die Basisklasse der View weiter.

Nun ist die Bereitstellung eines ViewModels nicht immer möglich oder sinnvoll oder wird unnötig komplex, wenn beispielsweise einge Meldungstexte transportiert werden sollen. In solchen Fällen ist es wünschenswert, einen weiteren Weg zur Ansicht zu haben. Dies wird durch ein Verzeichnis (ViewDataDictionary) mit dem Namen ViewData erreicht.

Im Controller oder im Code-Block einer View kann dann der Zugriff folgendermaßen erfolgen:

1 ViewData["Title"] = "Index";

ViewData ist eine Eigenschaft der Basisklasse des Controllers. Sie wird der Ansicht über eine Eigenschaft mit demselben Namen ViewData übergeben. Die Gültigkeit der Daten ist auf den Verarbeitungszyklus der Anforderung beschränkt. Bei einer Weiterleitung wird der Wert wieder null. Der Datentyp der Werte ist object und im Ziel muss eine Konvertierung (cast) erfolgen.

Alternativ besteht die Option, ein dynamisches Objekt mit dem Namen ViewBag zu benutzen. Durch den Datentyp dynamic können beliebige Eigenschaften hinzugefügt werden. Das Objekt verhält sich ansonsten wie ViewData. Durch die dynamische Natur – die Festlegung der Typen erfolgt erst zu Laufzeit, fehlt hier jegliche Unterstützung im Editor.

1 ViewBag.Title = "Index";

Der dritte Weg ist die Eigenschaft TempData, die den Typ TempDataDictionary hat. Auch dies ist ein Dictionary. Diese Daten überleben eine Weiterleitung zu einer anderen Ansicht. Danach werden die Daten ungültig. Dies ist ideal, wenn Fehlermeldungen oder andere Nachrichten nach dem Absenden einer Seite auf einer anderen Seite nochmals angezeigt werden sollen, danach aber keine Bedeutung mehr haben.

Abbildung: Vergleich der View-Daten
Abbildung: Vergleich der View-Daten

Alternativ steht für eine längerfristige Aufbewahrung der Daten das Session-Objekt zur Verfügung. Es wird über die Eigenschaft Session bereitgestellt und ist vom Typ HttpSessionStateBase.

Routing to Controller Actions

Error Handling

Filters

Dependency Injection and Controllers

Testing Controller Logic

5.5 Working with the Application Model

5.6 Razor

Razor wurde an einigen Stellen bereits benutzt. Es ist nicht neu in ASP.NET Core, steht aber nach wie vor an zentraler Stelle. Prinzipiell dient Razor dazu, C# in HTML einzubetten. Folgerichtig heißen die Views .cshtml. Beim Verarbeiten der Seite wird der Code ausgeführt. Wenn dabei Ausgaben erzeugt werden, so gelangt das Ergebnis als Text in die HTML-Seite. Dies kann dann alles mögliche sein, HTML, CSS, aber auch JavaScript.

Der Weg von HTML nach C# wird mit dem @-Zeichen angezeigt. Der Rückweg wird automatisch erkannt, wenn ein Zeichen in C# nicht mehr sinnvoll ist. Gibt es Zweifel, sind verschiedene Klammern einsetzbar. Nachfolgend werden die wichtigsten Elemente aufgezeigt.

Code Block

Ein Block

1 @{ 
2   int zahl = 42; 
3   string n = "Joerg";
4 }

Ausdruck

Html-Encoded sieht dies folgendermaßen aus:

1 <span>@model.Message</span>

Soll keine Kodierung erfolgen, wird Raw benutzt:

1 <span>
2 @Html.Raw(model.Message)
3 </span>

Befehle

Text und Markup kann eng kombiniert werden:

1 @foreach(var item in items) {
2   <span>@item.Prop</span> 
3 }

Wir einfach nur Text benötigt, kann beim Verarbeiten kein HTML erkannt werden. Dafür gibt es das Pseudo-Tag <text>:

1 @if (foo) {
2   <text>Plain Text</text> 
3 }

Ist der Text auf genau eine Zeile begrenzt, geht es auch einfacher:

1 @if (foo) {
2   @:Plain Text is @bar
3 }

Blöcke werden mit usingumschlossen:

1 @ using (Html.BeginForm()) {
2   <input type="text" value="input here">
3 }

Ausgabe des @-Zeichens

E-Mail-Adressen werden erkannt und müssen nicht maskiert werden.

1 Hi philha@example.com

Wenn aber ein Konstrukt entsteht, das als E-Mail erkannt werden könnt, aber keine ist, müssen Klammern benutzt werden:

1 <span>ISBN@(isbnNumber)</span>

Wird das @-Zeichen benötigt, soll aber nicht umschalten, wird es doppelt geschrieben:

1 <span>In Razor, you use the 
2 @@foo to display the value 
3 of foo</span>

”@@” erzeugt ein einzelnes “@” in der Ausgabe.

Kommentarre

1 @*
2 This is a server side 
3 multiline comment 
4 *@

Generische Ausdrücke

Generische Ausdrücke sind etwas schwer handhabbar, weil diese in C# spitze Klammern nutzen, was ziemlich nach HTML aussieht. Da kommt Razor schnell durcheinander. Deshalb sind hier die Klammern immer erforderlich:

1 @(MyClass.MyMethod<AType>())

Mit einer Funktion kann man sich das manchmal etwas erleichtern:

1 @{
2   Func<dynamic, object> b = @<strong>@item</strong>;
3 }
4 
5 @b("Bold this")

Dies erzeugt ein Objekt vom Typ Func<T, HelperResult>

Attribute

Der Einsatz innerhalb von Tags funktioniert intuitiv.

1 <div class="@className"></div>

Spannend ist daran, das Razor das gesamte Attribut nicht rendert, wenn der Wert null ist:

1 <div></div>

Ist className dagegen eine leere Zeichenkette, sieht die Ausgabe folgendermaßen aus:

1 <div class=""></div>

In Kombination mit statischen Werten wird auch das führende Leerzeichen entfernt:

1 <div class="@className foo bar">
2 </div>

Das Attribut wird so gerendert, wenn der Wert null ist:

1 <div class="foo bar"></div>

Dageben werden *data-**-Attribute immer gerendert, egal wie der Wert der Variablen ist.

1 <div data-x="@xpos"></div>

Mit oder ohne null wird daraus:

1 <div data-x=""></div>

Ebenso intelligent sind Boolesche Attribute.

1 <input type="checkbox" checked="@isChecked" />

Ist die Variable @isChecked true, dann entsteht folgendes:

1 <input type="checkbox"
2   checked="checked" />

Ist die Variable @isChecked false, dann entsteht folgendes:

1 <input type="checkbox" />

Auflösung der URL

Der Pfad zum Stamm-URL des Projekts erfolgt mit “~”. Die Auflösung findet überall statt, auch in reinem HTML:

1 <script src="~/myscript.js">
2 </script>

5.7 Gestaltungsvorlagen (Layouts)

Layouts sind Gestaltungsvorlagen für Ansichten. Sie liefern das Grundgesicht der Anwendung. Die Ansichten liefern lediglich den Inhalt und Fragmente, die der Inhalt braucht, z.B. spezielle Skripte.

Die Standardgestaltungsvorlage

In ASP.NET Core heißt die Standardlayoutseite _Layout.cshtml und liegt im Ordner /Views/Shared. Sie wird in den Ansichten über die Eigenschaft Layout eingebunden.

Standardgestaltungsvorlage
 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4   <meta charset="utf-8" />
 5   <meta name="viewport" content="width=device-width, initial-scale=1\
 6 .0" />
 7   <title>@ViewData["Title"] - DemoWeb</title>
 8 
 9   <environment names="Development">
10     <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.\
11 css" />
12     <link rel="stylesheet" href="~/css/site.css" />
13   </environment>
14   <environment names="Staging,Production">
15     <link rel="stylesheet" 
16           href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.5/css/\
17 bootstrap.min.css"
18           asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.\
19 css"
20           asp-fallback-test-class="sr-only" 
21           asp-fallback-test-property="position" 
22           asp-fallback-test-value="absolute" />
23     <link rel="stylesheet" href="~/css/site.min.css" asp-append-vers\
24 ion="true" />
25   </environment>
26 </head>
27 <body>
28   <div class="navbar navbar-inverse navbar-fixed-top">
29     <div class="container">
30       <div class="navbar-header">
31         <button type="button" 
32                 class="navbar-toggle" 
33                 data-toggle="collapse" 
34                 data-target=".navbar-collapse">
35           <span class="sr-only">Toggle navigation</span>
36           <span class="icon-bar"></span>
37           <span class="icon-bar"></span>
38           <span class="icon-bar"></span>
39         </button>
40         <a asp-controller="Home" asp-action="Index" class="navbar-br\
41 and">DemoWeb</a>
42       </div>
43       <div class="navbar-collapse collapse">
44         <ul class="nav navbar-nav">
45           <li><a asp-controller="Home" asp-action="Index">Home</a></\
46 li>
47           <li><a asp-controller="Home" asp-action="About">About</a><\
48 /li>
49           <li><a asp-controller="Home" asp-action="Contact">Contact<\
50 /a></li>
51         </ul>
52         @await Html.PartialAsync("_LoginPartial")
53       </div>
54     </div>
55   </div>
56   <div class="container body-content">
57     @RenderBody()
58     <hr />
59     <footer>
60       <p>&copy; 2016 - DemoWeb</p>
61     </footer>
62   </div>
63 
64   <environment names="Development">
65     <script src="~/lib/jquery/dist/jquery.js"></script>
66     <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
67     <script src="~/js/site.js" asp-append-version="true"></script>
68   </environment>
69   <environment names="Staging,Production">
70     <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4\
71 .min.js"
72             asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
73             asp-fallback-test="window.jQuery">
74     </script>
75     <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.5/boo\
76 tstrap.min.js"
77             asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.\
78 js"
79             asp-fallback-test="window.jQuery && window.jQuery.fn && \
80 window.jQuery.fn.modal">
81     </script>
82     <script src="~/js/site.min.js" asp-append-version="true"></scrip\
83 t>
84   </environment>
85 
86   @RenderSection("scripts", required: false)
87 </body>
88 </html>

Weil es viele View-Dateien und nur eine oder wenige Gestaltungsvorlagen gibt, ist der sich ständig wiederholende Aufruf ziemlich lästig. Dies wird vermieden, indem eine Standarddatei jeder View vorangestellt wird: _ViewStart.

Views/_ViewStart.cshtml
1 @{
2     Layout = "_Layout";
3 }

Was hier steht, wird für jede View ausgeführt. Dies betrifft im Urzustand des Projekts lediglich die Zuweisung der Gestaltungsvorlage.

Verarbeitungsreihenfolge

Die Verarbeitungsreihenfolge der Gestaltungsvorlage ist möglicherweise etwas anders als erwartet. Der Inhalt der Seite wird quasi in die Vorlage eingebettet. Die Vorlage ist also der äußere Teil, der Inhalt der innere. Dennoch wird erst die Seite verarbeitet und dann die Gestaltungsvorlage aufgerufen. Die Abarbeitung erfolgt also von innen nach außen. Aus diesem Grund kann die Seite den Zustand und Aufbau der Gestaltungsvorlage bestimmen und ändern.

Inhaltsbereiche

Der Inhalt der View wird in sogenannten Sections eingeblendet. Über die Position entscheidet folgender Befehl: @RenderSection. Die Methode bekommt einen Namen für den Abschnitt und einen Hinweis, ob der Inhalt zwingend angeliefert werden muss oder optional ist.

1 @RenderSection("script", required: false)

Alle Teile der View, die nicht explizit einer Section zugeordnet wurden, werden an der Stelle @RenderBody ausgegeben.

Dynamische Zonen

Mit dem Tag Helper <environment> können Bereiche festgelegt werden, die auf bestimmte Umgebungsvariablen reagieren. Die Applikation kann so erkennen, ob sie auf einem Entwicklungs-, Test- oder Produktions-Server läuft. Dies sieht typischerweise folgendermaßen aus:

1 <environment names="Development">            
2     <link rel="stylesheet" href="~/css/site1.css" />
3     <link rel="stylesheet" href="~/css/site2.css" />
4 </environment>
5 <environment names="Staging,Production">
6     <link rel="stylesheet" href="~/css/site.min.css" asp-file-versio\
7 n="true"/>
8 </environment>

Der Name der Umgebungsvariablen lautet Hosting:Environment. Zum Testen können Profile hinterlegt werden, die die Werte dynamisch setzen. Dazu öffnen Sie die Eigenschaften des Projekts und dann den Tab Debug.

Abbildung: Einstellen der Umgebungsvariablen für ein Profil
Abbildung: Einstellen der Umgebungsvariablen für ein Profil

Beim Start des Debuggers wird dann das passende Profil zum Testen geladen. Dies kann im Startmenü des Debuggers gewählt werden:

Abbildung: Aufruf eines Profils beim Debuggen
Abbildung: Aufruf eines Profils beim Debuggen

5.8 Area

Umfangreiche ASP.NET-Applikationen können in Bereiche zerlegt werden – sogenannte Areas. Dies war bisher eine recht aufwändige Prozedur, die nur durch Funktionen in Visual Studio entschärft wurde. Die Unterstützung durch Visual Studio ist nun nicht mehr gegeben, dafür ist der Aufbau der Area vergleichsweise einfach.

Area anlegen

Das Anlegen einer Area erfolgt durch einen Ordner. Am besten alle Areas werden unter einem Ordner Areas gesammelt:

Abbildung: Areas
Abbildung: Areas

Die Controller in diesem Bereich erhalten nun ein spezielles Attribut: AreaAttribut.

1 [Area("Admin")]
2 public class HomeController : Controller
3 {
4     // GET: /<controller>/
5     public IActionResult Index()
6     {
7         return View();
8     }
9 }

Nun muss das Routing noch angepasst werden, damit der Controller erreichbar ist. Dies erfolgt in der globalen Datei Startup.cs:

 1 app.UseMvc(routes =>
 2 {
 3   routes.MapRoute(
 4       name: "AreaRoute",
 5       template: "{area:exists}/{controller=Home}/{action=Index}/{id?\
 6 }");
 7 
 8   routes.MapRoute(
 9         name: "default",
10         template: "{controller=Home}/{action=Index}/{id?}");
11 });

Dieser Code hat zwei Besonderheiten. Zum einen muss die spezialisiertere Route zuerst stehen, also vor der allgemeinen Route default. Zum anderen werden Areas nicht individuell definiert, sondern über den Platzhalter area. Hier wird außerdem als Bedingung exists benutzt. Die Route ist also gültig, wenn der URL den Bereich enthält. Die Area muss natürlich an der entsprechenden Stelle existieren.

Routen zu Areas

Um nun zu einer bestimmten Aktion in einer Area zu verweisen, kann der passende Tag Helper benutzt werden:

1 <a asp-route-area="Admin" 
2    asp-controller="Home" 
3    asp-action="Index">
4   Startseite Administration  
5 </a>

Das Attribut asp-route-area ist hier entscheidend. Wenn hier keine vollstädige Route erstellt wird, dann liegt das daran, dass die Standardpfade berücksichtigt werden und kein unnützer Code produziert wird. Das Beispiel oben erstellt folgendes Tag:

1 <a href="/Admin">
2   Startseite Administration  
3 </a>

Die Angaben Home und Index fehlen, weil dies die Rückfallwerte der Route sind.

6. Entity Framework Core 1.0

Leichtgewichtigkeit, Flexibilität, Erweiterbarkeit und ein vereinheitlichter Modellierungsprozess sind die wichtigen Motivationen bei der Entwicklung der neuen Version. Dabei wird vor allem auf die komplexen Designer und die Modellierung mit XML verzichtet. “Code Only” ist die Devise, wobei wie bisher vorhandene und neue Datenbanken gleichermaßen unterstützt werden.

6.1 Was ist neu?

Die Top-Level APIs ändern sich mit Entity Framework Core 1.0 nur wenig. Es gibt aber lang erwartete, signifikante Verbesserungen:

  • Batch CUD
    Erstmals wird das EF die Ausführung von multiplen Abfragen mit nur einem Datenbank-Roundtrip unterstützen. In den bisherigen Versionen des Frameworks wird pro INSERT-, UPDATE- und DELETE-Statement eine Abfrage zur Datenbank geschickt. Dies führt häufig zu enorm vielen Abfragen, die von einer Anwendung an die Datenbank gesendet werden, was natürlich zu Lasten der Performance geht. Jetzt werden in vielen Situationen Abfragen vom Framework zusammengefasst, um diese mit einem Roundtrip zur Datenbank zu schicken.
  • Unique Constraints
    Hiermit erfüllt das EF-Team einen weiteren Wunsch der Community, mit 3196 Stimmen in der User Voice genannten Plattform die am häufigsten geforderte Verbesserung. Mit diesem Feature ist es möglich, Fremdschlüsselbeziehungen auf eine definierte Menge von Spalten einer referenzierten Tabelle festzulegen, wobei die Menge der Spalten nicht der Primärschlüssel der referenzierten Tabelle sein muss. Damit können unabhängig vom Datenbankmanagementsystem referentielle Abhängigkeiten und Einschränkungen im Model definiert werden.
  • Generierung von SQL-Abfragen
    Bisher generiert Entity Framework aus einer Linq-Abfrage eine komplette SQL-Abfrage und sendet diese an die Datenbank. Dabei kommt es in vielen Situationen zu sehr komplexen Abfragen, die nur langsam von den Datenbankmanagementsystemen ausgeführt werden können. Um diese Problematik zu entschärfen, betrachtet EF Core Linq-Abfragen etwas differenzierter: Ein neu eingeführtes Model überlässt es dem Linq-Provider zu entscheiden, welche Teile der Abfrage auf welche Art und Weise in der Datenbank ausgeführt werden. Damit können die Provider noch weiter auf das spezifische DB-System hin optimiert werden. Individuelle Features einer Datenbank können so überhaupt erst genutzt und die Ausführung von Abfragen damit verbessert werden.
  • Leichtgewichtiger Unterbau
    Microsoft wird den OR-Mapper an vielen Stellen neu implementieren, das EF-Team möchte den kompletten Mapper auf einen flexiblen, leichtgewichtigen Unterbau setzen. Als Beispiel wird die Verwaltung der Metadaten eines Modells angeführt. Der MetadataWorkspace ist bisher eine sehr träge Komponente, was den vielen Spielarten der Modellierung geschuldet ist. Beispielsweise benötigt man zum Herausfinden einer Tabelle, die zu einer bestimmten Entität gemappt ist, bisher sieben Zeilen Code; vier davon sind selbst komplexere Linq-Abfragen. Mit dem neuen Unterbau des EF Core wird dazu nur noch eine Zeile Code benötigt. Dies ist natürlich weniger fehleranfällig, die Performance muss aber an dieser Stelle sicher noch einmal genauer betrachtet werden.

Der OR-Mapper wurde von Grund auf überarbeitet. Einige in alten Versionen des Entity Frameworks vorhandene Funktionen sind dem zum Opfer gefallen:

  • Multiple Entity Sets per Type (MEST)
    In Entity Framework Core 1.0 wird es nicht mehr möglich sein, verschiedene Tabellen auf dieselbe Entität zu mappen.
  • Komplexes Mapping zwischen Tabellen und Typen
    Das Kombinieren von TPH, TPT und TPC in einer Vererbungshierarchie wird zukünftig nicht mehr unterstützt. Diese Möglichkeit in alten Versionen des Frameworks ist ein weiterer Grund für die Komplexität des MetadataWorkspaces, das Eliminieren also eine Konsequenz aus der neuen Leichtgewichtigkeit.
  • EDMX
    Das EDMX-Modell-Format wird gänzlich abgeschafft. Das Speichern der Metadaten zu einem Modell in XML-Form wird es so nicht mehr geben. Code-basierte Modellierung wird der zentrale Ansatz.
  • ObjectContext
    Auch dem ObjectContext geht es an den Kragen. Der mit dem EF 4.1 eingeführte DbContext ist häufig die erste Wahl bei Entwicklern. ObjectContext-APIs werden in den DbContext verschoben, und der ObjectContext damit überflüssig gemacht – in Hinblick auf existierende Anwendungen wohl eine der gravierendsten Änderungen im neuen Entity Framework.

6.2 Primäre Rolle in ASP.NET MVC

Das Entity Framework als primärer Datenbankprovider kommt allein durch die Wahl des Authentifizierungsverfahrens in die ASP.NET-Applikation. Grundlage ist die Klasse IdentityDbContext, die die Benutzerverwaltung liefert, zugleich aber selbst von DbContext abstammt – der Basisklasse des Datenbankzugriffs. Wird also mit dem individuellen Authentifizierungsverfahren gearbeitet, geht dies nur mit Entity Framework. Es ist naheliegend, dies dann auch für alle anderen Datenbankaufgaben zu nutzen. Dabei spielt es keine Rolle, ob die Datenbank erst durch das Objektmodell erzeugt wird oder eine bereits vorhandene Datenbank benutzt wird. Auch ein Aufsplitten auf zwei Datenbanken ist denkbar, wenngleich dies die Abfragen an einigen Stellen etwas komplizierter macht, da die Objekte dann in zwei getrennten Kontexten existieren.

Entity Framework ist datenbankunabhängig. Die hier gezeigten Beispiele basieren jedoch ausnahmslos auf dem Microsoft SQL Server. Zum Test habe ich hier MS SQL 2014 benutzt, es sollte aber auch mit den vorherigen Versionen funktionieren. EF Core 1.0 ist sehr jung – Erfahrungen mit anderen Datenbanken und der Qualität der Provider, soweit überhaupt verfügbar, gibt es noch nicht.

Installation ohne die Authentifizierung

Um sicherzustellen, dass der SQL Server-Provider vorliegt, installieren Sie zuerst das passende Paket via Nuget:

Tools > NuGet Package Manager > Package Manager Console

Install-Package EntityFramework.MicrosoftSqlServer –Pre

Achten Sie hier auf die Angabe -Pre – andernfalls installieren Sie die aktuelle Version EF 6 (Stand dieser Aussage: Januar 2016). EF 6 läuft allerdings nicht auf dem Core-Framework, was das gesamte Projekt ad absurbum führt. Die Installation erfolgt im Hintergrund. Nun installieren Sie die Kommandos:

Install-Package EntityFramework.Commands –Pre

Öffnen Sie nun die Datei project.json im Wurzelverzeichnis des Projekts. Suchen Sie den Abschnitt “commands” und ändern Sie den Inhalt wie folgt:

1 "commands": {
2   "web": "Microsoft.AspNet.Server.Kestrel",
3   "ef": "EntityFramework.Commands"
4 },

6.3 Grundlagen des Entity Framework

In diesem Abschnitt soll es vor allem um architektonische und konzeptionelle Aspekte rund um das Entity Framework gehen.

Primär geht es darum, modellbasiert zu entwickeln, nicht Datenbankschema-basiert. Mit dem Einsatz von ADO.NET verwenden Sie viel Zeit auf die Abfrage von Daten aus einer Datenbank: das Durchlaufen der Ergebnisse und Herausfiltern der relevanten Felder sowie deren Inhalte. Mit dem Entity Framework ist das anders. Hier erfolgt die Abfrage nicht gegen ein Datenbankschema. Sie entwickeln Ihre Software gegen ein der Geschäftslogik entsprechendes konzeptionelles Objektmodell.

Die Daten aus der Datenbank werden automatisch in die Geschäftsobjekte übertragen. Änderungen werden gespeichert, indem die geänderten Objekte gespeichert werden, das Entity Framework erledigt den Rest automatisch. So werden Sie als Entwickler von der Aufgabe “befreit”, die Eigenschaften der Geschäftsobjekte in Reihen und Spalten der Datenbank zu konvertieren.

Entitäten

Der wichtigste Bestandteil des Entity Frameworks sind Entitäten. Es ist der Kern, um den sich die weitere Funktionalität anordnet. Das Entitäten-Modell beschreibt die Geschäftsobjekte, wie sie von der Geschäftsprogrammlogik verwendet werden sollen. Es gehört, im Gegensatz zu dem logischen Datenmodell, nicht zur verwendeten Datenbank.

Wichtige Begriffe im Zusammenhang mit dem Entity Framework sind:

  • Entity Set
    Eine Sammlung zusammengehörender Entities, denen ein Entity Key gemeinsam ist. Das bildet eine Tabelle im relationalen Modell ab.
  • Entity Container
    Gruppierung aus Entity Set, Beziehungen und Funktionen. Dies muss mindestens einmal im Modell vorhanden sein und bildet eine Datenbank im relationalen Modell ab. Es wird auch als Kontext (Context) bezeichnet.
  • Association Type
    Eine Beziehung von einem Entity Typy zu einem anderen. Die Abbildung erfolgt durch eine Eigenschaft (“Navigation Property” genannt). Sie hat eine Multiplizität: 1, 0..1, * und bildet Relationen im relationalen Modell ab.
  • Datentypen
    Dies ist die Abbildung des relationalen Modells auf Typen des Objektmodells. Es werden viele .NET-Datentypen wie Boolean, String, Int32, Guid, Byte[] und DateTime direkt abgebildet. Komplexere Typen werden durch Klassen abgebildet.
Abbildung: Schematische Darstellung eines Entity Containers
Abbildung: Schematische Darstellung eines Entity Containers

Bausteine

Die folgende Abbildung zeigt, wie sich das Entity Framework in die Applikation einordnet.

Abbildung: Einordnung des Entity Frameworks in die Applikation
Abbildung: Einordnung des Entity Frameworks in die Applikation

Taucht man tiefer ein, so werden die einzelnen Dienste sichtbar.

Abbildung: Innerer Aufbau des Entity Frameworks
Abbildung: Innerer Aufbau des Entity Frameworks

Wichtig ist der Umgang mit den bereits erwähnten Schnittstellen IEnumerable<T> und IQueryable<T>.

IEnumerable<T>

Diese Schnittstelle unterstützt Iterationen, also speziell den Zugriff mittels foreach. Basis ist die Methode GetEnumerator, die einen Enumerator mit den folgenden Mitgliedern zurückgibt:

  • Reset
  • MoveNext
  • Current<T>

Objekte von diesem Type sind immer Speicherobjekte. Man spricht im Entity Framework davon, dass diese “materialisiert” sind. Abfragen auf solchen Objekten werden nunmehr im Speicher und nicht mehr auf der Datenbank ausgeführt.

IQueryable<T>

Diese Schnittstelle liefert IEnumerable<T> und zusätzlich folgende Eigenschaften:

  • Type vom Typ ElementType
  • Expression vom Typ Expression
  • IQueryProvider vom Typ Provider

Abfragen stehen in Expression und werden vom Provider ausgeführt. Damit ist dies der Schlüssel, um die Abfrage weiter zu verfeinern (Expression wird erweitert) und dann auf der Datenbank ausgeführt zu werden.

Soll die Abfrage auf der Datenbank ausgeführt und dann materialisiert werden, wird folgendes geschrieben:

1 MyDataSource.Where(d => d.Name.Contains("Test").ToList()

Soll die Datenbank komplett abgerufen und dann materialisiert und die Abfrage danach im Speicher ausgeführt werden, wird folgendes geschrieben:

1 MyDataSource.ToList().Where(d => d.Name.Contains("Test")

Der Aufruf von ToList() erzwingt die Materialisierung und gibt (unter anderem) IEnumerable<T>zurück.

Abbildung: Vergleich IEnumerable und IQueryable
Abbildung: Vergleich IEnumerable und IQueryable

Kontext

Der Kontext ist eine zusätzliche Schicht, welche mit Hilfe des Entity Readers C#-Objekte zur Verfügung stellt. Die Dienste des Entity Frameworks unterstützen Entity SQL oder LINQ-To-Entities durch Implementierung der entsprechenden Schnittstellen mittels Providern. Die Provider sorgen dafür, dass datenbankunabhängig gearbeitet wird.

Die Klasse DbContext ist der zentrale Bestandteil der Dienste. Diese Klasse stellt Funktionen für die Interaktion mit CLR-Objekten zur Verfügung. Abfragen werden in Form eines Ausdrucksbaums (Befehlsstruktur) gesendet. Als Rückgabewert kommt ein Auszählungstyp der Schnittstelle IQueryable<T> zurück.

Die Abfragesprache Entity-SQL ist eine eigens für das Entity Framework entwickelte Abfragesprache, welche T-SQL sehr ähnlich ist. Sie erlaubt SQL-Anweisungen die vor allem dann nützlich sind, wenn der Zugriff mit LINQ nicht gelingt oder Funktionen erreicht werden sollen, die der LINQ-Provider nicht kennt. Entity-SQL stellt eine generische, von der verwendeten Datenquelle unabhängige, abstrakte Abfrage zur Verfügung. Ausgeführt wird diese Abfrage unter Verwendung von datenquellenspezifischen Abfrageprovidern, beispielsweise ADO.NET. Zwar kann EntitySQL direkt verwendet werden, jedoch ist es oft leichter den LINQ-Provider zu verwenden, weil LINQ für Abfragen auf Objekten und Strukturen optimiert wurde.

Dieses Verfahren wird dann LINQ-To-Entities genannt. Der Datenclient LINQ-To-Entities greift direkt auf die clientseitigen Komponenten des Entity Frameworks zurück. Dabei wird der Objektdienst als LINQ-Provider verwendet. Die LINQ-Abfrage wird in einen Ausdrucksbaum gewandelt, welcher an den Entity-Datenprovider übergeben wird. Hier erfolgt eine weitere Anpassung in einen ADO.NET kompatiblen Ausdrucksbaum. Der ADO.NET-Provider erzeugt das entsprechende SQL für die Abfrage und liefert das Ergebnis an den Provider. Im letzten Schritt wandelt der LINQ-Provider die im EntityReader enthaltenen Informationen in Objekte, auf die über die Schnittstelle IQueryable<T> zugegriffen werden kann.

Eigenschaften

Entity Framework hat einige grundlegend Eigenschaften, die bei jedem Zugriff berücksichtigt werden sollten.

Lazy Loading

Lazy Loading heißt, das Daten nur dann geladen werden, wenn Sie auch gebraucht werden. In der aktuellen Version 1.0 unterstützt Entity Framework diess Verfahren nicht.

Eager Loading

Hier wird das Laden explizit gesteuert. Nutzen Sie folgenden Aufruf:

1 var usersWithClaim = Users.Include(u => u.Claims);

Weitere Ladevorgänge werden mit .ThenInclude ausgeführt.

Explicit Loading

Hier wird der gesamte Objektgraph geladen:

1 context.Entry(Person)       
2        .Reference(p => p.Bookings)       
3        .Load()
Smart Loading

Hier erfolgt kein Laden, wenn nur eine Aggregation auf den Objekten erfolgt:

1 context.Entry(Person)
2        .Collection(p => p.Bookings)
3        .Query()      
4        .Count()

Dies lässt sich mit dem expliziten Laden kombinieren:

1 context.Entry(sess)
2        .Collection(b => b.Speakers)
3        .Query()
4        .Where(p => p.Name.Contains("EF")
5        .Load();

Umgang mit Mulitplizität

Per Konvention werden Navigationseigenschaften (lies: Relationen) erstellt, wenn der Name auf “ID” endet. Eine explizite Deklaration ist mit dem Attribut [Key] möglich. Folgende Multiplizitäten gibt es:

  • 0..1: Eigenschaftentyp ist Entity Type
  • 1: Eigenschaftentyp ist Entity Type + [Required]
  • n: ICollection<Entity Type> + [Required]
  • 0..n: ICollection<Entity Type>

Assoziationen wirken sich auf das Ladeverhalten aus. Die “n”-Seite der Beziehungen wird durch drei Schnittstellen bestimmt:

  • IEnumerable<T>: Erlaubt nur Lesen, es gibt keine Änderungsverfolgung (Tracking) und kein Lazy Loading.
  • ICollection<T>: Mit Änderungsverfolgung (Add/Remove) und Lazy Loading
  • IList<T>: Zusätzlich mit Sortieren, Filtern und weiteren Abfragen mit LINQ to Objects

Fremdschlüssel können, wenn keine Navigationseigenschaften möglich sind, mit dem Attribut [ForeignKey] bestimmt:

1 public class Project { 
2 
3    public int ProjectId { get; set; } 
4     public string Name { get; set; }    
5     public int ManagerId { get; set; }    
6   
7     [ForeignKey("ManagerId")]    
8     public Person Manager { get; set; } } 

Umgang mit dem Kontext

Die wichtigsten Funktionen sind:

  • SaveChanges: Speichern, auslösen des Datenbankzugriffs
  • ExecuteStoreQuery: Ausführen von Entity-SQL-Abfragen
  • Entry: Zugriff auf die Objekttabelle
SaveChanges

Mit SaveChanges erfolgt das Speichern, also das Auslösen des Datenbankzugriffs. Die Funktion gibt es synchron und asynchron als SaveChangesAsync. Wenn Sie global das Speicherverhalten anpassen möchten, überschreiben Sie die Methode und fügen eigene Aktionen davor ein. Insbesondere besteht hier Zugriff auf den ChangeTracker, der alle vom Entity Framework überwachten Objekte enthält. Damit lassen sich typische Aktionen steuern:

  • Setzen eines Löschkennzeichens für gelöschte Objekte statt physischem Löschen
  • Setzen von globalen Feldern wie CreatedBy, CreatedAt, ModifiedBy, ModifiedAt

Der Aufruf ist darüber hinaus atomar und kapselt alle anstehenden SQL-Abfragen in eine Transaktion. Sie müssen sich nur dann um Transaktionen kümmern, wenn die betroffenen Aktionen in mehrere Aufrufe von SaveChanges zerfallen.

ExecuteStoreQuery

Hiermit erfolgt das Ausführen von Entity-SQL-Abfragen. Die Funktion gibt es synchron und asynchron als ExecuteStoreQueryAsync. Entity-SQL-Abfragen ist ein plattformunabhängiger SQL-Dialekt, der es erlaubt, die Abfragestruktur selbst zu schreiben. Dies ist sinnvoll, wenn es Entity Framework nicht gelingt, eine brauchbare SQL-Abfrage zu erstellen.

Entry und DbSet/IDbSet

Mit Entry erfolgt der Zugriff auf die Objekttabelle der vom Tracker behandelten Objekte. Objekte in der Objekttabelle haben einen definierten Zustand (State, vom Typ EntityState), der darüber entscheidet, welche Art von SQL beim Speichern erzeugt wird.

Objekte lassen sich folgendermaßen der Objekttabelle hinzufügen und in einen bestimmten Zustand versetzen:

1 using (var context = new SessionContext())
2 {
3   var session = new Session { Name = "EF Core 1.0" };
4   context.Entry(session).State = EntityState.Added;   
5   context.SaveChanges();
6 }

Mit Hilfe dieses Verfahrens ist es möglich, generische Repositories aufzubauen, die nicht auf bestimmte DbSet-Methoden angewiesen sind. Der gezeigte Code-Block entspricht folgender Vorgehensweise:

1 using (var context = new SessionContext())
2 {
3   var session = new Session { Name = "EF Core 1.0" };
4   context.Sessions.Add(session);   
5   context.SaveChanges();
6 }

Die Eigenschaft Sessions ist hier im Kontext folgendermaßen definiert:

1 public DbSet<Session> Sessions { get; set; }
Weitere Methoden des Kontexts

Einige weitere Methoden werden nicht immer benötigt, helfen aber dabei, eine leistungsfähigere API für den Datenbankzugriff aufzubauen.

  • Attach
    Hiermit erfolgt das Anhängen eines vorher abgehängten Objekts. Dieses muss in der Datenbank bereits existieren, hat aber möglicherweise Tracking verloren, weil ein Webdienst oder ein ähnlicher Serialisierungsvorgang benutzt worden ist.
  • Find
    Hiermit erfolgt die Suche nach einem Primärschlüssel. Als Parameter wird auch ein Array akzeptiert, falls es sich um einen zusammengesetzten Primärschlüssel handelt.
  • Remove
    Hier erfolgt das Entfernen (DELETE) aus der Objekttabelle und damit das Löschen beim nächsten Speichervorgang. Dies setzt voraus, dass das Objekt vorher geladen wurde. Das können Sie sich sparen, wenn folgendermaßen vorgegangen wird:
1    Ctx.Entry(cust).State = EntityState.Deleted; 
  • Local
    Dies erzeugt eine ObservableCollection<T>, deren Änderungen Teil des Tracking sind und bei SaveChanges beachtet werden
  • Create
    Erzeugt einen Proxy (sehen Sie sich dazu den Abschnitt Proxies an). Der Befehl fügt das Objekt nicht sofort ein; dies würde ein weiteres Add/Attach erfordern.
  • Include
    Dient der Erweiterung des Objektgraphen zum Abruf folgender Stufen verbundener Objekte. Gesteuert wird damit das Eager Loading. Ohne diese Maßnahme wird immer nur eine Stufe geladen und dies auch nur dann, wenn dies mit dem Schlüsselwort virtual bei der Navigationseingenschaft angezeigt wird.
  • SqlQuery
    Erlaubt das direkte Abfragen mit SQL. Das Ergebnis wird Teil des Trackings.
Der Status einer Entität

Der Status steuert das Tracking – das Verfolgen von Änderungen an Proxy-Objekten. Er kann folgende Zustände haben:

  • Added
  • Unchanged
  • Modified
  • Deleted
  • Detached

Dies kann man nutzen, um mit Hilfe der bereits gezeigten Methode Entry eine Funktion zu entwickeln, die gleichzeitig Aktualisieren und Erstellen kann. Als Kriterium wird der Primärschüssel benutzt.

 1 public void InsertOrUpdate(Session session)
 2 {
 3   using (var context = new SessionContext())
 4   {
 5     context.Entry(session).State = session.Id == 0 ?
 6                                    EntityState.Added :
 7                                    EntityState.Modified;
 8  
 9     context.SaveChanges();
10   }
11 }

6.4 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 modelBuilde\
12 r)
13     {
14         // Eine Eigenschaft zum Pflichtfeld machen
15         modelBuilder.Entity<CompanyEvent>()
16             .Property(b => b.Title)
17             .IsRequired();
18     }
19   }
20 }

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["EventCo\
4 ntext"];
5     services.AddEntityFramework()
6      .AddSqlServer()
7      .AddDbContext<EventContext>(options => options.UseSqlServer(con\
8 nection));

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.

6.5 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 ClaimsIdentit\
20 y;
21     foreach (var entry in ChangeTracker.Entries<EntityBase>()) {
22       if (entry.State == EntityState.Added)
23       {
24         entry.Entity.CreatedAt = dn;
25         entry.Entity.ModifiedAt = dn;
26         entry.Entity.CreatedBy = identity.Name;
27       }
28       if (entry.State == EntityState.Modified) {
29         entry.Entity.ModifiedAt = dn;
30         entry.Entity.ModifiedBy = identity.Name;          
31       }
32     }
33 
34     return base.SaveChanges();
35   }
36 
37 
38   protected override void OnModelCreating(DbModelBuilder modelBuilde\
39 r) {
40     // first to avoid internal Identity names
41     base.OnModelCreating(modelBuilder);
42 
43     modelBuilder.Entity<EventUser>().ToTable("User");
44     modelBuilder.Entity<EventRole>().ToTable("Roles");
45     modelBuilder.Entity<EventUserRole>().ToTable("User_x_Roles");
46     modelBuilder.Entity<EventUserLogon>().ToTable("Logons");
47     modelBuilder.Entity<EventUserClaim>().ToTable("User_x_Claims");
48   }
49 }

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, EventUs\
 5 erRole, 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>, IEntity\
78 Base {
79     [Column(TypeName = "datetime2")]
80     public DateTime CreatedAt { get; set; }
81 
82     [Column(TypeName = "datetime2")]
83     public DateTime ModifiedAt { get; set; }
84 
85     [StringLength(200)]
86     public string ModifiedBy { get; set; }
87 
88     [StringLength(200)]
89     public string CreatedBy { get; set; }
90 
91   }
92 
93 }

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.

6.6 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.Add\
28 ed : EntityState.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 GenericRep\
 8 ository<Event>();
 9        protected GenericRepository<User> repUsers = new GenericRepos\
10 itory<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\
 8 ).ToList();
 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\
10 ="form-horizontal" role="form">
11     <div class="form-horizontal">
12         <div asp-validation-summary="ValidationSummary.All" class="t\
13 ext-danger"></div>
14         <div class="form-group">
15             <label asp-for="Url" class="col-md-2 control-label"></la\
16 bel>
17             <div class="col-md-10">
18                 <input asp-for="Url" class="form-control" />
19                 <span asp-validation-for="Url" class="text-danger"><\
20 /span>
21             </div>
22         </div>
23         <div class="form-group">
24             <div class="col-md-offset-2 col-md-10">
25                 <input type="submit" value="Create" class="btn btn-d\
26 efault" />
27             </div>
28         </div>
29     </div>
30 </form>

Starten der Anwendung

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

6.7 Umgang mit DataAnnotations

6.8 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 }"
35            , elm.Vorname, elm.Nachname, elm.City);
36       }
37    }
38 }