I Teil I – Schnellstart

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.

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)

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 }

2. .NET Core

Der .NET-Kern Core 2 ist eine kleine, optimierte Laufzeitumgebung. Sie ersetzt das bekannte .NET-Framework und wird eingesetzt, wenn die gesamte Funktionsbreite des Frameworks nicht benötigt wird und stattdessen die Vorteile bei der Performance und Plattformunabhängigkeit relevant sind. Entfallen sind vor allem die Teile, die plattformspezifisch sind, also beispielsweise der Zugriff auf Active Directory, MSMQ, WCF, WPF etc.

Der Core bietet 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 das Kommandozeilenwerkzeug dotnet. 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 ASP.NET Core

Die neueste Entwicklungsumgebung für Web-Anwendungen ist ASP.NET Core. ASP.NET Core kann in der Laufzeitumgebung .NET 4.6 oder neuer oder auch mit .NET Core ausgeführt werden. Das ist freilich durch die wiederholte Benutzung des Begriffs “Core” etwas verwirrend. Generell gilt hier aber tatsächlich eine weitgehende Unabhängigkeit von der Laufzeit. In diesem Buch wird eine Applikationsumgebung entwickelt, die exemplarisch viele Funktionen vereint. Dazu gehört auch die Plattformunabhängigkeit, sodass der Code final mit .NET Core getestet wurde. Damit wurde auch bewusst auf jede API verzichtet, die hier nicht zur Verfügung steht.

2.2 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 sollte 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 bei Bedarf beide Referenzen:

1 "frameworks": {
2     "net472": { },
3     "netstandard2.0": { }
4 },

Dabei steht net472 für .NET 4.7.2 und netstandard für die .NET Core-Umgebung. netstandard2.0 dient dem Erstellen von portierbaren Assemblies. Wählen Sie stattdessen netcoreapp2.0, um eine eigenständige Applikation zu erstellen. Dies ist der Fall, wenn Sie eine ASP.NET Core 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 NET472
2   // Code der nur mit .NET-Framework läuft
3 #endif

.NET Standard

.NET Standard ist eine API-Beschreibung – nur Text, kein Code – die die vielen Laufzeit-Implementierungen vereinheitlichen soll. Aktuell ist .NET Standard Version 2.0. Wenn eine Assembly erstellt wird und als Ziel netstandard2.0 benutzt wird, so lässt sich diese Assembly gegen alle kompatiblen Projekte linken. Das schließt .NET 4.x ebenso ein wie .NET Core 2. Die Entscheidung, das Deployment der Anwendung gegen das eine oder andere oder beide Laufzeiten auszuführen, ist dem Bibliotheksentwickler deshalb egal. Dies ist ein großer Vorteil für die Zukunft wiederverwendbaren Codes.

Wechseln des Frameworks

Dieser Abschnitt ist nur für Windows und Visual Studio zutreffend. Auf den anderen Plattformen gibt es kein .NET 4 und deshalb auch keine Alternativen zu .NET Core.

Zum Wechseln des Frameworks ändern Sie den Eintrag in der Projektdatei .csproj. 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. Wird nur ein Ziel unterstützt, sieht das folgendermaßen aus:

1 <PropertyGroup>
2   <TargetFramework>net462</TargetFramework>
3 </PropertyGroup>

Bei zwei Zielen wäre eine solche Variante denkbar (beachten Sie den Plural für den Tag-Namen):

1 <PropertyGroup>
2   <TargetFrameworks>netstandard2.0;net47</TargetFrameworks>
3 </PropertyGroup>

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 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.3 Die Ausführungsumgebung

Die Ausführungsumgebung (.NET Execution Environment) besteht aus dem Entwickler-Kit (SDK) und der Laufzeitumgebung. Sie ist 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 Core-Projekt ist primäre ein Ordner mit Codedateien. Der Name des Projekts ist der Name des Ordners. Visual Studio – unter Windows – hat eine Projektverwaltung. Falls diese benutzt wird (ab VS 2017 ist dies optional), wird die Projektdatei benötigt. Alle Dateien im Ordner sind automatisch Teil des Projekts.

Die Werkzeuge

Paketabhängigkeiten können nur aufgelöst werden, wenn das Basispaket installiert wurde. Um Pakete manuell nachinstallieren zu können, wird das Kommandozeilenwerkzeug. 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 ursprü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

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.

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 entsteht 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 entsteht 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.0.0-r\
16 c1-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-final"
22   },
23 
24   "commands": {
25     "web": "Microsoft.AspNet.Server.Kestrel"
26   },
27 
28   "frameworks": {
29     "dnx461": { },
30     "netstandard1.0": { }
31   },
32 
33   "exclude": [
34     "wwwroot",
35     "node_modules"
36   ],
37   "publishExclude": [
38     "**.user",
39     "**.vspscc"
40   ],
41   "scripts": {
42     "prepublish": [ "npm install", "bower install", "gulp clean", "gulp mi\
43 n" ]
44   }
45 }
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=aspnetCo\
15 re-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', 'co\
17 py']); 

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