Entwickeln von ASP.NET Core MVC-Apps

Tipp

Diese Inhalte sind ein Auszug aus dem E-Book „Architect Modern Web Applications with ASP.NET Core and Azure“, das unter .NET Docs oder als kostenlos herunterladbare PDF-Datei verfügbar ist, die offline gelesen werden kann.

Miniaturansicht des Deckblatts des eBooks „Architect Modern Web Applications with ASP.NET Core and Azure“.

„Sie müssen nicht schon beim ersten Mal alles richtig machen, aber unbedingt beim letzten Mal.“ – Andrew Hunt and David Thomas

ASP.NET Core ist ein plattformübergreifendes Open-Source-Framework zum Erstellen moderner cloudoptimierter Webanwendungen. ASP.NET Core-Apps sind einfach und modular aufgebaut. Sie verfügen über integrierte Unterstützung für Dependency Injection, wodurch ihre Testfähigkeit und Verwaltbarkeit verbessert wird. In Kombination mit MVC (einem Muster, das neben ansichtsbasierten Apps das Erstellen von modernen Web-APIs unterstützt) stellt ASP.NET Core ein leistungsstarkes Framework zum Erstellen von Unternehmenswebanwendungen dar.

MVC und Razor Pages

ASP.NET Core MVC enthält viele Features, die für das Erstellen von webbasierten APIs und Apps nützlich sind. MVC bedeutet „Model View Controller“. Dabei handelt es sich um ein Benutzeroberflächenmuster, das die Zuständigkeit für die Reaktion auf Anforderungen von Benutzern aufteilt. Wenn Sie dieses Muster verwenden, können Sie ebenfalls Features als sogenannte Razor Pages in Ihre ASP.NET Core-Apps implementieren.

Razor Pages werden in ASP.NET Core MVC integriert und verwenden die gleichen Features für das Routing, Modellbindungen, Filter und die Autorisierung. Dafür werden jedoch nicht wie üblich verschiedene Ordner und Dateien für Controller, Modelle oder Ansichten und auch kein attributbasiertes Routing verwendet. Razor Pages werden in einem einzelnen Ordner (/Pages) gespeichert, das Routing basiert auf deren relativem Speicherort in diesem Ordner, und Anforderungen werden mit Handlern anstatt mit Controlleraktionen verarbeitet. Folglich werden bei der Arbeit mit Razor Pages alle benötigten Dateien und Klassen in der Regel zusammen und nicht verteilt im Webprojekt bereitgestellt.

Erfahren Sie mehr über die Anwendung von MVC, Razor Pages und verwandten Mustern in der eShopOnWeb-Beispielanwendung.

Wenn Sie eine neue ASP.NET Core-App erstellen, sollten Sie sich zuvor genau überlegen, was die erstellte App leisten soll. Beim Erstellen eines neuen Projekts in Ihrer IDE oder mithilfe des CLI-Befehls dotnet new können Sie aus mehreren Vorlagen auswählen. Die gängigsten Projektvorlagen sind eine leere Vorlage, „Web-API“, „Web-App“ und „Web-App (Model View Controller)“. Sie können sich zwar nur beim Erstellen des Projekts für eine Vorlage entscheiden, aber die Entscheidung ist nicht endgültig. Ein Projekt für Web-APIs verwendet Standard-MVCs, enthält aber standardmäßig keine Ansichten. Die Standardvorlage für Web-Apps verwendet Razor Pages, enthält aber ebenfalls keinen Ordner für Ansichten. Sie können einen entsprechenden Ordner im Nachhinein zu diesen Projekten hinzufügen, um auf Ansichten basierendes Verhalten zu unterstützen. Web-API- und Model View Controller-Projekte enthalten standardmäßig keinen Pages-Ordner. Sie können diesen jedoch im Nachhinein hinzufügen, um Razor Pages zu unterstützen. Diese drei Vorlagen unterstützen drei verschiedene Arten der Standardbenutzerinteraktion: datenbasierte (Web-API), seitenbasierte und ansichtsbasierte Interaktionen. Diese Vorlagen können Sie jedoch wie gewünscht in einem Projekt kombinieren.

Was spricht für Razor Pages?

Razor Pages werden standardmäßig für neue Webanwendungen in Visual Studio verwendet. Mithilfe von Razor Pages können Sie seitenbasierte Anwendungsfeatures (wie Nicht-Single-Page-Formulare) einfacher erstellen. Wenn Controller und Ansichten verwendet werden, enthalten Anwendungen häufig sehr große Controller, die mit mehreren Abhängigkeiten und Ansichtsmodellen arbeiten und viele unterschiedliche Ansichten zurückgeben. Dadurch wird die Anwendung komplexer, und häufig sind Controller vorhanden, die das Single Responsibility Principle (Prinzip der eindeutigen Verantwortlichkeit) oder das Offen-Geschlossen-Prinzip nicht befolgen. Dieses Problem wird durch Razor Pages behoben, indem die serverseitige Logik für eine bestimmte lokale Seite in einer Webanwendung mit entsprechendem Razor-Markup gekapselt wird. Eine Razor-Seite ohne serverseitige Logik kann nur aus einer Razor-Datei bestehen (z. B. „Index.cshtml“). Den meisten nicht trivialen Razor Pages ist jedoch eine Seitenmodellklasse zugeordnet, die üblicherweise genauso wie die Razor-Datei benannt wird, aber die Erweiterung „.cs“ aufweist (z.B. „Index.cshtml.cs“).

Das Seitenmodell einer Razor Page kombiniert die Zuständigkeit eines MVC-Controllers und eines Ansichtsmodells. Anforderungen werden nicht mit Controlleraktionsmethoden verarbeitet, sondern Seitenmodellhandler wie OnGet() werden ausgeführt, um die zugehörige Seite standardmäßig zu rendern. Durch Razor Pages wird das Erstellen einzelner Seiten in einer ASP.NET Core-App vereinfacht, während alle Architekturfeatures von ASP.NET Core MVC genutzt werden können. Diese sind für neue seitenbasierte Funktionen gut geeignet.

Wann sollten Sie MVC verwenden?

Wenn Sie Web-APIs erstellen, ist das MVC-Muster besser als Razor Pages geeignet. Wenn Ihr Projekt nur Web-API-Endpunkte verfügbar macht, sollten Sie idealerweise mit der Web-API-Projektvorlage beginnen. Andernfalls ist es auch ganz einfach, Controller und zugehörige API-Endpunkte zu einer beliebigen ASP.NET Core-App hinzuzufügen. Verwenden Sie den ansichtsbasierten MVC-Ansatz, wenn Sie eine vorhandene Anwendung mit geringem Aufwand von ASP.NET Core MVC 5 oder früher zu ASP.NET Core MVC migrieren möchten. Nach der Migration können Sie überprüfen, ob Razor Pages für neue Feature oder als gesamte Migration sinnvoll eingesetzt werden kann. Weitere Informationen zum Portieren von .NET 4.x-Apps zu .NET 8 finden Sie im E-Book Portieren vorhandener ASP.NET-Apps zu .NET Core.

Die Leistung Ihrer Web-App hängt nur geringfügig davon ab, ob Sie Razor Pages oder MVC-Ansichten verwenden, außerdem werden bei beiden Methoden Features wie Abhängigkeitsinjektion, Filter, Modellbindung, Validierung usw. unterstützt.

Zuordnen von Anforderungen zu Antworten

Im Wesentlichen dienen ASP.NET Core-Apps dazu, eingehende Anforderungen ausgehenden Antworten zuzuordnen. Auf niedriger Ebene wird für diese Zuordnung Middleware verwendet. Daher kann es sein, dass ASP.NET Core-Apps und -Microservices ausschließlich aus benutzerdefinierter Middleware bestehen. Wenn Sie ASP.NET Core MVC verwenden, können Sie auf einer allgemeineren Ebene arbeiten und Routen, Controller und Aktionen hinzufügen. Jede eingehende Anforderung wird mit der Routingtabelle der Anwendung verglichen, und wenn eine übereinstimmende Route gefunden wird, wird die zugewiesene Aktionsmethode (des Controllers) aufgerufen, um die Anforderung zu verarbeiten. Wenn keine übereinstimmende Route gefunden wird, wird ein Fehlerhandler aufgerufen und das Ergebnis „NotFound“ zurückgegeben.

ASP.NET Core MVC-Apps können entweder herkömmliche Routen oder Attributrouten oder beides gleichzeitig verwenden. Herkömmliche Routen werden als Code definiert und geben unter Verwendung einer wie im Folgenden dargestellten Syntax Routingkonventionen an:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});

