Delen via


ASP.NET Core MVC-apps ontwikkelen

Tip

Deze inhoud is een fragment uit het eBook, Architect Modern Web Applications met ASP.NET Core en Azure, beschikbaar op .NET Docs of als een gratis downloadbare PDF die offline kan worden gelezen.

Moderne webtoepassingen ontwerpen met ASP.NET Core- en Azure eBook-omslagminiatuur.

"Het is niet belangrijk om het de eerste keer goed te krijgen. Het is van cruciaal belang om het de laatste keer goed te krijgen." - Andrew Hunt en David Thomas

ASP.NET Core is een platformoverschrijdend opensource-framework voor het bouwen van moderne, cloudgeoptimeerde webtoepassingen. ASP.NET Core-apps zijn lichtgewicht en modulair, met ingebouwde ondersteuning voor afhankelijkheidsinjectie, waardoor meer testbaarheid en onderhoudbaarheid mogelijk zijn. In combinatie met MVC, dat ondersteuning biedt voor het bouwen van moderne web-API's naast op weergave gebaseerde apps, is ASP.NET Core een krachtig framework waarmee u bedrijfswebtoepassingen kunt bouwen.

MVC- en Razor Pages

ASP.NET Core MVC biedt veel functies die handig zijn voor het bouwen van web-API's en apps. De term MVC staat voor 'Model-View-Controller', een UI-patroon dat de verantwoordelijkheden opsplitst van het reageren op gebruikersaanvragen in verschillende onderdelen. Naast het volgen van dit patroon, kunt u ook functies in uw ASP.NET Core-apps implementeren als Razor Pages.

Razor Pages zijn ingebouwd in ASP.NET Core MVC en gebruiken dezelfde functies voor routering, modelbinding, filters, autorisatie, enzovoort. In plaats van afzonderlijke mappen en bestanden te hebben voor Controllers, Modellen, Weergaven, enzovoort en het gebruik van routering op basis van kenmerken, worden Razor Pages echter in één map ('/Pages') geplaatst op basis van hun relatieve locatie in deze map en worden aanvragen verwerkt met handlers in plaats van controlleracties. Als u met Razor Pages werkt, worden alle bestanden en klassen die u nodig hebt doorgaans op een punt geplaatst, niet verspreid over het webproject.

Meer informatie over hoe MVC, Razor Pages en gerelateerde patronen worden toegepast in de voorbeeldtoepassing eShopOnWeb.

Wanneer u een nieuwe ASP.NET Core-app maakt, moet u rekening houden met het soort app dat u wilt bouwen. Wanneer u een nieuw project maakt, kiest u in uw IDE of met behulp van de dotnet new CLI-opdracht uit verschillende sjablonen. De meest voorkomende projectsjablonen zijn Leeg, Web-API, Web App en Web App (Model-View-Controller). Hoewel u deze beslissing alleen kunt nemen wanneer u voor het eerst een project maakt, is dit geen onherroepelijke beslissing. Het web-API-project maakt gebruik van standaard Model-View-Controller-controllers. Standaard ontbreken weergaven. Op dezelfde manier maakt de standaardweb-app-sjabloon gebruik van Razor Pages en ontbreekt er dus ook een map Weergaven. U kunt later een map Weergaven toevoegen aan deze projecten om het gedrag op basis van weergaven te ondersteunen. Web-API- en Model-View-Controller-projecten bevatten standaard geen map Pagina's, maar u kunt er later een toevoegen ter ondersteuning van op Razor Pages gebaseerd gedrag. U kunt deze drie sjablonen beschouwen als ondersteuning voor drie verschillende soorten standaardgebruikersinteractie: gegevens (web-API), op pagina's gebaseerd en op weergave gebaseerd. U kunt echter desgewenst een of al deze sjablonen in één project combineren en afstemmen.

Waarom Razor Pages?

Razor Pages is de standaardbenadering voor nieuwe webtoepassingen in Visual Studio. Razor Pages biedt een eenvoudigere manier om op pagina's gebaseerde toepassingsfuncties te bouwen, zoals formulieren zonder spa. Met behulp van controllers en weergaven was het gebruikelijk dat toepassingen zeer grote controllers hebben die met veel verschillende afhankelijkheden werkten en modellen weergeven en veel verschillende weergaven hebben geretourneerd. Dit resulteerde in meer complexiteit en resulteerde vaak in controllers die niet effectief het principe van één verantwoordelijkheid of open/gesloten principes hebben gevolgd. Razor Pages lost dit probleem op door de logica aan de serverzijde in te kapselen voor een bepaalde logische pagina in een webtoepassing met de Bijbehorende Razor-opmaak. Een Razor-pagina met geen logica aan de serverzijde kan alleen bestaan uit een Razor-bestand (bijvoorbeeld 'Index.cshtml'). De meeste niet-triviale Razor Pages hebben echter een gekoppelde paginamodelklasse, die volgens conventie hetzelfde wordt genoemd als het Razor-bestand met de extensie '.cs' (bijvoorbeeld 'Index.cshtml.cs').

Het paginamodel van een Razor Page combineert de verantwoordelijkheden van een MVC-controller en een viewmodel. In plaats van aanvragen te verwerken met methoden voor controlleracties, worden paginamodelhandlers zoals 'OnGet()' uitgevoerd, waardoor de bijbehorende pagina standaard wordt weergegeven. Razor Pages vereenvoudigt het proces van het bouwen van afzonderlijke pagina's in een ASP.NET Core-app en biedt nog steeds alle architectuurfuncties van ASP.NET Core MVC. Ze zijn een goede standaardkeuze voor nieuwe functionaliteit op basis van pagina's.

Wanneer gebruikt u MVC?

Als u web-API's bouwt, is het MVC-patroon logischer dan razor Pages te gebruiken. Als uw project alleen web-API-eindpunten beschikbaar maakt, moet u idealiter beginnen met de web-API-projectsjabloon. Anders kunt u eenvoudig controllers en bijbehorende API-eindpunten toevoegen aan elke ASP.NET Core-app. Gebruik de op weergave gebaseerde MVC-benadering als u een bestaande toepassing migreert van ASP.NET MVC 5 of eerder naar ASP.NET Core MVC en u dit wilt doen met de minste hoeveelheid inspanning. Zodra u de eerste migratie hebt uitgevoerd, kunt u evalueren of het zinvol is Razor Pages te gebruiken voor nieuwe functies of zelfs als een groothandelsmigratie. Zie Bestaande ASP.NET-apps overzetten naar ASP.NET Core eBook voor meer informatie over het overzetten van .NET 4.x-apps naar .NET 8.

Of u ervoor kiest om uw web-app te bouwen met razor Pages- of MVC-weergaven, uw app heeft vergelijkbare prestaties en bevat ondersteuning voor afhankelijkheidsinjectie, filters, modelbinding, validatie, enzovoort.

Aanvragen toewijzen aan antwoorden

In het hart wijst ASP.NET Core-apps binnenkomende aanvragen toe aan uitgaande antwoorden. Op laag niveau wordt deze toewijzing uitgevoerd met middleware en eenvoudige ASP.NET Core-apps en microservices kunnen uitsluitend bestaan uit aangepaste middleware. Wanneer u ASP.NET Core MVC gebruikt, kunt u op een iets hoger niveau werken, denken aan routes, controllers en acties. Elke binnenkomende aanvraag wordt vergeleken met de routeringstabel van de toepassing en als er een overeenkomende route wordt gevonden, wordt de bijbehorende actiemethode (behorend bij een controller) aangeroepen om de aanvraag te verwerken. Als er geen overeenkomende route wordt gevonden, wordt een fouthandler (in dit geval een NotFound-resultaat geretourneerd) aangeroepen.

