Freigeben über


Entwickeln ASP.NET Core MVC-Apps

Tipp

Dieser Inhalt ist ein Auszug aus dem eBook, Architect Modern Web Applications mit ASP.NET Core und Azure, verfügbar auf .NET Docs oder als kostenloses herunterladbares PDF, das offline gelesen werden kann.

Miniaturansicht des E-Books „Architect Modern Web Applications with ASP.NET Core and Azure“.

Es ist nicht wichtig, es beim ersten Versuch richtig zu machen. Es ist von entscheidender Bedeutung, es das letzte Mal richtig zu bekommen." - Andrew Hunt und David Thomas

ASP.NET Core ist ein plattformübergreifendes Open-Source-Framework zum Erstellen moderner cloudoptimierter Webanwendungen. ASP.NET Core-Apps sind leicht und modular, mit integrierter Unterstützung für Abhängigkeitsinjektion und ermöglicht eine höhere Testbarkeit und Wartungsbarkeit. In Kombination mit MVC, das das Erstellen moderner Web-APIs zusätzlich zu ansichtsbasierten Apps unterstützt, ist ASP.NET Core ein leistungsstarkes Framework, mit dem Unternehmenswebanwendungen erstellt werden können.

MVC und Razor Pages

ASP.NET Core MVC bietet viele Features, die für die Erstellung webbasierter APIs und Apps nützlich sind. Der Begriff MVC steht für "Model-View-Controller", ein UI-Muster, das die Verantwortlichkeiten der Reaktion auf Benutzeranforderungen in mehrere Teile aufbricht. Zusätzlich zum Befolgen dieses Musters können Sie auch Funktionen in Ihren ASP.NET Core-Apps als Razor Pages implementieren.

Razor-Seiten sind in ASP.NET Core MVC integriert und nutzen die gleichen Funktionen für Routing, Modellbindung, Filter, Autorisierung usw. Statt jedoch separate Ordner und Dateien für Controller, Modelle, Ansichten usw. zu haben und attributbasiertes Routing zu verwenden, werden Razor-Seiten in einem einzigen Ordner ("/Pages") abgelegt, routen basierend auf ihrem relativen Speicherort in diesem Ordner und bearbeiten Anfragen mit Handlern anstelle von Controller-Aktionen. Daher sind beim Arbeiten mit Razor Pages alle benötigten Dateien und Klassen in der Regel am selben Ort gebündelt und nicht im gesamten Webprojekt verteilt.

Erfahren Sie mehr darüber , wie MVC, Razor Pages und verwandte Muster in der eShopOnWeb-Beispielanwendung angewendet werden.

Wenn Sie eine neue ASP.NET Core-App erstellen, sollten Sie einen Plan für die Art der App berücksichtigen, die Sie erstellen möchten. Beim Erstellen eines neuen Projekts wählen Sie in Ihrer IDE oder mit dem dotnet new CLI-Befehl aus mehreren Vorlagen aus. Die gängigsten Projektvorlagen sind "Empty", "Web-API", "Web App" und "Web App" (Model-View-Controller). Obwohl Sie diese Entscheidung nur treffen können, wenn Sie ein Projekt zum ersten Mal erstellen, handelt es sich nicht um eine unwiderrufliche Entscheidung. Ein Projekt für Web-APIs verwendet Standard-MVCs, enthält aber standardmäßig keine Ansichten. Ebenso verwendet die Standardmäßige Web App-Vorlage Razor Pages, und daher fehlt auch ein Views-Ordner. Sie können diesen Projekten später einen Ordner "Views" hinzufügen, um das ansichtsbasierte Verhalten zu unterstützen. Web-API und Modell-View-Controller-Projekte enthalten standardmäßig keinen Ordner "Seiten", aber Sie können später einen hinzufügen, um Verhalten auf Razor Pages-Basis zu unterstützen. Sie können sich diese drei Vorlagen als Unterstützung von drei verschiedenen Arten von Standardbenutzerinteraktion vorstellen: Daten (Web-API), seitenbasiert und ansichtsbasiert. Sie können jedoch beliebige oder alle diese Vorlagen in einem einzelnen Projekt kombinieren und abgleichen.

Warum Razor Pages?

Razor Pages ist der Standardansatz für neue Webanwendungen in Visual Studio. Razor Pages bietet eine einfachere Möglichkeit, seitenbasierte Anwendungsfeatures wie z. B. Nicht-SPA-Formulare zu erstellen. Bei Verwendung von Controllern und Ansichten war es üblich, dass Anwendungen sehr große Controller haben, die mit vielen verschiedenen Abhängigkeiten und Ansichtsmodellen gearbeitet und viele verschiedene Ansichten zurückgegeben haben. Dies führte zu einer größeren Komplexität und führte oft zu Controllern, die nicht dem Grundsatz der einheitlichen Verantwortung oder offenen/geschlossenen Prinzipien effektiv folgen. Razor Pages behebt dieses Problem, indem die serverseitige Logik für eine bestimmte logische "Seite" in einer Webanwendung mit ihrem Razor-Markup verkapselt wird. Eine Razor-Seite ohne serverseitige Logik kann nur aus einer Razor-Datei bestehen (z. B. "Index.cshtml"). Die meisten nicht trivialen Razor-Seiten haben jedoch eine zugeordnete Seitenmodellklasse, die nach Konvention denselben Namen wie die Razor-Datei trägt und die Endung ".cs" hat (z. B. "Index.cshtml.cs").

Das Seitenmodell einer Razor-Seite kombiniert die Verantwortlichkeiten eines MVC-Controllers und eines Viewmodels. Anstatt Anforderungen mit Controlleraktionsmethoden zu verarbeiten, werden Seitenmodellhandler wie "OnGet()" ausgeführt, wodurch standardmäßig die zugehörige Seite gerendert wird. Razor Pages vereinfacht den Prozess des Erstellens einzelner Seiten in einer ASP.NET Core-App und bietet weiterhin alle architektonischen Features von ASP.NET Core MVC. Sie sind eine gute Standardauswahl für neue seitenbasierte Funktionen.

Wann MVC verwendet werden soll

Wenn Sie Web-APIs erstellen, ist das MVC-Muster sinnvoller als der Versuch, Razor Pages zu verwenden. Wenn Ihr Projekt nur Web-API-Endpunkte verfügbar macht, sollten Sie idealerweise mit der Web-API-Projektvorlage beginnen. Andernfalls ist es einfach, Controller und zugeordnete API-Endpunkte einer beliebigen ASP.NET Core-App hinzuzufügen. Verwenden Sie den ansichtsbasierten MVC-Ansatz, wenn Sie eine vorhandene Anwendung von ASP.NET MVC 5 oder früher zu ASP.NET Core MVC migrieren und dies mit dem geringsten Aufwand tun möchten. Nachdem Sie die erste Migration vorgenommen haben, können Sie bewerten, ob es sinnvoll ist, Razor Pages für neue Features oder sogar als Großhandelsmigration zu übernehmen. Weitere Informationen zum Portieren von .NET 4.x-Apps zu .NET 8 finden Sie unter Portieren vorhandener ASP.NET Apps zu ASP.NET Core eBook.

Unabhängig davon, ob Sie Ihre Web-App mit Razor Pages oder MVC-Ansichten erstellen möchten, hat Ihre App eine ähnliche Leistung und unterstützt Abhängigkeitseinfügungen, Filter, Modellbindung, Validierung usw.

Zuordnen von Anforderungen zu Antworten