In diesem Beispiel wurde eine Route mit dem Namen „Standard“ der Routingtabelle hinzugefügt. Sie definiert eine Routenvorlage mit Platzhaltern für controller, action und id. Für die Platzhalter controller und action ist die Standardroute festgelegt (jeweils Home und Index), und der Platzhalter id ist optional, da diese mit einem Fragezeichen („?“) versehen sind. Die hier definierte Konvention drückt aus, dass der erste Teil einer Anforderung dem Namen eines Controllers und der zweite Teil der Aktion entsprechen soll. Außerdem kann wenn nötig ein dritter Teil den ID-Parameter darstellen. Konventionelle Routen werden üblicherweise an einer bestimmten Stelle für die Anwendung definiert, z. B. in Program.cs, wo die Middleware-Anforderungspipeline konfiguriert ist.

Attributrouten gelten für Controller und Aktionen direkt und werden nicht global angegeben. Dieser Ansatz hat den Vorteil, dass sie besser zu finden sind, wenn Sie eine bestimmte Methode betrachten. Andererseits bedeutet dies aber auch, dass die Routinginformationen nicht an einer bestimmten Stelle in der Anwendung gespeichert sind. Sie können mit Attributrouten problemlos mehrere Routen für eine bestimmte Aktion festlegen und gleichzeitig aber auch Routen zwischen Controllern und Aktionen kombinieren. Zum Beispiel:

[Route("Home")]
public class HomeController : Controller
{
    [Route("")] // Combines to define the route template "Home"
    [Route("Index")] // Combines to define route template "Home/Index"
    [Route("/")] // Does not combine, defines the route template ""
    public IActionResult Index() {}
}

Routen können über [HttpGet] und ähnliche Attribute angegeben werden, weshalb keine separaten [Route]-Attribute hinzugefügt werden müssen. Ebenso können wie folgt Token von Attributrouten verwendet werden, damit die Namen von Controllern oder Aktionen seltener wiederholt werden müssen:

[Route("[controller]")]
public class ProductsController : Controller
{
    [Route("")] // Matches 'Products'
    [Route("Index")] // Matches 'Products/Index'
    public IActionResult Index() {}
}

Razor Pages nutzt kein Attributrouting. Sie können in der @page-Anweisung einer Razor Page jedoch zusätzliche Informationen zu Routingvorlagen angeben:

@page "{id:int}"

Im vorherigen Beispiel hat die Seite Routen mit einem ganzzahligen id-Parameter abgeglichen. Die Seite Products.cshtml, die sich im Stamm von /Pages befindet, würde beispielsweise auf Anforderungen wie diese antworten:

/Products/123

Nachdem eine bestimmte Anforderung einer Route zugeordnet wurde, aber noch bevor die Aktionsmethode aufgerufen wird, führt ASP.NET Core MVC Vorgänge zur Modellbindung und Modellvalidierung für die Anforderung aus. Die Modellbindung ist notwendig, um eingehende HTTP-Daten in .NET-Typen zu konvertieren, die als Parameter der aufzurufenden Aktionsmethode angegeben wurden. Wenn z. B. die Aktionsmethode einen int id-Parameter erwartet, versucht die Modellbindung, diesen Parameter über einen Wert bereitzustellen, der Teil der Anforderung ist. Dafür sucht die Modellbindung nach bereitgestellten Formularwerten, Werten in der Route selbst und Werten von Abfragezeichenfolgen. Wenn ein id-Wert gefunden wird, wird dieser in einen Integer konvertiert, bevor er an die Aktionsmethode übergeben wird.

Nach der Modellbindung, aber noch vor dem Aufruf der Aktionsmethode, wird eine Modellvalidierung vorgenommen. Die Modellvalidierung verwendet optionale Attribute für den Modelltyp. In diesem Zusammenhang kann ggf. sichergestellt werden, dass das bereitgestellte Modellobjekt mit bestimmten Anforderungen an Daten konform ist. Bestimmte Werte sind möglicherweise entsprechend den Anforderungen angegeben oder auf eine bestimmte Länge bzw. einen bestimmten numerischen Bereich beschränkt. Wenn Validierungsattribute angegeben sind, das Modell aber nicht deren Anforderungen entspricht, wird für die Eigenschaft „ModelState.IsValid“ FALSE zurückgegeben. Dann können die fehlerhaften Validierungsregeln an den Client gesendet werden, von dem die Anforderung ausgeht.

Wenn Sie die Modellvalidierung verwenden, sollten Sie stets überprüfen, ob das Modell gültig ist, bevor Sie einen Befehl ausführen, der Einfluss auf den Status haben kann. Dadurch stellen Sie sicher, dass die App nicht durch ungültige Daten beschädigt wird. Sie können einen Filter verwenden, damit Sie keinen Code für diese Validierung zu jeder Aktion hinzufügen müssen. Mithilfe von ASP.NET Core MVC-Filtern können Sie Gruppen von Anforderungen abfangen, damit allgemeine Richtlinien und übergreifende Aspekte gezielt angewendet werden können. Filter können sowohl auf individuelle Aktionen als auch auf vollständige Controller oder global auf eine ganze Anwendung angewendet werden.

Im Hinblick auf Web-APIs unterstützt ASP.NET Core MVC die Inhaltsaushandlung. Dadurch können Anforderungen angeben, wie Antworten formatiert werden sollen. Auf der Grundlage von in Anforderungen enthaltenen Headern formatieren Aktionen, die Daten zurückgeben, die Antworten in XML, JSON oder einem beliebigen anderen unterstützten Format. Mithilfe dieses Features kann dieselbe API in mehreren Clients mit unterschiedlichen Anforderungen an das Datenformat verwendet werden.

Für Web-API-Projekte sollte das [ApiController]-Attribut verwendet werden, das auf einzelne Controller, eine Basiscontrollerklasse oder die gesamte Assembly angewendet werden kann. Durch dieses Attribut wird eine automatische Modellüberprüfung hinzugefügt, und jede Aktion, bei der ein ungültiges Modell verwendet wird, gibt den Fehler „BadRequest“ mit Details zu den Überprüfungsfehlern zurück. Wenn dieses Attribut verwendet wird, müssen alle Aktionen eine Attributroute anstelle einer konventionellen Route aufweisen. Das Attribut gibt zudem ausführlichere Informationen zu aufgetretenen Fehlern zurück.

Steuern von Controllern

Bei seitenbasierten Anwendungen sorgen Razor Pages dafür, dass Controller nicht zu groß werden. Jede einzelne Seite erhält eigene Dateien und Klassen, die nur für ihre Handler dediziert sind. Vor der Einführung von Razor Pages verfügten viele Anwendungen mit Fokus auf Ansichten über große Controllerklassen, die für viele verschiedene Aktionen und Ansichten zuständig sind. Diese Klassen werden auf natürliche Weise so vergrößert, dass sie viele Verantwortlichkeiten und Abhängigkeiten haben, was die Verwaltung erschwert. Wenn Sie feststellen, dass Ihre ansichtsbasierten Controller zu groß werden, sollten Sie zur Verwendung von Razor Pages ein Refactoring in Erwägung ziehen oder ein Vermittlermuster einführen.

Das Vermittlerentwurfsmuster wird verwendet, um die Kopplung zwischen Klassen zu reduzieren und gleichzeitig die Kommunikation zwischen diesen zuzulassen. In ASP.NET Core MVC-Anwendungen wird dieses Muster häufig eingesetzt, um Controller in kleinere Teile aufzuteilen, indem Handler verwendet werden, die die Arbeit von Aktionsmethoden übernehmen. Hierzu wird häufig das beliebte MediatR-NuGet-Paket verwendet. In der Regel enthalten Controller viele verschiedene Aktionsmethoden, die jeweils möglicherweise bestimmte Abhängigkeiten erfordern. Diese für eine Aktion erforderlichen Abhängigkeiten müssen an den Konstruktor des Controllers übergeben werden. Bei der Verwendung von MediatR weist ein Controller als einzige Abhängigkeit typischerweise eine Instanz des Vermittlers auf. Jede Aktion verwendet dann die Vermittlerinstanz, um eine Nachricht zu senden, die von einem Handler verarbeitet wird. Der Handler ist für eine einzelne Aktion spezifisch und benötigt daher nur die für diese Aktion erforderlichen Abhängigkeiten. Hier finden Sie ein Beispiel für einen Controller, der MediatR verwendet:

public class OrderController : Controller
{
    private readonly IMediator _mediator;

    public OrderController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet]
    public async Task<IActionResult> MyOrders()
    {
        var viewModel = await _mediator.Send(new GetMyOrders(User.Identity.Name));
        return View(viewModel);
    }
    // other actions implemented similarly
}

Bei der MyOrders-Aktion wird der Send-Befehl zum Senden einer GetMyOrders-Nachricht von dieser Klasse verarbeitet:

public class GetMyOrdersHandler : IRequestHandler<GetMyOrders, IEnumerable<OrderViewModel>>
{
    private readonly IOrderRepository _orderRepository;
    public GetMyOrdersHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