ASP.NET Core MVC-apps kunnen conventionele routes, kenmerkroutes of beide gebruiken. Conventionele routes worden gedefinieerd in code, waarbij routeringsconventies worden opgegeven met behulp van syntaxis, zoals in het onderstaande voorbeeld:

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

In dit voorbeeld is een route met de naam 'standaard' toegevoegd aan de routeringstabel. Hiermee definieert u een routesjabloon met tijdelijke aanduidingen voor controller, actionen id. action De controller tijdelijke aanduidingen en tijdelijke aanduidingen hebben de standaardinstelling (Homeen Indexrespectievelijk) en de id tijdelijke aanduiding is optioneel (op grond van een "?" toegepast). De hier gedefinieerde conventie geeft aan dat het eerste deel van een aanvraag moet overeenkomen met de naam van de controller, het tweede deel aan de actie en vervolgens, indien nodig, een derde deel een id-parameter vertegenwoordigt. Conventionele routes worden doorgaans op één plaats gedefinieerd voor de toepassing, zoals in Program.cs waar de middleware-pijplijn van de aanvraag is geconfigureerd.

Kenmerkroutes worden rechtstreeks toegepast op controllers en acties, in plaats van globaal op te geven. Deze aanpak heeft het voordeel dat ze veel beter kunnen worden gedetecteerd wanneer u een bepaalde methode bekijkt, maar betekent wel dat routeringsinformatie niet op één plaats in de toepassing wordt bewaard. Met kenmerkroutes kunt u eenvoudig meerdere routes voor een bepaalde actie opgeven en routes tussen controllers en acties combineren. Voorbeeld:

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

Routes kunnen worden opgegeven op [HttpGet] en vergelijkbare kenmerken, waardoor u geen afzonderlijke [Route]-kenmerken hoeft toe te voegen. Kenmerkroutes kunnen ook tokens gebruiken om de noodzaak om controller- of actienamen te herhalen, zoals hieronder wordt weergegeven:

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

Razor Pages gebruiken geen kenmerkroutering. U kunt aanvullende routesjabloongegevens voor een Razor-pagina opgeven als onderdeel van de @page instructie:

@page "{id:int}"

In het vorige voorbeeld komt de betreffende pagina overeen met een route met een geheel getalparameter id . De pagina Products.cshtml die zich in de hoofdmap /Pages bevindt, reageert bijvoorbeeld op aanvragen zoals deze:

/Products/123

Zodra een bepaalde aanvraag is gekoppeld aan een route, maar voordat de actiemethode wordt aangeroepen, voert ASP.NET Core MVC modelbinding en modelvalidatie uit op de aanvraag. Modelbinding is verantwoordelijk voor het converteren van binnenkomende HTTP-gegevens naar de .NET-typen die zijn opgegeven als parameters van de actiemethode die moet worden aangeroepen. Als de actiemethode bijvoorbeeld een int id parameter verwacht, probeert modelbinding deze parameter op te geven op basis van een waarde die is opgegeven als onderdeel van de aanvraag. Hiervoor zoekt modelbinding naar waarden in een geplaatst formulier, waarden in de route zelf en queryreekswaarden. Ervan uitgaande dat een id waarde wordt gevonden, wordt deze geconverteerd naar een geheel getal voordat deze wordt doorgegeven aan de actiemethode.

Na het binden van het model, maar voordat u de actiemethode aanroept, vindt modelvalidatie plaats. Modelvalidatie maakt gebruik van optionele kenmerken van het modeltype en kan ervoor zorgen dat het opgegeven modelobject voldoet aan bepaalde gegevensvereisten. Bepaalde waarden kunnen worden opgegeven als vereist, of beperkt tot een bepaalde lengte of numeriek bereik, enzovoort. Als validatiekenmerken zijn opgegeven, maar het model niet voldoet aan hun vereisten, is de eigenschap ModelState.IsValid onwaar en is de set mislukte validatieregels beschikbaar om te verzenden naar de client die de aanvraag indient.

Als u modelvalidatie gebruikt, moet u ervoor zorgen dat u altijd controleert of het model geldig is voordat u eventuele opdrachten voor het wijzigen van de status uitvoert om ervoor te zorgen dat uw app niet beschadigd is door ongeldige gegevens. U kunt een filter gebruiken om te voorkomen dat u in elke actie code voor deze validatie hoeft toe te voegen. ASP.NET Core MVC-filters bieden een manier om groepen aanvragen te onderscheppen, zodat gemeenschappelijk beleid en kruislingse problemen op een gerichte basis kunnen worden toegepast. Filters kunnen worden toegepast op afzonderlijke acties, hele controllers of globaal voor een toepassing.

Voor web-API's ondersteunt ASP.NET Core MVC inhoudsonderhandeling, zodat aanvragen kunnen opgeven hoe antwoorden moeten worden opgemaakt. Op basis van headers die zijn opgegeven in de aanvraag, worden met acties die gegevens retourneren het antwoord opgemaakt in XML, JSON of een andere ondersteunde indeling. Met deze functie kan dezelfde API worden gebruikt door meerdere clients met verschillende vereisten voor gegevensindeling.

Web-API-projecten moeten overwegen het [ApiController] kenmerk te gebruiken, dat kan worden toegepast op afzonderlijke controllers, op een basiscontrollerklasse of op de hele assembly. Dit kenmerk voegt automatische modelvalidatiecontrole toe en elke actie met een ongeldig model retourneert een BadRequest met de details van de validatiefouten. Het kenmerk vereist ook dat alle acties een kenmerkroute hebben, in plaats van een conventionele route te gebruiken, en retourneert meer gedetailleerde informatie over ProblemDetails als reactie op fouten.

Controllers onder controle houden

Voor toepassingen op basis van pagina's doet Razor Pages een uitstekende taak om te voorkomen dat controllers te groot worden. Elke afzonderlijke pagina krijgt zijn eigen bestanden en klassen die speciaal zijn toegewezen aan de handler(s). Vóór de introductie van Razor Pages zouden veel weergavegerichte toepassingen grote controllerklassen hebben die verantwoordelijk zijn voor veel verschillende acties en weergaven. Deze klassen zouden natuurlijk veel verantwoordelijkheden en afhankelijkheden hebben, waardoor ze moeilijker te onderhouden zijn. Als u merkt dat uw weergavecontrollers te groot worden, kunt u overwegen deze te herstructureren om Razor Pages te gebruiken of een patroon zoals een bemiddelaar te introduceren.