Im Kern ordnen ASP.NET Core-Apps eingehende Anforderungen ausgehenden Antworten zu. Auf niedriger Ebene erfolgt diese Zuordnung mit Middleware, und einfache ASP.NET Core-Apps und Microservices können ausschließlich aus benutzerdefinierter Middleware bestehen. Wenn Sie ASP.NET Core MVC verwenden, können Sie auf einer etwas höheren Ebene arbeiten, indem Sie in Bezug auf Routen, Controller und Aktionen nachdenken. Jede eingehende Anforderung wird mit der Routingtabelle der Anwendung verglichen, und wenn eine übereinstimmende Route gefunden wird, wird die zugeordnete Aktionsmethode (die zu einem Controller gehört) aufgerufen, um die Anforderung zu verarbeiten. Wenn keine übereinstimmende Route gefunden wird, wird ein Fehlerhandler (in diesem Fall das Zurückgeben eines NotFound-Ergebnisses) aufgerufen.

ASP.NET Core MVC-Apps können herkömmliche Routen, Attributrouten oder beides verwenden. Herkömmliche Routen werden im Code definiert, wobei Routingkonventionen mithilfe von Syntax wie im folgenden Beispiel angegeben werden:

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

In diesem Beispiel wurde der Routingtabelle eine Route mit dem Namen "default" hinzugefügt. Sie definiert eine Routenvorlage mit Platzhaltern für controller, actionund id. Die Platzhalter controller und action haben den angegebenen Standardwert (Home und Index jeweils), und der id Platzhalter ist optional (infolgedessen wird ein "?" darauf angewendet). Die hier definierte Konvention gibt an, dass der erste Teil einer Anforderung dem Namen des Controllers, dem zweiten Teil der Aktion entsprechen soll und dann bei Bedarf ein dritter Teil einen ID-Parameter darstellt. Herkömmliche Routen werden in der Regel an einem Ort für die Anwendung definiert, z. B. in Program.cs , in dem die Anforderungs-Middleware-Pipeline konfiguriert ist.

Attributrouten gelten für Controller und Aktionen direkt und werden nicht global angegeben. Dieser Ansatz hat den Vorteil, dass sie viel besser auffindbar sind, wenn Sie eine bestimmte Methode betrachten, aber bedeutet, dass Routinginformationen nicht an einem Ort in der Anwendung aufbewahrt werden. Mit Attributrouten können Sie problemlos mehrere Routen für eine bestimmte Aktion angeben sowie Routen zwischen Controllern und Aktionen kombinieren. 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 in [HttpGet] und ähnlichen Attributen angegeben werden und vermeiden, dass separate [Route]-Attribute hinzugefügt werden müssen. Attributrouten können auch Token verwenden, um die Notwendigkeit der wiederholten Angabe von Controller- oder Aktionsnamen zu verringern, wie unten dargestellt.

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

Razor Pages nutzt kein Attributrouting. Im Rahmen der @page Richtlinie können Sie zusätzliche Routenvorlageninformationen für eine Razor-Seite angeben:

@page "{id:int}"

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

/Products/123

Sobald eine bestimmte Anforderung mit einer Route abgeglichen wurde, aber bevor die Aktionsmethode aufgerufen wird, führt ASP.NET Core MVC eine Modellbindung und Modellüberprüfung für die Anforderung durch. Die Modellbindung ist für das Konvertieren eingehender HTTP-Daten in die .NET-Typen verantwortlich, die als Parameter der aufgerufenen Aktionsmethode angegeben sind. Wenn die Aktionsmethode beispielsweise einen int id Parameter erwartet, versucht die Modellbindung, diesen Parameter aus einem Wert bereitzustellen, der als Teil der Anforderung bereitgestellt wird. Dazu sucht die Modellbindung nach Werten in einem geposteten Formular, Werten in der Route selbst und Abfragezeichenfolgenwerten. Wenn ein id Wert gefunden wird, wird er in eine ganze Zahl konvertiert, bevor er an die Aktionsmethode übergeben wird.

Nach dem Binden des Modells, aber vor dem Aufrufen der Aktionsmethode tritt die Modellüberprüfung auf. Die Modellüberprüfung verwendet optionale Attribute für den Modelltyp und kann sicherstellen, dass das bereitgestellte Modellobjekt bestimmten Datenanforderungen entspricht. Bestimmte Werte können wie erforderlich angegeben oder auf eine bestimmte Länge oder einen bestimmten numerischen Bereich beschränkt werden usw. Wenn Validierungsattribute angegeben werden, das Modell jedoch nicht ihren Anforderungen entspricht, ist die Eigenschaft ModelState.IsValid falsch, und der Satz der fehlerhaften Überprüfungsregeln ist verfügbar, um an den Client zu senden, der die Anforderung sendet.

Wenn Sie die Modellüberprüfung verwenden, sollten Sie unbedingt sicherstellen, dass das Modell gültig ist, bevor Sie Zustandsveränderungsbefehle ausführen, um sicherzustellen, dass Ihre App nicht durch ungültige Daten beschädigt ist. Sie können einen Filter verwenden, um zu vermeiden, dass in jeder Aktion Code für diese Überprüfung hinzugefügt werden muss. ASP.NET Core MVC-Filter bieten eine Möglichkeit, Gruppen von Anforderungen abzufangen, sodass gemeinsame Richtlinien und grenzüberschreitende Bedenken gezielt angewendet werden können. Filter können auf einzelne Aktionen, ganze Controller oder global für eine Anwendung angewendet werden.

Bei Web-APIs unterstützt ASP.NET Core MVC Inhaltsverhandlung, sodass Anforderungen angeben können, wie Antworten formatiert werden sollen. Basierend auf headern, die in der Anforderung bereitgestellt werden, formatieren Aktionen, die Daten zurückgeben, die Antwort in XML, JSON oder einem anderen unterstützten Format. Mit diesem Feature kann dieselbe API von mehreren Clients mit unterschiedlichen Datenformatanforderungen verwendet werden.

Web-API-Projekte sollten die Verwendung des [ApiController] Attributs in Betracht ziehen, das auf einzelne Controller, auf eine Basiscontrollerklasse oder auf die gesamte Assembly angewendet werden kann. Dieses Attribut fügt die automatische Überprüfung des Modells hinzu, und jede Aktion mit einem ungültigen Modell gibt eine BadRequest mit den Details der Überprüfungsfehler zurück. Das Attribut erfordert auch, dass alle Aktionen über eine Attributroute verfügen, anstatt eine herkömmliche Route zu verwenden, und es werden detailliertere ProblemDetails-Informationen als Reaktion auf Fehler zurückgegeben.

Steuerungen unter Kontrolle halten