  public async Task<IEnumerable<OrderViewModel>> Handle(GetMyOrders request, CancellationToken cancellationToken)
    {
        var specification = new CustomerOrdersWithItemsSpecification(request.UserName);
        var orders = await _orderRepository.ListAsync(specification);
        return orders.Select(o => new OrderViewModel
            {
                OrderDate = o.OrderDate,
                OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel()
                  {
                    PictureUrl = oi.ItemOrdered.PictureUri,
                    ProductId = oi.ItemOrdered.CatalogItemId,
                    ProductName = oi.ItemOrdered.ProductName,
                    UnitPrice = oi.UnitPrice,
                    Units = oi.Units
                  }).ToList(),
                OrderNumber = o.Id,
                ShippingAddress = o.ShipToAddress,
                Total = o.Total()
        });
    }
}

Das Endergebnis dieses Ansatzes ist, dass Controller viel kleiner und hauptsächlich auf das Routing und Modellbindungen ausgerichtet sind, während einzelne Handler für die spezifischen Aufgaben verantwortlich sind, die für einen bestimmten Endpunkt erforderlich sind. Anstelle von MediatR können Sie bei diesem Ansatz auch das ApiEndpoints-NuGet-Paket verwenden, das versucht, API-Controllern dieselben Vorteile zu bieten, die Razor Pages ansichtsbasierten Controllern bieten.

Ressourcen: Zuordnen von Anforderungen zu Antworten

Arbeiten mit Abhängigkeiten

Die Technik Dependency Injection wird von ASP.NET Core unterstützt und intern verwendet. Es handelt sich dabei um eine Technik, die die lose Kopplung von unterschiedlichen Teilen einer Anwendung ermöglicht. Eine losere Kopplung stellt einen Vorteil dar, da dadurch verschiedene Teile der Anwendung besser isoliert voneinander getestet oder ersetzt werden können. Außerdem wird es dadurch unwahrscheinlicher, dass eine Änderung eines Teils der Anwendung zu unerwarteten Auswirkungen auf die restliche Anwendung führen kann. Dependency Injection basiert auf dem Prinzip der Dependency Inversion und stellt häufig ein wichtiges Mittel dar, um das Offen/Geschlossen-Prinzip durchzusetzen. Wenn Sie auswerten, wie die Anwendung mit ihren Abhängigkeiten funktioniert, sollten Sie schlecht strukturierten Code im statischen Zusammenhang vermeiden und den Leitsatz New is Glue („New“ ist klebrig) beachten.

Es entsteht ein statischer Zusammenhang, wenn Ihre Klassen statische Methoden aufrufen oder auf statische Eigenschaften zugreifen, die Nebenwirkungen oder Abhängigkeiten von der Infrastruktur umfassen. Wenn Sie z.B. über eine Methode verfügen, die eine statische Methode aufruft, die wiederum in eine Datenbank schreibt, wird Ihre Methode eng an die Datenbank gekoppelt. Jegliches Element, das den Datenbankaufruf unterbricht, unterbricht auch die Methode. Es ist bekannt, dass es sehr schwierig ist, diese Methode zu testen, da dafür entweder kommerzielle Testbibliotheken erforderlich sind, die statische Aufrufe testen, oder die Tests nur mit einer aktiven Testdatenbank ausgeführt werden können. Statische Aufrufe, bei denen keine Abhängigkeiten von der Infrastruktur bestehen, insbesondere die vollständig zustandslosen Aufrufe, können ohne Bedenken aufgerufen werden und haben (abgesehen von Kopplungscode und statischen Aufrufen an sich) keine Auswirkung auf die Kopplung oder Testfähigkeit.

Viele Entwickler kennen zwar das Risiko von statischen Zusammenhängen und globalen Status, koppeln ihren Code aber dennoch über direkte Instanziierung eng an bestimmte Implementierungen. Der Leitsatz „new is glue“ („new“ fungiert als Klebstoff) soll an diese Kopplung erinnern und stellt keine generelle Verurteilung der Verwendung des Schlüsselworts new dar. Genauso wie statische Methodenaufrufe koppeln neue Instanzen von Typen ohne externe Abhängigkeiten Code nicht eng an die Implementierungsdetails, und sie erschweren auch nicht den Testvorgang. Sie sollten aber jedes Mal, wenn eine Klasse instanziiert wird, überlegen, ob es sinnvoll ist, vordefinierten Code für diese Instanz an einem bestimmten Ort zu verwenden, oder ob Sie besser festlegen sollten, dass diese Instanz als eine Abhängigkeit abgefragt wird.

Deklarieren Ihrer Abhängigkeiten

ASP.NET Core ist so konzipiert, dass Methoden und Klassen ihre jeweiligen Abhängigkeiten deklarieren und diese als Argumente anfordern. ASP.NET-Anwendungen werden in der Regel in Program.cs oder in einer Startup-Klasse eingerichtet.

Hinweis

Das vollständige Konfigurieren von Apps in Program.cs ist der Standardansatz für .NET 6 (und höher)- und Visual Studio 2022-Apps. Die Projektvorlagen wurden aktualisiert, um Ihnen den Einstieg in diesen neuen Ansatz zu erleichtern. ASP.NET Core-Projekte können weiterhin eine Startup-Klasse verwenden, falls gewünscht.

Konfigurieren von Diensten in Program.cs

Für sehr einfache Apps können Sie Abhängigkeiten mithilfe von WebApplicationBuilder direkt in der Datei Program.cs verknüpfen. Nachdem alle erforderlichen Dienste hinzugefügt wurden, wird der Builder verwendet, um die App zu erstellen.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

Konfigurieren von Diensten in Startup.cs

Die Datei Startup.cs ist selbst so konfiguriert, dass sie eine Abhängigkeitsinjektion an mehreren Punkten unterstützt. Wenn Sie eine Startup-Klasse verwenden, können Sie einen Konstruktor bereitstellen, um darüber Abhängigkeiten anzufordern:

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
    }
}

Die Klasse Startup ist insofern interessant, als dass für sie keine expliziten Typanforderungen gelten. Sie erbt weder von einer besonderen Startup-Basisklasse und implementiert auch keine bestimmte Schnittstelle. Sie können ihr optional einen Konstruktor zuweisen und beliebig viele Parameter für diesen angeben. Wenn der für Ihre Anwendung konfigurierte Webhost startet, ruft er die Startup-Klasse auf (sofern von Ihnen angewiesen) und füllt per Abhängigkeitsinjektion alle Abhängigkeiten auf, die die Startup-Klasse benötigt. Wenn Sie Parameter anfordern, die nicht in den von ASP.NET Core verwendeten Dienstcontainern konfiguriert sind, wird eine Ausnahme ausgelöst. Wenn Sie sich jedoch nur auf die Abhängigkeiten beziehen, die dem Container bekannt sind, können Sie jede beliebige Anforderung senden.

Dependency Injection ist in ASP.NET Core-Apps schon von Beginn an integriert, wenn Sie die Startinstanz erstellen. Dies bezieht sich auch auf die Startklasse. Außerdem können Sie Abhängigkeiten in der Methode Configure anfordern:

public void Configure(IApplicationBuilder app,
    IHostingEnvironment env,
    ILoggerFactory loggerFactory)
{

}

Die Methode „ConfigureServices“ stellt eine Ausnahme in Bezug auf dieses Verhalten dar, da sie nur einen Parameter des Typs IServiceCollection benötigt. Sie muss nicht unbedingt Abhängigkeitsinjektion (DI) unterstützen, da sie einerseits dafür verantwortlich ist, dem Dienstcontainer Objekte hinzuzufügen, und andererseits über den IServiceCollection-Parameter Zugriff auf alle zu diesem Zeitpunkt konfigurierten Dienste hat. Daher können Sie in der gesamten Startup-Klasse mit Abhängigkeiten arbeiten, die in der ASP.NET Core-Dienstauflistung definiert sind, indem Sie entweder den benötigten Dienst als Parameter anfordern oder mit IServiceCollection in ConfigureServices arbeiten.

Hinweis

Wenn Sie sicherstellen müssen, dass bestimmte Dienste für Ihre Startup-Klasse verfügbar sind, können Sie diese mithilfe von IWebHostBuilder und der Methode ConfigureServices im CreateDefaultBuilder-Aufruf konfigurieren.

Bei der Startklasse handelt es sich um ein Modell zur Vorgehensweise bei der Strukturierung anderer Bestandteile Ihrer ASP.NET Core-Anwendung – angefangen bei Controllern über Middleware bis hin zu Filtern Ihrer eigenen Dienste. Auf jeden Fall sollten Sie das Prinzip der expliziten Abhängigkeiten beachten und besser Ihre Abhängigkeiten anfordern als sie direkt zu erstellen sowie in der gesamten Anwendung Dependency Injection nutzen. Achten Sie darauf, wo und wie Sie Implementierungen direkt instanziieren, insbesondere wenn es um Dienste und Objekte geht, die mit der Infrastruktur arbeiten und Nebenwirkungen haben. Sie sollten besser mit Abstraktionen arbeiten, die im Anwendungskern definiert sind und als Argumente an vordefinierte Verweise auf bestimmte Implementierungstypen übergeben wurden.