Het ontwerppatroon van de bemiddelaar wordt gebruikt om de koppeling tussen klassen te verminderen en tegelijkertijd communicatie tussen deze klassen mogelijk te maken. In ASP.NET Core MVC-toepassingen wordt dit patroon vaak gebruikt om controllers op te splitsen in kleinere onderdelen door handlers te gebruiken om het werk van actiemethoden uit te voeren. Het populaire MediatR NuGet-pakket wordt vaak gebruikt om dit te bereiken. Controllers bevatten doorgaans veel verschillende actiemethoden, die elk bepaalde afhankelijkheden kunnen vereisen. De set met alle afhankelijkheden die door een actie zijn vereist, moet worden doorgegeven aan de constructor van de controller. Wanneer u MediatR gebruikt, is de enige afhankelijkheid die een controller doorgaans heeft een instantie van de bemiddelaar. Elke actie gebruikt vervolgens de bemiddelaarinstantie om een bericht te verzenden, dat door een handler wordt verwerkt. De handler is specifiek voor één actie en heeft dus alleen de afhankelijkheden nodig die voor die actie zijn vereist. Hier ziet u een voorbeeld van een controller met MediatR:

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
}

In de MyOrders actie wordt de aanroep van Send een GetMyOrders bericht verwerkt door deze klasse:

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

Het eindresultaat van deze aanpak is dat controllers veel kleiner en voornamelijk gericht zijn op routering en modelbinding, terwijl afzonderlijke handlers verantwoordelijk zijn voor de specifieke taken die nodig zijn voor een bepaald eindpunt. Deze aanpak kan ook worden bereikt zonder MediatR met behulp van het NuGet-pakket ApiEndpoints, waarmee wordt geprobeerd om API-controllers dezelfde voordelen te bieden als Razor Pages voor op weergave gebaseerde controllers.

Verwijzingen : aanvragen toewijzen aan antwoorden

Werken met afhankelijkheden

ASP.NET Core biedt ingebouwde ondersteuning voor en maakt intern gebruik van een techniek die afhankelijkheidsinjectie wordt genoemd. Afhankelijkheidsinjectie is een techniek die losse koppeling tussen verschillende onderdelen van een toepassing mogelijk maakt. Losse koppeling is wenselijk omdat het gemakkelijker maakt om delen van de toepassing te isoleren, waardoor het testen of vervangen mogelijk is. Het maakt het ook minder waarschijnlijk dat een wijziging in één deel van de toepassing een onverwachte impact heeft ergens anders in de toepassing. Afhankelijkheidsinjectie is gebaseerd op het afhankelijkheidsinversion-principe en is vaak essentieel voor het bereiken van het open/gesloten principe. Wanneer u evalueert hoe uw toepassing werkt met de bijbehorende afhankelijkheden, moet u rekening houden met de statische cling-code en onthoudt u het aforisme 'nieuw is lijm'.

Statische cling treedt op wanneer uw klassen aanroepen naar statische methoden uitvoeren of toegang hebben tot statische eigenschappen, die neveneffecten of afhankelijkheden hebben van de infrastructuur. Als u bijvoorbeeld een methode hebt die een statische methode aanroept, die op zijn beurt naar een database schrijft, is uw methode nauw gekoppeld aan de database. Alles wat de aanroep van de database onderbreekt, wordt uw methode verbroken. Het testen van dergelijke methoden is notoir moeilijk, omdat voor dergelijke tests commerciële mockingbibliotheken nodig zijn om de statische aanroepen te bespotten of alleen kunnen worden getest met een testdatabase. Statische aanroepen die geen afhankelijkheid hebben van infrastructuur, met name die aanroepen die volledig staatloos zijn, kunnen prima worden aangeroepen en hebben geen invloed op koppeling of testbaarheid (naast koppelingscode naar de statische aanroep zelf).

Veel ontwikkelaars begrijpen de risico's van statische cling en wereldwijde status, maar zullen hun code nog steeds nauw koppelen aan specifieke implementaties via directe instantiëring. "Nieuw is lijm" is bedoeld als herinnering aan deze koppeling, en niet een algemene veroordeling van het gebruik van het new trefwoord. Net als bij statische methodeaanroepen maken nieuwe exemplaren van typen die geen externe afhankelijkheden hebben doorgaans niet nauw code aan implementatiedetails of maken het testen moeilijker. Maar elke keer dat een klasse wordt geïnstantieerd, moet u even nadenken of het zinvol is om dat specifieke exemplaar op die specifieke locatie vast te stellen of dat het een beter ontwerp is om dat exemplaar aan te vragen als een afhankelijkheid.

Uw afhankelijkheden declareren

ASP.NET Core is gebouwd om methoden en klassen hun afhankelijkheden te laten declareren en deze als argumenten aan te vragen. ASP.NET toepassingen worden doorgaans ingesteld in Program.cs of in een Startup klasse.

Notitie

Het configureren van apps in Program.cs is de standaardbenadering voor .NET 6 (en hoger) en Visual Studio 2022-apps. Projectsjablonen zijn bijgewerkt om u te helpen aan de slag te gaan met deze nieuwe aanpak. ASP.NET Core-projecten kunnen desgewenst nog steeds een Startup klasse gebruiken.

Services configureren in Program.cs

Voor zeer eenvoudige apps kunt u afhankelijkheden rechtstreeks in Program.cs bestand verbinden met behulp van een WebApplicationBuilder. Zodra alle benodigde services zijn toegevoegd, wordt de opbouwfunctie gebruikt om de app te maken.

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

Services configureren in Startup.cs

De Startup.cs is zelf geconfigureerd ter ondersteuning van afhankelijkheidsinjectie op verschillende punten. Als u een Startup klasse gebruikt, kunt u deze een constructor geven en hiervoor afhankelijkheden aanvragen, zoals:

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

De Startup klasse is interessant omdat er geen expliciete typevereisten voor zijn. Deze wordt niet overgenomen van een speciale Startup basisklasse en implementeert ook geen specifieke interface. U kunt het een constructor geven of niet, en u kunt zo veel parameters voor de constructor opgeven als u wilt. Wanneer de webhost die u voor uw toepassing hebt geconfigureerd, wordt de Startup klasse aangeroepen (als u hebt verteld dat deze er een moet gebruiken) en wordt afhankelijkheidsinjectie gebruikt om afhankelijkheden te vullen die de Startup klasse nodig heeft. Als u natuurlijk parameters aanvraagt die niet zijn geconfigureerd in de servicescontainer die wordt gebruikt door ASP.NET Core, krijgt u een uitzondering, maar zolang u zich houdt aan afhankelijkheden die de container kent, kunt u alles aanvragen wat u wilt.

Afhankelijkheidsinjectie is rechtstreeks vanaf het begin ingebouwd in uw ASP.NET Core-apps wanneer u het opstartexemplaren maakt. Het stopt daar niet voor de opstartklasse. U kunt ook afhankelijkheden aanvragen in de Configure methode:

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

}

De methode ConfigureServices is de uitzondering op dit gedrag; het moet slechts één parameter van het type IServiceCollection. Het hoeft geen ondersteuning te bieden voor afhankelijkheidsinjectie, omdat het enerzijds verantwoordelijk is voor het toevoegen van objecten aan de servicescontainer, en anderzijds heeft het toegang tot alle momenteel geconfigureerde services via de IServiceCollection parameter. U kunt dus werken met afhankelijkheden die zijn gedefinieerd in de verzameling ASP.NET Core-services in elk deel van de Startup klasse, door de benodigde service aan te vragen als parameter of door met de in ConfigureServiceste IServiceCollection werken.

Notitie

Als u ervoor wilt zorgen dat bepaalde services beschikbaar zijn voor uw Startup klas, kunt u deze configureren met behulp van een IWebHostBuilder en de ConfigureServices bijbehorende methode in de CreateDefaultBuilder aanroep.