Bei seitenbasierten Anwendungen leisten Razor Pages hervorragende Arbeit, indem sie verhindern, dass Controller zu groß werden. Jede einzelne Seite erhält eigene Dateien und Klassen, die nur ihren Handlern zugeordnet sind. Vor der Einführung von Razor Pages hätten viele ansichtsorientierte Anwendungen große Controllerklassen, die für viele verschiedene Aktionen und Ansichten verantwortlich wären. Diese Klassen würden natürlich wachsen, um viele Verantwortlichkeiten und Abhängigkeiten zu haben, wodurch sie schwieriger zu verwalten sind. 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 Vermittlungsdesignmuster wird verwendet, um die Kopplung zwischen Klassen zu reduzieren und gleichzeitig die Kommunikation zwischen ihnen zu ermöglichen. In ASP.NET Core MVC-Anwendungen wird dieses Muster häufig verwendet, um Controller in kleinere Teile aufzuteilen, indem Handler verwendet werden, um die Arbeit von Aktionsmethoden zu erledigen. Das beliebte MediatR NuGet-Paket wird häufig verwendet, um dies zu erreichen. In der Regel enthalten Controller viele verschiedene Aktionsmethoden, von denen jede bestimmte Abhängigkeiten erfordern kann. Der Satz aller Abhängigkeiten, die für jede Aktion erforderlich sind, muss an den Konstruktor des Controllers übergeben werden. Bei der Verwendung von MediatR ist die einzige Abhängigkeit, über die ein Controller in der Regel verfügt, eine Instanz des Mediators. Jede Aktion verwendet dann die Vermittlungsinstanz, um eine Nachricht zu senden, die von einem Handler verarbeitet wird. Der Handler ist spezifisch für eine einzelne Aktion und benötigt daher nur die Abhängigkeiten, die für diese Aktion erforderlich sind. Ein Beispiel für einen Controller mit MediatR wird hier gezeigt:

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 besteht darin, dass Controller viel kleiner und hauptsächlich auf Routing und Modellbindung ausgerichtet sind, während einzelne Handler für die spezifischen Aufgaben verantwortlich sind, die von einem bestimmten Endpunkt benötigt werden. Dieser Ansatz kann auch ohne MediatR mithilfe des ApiEndpoints NuGet-Pakets erreicht werden, das versucht, den API-Controllern die gleichen Vorteile zu bieten, wie sie Razor Pages den ansichtsbasierten Controllern bringen.

Verweise – Zuordnen von Anforderungen zu Antworten

Arbeiten mit Abhängigkeiten

ASP.NET Core verfügt über integrierte Unterstützung für und intern verwendet eine Technik, die als Abhängigkeitsinjektion bezeichnet wird. Es handelt sich dabei um eine Technik, die die lose Kopplung von unterschiedlichen Teilen einer Anwendung ermöglicht. Lose Kopplung ist wünschenswert, da Teile der Anwendung leichter isoliert werden können, was Tests oder Ersatz ermöglicht. Es macht auch weniger wahrscheinlich, dass eine Änderung in einem Teil der Anwendung unerwartete Auswirkungen an einer anderen Stelle in der Anwendung hat. Abhängigkeitsinjektion basiert auf dem Abhängigkeitsinversionsprinzip und ist häufig der Schlüssel zum Erreichen des offenen/geschlossenen Prinzips. 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.

Statische Anhaftung tritt auf, wenn Ihre Klassen statische Methoden aufrufen oder auf statische Eigenschaften zugreifen, die Seiteneffekte oder Abhängigkeiten von der Infrastruktur haben. Wenn Sie beispielsweise über eine Methode verfügen, die eine statische Methode aufruft, die wiederum in eine Datenbank schreibt, ist ihre Methode eng mit der Datenbank gekoppelt. Alle Elemente, die diesen Datenbankaufruf unterbrechen, unterbrechen Ihre Methode. Das Testen solcher Methoden ist berüchtigt schwierig, da solche Tests entweder kommerzielle Modellbibliotheken erfordern, um die statischen Aufrufe zu modellieren, oder nur mit einer Testdatenbank getestet werden können. Statische Aufrufe, die keine Abhängigkeit von der Infrastruktur haben, insbesondere die Aufrufe, die vollständig zustandslos sind, sind in Ordnung und haben keine Auswirkungen auf die Kopplung oder Testbarkeit (über die Kopplung von Code an den statischen Aufruf selbst hinaus).

Viele Entwickler verstehen die Risiken statischer Bindung und des globalen Zustands, aber koppeln dennoch ihren Code eng mit bestimmten Implementierungen, indem sie direkt instanziieren. 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. Aber jedes Mal, wenn eine Klasse instanziiert wird, nehmen Sie sich einen kurzen Moment Zeit, um zu überlegen, ob es sinnvoll ist, diese bestimmte Instanz an diesem bestimmten Speicherort hart zu codieren, oder wenn es ein besseres Design wäre, diese Instanz als Abhängigkeit anzufordern.

Deklarieren Ihrer Abhängigkeiten

ASP.NET Core basiert darauf, dass Methoden und Klassen ihre Abhängigkeiten deklarieren und als Argumente anfordern. ASP.NET Anwendungen werden in der Regel in Program.cs oder in einer Startup Klasse eingerichtet.

Hinweis

Die vollständige Konfiguration von Apps in Program.cs ist der Standardansatz für .NET 6 (und höher) und Visual Studio 2022 (und höher) Apps. Projektvorlagen wurden aktualisiert, um Ihnen bei den ersten Schritten mit diesem neuen Ansatz zu helfen. ASP.NET Core-Projekte können bei Bedarf weiterhin eine Startup Klasse verwenden.

Konfigurieren von Diensten in Program.cs

Für sehr einfache Apps können Sie Abhängigkeiten mithilfe von direkt in der Datei WebApplicationBuilder verknüpfen. Nachdem alle erforderlichen Dienste hinzugefügt wurden, wird der Generator zum Erstellen der App verwendet.

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

Konfigurieren von Diensten in Startup.cs

Die Startup.cs ist selbst so konfiguriert, dass die Abhängigkeitsinjektion an mehreren Stellen unterstützt wird. Wenn Sie eine Startup Klasse verwenden, können Sie ihm einen Konstruktor zuordnen und damit Abhängigkeiten anfordern, z. B.:

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 Startup Klasse ist interessant darin, dass es keine expliziten Typanforderungen dafür gibt. Sie erbt weder von einer speziellen Startup Basisklasse noch implementiert sie eine bestimmte Schnittstelle. Sie können ihm einen Konstruktor geben oder nicht, und Sie können beliebig viele Parameter für den Konstruktor angeben. Wenn der Webhost, den Sie für Ihre Anwendung konfiguriert haben, gestartet wird, ruft er die Startup Klasse auf (wenn Sie ihm die Verwendung mitgeteilt haben), und verwendet die Abhängigkeitsinjektion, um alle Abhängigkeiten aufzufüllen, die die Startup Klasse erfordert. Wenn Sie natürlich Parameter anfordern, die nicht im von ASP.NET Core verwendeten Dienstcontainer konfiguriert sind, erhalten Sie eine Fehlermeldung. Solange Sie jedoch bei Abhängigkeiten bleiben, die dem Container bekannt sind, können Sie alles anfordern, was Sie wünschen.

Die Abhängigkeitseinfügung ist direkt von Anfang an in Ihre ASP.NET Core-Apps integriert, wenn Sie die Startinstanz erstellen. Dies bezieht sich auch auf die Startklasse. Sie können abhängigkeiten auch in der Configure Methode anfordern:

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

}

Die ConfigureServices-Methode ist die Ausnahme für dieses Verhalten. es muss nur einen Parameter vom Typ IServiceCollectionverwenden. Es muss keine Abhängigkeitsinjektion unterstützt werden, da sie einerseits für das Hinzufügen von Objekten zum Dienstcontainer verantwortlich ist und andererseits Zugriff auf alle aktuell konfigurierten Dienste über den IServiceCollection Parameter hat. Auf diese Weise können Sie mit Abhängigkeiten arbeiten, die in der ASP.NET Core Services-Auflistung in jedem Teil der Startup Klasse definiert sind, entweder indem Sie den erforderlichen Dienst als Parameter anfordern oder mit dem IServiceCollection In ConfigureServicesarbeiten.

Hinweis