Strukturieren der Anwendung

Monolithische Anwendungen verfügen in der Regel über einen festgelegten Einstiegspunkt. Bei ASP.NET Core-Webanwendungen stellt das ASP.NET Core-Webprojekt den Einstiegspunkt dar. Das bedeutet jedoch nicht, dass die Projektmappe aus nur einem Projekt bestehen sollte. Sie sollten die Anwendung in verschiedene Schichten unterteilen, um dem Prinzip „Separation of Concerns“ zu folgen. Wenn Sie dies tun, sollten Sie Ordner erstellen, um Projekte voneinander zu trennen und um so die Kapselung zu verbessern. Der beste Ansatz, um diese Ziele mit einer ASP.NET Core-Anwendung zu erreichen, ist eine Abwandlung der in Kapitel 5 erläuterten sogenannten „Clean Architecture“. Wenn Sie diesem Ansatz folgen, besteht die Projektmappe dieser Anwendung aus separaten Bibliotheken für die Benutzeroberfläche, die Infrastruktur und das ApplicationCore-Projekt.

Neben diesen Projekten sind dann auch Testprojekte in der Anwendung enthalten (Informationen zum Testen finden Sie in Kapitel 9).

Das Objektmodell und die Schnittstellen der Anwendung sollten im ApplicationCore-Projekt enthalten sein. Dann hat das Projekt so wenige Abhängigkeiten wie möglich (und ist auch nicht von einer bestimmten Infrastruktur abhängig), und die weiteren Projekte in der Projektmappe verweisen auf das Projekt. Sowohl Geschäftsentitäten, die beibehalten werden sollen, als auch Dienste, die nicht direkt von der Infrastruktur abhängig sind, werden im ApplicationCore-Projekt definiert.

Im Infrastrukturprojekt sind Informationen zur Implementierung enthalten. Diese umfassen Hinweise zur Vorgehensweise im Hinblick auf die Persistenz und beim möglichen Senden von Benachrichtigungen an den Benutzer. Dieses Projekt verweist dann zwar auf implementierungsspezifische Pakete wie Entity Framework Core, sollte aber keine Informationen zu diesen Implementierungen außerhalb des Projekts verfügbar machen. Infrastrukturdienste und Repositorys sollten Schnittstellen implementieren, die im ApplicationCore-Projekt definiert sind. Über die Persistenzimplementierungen dieses Projekts werden darin implementierte Entitäten abgerufen und gespeichert.

Das ASP.NET Core-Benutzeroberflächenprojekt ist zwar verantwortlich für jegliche Aspekte im Hinblick auf die Benutzeroberflächenebene, es sollte jedoch keine Geschäftslogik oder Informationen zur Infrastruktur enthalten. Bestenfalls sollte es sogar unabhängig vom Infrastrukturprojekt sein. Dadurch wird sichergestellt, dass nicht versehentlich eine Abhängigkeit zwischen zwei Projekten hergestellt wird. Dies erreichen Sie, indem Sie einen Dependency Injection-Container eines Drittanbieters wie Autofac verwenden. Damit können Sie Regeln für die einzelnen Projekte zu Dependency Injection in Module-Klassen festlegen.

Alternativ können Sie auch festlegen, dass die Anwendung Microservices aufruft, die möglicherweise in individuellen Docker-Containern bereitgestellt werden, um die Anwendung von Informationen zur Implementierung zu entkoppeln. Dadurch wird das Prinzip „Separation of Concerns“ noch deutlicher eingehalten, und es wird eine bessere Entkopplung vorgenommen als bei der Verwendung von Dependency Injection zwischen zwei Projekten. Allerdings ist diese Methode auch komplexer.

Organisieren von Features

Standardmäßig stellen ASP.NET Core-Anwendungen ihre eigene Ordnerstruktur her, die Controller, Ansichten und häufig auch ViewModels umfasst. Clientseitiger Code, der diese Strukturen auf Serverseite unterstützen soll, wird in der Regel separat im wwwroot-Ordner gespeichert. Es kann jedoch sein, dass bei größeren Anwendungen im Zusammenhang mit dieser Ordnerstruktur Probleme auftreten, da Sie häufig zwischen diesen Ordnern hin- und herwechseln müssen, wenn Sie an einem bestimmten Feature arbeiten. Je mehr Dateien und Unterordner in einem Ordner gespeichert werden, desto schwieriger wird dies, und desto mehr müssen Sie im Projektmappen-Explorer scrollen. Wenn Sie dieses Problem vermeiden möchten, können Sie Anwendungscode anstatt nach Dateityp nach Feature ordnen. Diese Strukturierung wird häufig als Featureordner oder Feature Slices bezeichnet (siehe auch: Vertical Slices (Vertikale Slices)).

In diesem Zusammenhang unterstützt ASP.NET Core MVC die Verwendung verschiedener Bereiche. Wenn Sie verschiedene Bereiche verwenden, können Sie verschiedene separate Ordner für Controller und Ansichten (sowie für jegliche zugeordneten Modelle) in jedem Bereichsordner erstellen. In Abbildung 7-1 wird eine Ordnerstruktur dargestellt, in der Bereiche verwendet werden.

Beispielstruktur mit Bereichen

Abbildung 7-1. Beispielstruktur mit Bereichen

Wenn Sie Bereiche verwenden, müssen Sie Attribute verwenden, um Ihre Controller mit den Namen der Bereiche zu versehen, zu denen sie gehören:

[Area("Catalog")]
public class HomeController
{}

Außerdem müssen Sie die Bereichsunterstützung zu Ihren Routen hinzufügen:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(name: "areaRoute", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
    endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});

Neben der bereits integrierten Unterstützung von Bereichen können Sie auch ihre eigene Ordnerstruktur und Konventionen anstelle von Attributen und benutzerdefinierten Routen verwenden. Dadurch können Sie über Featureordner verfügen, die keine separaten Ordner für Ansichten, Controller usw. umfassen. Dadurch bleibt die Hierarchie flach, und es werden alle verwandten Dateien für ein Feature gleichzeitig an einem Ort angezeigt. Für APIs können Ordner verwendet werden, um Controller zu ersetzen, und jeder Ordner kann alle API-Endpunkte und die zugehörigen DTOs enthalten.

ASP.NET Core verwendet integrierte Konventionstypen, um das Verhalten dieses Frameworks zu kontrollieren. Sie können diese Konventionen verändern oder ersetzen. Sie können beispielsweise eine Konvention erstellen, die automatisch anhand des Namespace, der in der Regel mit dem Ordner korreliert, in dem der Controller platziert ist, den Featurenamen für einen bestimmten Controller abruft:

public class FeatureConvention : IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        controller.Properties.Add("feature",
        GetFeatureName(controller.ControllerType));
    }

    private string GetFeatureName(TypeInfo controllerType)
    {
        string[] tokens = controllerType.FullName.Split('.');
        if (!tokens.Any(t => t == "Features")) return "";
        string featureName = tokens
            .SkipWhile(t => !t.Equals("features", StringComparison.CurrentCultureIgnoreCase))
            .Skip(1)
            .Take(1)
            .FirstOrDefault();
        return featureName;
    }
}

Geben Sie dann diese Konvention als eine Option an, wenn Sie Ihrer Anwendung in ConfigureServices (oder in Program.cs) MVC-Unterstützung hinzufügen:

// ConfigureServices
services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));

// Program.cs
builder.Services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));

ASP.NET Core MVC verwendet außerdem eine Konvention, um Ansichten zu finden. Sie können diese Konvention mit einer benutzerdefinierten Konvention überschreiben, indem Sie den obenstehend unter FeatureConvention angegebenen Featurenamen verwenden, damit in Ihren Featureordnern Ansichten gefunden werden. Weitere Informationen zu diesem Ansatz finden Sie im MSDN Magazine-Artikel ASP.NET Core: Feature Slices für ASP.NET Core MVC. Auf dieser Seite können Sie auch ein Beispiel herunterladen.

APIs und Blazor-Anwendungen

Wenn Ihre Anwendung mehrere Web-APIs enthält, die geschützt werden müssen, sollten diese im Idealfall als separates Projekt von Ihrer Ansichts- oder Razor Pages-Anwendung konfiguriert werden. Das Trennen von APIs (insbesondere von öffentlichen APIs) von der serverseitigen Webanwendung hat mehrere Vorteile. Diese Anwendungen weisen häufig eindeutige Bereitstellungs- und Lastmerkmale auf. Es ist auch sehr wahrscheinlich, dass unterschiedliche Sicherheitsmechanismen verwendet werden, bei denen formularbasierte Standardanwendungen die cookiebasierte Authentifizierung und APIs verwenden, die wahrscheinlich eine tokenbasierte Authentifizierung nutzen.