De opstartklasse is een model voor de structuur van andere onderdelen van uw ASP.NET Core-toepassing, van Controllers tot Middleware tot Filters naar uw eigen Services. In elk geval moet u het principe expliciete afhankelijkheden volgen, uw afhankelijkheden aanvragen in plaats van ze rechtstreeks te maken en afhankelijkheidsinjectie in uw toepassing gebruiken. Wees voorzichtig met waar en hoe u rechtstreeks implementaties instantiëren, met name services en objecten die met infrastructuur werken of bijwerkingen hebben. Werk liever met abstracties die zijn gedefinieerd in uw toepassingskern en worden doorgegeven als argumenten voor het coderen van verwijzingen naar specifieke implementatietypen.

De toepassing structureren

Monolithische toepassingen hebben doorgaans één toegangspunt. In het geval van een ASP.NET Core-webtoepassing is het toegangspunt het ASP.NET Core-webproject. Dat betekent echter niet dat de oplossing uit slechts één project moet bestaan. Het is handig om de toepassing op te splitsen in verschillende lagen om de scheiding van problemen te volgen. Zodra het in lagen is opgesplitst, is het handig om verder te gaan dan mappen om projecten te scheiden, wat kan helpen om betere inkapseling te bereiken. De beste aanpak om deze doelen te bereiken met een ASP.NET Core-toepassing is een variant van de schone architectuur die in hoofdstuk 5 wordt besproken. Na deze aanpak bestaat de oplossing van de toepassing uit afzonderlijke bibliotheken voor de gebruikersinterface, infrastructuur en ApplicationCore.

Naast deze projecten worden ook afzonderlijke testprojecten opgenomen (Testen wordt in hoofdstuk 9 besproken).

Het objectmodel en de interfaces van de toepassing moeten in het ApplicationCore-project worden geplaatst. Dit project heeft zo weinig mogelijk afhankelijkheden (en geen voor specifieke infrastructuurproblemen) en de andere projecten in de oplossing verwijzen ernaar. Bedrijfsentiteiten die moeten worden persistent gemaakt, worden gedefinieerd in het ApplicationCore-project, zoals services die niet rechtstreeks afhankelijk zijn van de infrastructuur.

Implementatiedetails, zoals hoe persistentie wordt uitgevoerd of hoe meldingen naar een gebruiker kunnen worden verzonden, worden bewaard in het infrastructuurproject. Dit project verwijst naar implementatiespecifieke pakketten, zoals Entity Framework Core, maar mag geen details weergeven over deze implementaties buiten het project. Infrastructuurservices en opslagplaatsen moeten interfaces implementeren die zijn gedefinieerd in het ApplicationCore-project en de persistentie-implementaties zijn verantwoordelijk voor het ophalen en opslaan van entiteiten die zijn gedefinieerd in ApplicationCore.

Het ASP.NET Core UI-project is verantwoordelijk voor problemen op gebruikersinterfaceniveau, maar mag geen bedrijfslogica of infrastructuurdetails bevatten. In het ideale geval mag het niet eens een afhankelijkheid hebben van het infrastructuurproject, waardoor er geen afhankelijkheid tussen de twee projecten per ongeluk wordt geïntroduceerd. Dit kan worden bereikt met behulp van een di-container van derden, zoals Autofac, waarmee u DI-regels in moduleklassen in elk project kunt definiëren.

Een andere benadering voor het loskoppelen van de toepassing van implementatiedetails is het aanroepen van microservices voor de toepassing, mogelijk geïmplementeerd in afzonderlijke Docker-containers. Dit biedt nog meer scheiding van zorgen en ontkoppeling dan het gebruik van DI tussen twee projecten, maar heeft extra complexiteit.

Functieorganisatie

Standaard ordent ASP.NET Core-toepassingen hun mapstructuur om controllers en weergaven en vaak ViewModels op te nemen. Code aan de clientzijde ter ondersteuning van deze structuren aan de serverzijde wordt doorgaans afzonderlijk opgeslagen in de map wwwroot. Grote toepassingen kunnen echter problemen ondervinden met deze organisatie, omdat het werken aan een bepaalde functie vaak tussen deze mappen vereist. Dit wordt steeds moeilijker naarmate het aantal bestanden en submappen in elke map groeit, wat resulteert in een groot deel van het schuiven door Solution Explorer. Een oplossing voor dit probleem is het ordenen van toepassingscode op functie in plaats van op bestandstype. Deze organisatiestijl wordt meestal functiemappen of functiesegmenten genoemd (zie ook: Verticale segmenten).

ASP.NET Core MVC ondersteunt gebieden voor dit doel. Met behulp van gebieden kunt u afzonderlijke sets controllers en weergaven mappen (evenals alle bijbehorende modellen) maken in elke gebiedsmap. Afbeelding 7-1 toont een voorbeeldmapstructuur met behulp van Gebieden.

Voorbeeldgebiedorganisatie

Afbeelding 7-1. Voorbeeldgebiedorganisatie

Wanneer u Gebieden gebruikt, moet u kenmerken gebruiken om uw controllers te versieren met de naam van het gebied waartoe ze behoren:

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

U moet ook gebiedsondersteuning toevoegen aan uw routes:

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?}");
});

Naast de ingebouwde ondersteuning voor Gebieden kunt u ook uw eigen mapstructuur en conventies gebruiken in plaats van kenmerken en aangepaste routes. Hierdoor kunt u functiemappen hebben die geen afzonderlijke mappen voor weergaven, controllers, enzovoort bevatten, waardoor de hiërarchie platter blijft en het gemakkelijker wordt om alle gerelateerde bestanden op één plaats voor elke functie weer te geven. Voor API's kunnen mappen worden gebruikt om controllers te vervangen en elke map kan alle API-eindpunten en de bijbehorende DTU's bevatten.

ASP.NET Core maakt gebruik van ingebouwde conventietypen om het gedrag ervan te beheren. U kunt deze conventies wijzigen of vervangen. U kunt bijvoorbeeld een conventie maken waarmee automatisch de functienaam voor een bepaalde controller wordt opgehaald op basis van de naamruimte (die meestal overeenkomt met de map waarin de controller zich bevindt):

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