Wenn Sie sicherstellen müssen, dass bestimmte Dienste für Ihre Startup Klasse verfügbar sind, können Sie sie mithilfe einer IWebHostBuilder und ihrer ConfigureServices Methode innerhalb des CreateDefaultBuilder Aufrufs konfigurieren.

Die Startup-Klasse ist ein Modell für die Struktur anderer Teile Ihrer ASP.NET Core-Anwendung, von Controllern über Middleware bis hin zu Filtern zu Ihren eigenen Diensten. In jedem Fall sollten Sie dem Prinzip " Explizite Abhängigkeiten" folgen, Ihre Abhängigkeiten anfordern, anstatt sie direkt zu erstellen und die Abhängigkeitseinfügung in der gesamten Anwendung zu nutzen. Achten Sie darauf, wo und wie Sie Implementierungen direkt instanziieren, insbesondere Dienste und Objekte, die mit Infrastruktur arbeiten oder Nebenwirkungen haben. Bevorzugen Sie das Arbeiten mit in Ihrem Anwendungskern definierten Abstraktionen und das Übergeben dieser als Argumente, anstatt Verweise auf bestimmte Implementierungstypen zu hartcodieren.

Strukturieren der Anwendung

Monolithische Anwendungen weisen in der Regel einen einzigen Einstiegspunkt auf. Bei einer ASP.NET Core-Webanwendung ist der Einstiegspunkt das ASP.NET Core-Webprojekt. Das bedeutet jedoch nicht, dass die Lösung nur aus einem einzelnen Projekt besteht. Es ist nützlich, die Anwendung in verschiedene Ebenen aufzuteilen, um die Trennung von Bedenken zu verfolgen. Nach dem Aufteilen in Ebenen ist es hilfreich, über Ordner hinauszugehen, um Projekte zu trennen, die dazu beitragen können, eine bessere Kapselung zu erzielen. Der beste Ansatz, um diese Ziele mit einer ASP.NET Core-Anwendung zu erreichen, ist eine Variante der in Kapitel 5 erläuterten Clean Architecture. Nach diesem Ansatz umfasst die Lösung der Anwendung separate Bibliotheken für die Benutzeroberfläche, Infrastruktur und ApplicationCore.

Zusätzlich zu diesen Projekten werden auch separate Testprojekte einbezogen (Tests werden in Kapitel 9 erläutert).

Das Objektmodell und die Schnittstellen der Anwendung sollten im ApplicationCore-Projekt platziert werden. Dieses Projekt wird so wenige Abhängigkeiten wie möglich haben (und keine spezifischen Infrastrukturanforderungen), und die anderen Projekte in der Lösung werden darauf verweisen. Geschäftsentitäten, die beibehalten werden müssen, werden im ApplicationCore-Projekt definiert, ebenso wie Dienste, die nicht direkt von der Infrastruktur abhängen.

Implementierungsdetails, z. B. wie persistenz ausgeführt wird oder wie Benachrichtigungen an einen Benutzer gesendet werden, werden im Infrastrukturprojekt aufbewahrt. Dieses Projekt verweist auf implementierungsspezifische Pakete wie Entity Framework Core, sollte jedoch keine Details zu diesen Implementierungen außerhalb des Projekts verfügbar machen. Infrastrukturdienste und Repositorys sollten Schnittstellen implementieren, die im ApplicationCore-Projekt definiert sind, und ihre Persistenzimplementierungen sind für das Abrufen und Speichern von Entitäten verantwortlich, die in ApplicationCore definiert sind.

Das ASP.NET Core UI-Projekt ist für alle Bedenken auf Benutzeroberflächenebene verantwortlich, sollte jedoch keine Geschäftslogik oder Infrastrukturdetails enthalten. Im Idealfall sollte es nicht einmal eine Abhängigkeit vom Infrastrukturprojekt haben, wodurch sichergestellt wird, dass keine Abhängigkeit zwischen den beiden Projekten versehentlich eingeführt wird. Dies kann mithilfe eines DI-Containers von Drittanbietern wie Autofac erreicht werden, mit dem Sie DI-Regeln in Modulklassen in jedem Projekt definieren können.

Ein weiterer Ansatz zum Decoupieren der Anwendung von Implementierungsdetails ist das Aufrufen von Microservices der Anwendung, die möglicherweise in einzelnen Docker-Containern bereitgestellt werden. Dies bietet noch größere Trennung von Bedenken und Entkoppelung als die Nutzung von DI zwischen zwei Projekten, hat aber zusätzliche Komplexität.

Organisieren von Features

Standardmäßig organisieren ASP.NET Core-Anwendungen ihre Ordnerstruktur so, dass sie Controller und Ansichten und häufig ViewModels enthalten. Clientseitiger Code zur Unterstützung dieser serverseitigen Strukturen wird in der Regel separat im Ordner "wwwroot" gespeichert. Große Anwendungen können jedoch Probleme mit dieser Organisation haben, da die Arbeit an einem bestimmten Feature häufig einen Sprung zwischen diesen Ordnern erfordert. Dies wird immer schwieriger, da die Anzahl der Dateien und Unterordner in jedem Ordner wächst, was zu einem großen Teil zum Scrollen im Projektmappen-Explorer führt. Eine Lösung für dieses Problem ist das Organisieren von Anwendungscode nach Feature statt nach Dateityp. Dieser Organisationsstil wird in der Regel als Featureordner oder Featuresegmente bezeichnet (siehe auch: Vertikale Segmente).

ASP.NET Core MVC unterstützt bereiche für diesen Zweck. Mithilfe von Bereichen können Sie separate Sätze von Controller- und Ansichtenordnern (sowie alle zugehörigen Modelle) in jedem Bereichsordner erstellen. Abbildung 7-1 zeigt eine Beispielordnerstruktur mit Bereichen.

Organisation des Beispielbereichs

Abbildung 7-1. Beispielstruktur mit Bereichen

Wenn Sie Bereiche verwenden, müssen Sie Attribute verwenden, um Ihre Controller mit dem Namen des Bereichs zu versehen, dem sie angehö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?}");
});

Zusätzlich zur integrierten Unterstützung für Bereiche können Sie auch Ihre eigene Ordnerstruktur und Konventionen anstelle von Attributen und benutzerdefinierten Routen verwenden. Auf diese Weise können Sie Featureordner haben, die keine separaten Ordner für Ansichten, Controller usw. enthalten, die Hierarchie flacher halten und das Anzeigen aller zugehörigen Dateien an einem zentralen Ort für jedes Feature vereinfachen. Bei APIs können Ordner zum Ersetzen von Controllern verwendet werden, und jeder Ordner kann alle API-Endpunkte und die zugehörigen DTOs enthalten.

ASP.NET Core verwendet integrierte Konventionstypen, um das Verhalten zu steuern. Sie können diese Konventionen ändern oder ersetzen. Sie können z. B. eine Konvention erstellen, die automatisch den Featurenamen für einen bestimmten Controller basierend auf dem Namespace erhält (der normalerweise mit dem Ordner korreliert, in dem sich der Controller befindet):

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

Anschließend geben Sie diese Konvention als Option an, wenn Sie Ihrer Anwendung ConfigureServices Unterstützung für MVC hinzufügen (oder in Program.cs):

// 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 auch eine Konvention zum Auffinden von Ansichten. Sie können diese Einstellung durch eine benutzerdefinierte Konvention überschreiben, damit Ansichten in Ihren Feature-Ordnern liegen (mithilfe des Featurenamens, der von der oben genannten Feature-Konvention bereitgestellt wird). Weitere Informationen zu diesem Ansatz und zum Herunterladen eines funktionierenden Beispiels finden Sie im MSDN Magazine-Artikel Feature Slices für ASP.NET Core MVC.