Außerdem sollten Blazor-Anwendungen unabhängig von der Verwendung von Blazor Server oder BlazorWebAssembly als separate Projekte erstellt werden. Die Anwendungen haben unterschiedliche Runtimeeigenschaften und Sicherheitsmodelle. Sie verwenden wahrscheinlich gemeinsame gängige Typen für die serverseitige Webanwendung (oder das API-Projekt), und diese Typen sollten in einem gängigen freigegebenen Projekt definiert werden.

Wenn Sie eine BlazorWebAssembly-Administratorbenutzeroberfläche zu eShopOnWeb hinzufügen, erfordert dies auch das Hinzufügen mehrerer neuer Projekte. Dazu gehört mit BlazorAdmin auch das BlazorWebAssembly-Projekt selbst. Im PublicApi-Projekt werden mehrere neue API-Endpunkte definiert, die von BlazorAdmin verwendet und für die Verwendung der tokenbasierten Authentifizierung konfiguriert werden. Bestimmte freigegebene Typen, die von beiden Projekten verwendet werden, werden in dem neuen BlazorShared-Projekt gespeichert.

Nun kommt möglicherweise die Frage auf, warum ein separates BlazorShared-Projekt hinzugefügt wird, wenn bereits ein gemeinsames ApplicationCore-Projekt vorhanden ist, das verwendet werden kann, um alle für PublicApi und BlazorAdmin erforderlichen Typen freizugeben. Dies liegt daran, dass dieses Projekt die gesamte Geschäftslogik der Anwendung enthält und somit wesentlich größer als notwendig ist, weshalb auch der Schutz auf dem Server notwendig ist. Beachten Sie, dass jede Bibliothek, auf die BlazorAdmin verweist, beim Laden der Blazor-Anwendung über die Browser der Benutzer heruntergeladen wird.

Abhängig davon, ob das „Back-Ends für Front-Ends“-Muster (BFF) verwendet wird, dürfen die von der BlazorWebAssembly-App genutzten APIs ihre Typen nicht zu 100 % mit Blazor teilen. Insbesondere kann eine öffentliche API, die von vielen verschiedenen Clients verwendet werden soll, eigene Anforderungs- und Ergebnistypen definieren, anstatt diese in einem clientspezifischen freigegebenen Projekt freizugeben. Im eShopOnWeb-Beispiel wird davon ausgegangen, dass das PublicApi-Projekt tatsächlich eine öffentliche API hostet, sodass nicht alle Anforderungs- und Antworttypen aus dem BlazorShared-Projekt stammen.

Übergreifende Belange

Je größer die Anwendungen werden, desto wichtiger ist es, übergreifende Belange zu vermeiden, um Duplizierungen zu vermeiden und Konsistenz zu gewährleisten. Unter übergreifenden Belangen für ASP.NET Core-Anwendungen sind u.a. die Authentifizierung, Modellvalidierungsregeln, Ausgabezwischenspeicherung und die Fehlerbehandlung zu verstehen. In ASP.Net Core MVC ermöglichen Filter Ihnen, Code vor oder nach bestimmten Schritten der Anforderungsverarbeitungspipeline auszuführen. Beispielsweise kann ein Filter sowohl vor als auch nach der Modellbindung, einer Aktion oder dem Ergebnis einer Aktion ausgeführt werden. Sie können auch einen Autorisierungsfilter verwenden, um den Zugriff auf die restliche Pipeline zu steuern. In Abbildung 7-2 wird dargestellt, wie Sie über möglicherweise konfigurierte Filter Ausführungsflows anfordern können.

Die Anforderung wird von Autorisierungsfiltern, Ressourcenfiltern, Modellbindung, Aktionsfiltern, Aktionsausführung und Aktionsergebniskonvertierung, Ausnahmefiltern, Ergebnisfiltern und Ergebnisausführung verarbeitet. Auf dem Weg nach draußen wird die Anforderung nur von Ergebnisfiltern und Ressourcenfiltern verarbeitet, bevor sie als Antwort an den Client gesendet wird.

Abbildung 7-2. Anfordern der Ausführung über Filter und Anforderungspipeline

Filter werden in der Regel als Attribute implementiert, damit Sie sie auf Controller oder Aktionen (oder sogar global) anwenden können. Wenn auf diese Weise Filter hinzugefügt werden, überschreiben die auf Aktionsebene angegebenen Filter entweder die auf Controllerebene angegebenen Filter, oder sie bauen auf diesen Filtern auf, die selbst globale Filter überschreiben. Beispielsweise kann das Attribut [Route] verwendet werden, um Routen zwischen Controllern und Aktionen zu erstellen. Genauso kann auch die Autorisierung auf Controllerebene konfiguriert und dann von individuellen Aktionen überschrieben werden. Dies wird im folgenden Beispiel dargestellt:

[Authorize]
public class AccountController : Controller
{
    [AllowAnonymous] // overrides the Authorize attribute
    public async Task<IActionResult> Login() {}
    public async Task<IActionResult> ForgotPassword() {}
}

Die erste Methode (Login) verwendet den [AllowAnonymous]-Filter (Attribut), um den auf Controllerebene festgelegten Authorize-Filter außer Kraft zu setzen. Die ForgotPassword-Aktion (sowie jede andere Aktion in der Klasse, die kein AllowAnonymous-Attribut besitz), erfordert eine authentifizierte Anforderung.

Filter können verwendet werden, um Duplizierungen in Form von allgemeinen Richtlinien zur Fehlerbehandlung für APIs zu vermeiden. Eine typische API-Richtlinie soll z.B. eine NotFound-Antwort auf Anforderungen zurückgeben, die auf nicht vorhandene Schlüssel verweisen. Wenn die Modellvalidierung fehlschlägt, soll hingegen eine BadRequest-Anforderung zurückgegeben werden. Das folgende Beispiel veranschaulicht die Verwendung dieser beiden Richtlinien:

[HttpPut("{id}")]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
    if ((await _authorRepository.ListAsync()).All(a => a.Id != id))
    {
        return NotFound(id);
    }
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    author.Id = id;
    await _authorRepository.UpdateAsync(author);
    return Ok();
}

Vermeiden Sie, dass Ihre Aktionsmethoden mit bedingtem Code wie diesem überladen werden. Pullen Sie die Richtlinien stattdessen in Filter, die nach Bedarf angewendet werden können. In diesem Beispiel kann die Modellvalidierung, die jedes Mal erfolgen sollte, wenn ein Befehl an die API gesendet wird, durch das folgende Attribut ersetzt werden:

public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(context.ModelState);
        }
    }
}

Sie können ValidateModelAttribute als NuGet-Abhängigkeit zu Ihrem Projekt hinzufügen, indem Sie das Paket Ardalis.ValidateModel einfügen. Für APIs können Sie das ApiController-Attribut verwenden, um dieses Verhalten zu erzwingen, ohne einen separaten ValidateModel-Filter zu verwenden.

Ebenso kann ein Filter verwendet werden, um zu überprüfen, ob ein Datensatz vorhanden ist, und um den Fehler 404 zurückzugeben, bevor die Aktion ausgeführt wird. Deshalb müssen diese Überprüfungen nicht mehr innerhalb der Aktion durchgeführt werden. Wenn Sie häufig verwendete Konventionen entfernt haben und Ihre Projektmappe so geordnet haben, dass Infrastrukturcode und Geschäftslogik von Ihrer Benutzeroberfläche getrennt sind, sollten Ihre MVC-Aktionsmethoden sehr dünn sein:

[HttpPut("{id}")]
[ValidateAuthorExists]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
    await _authorRepository.UpdateAsync(author);
    return Ok();
}

Weitere Informationen zum Implementieren von Filtern und ein Arbeitsbeispiel zum Herunterladen finden Sie im MSDN Magazine-Artikel ASP.NET Core – ASP.NET Core MVC-Filter in der Praxis.

Wenn Sie feststellen, dass Sie häufig allgemeine API-Antworten erhalten, die auf gängigen Szenarien wie Validierungsfehlern (Ungültige Anforderung), nicht gefundenen Ressourcen und Serverfehlern beruhen, können Sie die Verwendung einer Ergebnisabstraktion in Betracht ziehen. Die Ergebnisabstraktion wird von Diensten zurückgegeben, die von API-Endpunkten genutzt werden, und die Controlleraktion oder der Endpunkt verwendet einen Filter für die Übersetzung in IActionResults.

Ressourcen: Strukturieren von Anwendungen

Sicherheit

Das Sichern von Webanwendungen stellt ein umfangreiches und komplexes Thema dar. Im Grunde genommen müssen Sie beim Thema Sicherheit sicherstellen, dass Sie wissen, wer eine Anforderung sendet. Dann müssen Sie sicherstellen, dass die Anforderung nur Zugriff auf die Ressourcen hat, auf die sie Zugriff haben sollte. Im Rahmen der Authentifizierung werden Anmeldeinformationen miteinander verglichen, die zusammen mit einer Anforderung an die Anmeldeinformationen in einem vertrauenswürdigen Datenspeicher bereitgestellt werden, um zu überprüfen, ob die Anforderung behandelt werden soll, als würde sie von einer bekannten Entität gesendet werden. Bei der Autorisierung wird auf der Grundlage der Benutzeridentität der Zugriff auf bestimmte Ressourcen eingeschränkt. Ein weiterer Punkt im Hinblick auf die Sicherheit ist das Schützen von Anforderungen vor Lauschangriffen durch Drittanbieter. Dafür sollten Sie sicherstellen, dass Ihre Anwendung zumindest SSL verwendet.