Vervolgens geeft u deze conventie op als optie wanneer u ondersteuning voor MVC toevoegt aan uw toepassing in ConfigureServices (of 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 gebruikt ook een conventie om weergaven te zoeken. U kunt deze overschrijven met een aangepaste conventie, zodat weergaven zich in uw functiemappen bevinden (met behulp van de functienaam die is opgegeven door de FeatureConvention, hierboven). Meer informatie over deze aanpak en een werkend voorbeeld downloaden uit het msDN Magazine-artikel, Feature Slices voor ASP.NET Core MVC.

API's en Blazor toepassingen

Als uw toepassing een set web-API's bevat die moeten worden beveiligd, moeten deze API's idealiter worden geconfigureerd als een afzonderlijk project van uw View- of Razor Pages-toepassing. Het scheiden van API's, met name openbare API's, van uw webtoepassing aan de serverzijde heeft een aantal voordelen. Deze toepassingen hebben vaak unieke implementatie- en belastingskenmerken. Ze zijn ook zeer waarschijnlijk van toepassing op verschillende mechanismen voor beveiliging, met standaardformuliertoepassingen die gebruikmaken van verificatie op basis van cookies en API's die waarschijnlijk gebruikmaken van verificatie op basis van tokens.

Blazor Bovendien moeten toepassingen, ongeacht of u Blazor Server of BlazorWebAssemblygebruikt, worden gebouwd als afzonderlijke projecten. De toepassingen hebben verschillende runtime-kenmerken en beveiligingsmodellen. Ze delen waarschijnlijk algemene typen met de webtoepassing aan de serverzijde (of API-project) en deze typen moeten worden gedefinieerd in een gemeenschappelijk gedeeld project.

De toevoeging van een BlazorWebAssembly beheerinterface aan eShopOnWeb vereist het toevoegen van verschillende nieuwe projecten. Het BlazorWebAssembly project zelf. BlazorAdmin In het PublicApi project wordt een nieuwe set openbare API-eindpunten gedefinieerd die worden gebruikt door BlazorAdmin en geconfigureerd voor het gebruik van verificatie op basis van tokens. En bepaalde gedeelde typen die door beide projecten worden gebruikt, worden in een nieuw BlazorShared project bewaard.

Misschien vraagt u waarom u een afzonderlijk BlazorShared project toevoegt wanneer er al een gemeenschappelijk ApplicationCore project is dat kan worden gebruikt om alle typen te delen die vereist zijn voor zowel als PublicApiBlazorAdmin? Het antwoord is dat dit project alle bedrijfslogica van de toepassing bevat en dus veel groter is dan nodig is en ook veel waarschijnlijker moet worden beveiligd op de server. Houd er rekening mee dat een bibliotheek waarnaar wordt verwezen BlazorAdmin , wordt gedownload naar de browsers van gebruikers wanneer ze de Blazor toepassing laden.

Afhankelijk van of één het BFF-patroon (Backends-For-Frontends) gebruikt, delen de API's die door de BlazorWebAssembly app worden gebruikt, hun typen mogelijk niet met 100%Blazor. Met name een openbare API die door veel verschillende clients moet worden gebruikt, kan een eigen aanvraag- en resultaattype definiëren in plaats van deze te delen in een clientspecifiek gedeeld project. In het voorbeeld eShopOnWeb wordt ervan uitgegaan dat het PublicApi project in feite een openbare API host, zodat niet alle aanvraag- en antwoordtypen afkomstig zijn van het BlazorShared project.

Algemene problemen

Naarmate toepassingen groeien, wordt het steeds belangrijker om kruislingse problemen weg te nemen om duplicatie te elimineren en consistentie te behouden. Enkele voorbeelden van kruislingse problemen in ASP.NET Core-toepassingen zijn verificatie, modelvalidatieregels, uitvoercaching en foutafhandeling, hoewel er vele andere zijn. ASP.NET Core MVC-filters kunt u code uitvoeren voor of na bepaalde stappen in de pijplijn voor aanvraagverwerking. Een filter kan bijvoorbeeld worden uitgevoerd voor en na modelbinding, voor en na een actie, of vóór en na het resultaat van een actie. U kunt ook een autorisatiefilter gebruiken om de toegang tot de rest van de pijplijn te beheren. Afbeelding 7-2 laat zien hoe de uitvoering van aanvragen via filters stroomt, indien geconfigureerd.

De aanvraag wordt verwerkt via autorisatiefilters, resourcefilters, modelbinding, actiefilters, actieuitvoering en conversie van actieresultaten, uitzonderingsfilters, resultaatfilters en resultatenuitvoering. Tijdens de uitweg wordt de aanvraag alleen verwerkt door resultaatfilters en resourcefilters voordat deze een antwoord naar de client wordt verzonden.

Afbeelding 7-2. Uitvoering aanvragen via filters en aanvraagpijplijn.

Filters worden meestal geïmplementeerd als kenmerken, zodat u ze kunt toepassen op controllers of acties (of zelfs wereldwijd). Wanneer filters op deze manier worden toegevoegd, worden filters die zijn opgegeven op actieniveau overschrijven of bouwen op filters die zijn opgegeven op controllerniveau, die zelf globale filters overschrijven. Het kenmerk kan bijvoorbeeld [Route] worden gebruikt om routes tussen controllers en acties op te bouwen. Op dezelfde manier kan autorisatie worden geconfigureerd op controllerniveau en vervolgens worden overschreven door afzonderlijke acties, zoals in het volgende voorbeeld wordt gedemonstreerd:

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

De eerste methode, Login, gebruikt het [AllowAnonymous] filter (kenmerk) om het autorisatiefilter te overschrijven dat is ingesteld op controllerniveau. Voor de ForgotPassword actie (en een andere actie in de klasse die geen kenmerk AllowAnonymous heeft) is een geverifieerde aanvraag vereist.

Filters kunnen worden gebruikt om duplicatie te elimineren in de vorm van veelvoorkomend beleid voor foutafhandeling voor API's. Een typisch API-beleid is bijvoorbeeld het retourneren van een NotFound-antwoord op aanvragen die verwijzen naar sleutels die niet bestaan en een BadRequest antwoord als de modelvalidatie mislukt. In het volgende voorbeeld ziet u deze twee beleidsregels in actie:

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

Sta niet toe dat uw actiemethoden onoverzichtelijk worden met voorwaardelijke code zoals deze. Haal in plaats daarvan het beleid op in filters die naar behoefte kunnen worden toegepast. In dit voorbeeld kan de modelvalidatiecontrole, die moet plaatsvinden wanneer een opdracht naar de API wordt verzonden, worden vervangen door het volgende kenmerk:

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

U kunt het ValidateModelAttribute aan uw project toevoegen als een NuGet-afhankelijkheid door het Ardalis.ValidateModel-pakket op te slaan. Voor API's kunt u het ApiController kenmerk gebruiken om dit gedrag af te dwingen zonder dat er een afzonderlijk ValidateModel filter nodig is.

Op dezelfde manier kan een filter worden gebruikt om te controleren of een record bestaat en een 404 retourneert voordat de actie wordt uitgevoerd, waardoor deze controles niet meer nodig zijn. Zodra u algemene conventies hebt opgehaald en uw oplossing hebt georganiseerd om de infrastructuurcode en bedrijfslogica van uw gebruikersinterface te scheiden, moeten uw MVC-actiemethoden uiterst dun zijn:

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

Meer informatie over het implementeren van filters en het downloaden van een werkvoorbeeld uit het MSDN Magazine-artikel, Real-World ASP.NET Core MVC-filters.

Als u merkt dat u een aantal algemene antwoorden van API's hebt op basis van veelvoorkomende scenario's zoals validatiefouten (ongeldige aanvraag), resource niet gevonden en serverfouten, kunt u overwegen een resultaatabstractie te gebruiken. De resultaatabstractie wordt geretourneerd door services die worden gebruikt door API-eindpunten en de controlleractie of het eindpunt gebruikt een filter om deze te vertalen naar IActionResults.

Verwijzingen : toepassingen structureren

Beveiliging

Het beveiligen van webtoepassingen is een groot onderwerp, met veel overwegingen. Op het meest elementaire niveau moet u ervoor zorgen dat u weet van wie een bepaalde aanvraag afkomstig is en vervolgens ervoor zorgen dat de aanvraag alleen toegang heeft tot resources. Verificatie is het proces van het vergelijken van referenties die worden geleverd met een aanvraag voor die in een vertrouwd gegevensarchief, om te zien of de aanvraag moet worden behandeld als afkomstig van een bekende entiteit. Autorisatie is het proces van het beperken van de toegang tot bepaalde resources op basis van gebruikersidentiteit. Een derde beveiligingsprobleem is het beschermen van aanvragen tegen afluisteren door derden, waarvoor u er ten minste voor moet zorgen dat SSL wordt gebruikt door uw toepassing.

Identiteit

ASP.NET Core Identity is een lidmaatschapssysteem dat u kunt gebruiken om aanmeldingsfunctionaliteit voor uw toepassing te ondersteunen. Het biedt ondersteuning voor lokale gebruikersaccounts en externe aanmeldingsproviderondersteuning van providers zoals Microsoft-account, Twitter, Facebook, Google en meer. Naast ASP.NET Core Identity kan uw toepassing gebruikmaken van Windows-verificatie of een id-provider van derden, zoals Identity Server.

ASP.NET Core Identity is opgenomen in nieuwe projectsjablonen als de optie Afzonderlijke gebruikersaccounts is geselecteerd. Deze sjabloon bevat ondersteuning voor registratie, aanmelding, externe aanmeldingen, vergeten wachtwoorden en aanvullende functionaliteit.

Afzonderlijke gebruikersaccounts selecteren om identiteit vooraf te configureren

Afbeelding 7-3. Selecteer afzonderlijke gebruikersaccounts om identiteit vooraf te configureren.

Identiteitsondersteuning is geconfigureerd in Program.cs of Startup, en omvat het configureren van services en middleware.

Identiteit configureren in Program.cs

In Program.cs configureert u services van het WebHostBuilder exemplaar en configureert u de middleware zodra de app is gemaakt. De belangrijkste punten die u moet noteren, zijn de aanroep voor AddDefaultIdentity vereiste services en de UseAuthentication aanroepen UseAuthorization die vereiste middleware toevoegen.

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

Identiteit configureren bij het opstarten van de 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();

Het is belangrijk dat UseAuthentication en UseAuthorization worden weergegeven voordat MapRazorPages. Wanneer u Identity Services configureert, ziet u een aanroep naar AddDefaultTokenProviders. Dit heeft niets te maken met tokens die kunnen worden gebruikt om webcommunicatie te beveiligen, maar verwijst in plaats daarvan naar providers die prompts maken die via sms of e-mail naar gebruikers kunnen worden verzonden om hun identiteit te bevestigen.

Meer informatie over het configureren van tweeledige verificatie en het inschakelen van externe aanmeldingsproviders vindt u in de officiële ASP.NET Core-documenten.

Verificatie

Verificatie is het proces om te bepalen wie toegang heeft tot het systeem. Als u ASP.NET Core Identity en de configuratiemethoden gebruikt die in de vorige sectie worden weergegeven, worden er automatisch enkele standaardinstellingen voor verificatie in de toepassing geconfigureerd. U kunt deze standaardinstellingen echter ook handmatig configureren of de standaardwaarden overschrijven die zijn ingesteld door AddIdentity. Als u identiteit gebruikt, wordt verificatie op basis van cookies geconfigureerd als het standaardschema.

Bij webverificatie zijn er meestal maximaal vijf acties die kunnen worden uitgevoerd tijdens het verifiëren van een client van een systeem. De volgende stappen moeten worden uitgevoerd:

  • Verifiëren. Gebruik de informatie van de client om een identiteit te maken die ze in de toepassing kunnen gebruiken.
  • Uitdaging. Deze actie wordt gebruikt om de client te verplichten zichzelf te identificeren.
  • Verbieden. Informeer de client dat ze verboden zijn om een actie uit te voeren.
  • Aanmelden. De bestaande client op een of andere manier behouden.
  • Afmelden. Verwijder de client uit persistentie.

Er zijn een aantal algemene technieken voor het uitvoeren van verificatie in webtoepassingen. Dit worden schema's genoemd. Een bepaald schema definieert acties voor sommige of alle bovenstaande opties. Sommige schema's ondersteunen alleen een subset van acties en vereisen mogelijk een afzonderlijk schema om uit te voeren die niet worden ondersteund. Het OIDC-schema (OpenId-Verbinding maken) biedt bijvoorbeeld geen ondersteuning voor aanmelden of afmelden, maar is meestal geconfigureerd voor het gebruik van cookieverificatie voor deze persistentie.

In uw ASP.NET Core-toepassing kunt u een DefaultAuthenticateScheme evenals optionele specifieke schema's configureren voor elk van de hierboven beschreven acties. Bijvoorbeeld DefaultChallengeScheme en DefaultForbidScheme. Aanroepen AddIdentity configureren een aantal aspecten van de toepassing en voegt veel vereiste services toe. Het bevat ook deze aanroep om het verificatieschema te configureren:

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

Deze schema's maken standaard gebruik van cookies voor persistentie en omleiding naar aanmeldingspagina's voor verificatie. Deze schema's zijn geschikt voor webtoepassingen die communiceren met gebruikers via webbrowsers, maar niet aanbevolen voor API's. In plaats daarvan gebruiken API's doorgaans een andere vorm van verificatie, zoals JWT bearer-tokens.

Web-API's worden gebruikt door code, zoals HttpClient in .NET-toepassingen en equivalente typen in andere frameworks. Deze clients verwachten een bruikbaar antwoord van een API-aanroep of een statuscode die aangeeft wat er, indien aanwezig, een probleem is opgetreden. Deze clients werken niet via een browser en geven geen HTML weer of interactie met HTML die een API mogelijk retourneert. Het is dus niet geschikt voor API-eindpunten om hun clients om te leiden naar aanmeldingspagina's als ze niet worden geverifieerd. Een ander schema is beter geschikt.

Als u verificatie voor API's wilt configureren, kunt u verificatie zoals de volgende instellen, die wordt gebruikt door het PublicApi project in de referentietoepassing eShopOnWeb:

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

Hoewel het mogelijk is om meerdere verschillende verificatieschema's binnen één project te configureren, is het veel eenvoudiger om één standaardschema te configureren. Daarom scheidt de eShopOnWeb-referentietoepassing de API's in hun eigen project, PublicApigescheiden van het hoofdproject Web dat de weergaven van de toepassing en Razor Pages bevat.

Verificatie in Blazor apps

Blazor Servertoepassingen kunnen gebruikmaken van dezelfde verificatiefuncties als andere ASP.NET Core-toepassing. BlazorWebAssembly toepassingen kunnen de ingebouwde id- en verificatieproviders echter niet gebruiken, omdat ze worden uitgevoerd in de browser. BlazorWebAssembly toepassingen kunnen de status van gebruikersverificatie lokaal opslaan en hebben toegang tot claims om te bepalen welke acties gebruikers moeten kunnen uitvoeren. Alle verificatie- en autorisatiecontroles moeten echter worden uitgevoerd op de server, ongeacht de logica die in de BlazorWebAssembly app is geïmplementeerd, omdat gebruikers de app eenvoudig kunnen omzeilen en rechtstreeks met de API's kunnen werken.

Verwijzingen – Verificatie

Autorisatie

De eenvoudigste vorm van autorisatie omvat het beperken van de toegang tot anonieme gebruikers. Deze functionaliteit kan worden bereikt door het [Authorize] kenmerk toe te passen op bepaalde controllers of acties. Als rollen worden gebruikt, kan het kenmerk verder worden uitgebreid om de toegang te beperken tot gebruikers die tot bepaalde rollen behoren, zoals wordt weergegeven:

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

}