APIs und Blazor Anwendungen

Wenn Ihre Anwendung eine Reihe von Web-APIs enthält, die gesichert werden müssen, sollten diese APIs idealerweise als separates Projekt von Ihrer Anwendung "View" oder "Razor Pages" konfiguriert werden. Das Trennen von APIs, insbesondere öffentlichen APIs, von Ihrer serverseitigen Webanwendung hat eine Reihe von Vorteilen. Diese Anwendungen weisen häufig eindeutige Bereitstellungs- und Ladeeigenschaften auf. Sie sind auch sehr wahrscheinlich, verschiedene Mechanismen für die Sicherheit zu übernehmen, wobei standardmäßige formularbasierte Anwendungen die cookiebasierte Authentifizierung und APIs nutzen, die höchstwahrscheinlich tokenbasierte Authentifizierung verwenden.

Außerdem sollten Blazor-Anwendungen unabhängig von der Verwendung von Blazor Server oder BlazorWebAssembly als separate Projekte erstellt werden. Die Anwendungen weisen unterschiedliche Laufzeiteigenschaften sowie Sicherheitsmodelle auf. Sie werden wahrscheinlich gemeinsame Typen mit der serverseitigen Webanwendung (oder dem API-Projekt) teilen, und diese Typen sollten in einem gemeinsamen freigegebenen Projekt definiert werden.

Das Hinzufügen einer BlazorWebAssembly Administratorschnittstelle zu eShopOnWeb erforderte das Hinzufügen mehrerer neuer Projekte. Das BlazorWebAssembly Projekt selbst, BlazorAdmin. Eine neue Gruppe von öffentlichen API-Endpunkten, die von BlazorAdmin genutzt werden und so konfiguriert sind, dass sie die tokenbasierte Authentifizierung verwenden, wird im PublicApi Projekt definiert. Und bestimmte gemeinsam genutzte Typen, die von beiden Projekten verwendet werden, werden in einem neuen BlazorShared Projekt aufbewahrt.

Man könnte fragen, warum ein separates BlazorShared Projekt hinzugefügt wird, wenn bereits ein gemeinsames ApplicationCore Projekt vorhanden ist, das verwendet werden kann, um alle Typen zu teilen, die sowohl von PublicApi als auch von BlazorAdmin benötigt werden. Die Antwort ist, dass dieses Projekt alle Geschäftslogik der Anwendung umfasst und somit viel größer als nötig ist und auch viel wahrscheinlicher auf dem Server sicher gehalten werden muss. Denken Sie daran, dass alle Bibliotheken, die von BlazorAdmin referenziert werden, beim Laden der Blazor Anwendung in die Browser der Benutzer heruntergeladen werden.

Je nachdem, ob eines das Back-End-For-Frontends (BFF)-Muster verwendet, teilen die von der BlazorWebAssembly App verbrauchten APIs möglicherweise nicht ihre Typen 100% mit Blazor. Insbesondere kann eine öffentliche API, die von vielen verschiedenen Clients genutzt werden soll, eigene Anforderungs- und Ergebnistypen definieren, anstatt sie in einem clientspezifischen freigegebenen Projekt freizugeben. Im Beispiel "eShopOnWeb" wird davon ausgegangen, dass das PublicApi Projekt tatsächlich eine öffentliche API hosten kann, sodass nicht alle Anforderungs- und Antworttypen aus dem BlazorShared Projekt stammen.

Übergreifende Belange

Wenn Anwendungen wachsen, wird es immer wichtiger, übergreifende Bedenken zu berücksichtigen, um Duplikate zu beseitigen und Konsistenz aufrechtzuerhalten. Einige Beispiele für grenzüberschreitende Bedenken in ASP.NET Kernanwendungen sind Authentifizierung, Modellüberprüfungsregeln, Ausgabezwischenspeicherung und Fehlerbehandlung, obwohl viele andere vorhanden sind. ASP.NET Core MVC-Filter ermöglichen es Ihnen, Code vor oder nach bestimmten Schritten in der Anforderungsverarbeitungspipeline auszuführen. Beispielsweise kann ein Filter vor und nach der Modellbindung vor und nach einer Aktion oder vor und nach dem Ergebnis einer Aktion ausgeführt werden. Sie können auch einen Autorisierungsfilter verwenden, um den Zugriff auf den Rest der Pipeline zu steuern. Abbildung 7-2 zeigt, wie die Anforderungsausführung durch Filter fließt, falls konfiguriert.

Die Anforderung wird über Autorisierungsfilter, Ressourcenfilter, Modellbindung, Aktionsfilter, Aktionsausführung und Aktionsergebniskonvertierung, Ausnahmefilter, Ergebnisfilter und Ergebnisausführung verarbeitet. Beim Ausweg wird die Anforderung nur von Ergebnisfiltern und Ressourcenfiltern verarbeitet, bevor sie eine Antwort an den Client gesendet wird.

Abbildung 7-2. Fordern Sie die Ausführung über Filter und Anforderungspipeline an.

Filter werden in der Regel als Attribute implementiert, sodass Sie sie auf Controller oder Aktionen (oder sogar global) anwenden können. Wenn Filter auf diese Weise hinzugefügt werden, überschreiben oder erweitern sie die auf Aktionsebene angegebenen Filter, welche ihrerseits die auf der Controllerebene spezifizierten Filter außer Kraft setzen, die wiederum globale Filter überschreiben. Beispielsweise kann das [Route] Attribut verwendet werden, um Routen zwischen Controllern und Aktionen zu erstellen. Ebenso kann die Autorisierung auf Controllerebene konfiguriert und dann durch einzelne Aktionen außer Kraft gesetzt werden, wie im folgenden Beispiel veranschaulicht wird:

[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 Autorisierungsfilter auf Controllerebene außer Kraft zu setzen. Die ForgotPassword Aktion (und jede andere Aktion in der Klasse, die kein AllowAnonymous-Attribut besitzt) erfordert eine authentifizierte Anforderung.

Filter können verwendet werden, um Duplizierungen in Form gängiger Fehlerbehandlungsrichtlinien für APIs zu beseitigen. Eine typische API-Strategie besteht beispielsweise darin, eine NotFound-Antwort auf Anforderungen zurückzugeben, die auf nicht vorhandene Schlüssel verweisen, und eine BadRequest-Antwort zu senden, wenn die Modellüberprüfung fehlschlägt. Im folgenden Beispiel werden diese beiden Richtlinien in Aktion veranschaulicht:

[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();
}

Lassen Sie nicht zu, dass Ihre Aktionsmethoden mit bedingtem Code wie diesem unübersichtlich werden. Überführen Sie stattdessen die Richtlinien in Filter, die nach Bedarf angewendet werden können. In diesem Beispiel kann die Modellüberprüfung, die bei jedem Senden eines Befehls an die API erfolgen soll, 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 einschließen. Für APIs können Sie das ApiController Attribut verwenden, um dieses Verhalten zu erzwingen, ohne dass ein separater ValidateModel Filter erforderlich ist.

Ebenso kann ein Filter verwendet werden, um zu überprüfen, ob ein Datensatz vorhanden ist und eine 404 zurückgibt, bevor die Aktion ausgeführt wird, ohne dass diese Prüfungen in der Aktion ausgeführt werden müssen. Nachdem Sie allgemeine Konventionen abgerufen und Ihre Lösung so organisiert haben, dass Infrastrukturcode und Geschäftslogik von der Benutzeroberfläche getrennt werden, sollten Ihre MVC-Aktionsmethoden extrem 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 Herunterladen eines Arbeitsbeispiels finden Sie im MSDN Magazine-Artikel Real-World ASP.NET Core MVC-Filter.

Wenn Sie feststellen, dass Sie eine Reihe häufiger Antworten von APIs haben, die auf allgemeinen Szenarien wie Überprüfungsfehlern (Ungültige Anforderung), nicht gefundene Ressource und Serverfehler basieren, können Sie eine Ergebnisstraktion verwenden. Die Ergebnisabstraktion würde von Diensten zurückgegeben, die von API-Endpunkten konsumiert werden, und die Controlleraktion oder der Endpunkt würde einen Filter verwenden, um diese in IActionResults zu übersetzen.

Referenzen – Strukturierung von Anwendungen

Sicherheit

Das Sichern von Webanwendungen ist ein großes Thema mit vielen Überlegungen. Auf der einfachsten Ebene stellt die Sicherheit sicher, dass Sie wissen, von wem eine bestimmte Anforderung stammt, und stellen Sie dann sicher, dass die Anforderung nur Zugriff auf ressourcen hat, die sie benötigen. Die Authentifizierung ist der Prozess, bei dem Anmeldeinformationen, die mit einer Anfrage bereitgestellt werden, mit denen in einem vertrauenswürdigen Datenspeicher verglichen werden, um festzustellen, ob die Anfrage von einer bekannten Entität stammt. Die Autorisierung ist der Prozess der Einschränkung des Zugriffs auf bestimmte Ressourcen basierend auf der Benutzeridentität. 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.

Identität

ASP.NET Core Identity ist ein Mitgliedschaftssystem, mit dem Sie Anmeldefunktionen für Ihre Anwendung unterstützen können. Es unterstützt lokale Benutzerkonten sowie externe Anmeldeanbieter-Unterstützung von Anbietern wie Microsoft-Konto, Twitter, Facebook, Google und mehr. Neben ASP.NET Core Identity kann Ihre Anwendung 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" ausgewählt ist. Diese Vorlage enthält Unterstützung für Registrierung, Anmeldung, externe Anmeldungen, vergessene Kennwörter und zusätzliche Funktionen.

Wählen Sie einzelne Benutzerkonten aus, um die Identität vorkonfiguriert zu haben

Abbildung 7-3. Wählen Sie einzelne Benutzerkonten aus, um die Identität vorkonfiguriert zu haben.

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

Konfigurieren der Identität in Program.cs

In Program.cs konfigurieren Sie Dienste aus der WebHostBuilder Instanz, und nachdem die App erstellt wurde, konfigurieren Sie die Middleware. Die wichtigsten Punkte, die Sie beachten müssen, sind der Aufruf AddDefaultIdentity für erforderliche Dienste und die Aufrufe UseAuthentication und UseAuthorization, die erforderliche Middleware hinzufügen.

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 der Identität 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 angezeigt werden. Beim Konfigurieren von Identity-Diensten werden Sie einen AddDefaultTokenProviders-Aufruf bemerken. Dies hat nichts mit Token zu tun, die zum Sichern der Webkommunikation verwendet werden können, sondern sich stattdessen auf Anbieter bezieht, die Eingabeaufforderungen erstellen, die über SMS oder E-Mail an Benutzer gesendet werden können, um ihre Identität zu bestätigen.

Weitere Informationen zum Konfigurieren der zweistufigen Authentifizierung und zum Aktivieren externer Anmeldeanbieter finden Sie in den offiziellen ASP.NET Core-Dokumenten.

Authentifizierung

Die Authentifizierung ist der Prozess der Ermittlung, wer auf das System zugreift. Wenn Sie ASP.NET Core Identity und die im vorherigen Abschnitt gezeigten Konfigurationsmethoden verwenden, werden automatisch einige Authentifizierungsstandardwerte in der Anwendung konfiguriert. Sie können diese Standardwerte jedoch auch manuell konfigurieren oder die von AddIdentity festgelegten überschreiben. Wenn Sie Identity verwenden, wird die cookiebasierte Authentifizierung als Standardschema konfiguriert.

Bei der webbasierten Authentifizierung gibt es in der Regel bis zu fünf Aktionen, die im Rahmen der Authentifizierung eines Clients eines Systems ausgeführt werden können. Diese lauten wie folgt:

  • Authentifizieren: Verwenden Sie die vom Client bereitgestellten Informationen, um eine Identität zu erstellen, die sie innerhalb der Anwendung verwenden können.
  • Herausforderung. Diese Aktion wird verwendet, um zu verlangen, dass der Client sich selbst identifiziert.
  • Verbieten. Informieren Sie den Kunden, dass er eine Aktion nicht ausführen kann.
  • Anmelden. Behalten Sie den vorhandenen Client auf irgendeine Weise bei.
  • Abmelden. Entfernen Sie den Client aus der Persistenzspeicherung.

Es gibt eine Reihe gängiger Techniken zum Ausführen der Authentifizierung in Webanwendungen. Diese werden als Schemas bezeichnet. Ein bestimmtes Schema definiert Aktionen für einige oder alle oben genannten Optionen. Einige Schemas unterstützen nur eine Teilmenge von Aktionen und erfordern möglicherweise ein separates Schema, um die Aktionen auszuführen, die es nicht unterstützt. Das OpenId-Connect(OIDC)-Schema unterstützt z. B. keine Anmeldung oder Abmeldung, ist jedoch häufig für die Verwendung der Cookie-Authentifizierung für diese Persistenz konfiguriert.

In Ihrer ASP.NET Core-Anwendung können Sie ein DefaultAuthenticateScheme sowie optionale spezifische Schemas für jede der oben beschriebenen Aktionen konfigurieren. Beispiel: DefaultChallengeScheme und DefaultForbidScheme. Das Aufrufen AddIdentity konfiguriert eine Reihe von Aspekten der Anwendung und fügt viele erforderliche Dienste hinzu. Es enthält auch diesen Aufruf zum Konfigurieren des Authentifizierungsschemas:

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

Diese Schemas verwenden Cookies zur Persistenz und Umleitung zu Anmeldeseiten für die Authentifizierung standardmäßig. Diese Schemas sind für Webanwendungen geeignet, die mit Benutzern über Webbrowser interagieren, aber nicht für APIs empfohlen. 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 angibt, was, falls vorhanden, ein Problem aufgetreten ist. Diese Clients interagieren nicht über einen Browser und rendern oder interagieren nicht mit HTML, die eine API zurückgeben kann. Daher ist es nicht für API-Endpunkte geeignet, ihre Clients auf Anmeldeseiten umzuleiten, wenn sie nicht authentifiziert sind. Ein anderes Schema ist besser geeignet.

Um die Authentifizierung für APIs zu konfigurieren, können Sie die Authentifizierung auf die folgende Weise einrichten, wie sie vom Projekt PublicApi 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
        };
    });

Es ist zwar möglich, mehrere verschiedene Authentifizierungsschemas innerhalb eines einzelnen Projekts zu konfigurieren, aber es ist viel einfacher, ein einzelnes Standardschema zu konfigurieren. Aus diesem und anderen Gründen trennt die eShopOnWeb-Referenzanwendung ihre APIs in einem eigenen Projekt, PublicApi, das getrennt vom Hauptprojekt Web ist, welches die Ansichten der Anwendung und Razor Pages enthält.