Identity

Bei ASP.NET Core Identity handelt es sich um ein Mitgliedschaftssystem, das Sie verwenden können, um die Anmeldefunktionen für Ihre Anwendung zu unterstützen. Dieses System unterstützt sowohl lokale Benutzerkonten als auch die Unterstützung von externen Protokollanbietern wie Microsoft Account, Twitter, Facebook und Google. Neben ASP.NET Core Identity kann Ihre Anwendung auch die Windows-Authentifizierung oder einen Identitätsanbieter eines Drittanbieters wie Identity Server verwenden.

ASP.NET Core Identity ist in neuen Projektvorlagen enthalten, wenn die Option „Einzelne Benutzerkonten“ aktiviert ist. Diese Vorlage umfasst die Unterstützung der Registrierung, Anmeldung, von externen Anmeldungen, vergessenen Kennwörtern und von zusätzlichen Funktionen.

Auswählen einzelner Benutzerkonten zur Vorabkonfiguration von Identity

Abbildung 7-3. Auswählen einzelner Benutzerkonten zur Vorabkonfiguration von Identity.

Die Identitätsunterstützung wird in Program.cs oder Startup konfiguriert und umfasst die Konfiguration von Diensten sowie Middleware.

Konfigurieren von Identity in Program.cs

In Program.cs konfigurieren Sie Dienste aus der WebHostBuilder-Instanz, und sobald die App erstellt wurde, konfigurieren Sie die zugehörige Middleware. Die wichtigsten Punkte, die Sie beachten müssen, sind der Aufruf von AddDefaultIdentity für erforderliche Dienste und die UseAuthentication- und UseAuthorization-Aufrufe zum Hinzufügen der erforderlichen Middleware.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
  app.UseExceptionHandler("/Error");
  // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
  app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.Run();

Konfigurieren von Identity beim Starten der App

// Add framework services.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();
builder.Services.AddMvc();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

Es ist wichtig, dass UseAuthentication und UseAuthorization vor MapRazorPages erscheinen. Beim Konfigurieren von Identity-Diensten werden Sie einen AddDefaultTokenProviders-Aufruf bemerken. Dies hat nichts mit Token zu tun, die möglicherweise verwendet werden, um die Webkommunikation zu sichern. Stattdessen bezieht es sich auf Anbieter, die Aufforderungen erstellen, die per SMS oder E-Mail an Benutzer gesendet werden können, damit diese ihre Identität bestätigen können.

Weitere Informationen zum Konfigurieren der zweistufigen Authentifizierung und zum Zulassen von externen Anmeldungsanbietern finden Sie in der offiziellen ASP.NET Core-Dokumentation.

Authentifizierung

Als Authentifizierung wird der Prozess bezeichnet, mit dem bestimmt wird, wer auf das System zugreift. Wenn Sie ASP.NET Core Identity und die im vorherigen Abschnitt gezeigten Konfigurationsmethoden verwenden, werden in der Anwendung automatisch einige Standardeinstellungen für die Authentifizierung konfiguriert. Sie können diese Standardeinstellungen jedoch auch manuell konfigurieren oder diejenigen überschreiben, die von AddIdentity festgelegt wurden. Wenn Sie eine Identität verwenden, wird die cookiebasierte Authentifizierung als Standardschema konfiguriert.

Bei der webbasierten Authentifizierung gibt es in der Regel bis zu fünf Aktionen, die möglicherweise im Verlauf der Authentifizierung eines Systemclients ausgeführt werden. Dies sind:

  • Authentifizieren: Verwenden Sie die vom Client bereitgestellten Informationen, um eine Identität zu erstellen, die in der Anwendung verwendet werden kann.
  • Abfragen: Diese Aktion wird verwendet, um die Clients zu identifizieren.
  • Unterbinden: Informieren Sie den Client, dass die Ausführung einer Aktion untersagt ist.
  • Anmelden: Behalten Sie den vorhandenen Client auf irgendeine Weise bei.
  • Abmelden: Entfernen Sie den Client aus dem Persistenzspeicher.

Es gibt eine Reihe allgemeiner Techniken zum Durchführen der Authentifizierung in Webanwendungen. Diese werden als Schemas bezeichnet. Ein bestimmtes Schema definiert Aktionen für einige oder alle der oben aufgeführten Optionen. Einige Schemas unterstützen nur manche Aktionen und erfordern ggf. ein separates Schema, um nicht unterstützte Schemas auszuführen. Das OIDC-Schema (OpenId-Connect) unterstützt beispielsweise weder die Anmeldung noch die Abmeldung, wird allerdings häufig für die Verwendung der Cookieauthentifizierung für diese Persistenz konfiguriert.

In Ihrer ASP.NET Core-Anwendung können Sie für jede der oben beschriebenen Aktionen eine DefaultAuthenticateScheme-Eigenschaft und optionale spezifische Schemas konfigurieren. Beispiel: DefaultChallengeScheme und DefaultForbidScheme. Durch Aufrufen von AddIdentity wird eine Reihe von Anwendungsaspekten konfiguriert, und es werden viele erforderliche Dienste hinzugefügt. Außerdem ist dieser Aufruf zum Konfigurieren des Authentifizierungsschemas enthalten:

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
    options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
    options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});

Diese Schemas verwenden Cookies für Persistenz und die Umleitung an Anmeldeseiten, die standardmäßig für die Authentifizierung verwendet werden. Diese Schemas sind für Webanwendungen geeignet, die über Webbrowser mit Benutzern interagieren, aber nicht für APIs empfohlen werden. Stattdessen verwenden APIs in der Regel eine andere Form der Authentifizierung (z. B. JWT-Bearertoken).

Web-APIs werden von Code wie beispielsweise HttpClient in .NET-Anwendungen und äquivalenten Typen in anderen Frameworks genutzt. Diese Clients erwarten eine verwendbare Antwort von einem API-Aufruf oder einen Statuscode, der ggf. angibt, welches Problem aufgetreten ist. Diese Clients interagieren nicht über einen Browser und können keinen von einer API zurückgegebenen HTML-Code rendern oder mit diesem interagieren. Daher eignet es sich im Zusammenhang mit API-Endpunkten nicht, deren Clients an Anmeldeseiten umzuleiten, wenn sie nicht authentifiziert sind. Ein anderes Schema ist besser geeignet.

Sie können eine Authentifizierung wie folgt einrichten, um die Authentifizierung für APIs zu konfigurieren, die vom PublicApi-Projekt in der eShopOnWeb-Referenzanwendung verwendet wird:

builder.Services
    .AddAuthentication(config =>
    {
      config.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(config =>
    {
        config.RequireHttpsMetadata = false;
        config.SaveToken = true;
        config.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false
        };
    });

Obwohl es möglich ist, mehrere verschiedene Authentifizierungsschemas innerhalb eines einzelnen Projekts zu konfigurieren, ist es viel einfacher, ein einzelnes Standardschema zu konfigurieren. Aus diesem Grund trennt die eShopOnWeb-Referenzanwendung ihre APIs unter anderem in ihr eigenes Projekt PublicApi, das vom Hauptprojekt Web getrennt ist und die Ansichten und Razor Pages der Anwendung enthält.

Authentifizierung in Blazor-Apps

Blazor Server-Anwendungen können dieselben Authentifizierungsfeatures wie jede andere ASP.NET Core-Anwendung nutzen. BlazorWebAssembly-Anwendungen können die integrierten Identitäts- und Authentifizierungsanbieter nicht verwenden, da diese im Browser ausgeführt werden. BlazorWebAssembly-Anwendungen können den Benutzerauthentifizierungsstatus lokal speichern und auf Ansprüche zugreifen, um zu bestimmen, welche Aktionen von Benutzern ausgeführt werden können. Allerdings sollten alle Authentifizierungs- und Autorisierungsprüfungen unabhängig von der in der BlazorWebAssembly-App implementierten Logik auf dem Server ausgeführt werden, da Benutzer die App problemlos umgehen und direkt mit den APIs interagieren können.

Verweise: Authentifizierung

Autorisierung

Die einfachste Art der Authentifizierung umfasst die Einschränkung des Zugriffs auf anonyme Benutzer. Diese Funktionalität können Sie einrichten, indem Sie das Attribut [Authorize] auf bestimmte Controller oder Aktionen anwenden. Wenn Rollen verwendet werden, kann das Attribut wie im Folgenden dargestellt erweitert werden, um den Zugriff auf Benutzer zu beschränken, denen bestimmte Rollen zugewiesen sind:

[Authorize(Roles = "HRManager,Finance")]
public class SalaryController : Controller
{

}

In diesem Fall verfügen Benutzer mit der Rolle HRManager oder Finance (oder mit beiden Rollen) über Zugriff auf „SalaryController“. Wenn Sie erfordern möchten, dass ein Benutzer mehreren Rollen statt nur einer von mehrern Rollen angehört, können Sie das Attribut mehrmals anwenden und dabei jeweils eine erforderliche Rolle angeben.

Wenn Sie in vielen verschiedenen Controllern und Aktionen bestimmte Rollen als Zeichenfolgen festlegen, kann dies zu ungewollten Wiederholungen führen. Definieren Sie mindestens Konstanten für diese Zeichenfolgenliterale, und verwenden Sie die benötigten Konstanten, um die Zeichenfolge anzugeben. Sie können auch Autorisierungsrichtlinien konfigurieren, die Autorisierungsregeln kapseln, und anschließend die Richtlinie anstelle von individuellen Rollen angeben, wenn Sie das Attribut [Authorize] anwenden:

[Authorize(Policy = "CanViewPrivateReport")]
public IActionResult ExecutiveSalaryReport()
{
    return View();
}

Wenn Sie auf diese Weise Richtlinien verwenden, können Sie die Arten von Aktionen unterteilen, die auf bestimmte Rollen und Regeln beschränkt sind, die für diese gelten. Wenn Sie später eine neue Rolle erstellen, die Zugriff auf bestimmte Ressourcen benötigt, können Sie einfach eine Richtlinie aktualisieren und müssen nicht mehr jede Liste mit Rollen für jedes [Authorize]-Attribut aktualisieren.

Ansprüche

Ansprüche sind Name-Wert-Paare, die Eigenschaften eines authentifizierten Benutzers darstellen. Möglicherweise möchten Sie die Personalnummer eines Benutzers als Anspruch speichern. Ansprüche können als Bestandteil von Autorisierungsrichtlinien verwendet werden. Daher können Sie eine Richtlinie mit dem Namen „EmployeeOnly“ erstellen, die, wie im folgenden Beispiel dargestellt, einen Anspruch mit dem Namen "EmployeeNumber" erfordert:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
    });
}

Diese Richtlinie kann dann mit dem Attribut [Authorize] verwendet werden, um wie obenstehend beschrieben einen Controller und/oder eine Aktion zu schützen.

Sichern von Web-APIs

Die meisten Web-APIs sollten ein tokenbasiertes Authentifizierungssystem implementieren. Die Tokenauthentifizierung ist zustandslos und skalierbar. In einem tokenbasierten Authentifizierungssystem muss sich der Client zuerst mit dem Authentifizierungsanbieter authentifizieren. Wenn dies erfolgreich ist, wird für den Client ein Token ausgestellt, bei dem es sich um eine kryptografische, sinnvolle Zeichenfolge handelt. Das gängigste Format für Token ist JSON Web Token oder JWT (häufig „Jot“ ausgesprochen). Wenn der Client dann eine Anforderung an eine API durchführen muss, fügt er dieses Token als Header der Anforderung hinzu. Der Server überprüft dann das im Anforderungsheader gefundene Token, bevor er die Anforderung abschließt. Abbildung 7-4 zeigt diesen Vorgang.

TokenAuth

Abbildung 7.4 Tokenbasierte Authentifizierung für Web-APIs.

Sie können einen eigenen Authentifizierungsdienst erstellen, Ihre API in Azure AD und OAuth integrieren oder einen Dienst über ein Open-Source-Tool wie z. B. IdentityServer implementieren.

JWT-Token können mit dem Benutzer verbundene Ansprüche einbetten, die auf dem Client oder Server gelesen werden können. Sie können ein Tool wie jwt.io verwenden, um den Inhalt eines JWT-Tokens anzuzeigen. Speichern Sie keine vertraulichen Daten wie Kennwörter oder Schlüssel in einem JTW-Token, da dessen Inhalte leicht lesbar sind.

Wenn Sie JWT-Token mit Single-Page-Webanwendung oder BlazorWebAssembly-Anwendungen verwenden, müssen Sie das Token an einer beliebigen Stelle auf dem Client speichern und dann jedem API-Aufruf hinzufügen. Diese Aktivität erfolgt in der Regel als Header. Dies wird im folgenden Code veranschaulicht:

// AuthService.cs in BlazorAdmin project of eShopOnWeb
private async Task SetAuthorizationHeader()
{
      var token = await GetToken();
      _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}

Nach dem Aufrufen der obigen Methode wird das Token bei mit _httpClient ausgeführten Anforderungen in den Headern der Anforderung eingebettet, sodass die serverseitige API die Anforderung authentifizieren und autorisieren kann.

Benutzerdefinierte Sicherheit

Achtung

Vermeiden Sie im Allgemeinen die Implementierung eigener benutzerdefinierter Sicherheitsimplementierungen.

Gehen Sie insbesondere mit Bedacht vor, wenn Sie eigene Kryptografien, Benutzermitgliedschaften oder Systeme zur Tokengenerierung implementieren. Es gibt viele im Handel erwerbliche oder Open Source-Alternativen, deren Sicherheit die einer benutzerdefinierten Implementierung in den meisten Fällen übertrifft.

Ressourcen: Sicherheit

Clientkommunikation

ASP.NET Core-Apps können nicht nur Seiten bereitstellen und über Web-APIs auf Anforderungen für Daten antworten, sondern auch direkt mit verbundenen Clients kommunizieren. Für diese ausgehende Kommunikation können verschiedene Transporttechnologien verwendet werden, wobei am häufigsten die Technologie WebSockets verwendet wird. ASP.NET Core SignalR ist eine Bibliothek, mit der Sie Ihren Anwendungen leicht Funktionen für die Echtzeitkommunikation zwischen Server und Client hinzufügen können. SignalR unterstützt verschiedene Transporttechnologien, einschließlich WebSockets, und nimmt dem Entwickler einen Großteil der Implementierungsdetails ab.

Die Clientkommunikation in Echtzeit erweist sich in vielen Anwendungsszenarios als nützlich. Dabei macht es keinen Unterschied, ob Sie WebSockets oder andere Methoden verwenden. Beispiele:

  • Anwendungen für Livechats

  • Überwachen von Anwendungen

  • Updates zum Auftragsstatus

  • Benachrichtigungen

  • Interaktive Forms-Anwendungen

Wenn Sie die Clientkommunikation in Ihre Anwendungen integrieren, gibt es in der Regel zwei Komponenten:

  • Einen serverseitigen Verbindungs-Manager (SignalR-Hub, WebSocketManager und WebSocketHandler)

  • Eine clientseitige Bibliothek

Clients sind nicht auf Browser beschränkt, denn auch mobile Apps, Konsolen-Apps und andere native Apps können mithilfe von SignalR/WebSockets kommunizieren. Das folgende einfache Programm ist Bestandteil einer WebSocketManager-Beispielanwendung und gibt jeglichen Inhalt, der an eine Chat-Anwendung gesendet wird, an die Konsole weiter:

public class Program
{
    private static Connection _connection;
    public static void Main(string[] args)
    {
        StartConnectionAsync();
        _connection.On("receiveMessage", (arguments) =>
        {
            Console.WriteLine($"{arguments[0]} said: {arguments[1]}");
        });
        Console.ReadLine();
        StopConnectionAsync();
    }

    public static async Task StartConnectionAsync()
    {
        _connection = new Connection();
        await _connection.StartConnectionAsync("ws://localhost:65110/chat");
    }

    public static async Task StopConnectionAsync()
    {
        await _connection.StopConnectionAsync();
    }
}

Ziehen Sie Möglichkeiten in Betracht, über die Anwendungen direkt mit Clientanwendungen kommunizieren können, und prüfen Sie, ob die Kommunikation in Echtzeit die Benutzerfreundlichkeit der App verbessern würde.

Ressourcen: Clientkommunikation

Sollten Sie die Methode „Domain-Driven Design“ verwenden?

Bei der Methode Domain-Driven Design (DDD) handelt es sich um einen nützlichen Ansatz zum Erstellen von Software, bei dem sich der Entwickler auf die Geschäftsdomäne konzentriert. Außerdem wird der Fokus auf Kommunikation und Interaktion mit Experten für Geschäftsdomänen gelegt, die den Entwicklern im Zusammenhang erläutern können, wie das System in der Praxis funktionieren soll. Wenn Sie z.B. ein System für den Aktienhandel erstellen, sollten Sie sich an einen erfahrenen Börsenmakler als Experten für die Geschäftsdomäne wenden. DDD soll dazu dienen, große, komplexe Geschäftsprobleme anzugehen und eignet sich häufig nicht für kleinere, einfachere Anwendungen, da es sich für diese nicht lohnt, viel Zeit darein zu investieren, die Domäne zu verstehen und zu modellieren.