In dit geval hebben gebruikers die behoren tot de HRManager of Finance rollen (of beide) toegang tot de SalaryController. Als u wilt vereisen dat een gebruiker deel uitmaakt van meerdere rollen (niet slechts één van de verschillende), kunt u het kenmerk meerdere keren toepassen, waarbij u elke keer een vereiste rol opgeeft.

Het opgeven van bepaalde sets rollen als tekenreeksen in veel verschillende controllers en acties kan leiden tot ongewenste herhaling. Definieer minimaal constanten voor deze letterlijke tekenreeksen en gebruik de constanten overal waar u de tekenreeks moet opgeven. U kunt ook autorisatiebeleid configureren, waarin autorisatieregels worden ingekapseld en vervolgens het beleid opgeven in plaats van afzonderlijke rollen bij het toepassen van het [Authorize] kenmerk:

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

Op deze manier kunt u beleidsregels gebruiken om de soorten acties die worden beperkt te scheiden van de specifieke rollen of regels die hierop van toepassing zijn. Als u later een nieuwe rol maakt die toegang moet hebben tot bepaalde resources, kunt u alleen een beleid bijwerken in plaats van elke lijst met rollen op elk [Authorize] kenmerk bij te werken.

Claims

Claims zijn naam-waardeparen die eigenschappen van een geverifieerde gebruiker vertegenwoordigen. U kunt bijvoorbeeld het werknemersnummer van gebruikers opslaan als een claim. Claims kunnen vervolgens worden gebruikt als onderdeel van autorisatiebeleid. U kunt een beleid maken met de naam EmployeeOnly waarvoor het bestaan van een claim "EmployeeNumber"is vereist, zoals wordt weergegeven in dit voorbeeld:

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