Authentifizierung in Blazor Apps

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

Referenzen – Authentifizierung

Autorisierung

Die einfachste Art der Autorisierung umfasst das Einschränken des Zugriffs auf anonyme Benutzer. Diese Funktionalität kann durch Anwenden des [Authorize] Attributs auf bestimmte Controller oder Aktionen erreicht werden. Wenn Rollen verwendet werden, kann das Attribut weiter erweitert werden, um den Zugriff auf Benutzer einzuschränken, die zu bestimmten Rollen gehören, wie gezeigt:

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

}

In diesem Fall hätten Benutzer, die entweder zu den HRManager Rollen oder Finance rollen (oder beides) gehören, Zugriff auf den SalaryController. Um zu verlangen, dass ein Benutzer mehreren Rollen angehört (nicht nur einer von mehreren), können Sie das Attribut mehrmals anwenden und jeweils eine erforderliche Rolle angeben.

Die Angabe bestimmter Rollengruppen als Zeichenfolgen in vielen verschiedenen Controllern und Aktionen kann zu unerwünschter Wiederholung führen. Definieren Sie mindestens Konstanten für diese Zeichenfolgen und verwenden Sie die Konstanten überall dort, wo Sie die Zeichenfolge angeben müssen. Sie können auch Autorisierungsrichtlinien konfigurieren, die Autorisierungsregeln kapseln und dann die Richtlinie anstelle einzelner Rollen beim Anwenden des [Authorize] Attributs angeben:

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

Indem Sie Richtlinien auf diese Weise verwenden, können Sie die eingeschränkten Aktionen von den spezifischen Rollen oder Regeln trennen, die dafür gelten. Wenn Sie später eine neue Rolle erstellen, die Zugriff auf bestimmte Ressourcen haben muss, können Sie einfach eine Richtlinie aktualisieren, anstatt jede Liste der Rollen für jedes [Authorize] Attribut zu aktualisieren.

Ansprüche

Ansprüche sind Namenswertpaare, die Eigenschaften eines authentifizierten Benutzers darstellen. Sie können beispielsweise die Mitarbeiternummer der Benutzer als Anspruch speichern. Ansprüche können dann als Teil von Autorisierungsrichtlinien verwendet werden. Sie können eine Richtlinie namens "EmployeeOnly" erstellen, die das Vorhandensein eines Anspruchs namens "EmployeeNumber" erfordert, wie in diesem Beispiel gezeigt:

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

Diese Richtlinie kann dann mit dem [Authorize] Attribut verwendet werden, um alle Controller und/oder Aktionen zu schützen, wie oben beschrieben.

Schützen von Web-APIs

Die meisten Web-APIs sollten ein tokenbasiertes Authentifizierungssystem implementieren. Die Tokenauthentifizierung ist zustandslos und soll skalierbar sein. In einem tokenbasierten Authentifizierungssystem muss sich der Client zuerst beim Authentifizierungsanbieter authentifizieren. Wenn der Client erfolgreich ist, wird ein Token ausgegeben, bei dem es sich einfach um eine kryptografisch aussagekräftige Zeichenfolge handelt. Das am häufigsten verwendete Format für Token ist JSON-Webtoken oder JWT (häufig als "jot" ausgesprochen). Wenn der Client dann eine Anforderung an eine API ausstellen muss, wird dieses Token als Header der Anforderung hinzugefügt. Der Server überprüft dann das Token im Anforderungsheader, bevor die Anforderung abgeschlossen wird. Abbildung 7-4 veranschaulicht diesen Prozess.

TokenAuth

Abbildung 7-4. Tokenbasierte Authentifizierung für Web-APIs.

Sie können Ihren eigenen Authentifizierungsdienst erstellen, in Azure AD und OAuth integrieren oder einen Dienst mithilfe eines Open-Source-Tools wie IdentityServer implementieren.

JWT-Token können Ansprüche über den Benutzer 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 vertrauliche Daten wie Kennwörter oder Schlüssel nicht in JTW-Token, da ihre Inhalte leicht zu lesen sind.

Wenn Sie JWT-Token mit SPA 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 wird in der Regel als Kopfzeile ausgeführt, wie der folgende 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 werden anforderungen, die mit dem _httpClient Token vorgenommen wurden, in die Header der Anforderung eingebettet, sodass die serverseitige API die Anforderung authentifizieren und autorisieren kann.

Benutzerdefinierte Sicherheit

Vorsicht

Vermeiden Sie in der Regel die Implementierung eigener benutzerdefinierter Sicherheitsimplementierungen.

Gehen Sie insbesondere mit Bedacht vor, wenn Sie eigene Kryptografien, Benutzermitgliedschaften oder Systeme zur Tokengenerierung implementieren. Es gibt viele kommerzielle und Open-Source-Alternativen, die fast sicher bessere Sicherheit haben als eine benutzerdefinierte Implementierung.

Referenzen – Sicherheit

Clientkommunikation

Zusätzlich zum Bereitstellen von Seiten und Reagieren auf Datenanforderungen über Web-APIs können ASP.NET Core-Apps direkt mit verbundenen Clients kommunizieren. Diese ausgehende Kommunikation kann eine Vielzahl von Transporttechnologien verwenden, die am häufigsten WebSockets sind. ASP.NET Core SignalR ist eine Bibliothek, die es einfach macht, Ihren Anwendungen Echtzeit-Server-zu-Client-Kommunikationsfunktionen hinzuzufügen. SignalR unterstützt eine Vielzahl von Transporttechnologien, einschließlich WebSockets, und abstrahiert viele der Implementierungsdetails vom Entwickler.

Die Echtzeit-Clientkommunikation, unabhängig davon, ob Sie WebSockets direkt oder andere Techniken verwenden, sind in einer Vielzahl von Anwendungsszenarien nützlich. Einige Beispiele sind:

  • Live-Chatroomanwendungen

  • Überwachen von Anwendungen

  • Updates zum Auftragsstatus

  • Benachrichtigungen

  • Interaktive Formularanwendungen

Beim Erstellen der Clientkommunikation in Ihre Anwendungen gibt es in der Regel zwei Komponenten:

  • Serverseitiger Verbindungs-Manager (SignalR Hub, WebSocketManager WebSocketHandler)

  • Clientseitige Bibliothek

Clients sind nicht auf Browser beschränkt – mobile Apps, Konsolen-Apps und andere native Apps können auch mit 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();
    }
}

Überlegen Sie, wie Ihre Anwendungen direkt mit Clientanwendungen kommunizieren, und überlegen Sie, ob die Kommunikation in Echtzeit die Benutzererfahrung Ihrer App verbessern würde.

Referenzen – Clientkommunikation

Domänengesteuertes Design – Sollten Sie es anwenden?

Domain-Driven Design (DDD) ist ein agiler Ansatz zur Entwicklung von Software, der sich auf die Geschäftsdomäne konzentriert. Es legt großen Wert auf Kommunikation und Interaktion mit Experten des Geschäftsbereichs, die den Entwicklern vermitteln können, wie das reale System funktioniert. Wenn Sie z. B. ein System erstellen, das Aktienhandel verarbeitet, ist Ihr Domänenexperte möglicherweise ein erfahrener Aktienbroker. DDD wurde entwickelt, um große, komplexe Geschäftsprobleme zu beheben und eignet sich häufig nicht für kleinere, einfachere Anwendungen, da die Investition in das Verständnis und die Modellierung der Domäne nicht wert ist.