Wenn Sie Software anhand der DDD-Technik erstellen, sollte Ihr Team (das auch Projektbeteiligte und Mitwirkende außerhalb des technischen Bereichs umfasst) eine ubiquitäre Sprache für den Problembereich entwickeln. Das bedeutet, das für das praxisorientierte Konzept, das modelliert wird, dessen Softwareäquivalent und jegliche andere Strukturen, die möglicherweise zu dem Konzept beitragen (z.B. Datentabellen), dieselbe Terminologie verwendet werden soll. Die in der ubiquitären Sprache beschriebenen Konzepte sollen die Grundlage für Ihr Domänenmodell darstellen.

Ihr Domänenmodell besteht aus Objekten, die miteinander interagieren, um das Verhalten des Systems darzustellen. Diese Objekte können in die folgenden Kategorien eingeteilt werden:

  • Entitäten, die Objekte mit Identitätsthreads darstellen. Entitäten werden in der Regel dauerhaft mit einem Schlüssel gespeichert, über den sie zu einem späteren Zeitpunkt wieder abgerufen werden können.

  • Aggregate, die Objektgruppen darstellen, die als eine Einheit beibehalten werden sollten.

  • Wertobjekte, die Konzepte darstellen, die auf der Grundlage der Summe ihrer Eigenschaftswerte miteinander verglichen werden können. Beispielsweise das DateRange-Objekt, das aus einem Start- und einem Enddatum besteht.

  • Domänenereignisse, die Vorgänge darstellen, die innerhalb des System ausgeführt werden und für andere Bestandteile des Systems von Bedeutung sind.

Ein DDD-Domänenmodell muss komplexes Verhalten innerhalb des Modells kapseln. Insbesondere Entitäten sollten nicht nur Auflistungen von Eigenschaften sein. Wenn das Domänenmodell kein Verhalten aufweist und nur den Status des Systems darstellt, wird es als anämisches Modell bezeichnet. Diese Art von Modellen sollten in Verbindung mit DDD nicht auftreten.

Neben diesen Modelltypen verwendet DDD in der Regel verschiedene Muster:

  • Repository zum Zusammenfassen von Informationen zur Persistenz.

  • Factory zum Kapseln der Erstellung von komplexen Objekten.

  • Dienste zum Kapseln von komplexem Verhalten und/oder Informationen zur Implementierung der Infrastruktur.

  • Kommando zum Entkoppeln von ausgebenden Befehlen und Ausführen des Befehls selbst.

  • Spezifikation zum Entkoppeln von Abfragedetails.

Für DDD wird auch die Verwendung der bereits erwähnten Clean Architecture empfohlen, die die lose Kopplung, die Entkopplung und Code umfasst, der problemlos mithilfe von Komponententests überprüft werden kann.

Empfohlene Anwendung von DDD

DDD eignet sich besonders für große Anwendungen mit erheblicher Komplexität im geschäftlichen Bereich (nicht nur im technischen Bereich). Zum Erstellen der Anwendung wird dann das Fachwissen von Domänenexperten benötigt. Das Domänenmodell selbst sollte ein bedeutungsvolles Verhalten aufweisen, indem es Geschäftsregeln und Interaktionen darstellt und nicht nur den aktuellen Status verschiedener Datensätze aus Datenspeichern speichert und abfragt.

Nicht empfohlene Anwendung von DDD

Wenn Sie die DDD-Methode anwenden möchten, müssen Sie viel Zeit in die Modellierung, die Architektur und die Kommunikation investieren. Dies lohnt sich für kleinere Anwendungen oder CRUD-Anwendungen nicht (create/read/update/delete = Erstellen/Lesen/Aktualisieren/Löschen). Wenn Sie sich für diesen Ansatz entscheiden, dann aber feststellen, dass es sich bei Ihrem Domänenmodell um ein anämisches Modell handelt, das kein Verhalten aufweist, sollten Sie Ihre Wahl noch einmal überdenken. Entweder ist die DDD-Methode für diese Anwendung nicht notwendig, oder Sie benötigen möglicherweise Unterstützung beim Refactoring Ihrer Anwendung, um die Geschäftslogik anstatt in Ihrer Datenbank oder Benutzeroberfläche in das Domänenmodell zu kapseln.

Sie können auch einen hybriden Ansatz auswählen und DDD nur für Transaktionsbereiche bzw. komplexere Bereiche der Anwendung verwenden und für die CRUD-Bestandteile oder die schreibgeschützten Bestandteile der Anwendung eine andere Methode verwenden. Beispielsweise benötigen Sie nicht die Einschränkungen eines Aggregats, wenn Sie Daten abfragen, um einen Bericht abzufragen oder Daten für ein Dashboard zu visualisieren. Dann ist es vollkommen akzeptabel, ein separates, einfaches Lesemodell für solche Anforderungen zu verwenden.

Ressourcen: Domain-Driven Design

Bereitstellung

Unabhängig davon, wo die ASP.NET Core-Anwendung gehostet wird, besteht deren Bereitstellung aus einigen Schritten. Als Erstes muss die Anwendung veröffentlicht werden. Dafür können Sie den CLI-Befehl dotnet publish verwenden. Mit diesem Schritt wird die Anwendung kompiliert und alle für die Ausführung der Anwendung erforderlichen Dateien werden im festgelegten Ordner platziert. Wenn Sie die Bereitstellung über Visual Studio ausführen, wird dieser Schritt automatisch für Sie ausgeführt. Der Ordner „publish“ enthält EXE- und DLL-Dateien für die Anwendung und ihre Abhängigkeiten. Unabhängige Anwendungen umfassen außerdem eine Version der .NET-Runtime. Außerdem enthalten ASP.NET Core-Anwendungen Konfigurationsdateien, statische Clientobjekte und MVC-Ansichten.

Bei ASP.NET Core-Anwendungen handelt es sich um Konsolenanwendungen, die gestartet werden müssen, wenn der Server startet, und die neugestartet werden müssen, wenn die Anwendung bzw. der Server abstürzt. Wenn Sie diesen Vorgang automatisieren möchten, können Sie einen Prozess-Manager verwenden. Die am häufigsten verwendeten Prozess-Manager für ASP.NET Core sind Nginx und Apache unter Linux und IIS oder Windows Service unter Windows.

Zusätzlich zu einem Prozess-Manager können ASP.NET Core-Anwendungen einen Reverseproxyserver verwenden. Ein Reverseproxyserver empfängt HTTP-Anforderungen aus dem Internet und leitet diese nach einer vorbereitenden Verarbeitung an Kestrel weiter. Reverseproxyserver stellen eine Sicherheitsebene für die Anwendung bereit. Kestrel unterstützt das Hosten von mehreren Anwendungen auf einem Port nicht. Daher können Techniken wie Hostheader nicht verwendet werden, um das Hosten von mehreren Anwendungen auf einem Port und einer IP-Adresse zu ermöglichen.

Von Kestrel zum Internet

Abbildung 7-5. ASP.NET in Kestrel hinter einem Reverseproxyserver gehostet

Ein Reverseproxyserver kann sich außerdem als nützlich erweisen, um mehrere Anwendungen unter Verwendung von SSL/HTTPS zu sichern. In diesem Zusammenhang muss SSL nur für den Reverseproxy konfiguriert sein. Wie in Abbildung 7-6 dargestellt kann die Kommunikation zwischen dem Reverseproxyserver und Kestrel über HTTP hergestellt werden.

ASP.NET hinter einem über HTTPS gesicherten Reverseproxyserver gehostet

Abbildung 7-6. ASP.NET hinter einem über HTTPS gesicherten Reverseproxyserver gehostet

Ein immer häufiger verwendeter Ansatz ist das Hosten Ihrer ASP.NET Core-Anwendung in einem Docker-Container, der anschließend lokal gehostet oder in Azure für cloudbasiertes Hosting bereitgestellt werden kann. Dann kann der Docker-Container wie oben dargestellt Ihren Anwendungscode enthalten, der auf Kestrel ausgeführt und hinter einem Reverseproxyserver bereitgestellt wird.

Wenn Sie Ihre Anwendung auf Azure hosten, können Sie Microsoft Azure Application Gateway als reservierte virtuelle Anwendung verwenden, um verschiedene Dienste bereitzustellen. Application Gateway dient nicht nur als Reverseproxy für einzelne Anwendungen, sondern bietet auch die folgenden Features:

  • HTTP-Lastenausgleich

  • SSL-Offload (SSL nur für Internet)

  • End-to-End-SSL

  • Routing über mehrere Websites (Konsolidieren von bis zu 20 Websites für eine Application Gateway-Version)

  • Firewall der Webanwendung

  • WebSocket-Unterstützung

  • Erweiterte Diagnose

Weitere Informationen zu den Bereitstellungsoptionen für Azure finden Sie in Kapitel 10.

Ressourcen: Bereitstellung