1. Einführung in ASP.NET Core
ASP.NET Core ist eine umfassende Neuentwicklung der Programmierumgebung für Webanwendungen 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 Stellen wiederfinden.
Dieses Buch behandelt die aktuelle Version 2.2. Es wurde im Januar 2019 aktualisiert.
1.1 Was ist ASP.NET Core?
Neu ist, dass die gesamte Umgebung wie vieles andere bei Microsoft 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 – via Nuget. 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. 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 eine Methode Main. Das sieht nicht nur aus wie eine Konsolenanwendung, es ist auch eine. Zu finden ist die Methode in der Datei Program-cs:
1 public class Program {
2 public static void Main(string[] args) {
3 CreateWebHostBuilder(args).Build().Run();
4 }
5
6 public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
7 WebHost.CreateDefaultBuilder(args)
8 .UseStartup<Startup>();
9 }
Hiermit wird eine Grundkonfiguration geladen und der Host ausgeführt. Dieses Verhalten unterscheidet sich fundamental von der Vorgehensweise im klassischen ASP.NET. Statt auf einen vorhandenen Webserver zu setzen, wird der Host Teil der bereitgestellten Applikation. Der Vorteil (und die Motivation dafür) liegt in der damit erreichbaren Plattformunabhängigkeit. Da die Core-Umgebung nicht auf ein bestimmtes Betriebssystem angewiesen ist, kann auch die Existenz eines bestimmten Webservers auf einem System nicht voraugesetzt werden. Der integrierte Webserver heißt “Kestrel” und wird immer benutzt.
Die Methode UseStartup führt über einen generischen Typparameter zu der Klasse Startup. Diese hat folgenden prinzipiellen Aufbau:
1 public class Startup
2 {
3
4 public IConfigurationRoot Configuration { get; }
5
6 public void ConfigureServices(IServiceCollection services)
7 {
8 }
9
10 public void Configure(IApplicationBuilder app)
11 {
12 }
13 }
Die Methode ConfigureServices (Zeile 6) definiert die Dienste, die die Applikation verwendet. Die Methode Configure (Zeile 9) definiert die Middleware (Dienstschicht), die die Anforderungen in der Pipeline verarbeitet. Optional kann via Konstruktor oder in der Methode Configure auf weitere Funktionen zugegriffen werden, z.B.
-
IHostingEnvironment: Der Host -
IConfiguration: Das Konfigurationssystem -
ILoggerFactory: Der Protokollstack
Die Dienste werden über einen Container bereitgestellt, der elementare Funktionen einer Dependency Injection-Architektur ausführt. Das ist nicht ganz das Niveau typischer DI-Bibliotheken, reicht aber in den meisten Fällen. In jedem Fall ist die Benutzung zwingend und wirklich empfehlenswert.
Dienste
Ein Dienst ist ein Baustein der einer Anwendung für allgemeine Aufgaben zu Verfügung steht. Er wird über Dependency Injection bereitgestellt.
In ASP.NET Core 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. Idealerweise ist diese Umgebung dann kompatibel, was beispielsweise bei Autofac1 der Fall ist.
Dienste gibt es in drei Arten:
- Singleton
- Scoped
- Transient
Singleton Dienste werden nur einmal erstellt und dann unabhängig vom Aufrufer immer wieder benutzt. Typisch sind dies Service-Proxies oder Hilfsklassen.
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. Dies ist häufig der sicherste und beste Weg.
Transiente Dienste werden bei jeder Anforderung neu erstellt. Wenn ein Aufrufer also fünf Instanzen einer Klasse erzeugt, und im Konstruktor wird ein transienter Dienst verlangt, so werden fünf Instanzen des transienten Dienstes erstellt.
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. Dazu wird eine Erweiterungsmethode der Schnittstelle IApplicationBuilder in der Methode Configure benutzt.
Einige eingebaute Middleware-Komponenten erleichtern den Start:
- Arbeit mit statischen Dateien (JavaScript, Bilder)
- Routing
- Diagnose
- Authentifizierung
- Konfiguration für CORS (Cross-Origin Resource Sharing)
Dies ist freilich nur einen kleine Auswahl. Vielleicht kennen Sie noch die Pipeline der klassischen ASP.NET-Umngebung mit den IIS. Dort war der Ablauf fest kodiert und man konnte nur an bestimmten Punkte eingreifen. Deshalb der Begriff “Pipeline” – wie ein Rohr mit Zapfstellen. In ASP.NET Core ist dieser Ablauf nun komplett konfigurierbar. Was nicht benötigt wird, das entfällt. Das verbessert die Systemleistung. Es erfordert manchmal etwas mehr Nachdenken durch den Entwickler, was an welcher Stelle benötigt wird.
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 Kontext bereit. Die plattformunabhängige Implementierung eines Webservers, der unter dem Namen Kestrel bereitgestellt wird, ist das Fundament. Freilich kann dieser Webserver unterdrückt und durch einen eigene Version ersetzt werden.
Das Wurzelverzeichnis
Das Wurzelverzeichnis der Anwendung ist die Stelle, ab der statische Dateien ausgeliefert werden. Der Pfad zu dieser Position ist konfigurierbar und lautet im Standardprojekt wwwroot. In reinen Webservice-Projekten kann darauf verzichtet werden. Neben der blanken Position der Dateien muss der Zugriff erlaubt werden. Dazu dient eine spezielle Middleware:
1 public void Configure(IApplicationBuilder app) {
2 app.UseStaticFiles();
3 }
Alle statischen Dateien müssen in das Wurzelverzeichnis kopiert oder dort abgelegt werden. Am einfachsten erfolgt dies mit einem passenden Gulp-Task oder mittels WebPack beim Bauen der Applikation. Generell ist die Situation für .NET-Entwickler hier nicht einfacher geworden. Die Frontend-Welt setzt komplett auf das Java-Ökosystem und man kann deshalb nicht ernsthaft nur ein reines ASP.NET-Projekt erstellen und diesen Teil außer Acht lassen. Es ist deshalb unbedingt empfehlenswert, parallel NodeJS zu installieren und dann mit den unter npm typischen Aufgaben mit ASP.NET zu interagieren. In Bezug auf statische Dateien heißt dies, dass der Bundler von ASP.NET Core nicht benutzt wird (es gibt aber einen) und stattdessen npm-Tasks in den Erstellungsprozess mit einbezogen werden.
Konfiguration
ASP.NET Core benutzt ein neues Konfigurationsmodell. Es basiert auf der Datei application.json. Eine web.config wird nur benötigt, wenn das Hosting über den IIS erfolgt – mit oder ohne Kestrel ist nicht relevant.
Eine typische Bereitstellung sieht dann folgendermaßen aus:
1 public class Startup {
2
3 public Startup(IHostingEnvironment env) {
4 var builder = new ConfigurationBuilder()
5 .SetBasePath(env.ContentRootPath)
6 .AddJsonFile("appsettings.json", optional: false, reloadOnChange: \
7 true)
8 .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: \
9 true)
10 .AddEnvironmentVariables();
11 Configuration = builder.Build();
12 }
13
14 public IConfigurationRoot Configuration { get; }
15
16 // ... weiterer Code der Start-Datei folgt hier
17
18 }
Die Datei wird hier eingelesen und dann über eine Eigenschaft bereitgestellt. Der Aufbau teilt sich in Elemente und Abschnitte, sodass sich auch komplexe Strukturen vorbereiten lassen. Verbindungszeichenfolgen für Datenbanken haben einen eigenen, vordefinierten Abschnitt. Eine passende Konfigurationsdatei könnten folgendermaßen aussehen:
1 {
2 "Logging": {
3 "IncludeScopes": false,
4 "LogLevel": {
5 "Default": "Warning"
6 }
7 },
8 "backEndUri": "http://localhost:5001",
9 "JwtIssuerOptions": {
10 "Issuer": "joergIsAGeek",
11 "Audience": "http://localhost:5002"
12 },
13 "Keys": {
14 "BackendSecret": "D99BCD2C-1FD4-4374-B68F-45E84C59D510",
15 "TokenSecret": "iNivDmHLpUA223sqsfhqGbMRdRj1PVkH"
16 }
17 }