Dit beleid kan vervolgens worden gebruikt met het [Authorize] kenmerk om een controller en/of actie te beveiligen, zoals hierboven beschreven.

Web-API's beveiligen

De meeste web-API's moeten een verificatiesysteem op basis van tokens implementeren. Tokenverificatie is staatloos en ontworpen om schaalbaar te zijn. In een verificatiesysteem op basis van tokens moet de client eerst worden geverifieerd bij de verificatieprovider. Als dit lukt, krijgt de client een token uitgegeven. Dit is gewoon een cryptografische zinvolle tekenreeks. De meest voorkomende indeling voor tokens is JSON Web Token of JWT (vaak uitgesproken als 'jot'). Wanneer de client vervolgens een aanvraag naar een API moet uitgeven, wordt dit token toegevoegd als header voor de aanvraag. De server valideert vervolgens het token dat in de aanvraagheader is gevonden voordat de aanvraag wordt voltooid. Afbeelding 7-4 laat dit proces zien.

TokenAuth

Afbeelding 7-4. Verificatie op basis van tokens voor web-API's.

U kunt uw eigen verificatieservice maken, integreren met Azure AD en OAuth of een service implementeren met behulp van een opensource-hulpprogramma zoals IdentityServer.

JWT-tokens kunnen claims over de gebruiker insluiten, die kunnen worden gelezen op de client of server. U kunt een hulpprogramma zoals jwt.io gebruiken om de inhoud van een JWT-token weer te geven. Sla gevoelige gegevens, zoals wachtwoorden of sleutels, niet op in JTW-tokens, omdat de inhoud ervan gemakkelijk kan worden gelezen.

Wanneer u JWT-tokens met beveiligd-WACHTWOORDVERIFICATIE of BlazorWebAssembly toepassingen gebruikt, moet u het token ergens op de client opslaan en vervolgens toevoegen aan elke API-aanroep. Deze activiteit wordt meestal uitgevoerd als een header, zoals de volgende code laat zien:

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

Nadat u de bovenstaande methode hebt aangeroepen, worden aanvragen die zijn gedaan met het _httpClient token ingesloten in de headers van de aanvraag, zodat de API aan de serverzijde de aanvraag kan verifiëren en autoriseren.

Aangepaste beveiliging

Let op

Vermijd als algemene regel het implementeren van uw eigen aangepaste beveiligingsmplementaties.

Wees vooral voorzichtig met het implementeren van cryptografie, gebruikerslidmaatschap of het genereren van tokens. Er zijn veel commerciële en opensource-alternatieven beschikbaar, die bijna zeker betere beveiliging hebben dan een aangepaste implementatie.

Verwijzingen – Beveiliging

Clientcommunicatie

Naast het leveren van pagina's en het reageren op aanvragen voor gegevens via web-API's, kunnen ASP.NET Core-apps rechtstreeks communiceren met verbonden clients. Deze uitgaande communicatie kan verschillende transporttechnologieën gebruiken, de meest voorkomende websockets. ASP.NET Core SignalR is een bibliotheek waarmee u eenvoudig realtime server-naar-clientcommunicatiefunctionaliteit kunt toevoegen aan uw toepassingen. SignalR ondersteunt diverse transporttechnologieën, waaronder WebSockets, en abstrahert veel van de implementatiedetails van de ontwikkelaar.

Realtime clientcommunicatie, ongeacht of webSockets rechtstreeks of andere technieken worden gebruikt, zijn handig in verschillende toepassingsscenario's. Enkele voorbeelden:

  • Live chatruimtetoepassingen

  • Toepassingen bewaken

  • Updates voor taakvoortgang

  • Meldingen

  • Interactieve formulierentoepassingen

Bij het bouwen van clientcommunicatie in uw toepassingen zijn er meestal twee onderdelen:

  • Verbindingsbeheer aan de serverzijde (SignalR Hub, WebSocketManager WebSocketHandler)

  • Bibliotheek aan clientzijde

Clients zijn niet beperkt tot browsers: mobiele apps, console-apps en andere systeemeigen apps kunnen ook communiceren met SignalR/WebSockets. Met het volgende eenvoudige programma wordt alle inhoud die naar een chattoepassing naar de console wordt verzonden, herhaald als onderdeel van een WebSocketManager-voorbeeldtoepassing:

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

Denk na over manieren waarop uw toepassingen rechtstreeks communiceren met clienttoepassingen en overweeg of realtime communicatie de gebruikerservaring van uw app zou verbeteren.

Verwijzingen – Clientcommunicatie

Domeingestuurd ontwerp: moet u dit toepassen?

Domain-Driven Design (DDD) is een flexibele benadering voor het bouwen van software die de nadruk legt op het bedrijfsdomein. Het legt een grote nadruk op communicatie en interactie met zakelijke domeinexperts die zich kunnen verhouden tot de ontwikkelaars hoe het echte systeem werkt. Als u bijvoorbeeld een systeem bouwt waarmee aandelentransacties worden afgehandeld, is uw domeinexpert mogelijk een ervaren aandelenbroker. DDD is ontworpen om grote, complexe zakelijke problemen op te lossen en is vaak niet geschikt voor kleinere, eenvoudigere toepassingen, omdat de investering in het begrijpen en modelleren van het domein het niet waard is.

Wanneer u software bouwt op basis van een DDD-benadering, moet uw team (inclusief niet-technische belanghebbenden en inzenders) een alomtegenwoordige taal ontwikkelen voor de probleemruimte. Dat wil gezegd, dezelfde terminologie moet worden gebruikt voor het echte concept dat wordt gemodelleerd, het software-equivalent en alle structuren die kunnen bestaan om het concept te behouden (bijvoorbeeld databasetabellen). Daarom moeten de concepten die in de alomtegenwoordige taal worden beschreven, de basis vormen voor uw domeinmodel.