Beim Erstellen von Software nach einem DDD-Ansatz sollte Ihr Team (einschließlich nicht technischer Projektbeteiligter und Mitwirkender) eine allgegenwärtige Sprache für den Problemraum entwickeln. Das heißt, die gleiche Terminologie sollte für das modellierte Realkonzept, das Softwareäquivalent und alle Strukturen verwendet werden, die vorhanden sein können, um das Konzept beizubehalten (z. B. Datenbanktabellen). Daher sollten die in der allgegenwärtigen Sprache beschriebenen Konzepte die Basis für Ihr Domänenmodell bilden.

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

  • Entitäten, die Objekte mit einem Identitätsthread darstellen. Entitäten werden in der Regel in Persistenz mit einem Schlüssel gespeichert, mit dem sie später abgerufen werden können.

  • Aggregate, die Gruppen von Objekten darstellen, die als Einheit beibehalten werden sollen.

  • Wertobjekte, die Konzepte darstellen, die anhand der Summe ihrer Eigenschaftswerte verglichen werden können. Beispielsweise DateRange, das aus einem Anfangs- und Enddatum besteht.

  • Domänenereignisse, die Dinge im System darstellen, die für andere Teile des Systems von Interesse sind.

Ein DDD-Domänenmodell sollte das komplexe Verhalten innerhalb des Modells kapseln. Entitäten sollten insbesondere nicht nur Sammlungen von Eigenschaften sein. Wenn das Domänenmodell kein Verhalten aufweist und lediglich den Zustand des Systems darstellt, wird gesagt, dass es sich um ein anämisches Modell handelt, das in DDD nicht erwünscht ist.

Zusätzlich zu diesen Modelltypen verwendet DDD in der Regel eine Vielzahl von Mustern:

  • Repository zum Abstrahieren von Persistenzdetails.

  • Factory zur Kapselung komplexer Objekterstellung.

  • Dienste zum Kapseln komplexer Verhaltensweisen und/oder Infrastrukturimplementierungsdetails.

  • Befehl, um das Erteilen von Befehlen vom Ausführen des Befehls selbst zu entkoppeln.

  • Spezifikation zum Kapseln von Abfragedetails.

DDD empfiehlt auch die Verwendung der zuvor erörterten Clean Architecture, die lose Kopplung, Kapselung und Code ermöglicht, die mithilfe von Komponententests problemlos überprüft werden können.

Wann sollten Sie DDD anwenden?

DDD eignet sich gut für große Anwendungen mit erheblicher Geschäfts- und nicht nur technischer Komplexität. Die Anwendung sollte das Wissen von Domänenexperten erfordern. Es sollte ein erhebliches Verhalten im Domänenmodell selbst geben, das Geschäftsregeln und Interaktionen darstellt, die nicht nur den aktuellen Zustand verschiedener Datensätze aus Datenspeichern speichern und abrufen.

Wann sollten Sie DDD nicht anwenden

DDD umfasst Investitionen in Modellierung, Architektur und Kommunikation, die für kleinere Anwendungen oder Anwendungen, die im Wesentlichen nur CRUD (Create/read/update/delete) sind, nicht garantiert werden können. Wenn Sie sich für die Anwendung nach DDD entscheiden, aber feststellen, dass Ihre Domäne ein anemisches Modell ohne Verhalten aufweist, müssen Sie ihren Ansatz möglicherweise überdenken. Ihre Anwendung benötigt möglicherweise keine DDD, oder Sie benötigen Möglicherweise Unterstützung beim Umgestalten Ihrer Anwendung, um Geschäftslogik im Domänenmodell zu kapseln, anstatt in Ihrer Datenbank oder Benutzeroberfläche.

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 die Einschränkungen eines Aggregats nicht, wenn Sie Daten abfragen, um einen Bericht anzuzeigen oder Daten für ein Dashboard zu visualisieren. Es ist vollkommen akzeptabel, ein separates, einfacheres Lesemodell für solche Anforderungen zu haben.

Referenzen – Domain-Driven Design

Einsatz

Es gibt einige Schritte, die am Bereitstellungsprozess Ihrer ASP.NET Core-Anwendung beteiligt sind, unabhängig davon, wo sie gehostet wird. Der erste Schritt besteht darin, die Anwendung zu veröffentlichen, die mit dem dotnet publish CLI-Befehl ausgeführt werden kann. In diesem Schritt wird die Anwendung kompiliert und alle Dateien platziert, die zum Ausführen der Anwendung in einen bestimmten Ordner erforderlich sind. Wenn Sie aus Visual Studio bereitstellen, wird dieser Schritt automatisch für Sie ausgeführt. Der Veröffentlichungsordner enthält .exe und .dll Dateien für die Anwendung und deren Abhängigkeiten. Eine eigenständige Anwendung enthält auch eine Version der .NET-Laufzeit. ASP.NET Core-Anwendungen umfassen auch Konfigurationsdateien, statische Clientressourcen und MVC-Ansichten.

ASP.NET Core-Anwendungen sind Konsolenanwendungen, die gestartet werden müssen, wenn der Server gestartet und neu gestartet wird, wenn die Anwendung (oder der Server) abstürzt. Ein Prozess-Manager kann verwendet werden, um diesen Prozess zu automatisieren. Die häufigsten Prozessmanager 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 sie nach einer vorläufigen Verarbeitung an Kestrel weiter. Reverseproxyserver bieten eine Sicherheitsebene für die Anwendung. Kestrel unterstützt auch nicht das Hosten mehrerer Anwendungen am selben Port, sodass Techniken wie Hostheader nicht damit verwendet werden können, um das Hosten mehrerer Anwendungen auf demselben Port und derselben IP-Adresse zu ermöglichen.

Von Kestrel zum Internet

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

Ein weiteres Szenario, in dem ein Reverseproxy hilfreich sein kann, besteht darin, mehrere Anwendungen mit SSL/HTTPS zu sichern. In diesem Zusammenhang muss SSL nur für den Reverseproxy konfiguriert sein. Die Kommunikation zwischen dem Reverseproxyserver und Kestrel kann über HTTP erfolgen, wie in Abbildung 7-6 dargestellt.

ASP.NET hinter einem HTTPS-gesicherten Reverseproxyserver gehostet

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

Ein zunehmend beliebter Ansatz besteht darin, Ihre ASP.NET Core-Anwendung in einem Docker-Container zu hosten, die dann lokal gehostet oder für cloudbasiertes Hosting in Azure bereitgestellt werden kann. Der Docker-Container könnte Ihren Anwendungscode enthalten, der auf Kestrel ausgeführt wird und hinter einem Reverseproxyserver bereitgestellt wird, wie oben gezeigt.

Wenn Sie Ihre Anwendung auf Azure hosten, können Sie Microsoft Azure-Anwendungsgateway als dedizierte virtuelle Appliance verwenden, um mehrere Dienste bereitzustellen. Neben der Funktion als Reverseproxy für einzelne Anwendungen kann das Anwendungsgateway auch die folgenden Features bieten:

  • HTTP-Lastenausgleich

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

  • End-to-End-SSL

  • Routing mit mehreren Standorten (konsolidieren bis zu 20 Standorte auf einem einzigen Anwendungsgateway)

  • Webanwendungs-Firewall

  • Websocket-Unterstützung

  • Erweiterte Diagnose

Weitere Informationen zu Azure-Bereitstellungsoptionen finden Sie in Kapitel 10.

Ressourcen: Bereitstellung