Uw domeinmodel bestaat uit objecten die met elkaar communiceren om het gedrag van het systeem weer te geven. Deze objecten kunnen in de volgende categorieën vallen:

  • Entiteiten, die objecten vertegenwoordigen met een thread van identiteit. Entiteiten worden doorgaans opgeslagen in persistentie met een sleutel waarmee ze later kunnen worden opgehaald.

  • Aggregaties, die groepen objecten vertegenwoordigen die moeten worden bewaard als een eenheid.

  • Waardeobjecten, die concepten vertegenwoordigen die kunnen worden vergeleken op basis van de som van hun eigenschapswaarden. Bijvoorbeeld DateRange die bestaat uit een begin- en einddatum.

  • Domein gebeurtenissen, die dingen vertegenwoordigen die plaatsvinden binnen het systeem die van belang zijn voor andere onderdelen van het systeem.

Een DDD-domeinmodel moet complex gedrag in het model inkapselen. Entiteiten mogen met name niet alleen verzamelingen eigenschappen zijn. Wanneer het domeinmodel geen gedrag heeft en alleen de status van het systeem vertegenwoordigt, wordt gezegd dat het een anemisch model is, wat niet wenselijk is in DDD.

Naast deze modeltypen maakt DDD doorgaans gebruik van verschillende patronen:

  • Opslagplaats voor het abstraheren van persistentiedetails.

  • Factory, voor het inkapselen van complexe objecten.

  • Services, voor het inkapselen van complex gedrag en/of implementatiedetails van de infrastructuur.

  • Opdracht, voor het loskoppelen van opdrachten en het uitvoeren van de opdracht zelf.

  • Specificatie, voor het inkapselen van querydetails.

DDD raadt ook het gebruik aan van de clean architecture die eerder is besproken, waardoor losse koppeling, inkapseling en code kunnen worden geverifieerd met behulp van eenheidstests.

Wanneer moet u DDD toepassen

DDD is zeer geschikt voor grote toepassingen met een aanzienlijke zakelijke (niet alleen technische) complexiteit. De toepassing moet kennis van domeinexperts vereisen. Er moet aanzienlijk gedrag zijn in het domeinmodel zelf, wat bedrijfsregels en interacties vertegenwoordigt, behalve het opslaan en ophalen van de huidige status van verschillende records uit gegevensarchieven.

Wanneer moet u DDD niet toepassen

DDD omvat investeringen in modellering, architectuur en communicatie die mogelijk niet worden gerechtvaardigd voor kleinere toepassingen of toepassingen die in wezen alleen CRUD zijn (maken/lezen/bijwerken/verwijderen). Als u ervoor kiest om uw toepassing te benaderen na DDD, maar merkt dat uw domein een anemisch model zonder gedrag heeft, moet u mogelijk uw benadering herzien. Uw toepassing heeft mogelijk geen DDD nodig of u hebt hulp nodig bij het herstructureren van uw toepassing om bedrijfslogica in te kapselen in het domeinmodel, in plaats van in uw database of gebruikersinterface.

Een hybride benadering is om alleen DDD te gebruiken voor de transactionele of complexere gebieden van de toepassing, maar niet voor eenvoudigere CRUD- of alleen-lezengedeelten van de toepassing. U hebt bijvoorbeeld de beperkingen van een aggregaties niet nodig als u gegevens opvraagt om een rapport weer te geven of om gegevens voor een dashboard te visualiseren. Het is perfect acceptabel om een afzonderlijk, eenvoudiger leesmodel voor dergelijke vereisten te hebben.

Verwijzingen – Domeingestuurd ontwerp

Implementatie

Er zijn enkele stappen betrokken bij het implementeren van uw ASP.NET Core-toepassing, ongeacht waar deze wordt gehost. De eerste stap is het publiceren van de toepassing, die kan worden uitgevoerd met behulp van de dotnet publish CLI-opdracht. Met deze stap wordt de toepassing gecompileerd en worden alle bestanden geplaatst die nodig zijn om de toepassing uit te voeren in een aangewezen map. Wanneer u vanuit Visual Studio implementeert, wordt deze stap automatisch voor u uitgevoerd. De publicatiemap bevat .exe- en .dll-bestanden voor de toepassing en de bijbehorende afhankelijkheden. Een zelfstandige toepassing bevat ook een versie van de .NET-runtime. ASP.NET Core-toepassingen bevatten ook configuratiebestanden, statische clientassets en MVC-weergaven.

ASP.NET Core-toepassingen zijn consoletoepassingen die moeten worden gestart wanneer de server wordt opgestart en opnieuw wordt opgestart als de toepassing (of server) vastloopt. Een procesbeheerder kan worden gebruikt om dit proces te automatiseren. De meest voorkomende procesbeheerders voor ASP.NET Core zijn Nginx en Apache op Linux en IIS of Windows Service in Windows.

Naast een procesbeheerder kunnen ASP.NET Core-toepassingen gebruikmaken van een omgekeerde proxyserver. Een omgekeerde proxyserver ontvangt HTTP-aanvragen van internet en stuurt deze door naar Kestrel na enige voorbereidende verwerking. Omgekeerde proxyservers bieden een beveiligingslaag voor de toepassing. Kestrel biedt ook geen ondersteuning voor het hosten van meerdere toepassingen op dezelfde poort, dus technieken zoals hostheaders kunnen niet worden gebruikt om meerdere toepassingen op dezelfde poort en hetzelfde IP-adres te hosten.

Kestrel naar internet

Afbeelding 7-5. ASP.NET gehost in Kestrel achter een omgekeerde proxyserver

Een ander scenario waarin een omgekeerde proxy nuttig kan zijn, is om meerdere toepassingen te beveiligen met SSL/HTTPS. In dit geval hoeft alleen de omgekeerde proxy SSL te hebben geconfigureerd. Communicatie tussen de omgekeerde proxyserver en Kestrel kan plaatsvinden via HTTP, zoals weergegeven in afbeelding 7-6.

ASP.NET gehost achter een met HTTPS beveiligde omgekeerde proxyserver

Afbeelding 7-6. ASP.NET gehost achter een met HTTPS beveiligde omgekeerde proxyserver

Een steeds populairdere benadering is het hosten van uw ASP.NET Core-toepassing in een Docker-container, die vervolgens lokaal kan worden gehost of geïmplementeerd in Azure voor hosting in de cloud. De Docker-container kan uw toepassingscode bevatten, uitgevoerd op Kestrel en wordt geïmplementeerd achter een omgekeerde proxyserver, zoals hierboven wordt weergegeven.

Als u uw toepassing host in Azure, kunt u Microsoft Azure-toepassing Gateway gebruiken als een toegewezen virtueel apparaat om verschillende services te bieden. Naast het fungeren als een omgekeerde proxy voor afzonderlijke toepassingen, kan Application Gateway ook de volgende functies bieden:

  • HTTP-taakverdeling

  • SSL-offload (alleen SSL naar internet)

  • End-to-end SSL

  • Routering met meerdere sites (consolidatie van maximaal 20 sites op één Application Gateway)

  • Web Application Firewall

  • Websocket-ondersteuning

  • Geavanceerde diagnostische gegevens

Meer informatie over Azure-implementatieopties in hoofdstuk 10.

Verwijzingen – Implementatie