Utveckla ASP.NET Core MVC-appar
Dricks
Det här innehållet är ett utdrag från eBook, Architect Modern Web Applications med ASP.NET Core och Azure, som finns på .NET Docs eller som en kostnadsfri nedladdningsbar PDF som kan läsas offline.
– Det är inte viktigt att få det rätt första gången. Det är oerhört viktigt att få det rätt sista gången." - Andrew Hunt och David Thomas
ASP.NET Core är ett plattformsoberoende ramverk med öppen källkod för att skapa moderna molnoptimerade webbprogram. ASP.NET Core-appar är lätta och modulära, med inbyggt stöd för beroendeinmatning, vilket möjliggör större testbarhet och underhåll. Kombinerat med MVC, som har stöd för att skapa moderna webb-API:er utöver visningsbaserade appar, är ASP.NET Core ett kraftfullt ramverk för att skapa företagswebbprogram.
MVC- och Razor-sidor
ASP.NET Core MVC har många funktioner som är användbara för att skapa webbaserade API:er och appar. Termen MVC står för "Model-View-Controller", ett användargränssnittsmönster som delar upp ansvaret för att svara på användarförfrågningar i flera delar. Förutom att följa det här mönstret kan du även implementera funktioner i dina ASP.NET Core-appar som Razor Pages.
Razor Pages är inbyggda i ASP.NET Core MVC och använder samma funktioner för routning, modellbindning, filter, auktorisering osv. I stället för att ha separata mappar och filer för kontrollanter, modeller, vyer osv. och med hjälp av attributbaserad routning placeras Razor Pages i en enda mapp ("/Pages"), väg baserat på deras relativa plats i den här mappen och hanterar begäranden med hanterare i stället för kontrollantåtgärder. När du arbetar med Razor Pages är därför alla filer och klasser som du behöver vanligtvis samlokaliserade, inte spridda i webbprojektet.
Läs mer om hur MVC, Razor Pages och relaterade mönster tillämpas i exempelprogrammet eShopOnWeb.
När du skapar en ny ASP.NET Core App bör du ha en plan i åtanke för den typ av app som du vill skapa. När du skapar ett nytt projekt, i din IDE eller med hjälp av dotnet new
CLI-kommandot, väljer du bland flera mallar. De vanligaste projektmallarna är Tomma, Webb-API, Webbapp och Webbapp (Model-View-Controller). Även om du bara kan fatta det här beslutet när du först skapar ett projekt är det inte ett oåterkalleligt beslut. Webb-API-projektet använder Standard Model-View-Controller-styrenheter – det saknar bara vyer som standard. På samma sätt använder standardmallen för webbappar Razor Pages och saknar därför även en views-mapp. Du kan lägga till en views-mapp i dessa projekt senare för att stödja visningsbaserat beteende. Webb-API- och Model-View-Controller-projekt innehåller inte en pages-mapp som standard, men du kan lägga till en senare för att stödja Razor Pages-baserat beteende. Du kan se dessa tre mallar som stöd för tre olika typer av standardanvändarinteraktion: data (webb-API), sidbaserade och visningsbaserade. Du kan dock blanda och matcha alla dessa mallar i ett enda projekt om du vill.
Varför Razor Pages?
Razor Pages är standardmetoden för nya webbprogram i Visual Studio. Razor Pages erbjuder ett enklare sätt att skapa sidbaserade programfunktioner, till exempel icke-SPA-formulär. Med hjälp av kontrollanter och vyer var det vanligt att program hade mycket stora kontrollanter som fungerade med många olika beroenden och visningsmodeller och returnerade många olika vyer. Detta resulterade i mer komplexitet och resulterade ofta i kontrollanter som inte följde principen för enskilt ansvar eller öppna/stängda principer effektivt. Razor Pages löser det här problemet genom att kapsla in logiken på serversidan för en viss logisk "sida" i ett webbprogram med dess Razor-markering. En Razor-sida som inte har någon logik på serversidan kan bara bestå av en Razor-fil (till exempel "Index.cshtml"). De flesta icke-triviala Razor Pages har dock en associerad sidmodellklass, som enligt konventionen heter samma som Razor-filen med tillägget ".cs" (till exempel "Index.cshtml.cs").
En Razor-sidas sidmodell kombinerar ansvarsområdena för en MVC-styrenhet och en vymodell. I stället för att hantera begäranden med kontrollantåtgärdsmetoder körs sidmodellhanterare som "OnGet()" och återger sin associerade sida som standard. Razor Pages förenklar processen med att skapa enskilda sidor i en ASP.NET Core-app, samtidigt som alla arkitektoniska funktioner i ASP.NET Core MVC finns. De är ett bra standardval för nya sidbaserade funktioner.
När du ska använda MVC
Om du skapar webb-API:er är MVC-mönstret mer meningsfullt än att försöka använda Razor Pages. Om ditt projekt bara exponerar webb-API-slutpunkter bör du helst börja från webb-API-projektmallen. Annars är det enkelt att lägga till kontrollanter och associerade API-slutpunkter i valfri ASP.NET Core-app. Använd den vybaserade MVC-metoden om du migrerar ett befintligt program från ASP.NET MVC 5 eller tidigare till ASP.NET Core MVC och du vill göra det med minsta möjliga ansträngning. När du har gjort den första migreringen kan du utvärdera om det är meningsfullt att använda Razor Pages för nya funktioner eller till och med som en grossistmigrering. Mer information om hur du porterar .NET 4.x-appar till .NET 8 finns i Porting Existing ASP.NET Apps to ASP.NET Core eBook(Porting Existing ASP.NET Apps to ASP.NET Core eBook).
Oavsett om du väljer att skapa din webbapp med hjälp av Razor Pages- eller MVC-vyer har appen liknande prestanda och kommer att innehålla stöd för beroendeinmatning, filter, modellbindning, validering och så vidare.
Mappa begäranden till svar
I grunden mappar ASP.NET Core-appar inkommande begäranden till utgående svar. På en låg nivå görs den här mappningen med mellanprogram, och enkla ASP.NET Core-appar och mikrotjänster kan endast bestå av anpassade mellanprogram. När du använder ASP.NET Core MVC kan du arbeta på en något högre nivå med tanke på vägar, kontrollanter och åtgärder. Varje inkommande begäran jämförs med programmets routningstabell, och om en matchande väg hittas anropas den associerade åtgärdsmetoden (som tillhör en kontrollant) för att hantera begäran. Om ingen matchande väg hittas anropas en felhanterare (i det här fallet returnerar ett NotFound-resultat).
ASP.NET Core MVC-appar kan använda konventionella vägar, attributvägar eller både och. Konventionella vägar definieras i kod och anger routningskonventioner med syntax som i exemplet nedan:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
});
I det här exemplet har en väg med namnet "default" lagts till i routningstabellen. Den definierar en vägmall med platshållare för controller
, action
och id
. Platshållarna controller
och action
har standardvärdet angivet (Home
Index
respektive , och id
platshållaren är valfri (på grund av ett "?" som tillämpas på den). Konventionen som definieras här anger att den första delen av en begäran ska motsvara namnet på kontrollanten, den andra delen till åtgärden, och vid behov representerar en tredje del en ID-parameter. Konventionella vägar definieras vanligtvis på en plats för programmet, till exempel i Program.cs där pipelinen för mellanprogram för begäran har konfigurerats.
Attributvägar tillämpas direkt på kontrollanter och åtgärder i stället för att anges globalt. Den här metoden har fördelen att göra dem mycket mer identifierbara när du tittar på en viss metod, men innebär att routningsinformation inte sparas på en plats i programmet. Med attributvägar kan du enkelt ange flera vägar för en viss åtgärd, samt kombinera vägar mellan kontrollanter och åtgärder. Till exempel:
[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() {}
}
Vägar kan anges på [HttpGet] och liknande attribut, vilket undviker behovet av att lägga till separata [Route]-attribut. Attributvägar kan också använda token för att minska behovet av att upprepa kontrollant- eller åtgärdsnamn, enligt nedan:
[Route("[controller]")]
public class ProductsController : Controller
{
[Route("")] // Matches 'Products'
[Route("Index")] // Matches 'Products/Index'
public IActionResult Index() {}
}
Razor Pages använder inte attributroutning. Du kan ange ytterligare information om routningsmallar för en Razor-sida som en del av direktivet @page
:
@page "{id:int}"
I föregående exempel skulle sidan i fråga matcha en väg med en heltalsparameter id
. Till exempel svarar sidan Products.cshtml i roten på /Pages
begäranden som den här:
/Products/123
När en viss begäran har matchats till en väg, men innan åtgärdsmetoden anropas, utför ASP.NET Core MVC modellbindning och modellverifiering på begäran. Modellbindningen ansvarar för att konvertera inkommande HTTP-data till de .NET-typer som anges som parametrar för den åtgärdsmetod som ska anropas. Om åtgärdsmetoden till exempel förväntar sig en int id
parameter försöker modellbindningen ange den här parametern från ett värde som anges som en del av begäran. För att göra det söker modellbindningen efter värden i ett publicerat formulär, värden i själva vägen och frågesträngsvärden. Förutsatt att ett id
värde hittas konverteras det till ett heltal innan det skickas till åtgärdsmetoden.
När modellen har bindts men innan åtgärdsmetoden anropas sker modellverifiering. Modellvalidering använder valfria attribut för modelltypen och kan hjälpa till att säkerställa att det angivna modellobjektet uppfyller vissa datakrav. Vissa värden kan anges efter behov eller begränsas till ett visst längd- eller numeriskt intervall osv. Om valideringsattribut anges men modellen inte uppfyller deras krav är egenskapen ModelState.IsValid falskt och uppsättningen med verifieringsregler som misslyckas kommer att vara tillgänglig för att skicka till klienten som gör begäran.
Om du använder modellverifiering bör du alltid kontrollera att modellen är giltig innan du utför tillståndsförändrande kommandon för att säkerställa att appen inte är skadad av ogiltiga data. Du kan använda ett filter för att undvika behovet av att lägga till kod för den här verifieringen i varje åtgärd. ASP.NET Core MVC-filter erbjuder ett sätt att fånga upp grupper av begäranden, så att gemensamma principer och övergripande problem kan tillämpas på målbasis. Filter kan tillämpas på enskilda åtgärder, hela styrenheter eller globalt för ett program.
För webb-API:er stöder ASP.NET Core MVC innehållsförhandling, vilket gör att begäranden kan ange hur svar ska formateras. Baserat på rubriker som anges i begäran formaterar åtgärder som returnerar data svaret i XML, JSON eller något annat format som stöds. Med den här funktionen kan samma API användas av flera klienter med olika krav på dataformat.
Webb-API-projekt bör överväga att använda [ApiController]
attributet, som kan tillämpas på enskilda kontrollanter, på en basstyrenhetsklass eller på hela sammansättningen. Det här attributet lägger till automatisk modellverifieringskontroll och alla åtgärder med en ogiltig modell returnerar en BadRequest med information om valideringsfelen. Attributet kräver också att alla åtgärder har en attributväg i stället för att använda en konventionell väg och returnerar mer detaljerad ProblemDetails-information som svar på fel.
Hålla kontrollanter under kontroll
För sidbaserade program gör Razor Pages ett bra jobb med att hindra kontrollanter från att bli för stora. Varje enskild sida får sina egna filer och klasser dedikerade bara till sina hanterare. Före introduktionen av Razor Pages skulle många visningscentrerade program ha stora kontrollantklasser som ansvarar för många olika åtgärder och vyer. Dessa klasser skulle naturligt växa till att ha många ansvarsområden och beroenden, vilket gör dem svårare att underhålla. Om du tycker att dina visningsbaserade styrenheter blir för stora kan du överväga att omstrukturera dem för att använda Razor Pages eller introducera ett mönster som en medlare.
Designmönstret för medlare används för att minska kopplingen mellan klasser samtidigt som kommunikationen mellan dem tillåts. I ASP.NET Core MVC-program används det här mönstret ofta för att dela upp styrenheter i mindre delar genom att använda hanterare för att utföra åtgärdsmetoder. Det populära MediatR NuGet-paketet används ofta för att åstadkomma detta. Styrenheter innehåller vanligtvis många olika åtgärdsmetoder, som var och en kan kräva vissa beroenden. Uppsättningen med alla beroenden som krävs av en åtgärd måste skickas till styrenhetens konstruktor. När du använder MediatR är det enda beroende som en kontrollant vanligtvis har en instans av medlaren. Varje åtgärd använder sedan medlarinstansen för att skicka ett meddelande som bearbetas av en hanterare. Hanteraren är specifik för en enskild åtgärd och behöver därför bara de beroenden som krävs av den åtgärden. Ett exempel på en kontrollant som använder MediatR visas här:
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
}
I åtgärden MyOrders
hanteras anropet till Send
ett GetMyOrders
meddelande av den här klassen:
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()
});
}
}
Slutresultatet av den här metoden är att kontrollanter är mycket mindre och främst fokuserar på routning och modellbindning, medan enskilda hanterare ansvarar för de specifika uppgifter som krävs av en viss slutpunkt. Den här metoden kan också uppnås utan MediatR med hjälp av ApiEndpoints NuGet-paketet, som försöker ge API-kontrollanter samma fördelar som Razor Pages medför för visningsbaserade kontrollanter.
Referenser – Mappa begäranden till svar
- Routning till kontrollantåtgärder
https://learn.microsoft.com/aspnet/core/mvc/controllers/routing- Modellbindning
https://learn.microsoft.com/aspnet/core/mvc/models/model-binding- Modellverifiering
https://learn.microsoft.com/aspnet/core/mvc/models/validation- Filter
https://learn.microsoft.com/aspnet/core/mvc/controllers/filters- ApiController-attribut
https://learn.microsoft.com/aspnet/core/web-api/
Arbeta med beroenden
ASP.NET Core har inbyggt stöd för och använder internt en teknik som kallas beroendeinmatning. Beroendeinmatning är en teknik som möjliggör lös koppling mellan olika delar av ett program. Lösare koppling är önskvärt eftersom det gör det lättare att isolera delar av programmet, vilket möjliggör testning eller ersättning. Det gör det också mindre troligt att en ändring i en del av programmet får en oväntad inverkan någon annanstans i programmet. Beroendeinmatning baseras på principen om beroendeinversion och är ofta nyckeln till att uppnå principen öppen/stängd. När du utvärderar hur programmet fungerar med dess beroenden bör du akta dig för den statiska klamringskodens lukt och komma ihåg aforismen "nytt är lim".
Statisk klamring inträffar när dina klasser anropar statiska metoder eller får åtkomst till statiska egenskaper som har biverkningar eller beroenden i infrastrukturen. Om du till exempel har en metod som anropar en statisk metod, som i sin tur skriver till en databas, är din metod nära kopplad till databasen. Allt som bryter databasanropet bryter din metod. Att testa sådana metoder är notoriskt svårt, eftersom sådana tester antingen kräver kommersiella hånbibliotek för att håna de statiska anropen eller bara kan testas med en testdatabas på plats. Statiska anrop som inte har något beroende av infrastruktur, särskilt de anrop som är helt tillståndslösa, är bra att anropa och har ingen inverkan på koppling eller testbarhet (utöver kopplingskod till själva statiska anropet).
Många utvecklare förstår riskerna med statisk klamring och globalt tillstånd, men kommer fortfarande att nära koppla sin kod till specifika implementeringar genom direkt instansiering. "Nytt är lim" är tänkt att vara en påminnelse om denna koppling, och inte ett allmänt fördömande av användningen av nyckelordet new
. Precis som med statiska metodanrop är nya instanser av typer som inte har några externa beroenden vanligtvis inte nära kopplade kod för implementeringsinformation eller gör testningen svårare. Men varje gång en klass instansieras tar det bara en kort stund att överväga om det är meningsfullt att hårdkoda den specifika instansen på den specifika platsen eller om det skulle vara en bättre design att begära den instansen som ett beroende.
Deklarera dina beroenden
ASP.NET Core bygger på att metoder och klasser deklarerar sina beroenden och begär dem som argument. ASP.NET program konfigureras vanligtvis i Program.cs eller i en Startup
klass.
Kommentar
Att konfigurera appar helt i Program.cs är standardmetoden för .NET 6 (och senare) och Visual Studio 2022-appar. Projektmallar har uppdaterats för att hjälpa dig att komma igång med den här nya metoden. ASP.NET Core-projekt kan fortfarande använda en Startup
klass, om så önskas.
Konfigurera tjänster i Program.cs
För mycket enkla appar kan du koppla upp beroenden direkt i Program.cs fil med hjälp av en WebApplicationBuilder
. När alla nödvändiga tjänster har lagts till används byggverktyget för att skapa appen.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
var app = builder.Build();
Konfigurera tjänster i Startup.cs
Själva Startup.cs har konfigurerats för att stödja beroendeinmatning vid flera tillfällen. Om du använder en Startup
klass kan du ge den en konstruktor och den kan begära beroenden via den, så här:
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);
}
}
Klassen Startup
är intressant eftersom det inte finns några explicita typkrav för den. Den ärver inte från en särskild Startup
basklass och implementerar inte heller något visst gränssnitt. Du kan ge den en konstruktor, eller inte, och du kan ange så många parametrar på konstruktorn som du vill. När webbvärden som du har konfigurerat för ditt program startar anropas klassen (om du har sagt åt den Startup
att använda en) och använder beroendeinmatning för att fylla i eventuella beroenden som Startup
klassen kräver. Om du begär parametrar som inte är konfigurerade i tjänstcontainern som används av ASP.NET Core får du naturligtvis ett undantag, men så länge du håller dig till beroenden som containern känner till kan du begära vad du vill.
Beroendeinmatning är inbyggd i dina ASP.NET Core-appar redan från början när du skapar startinstansen. Den slutar inte där för startklassen. Du kan också begära beroenden Configure
i metoden:
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
}
Metoden ConfigureServices är undantaget från det här beteendet. det måste bara ta en parameter av typen IServiceCollection
. Den behöver egentligen inte stödja beroendeinmatning, eftersom den å ena sidan ansvarar för att lägga till objekt i tjänstcontainern och å andra sidan har åtkomst till alla för närvarande konfigurerade tjänster via parametern IServiceCollection
. Därför kan du arbeta med beroenden som definierats i samlingen ASP.NET Core-tjänster i varje del av Startup
klassen, antingen genom att begära den nödvändiga tjänsten som en parameter eller genom att arbeta med IServiceCollection
i ConfigureServices
.
Kommentar
Om du behöver se till att vissa tjänster är tillgängliga för din Startup
klass kan du konfigurera dem med hjälp av en IWebHostBuilder
och dess ConfigureServices
-metod i anropet CreateDefaultBuilder
.
Startklassen är en modell för hur du ska strukturera andra delar av ditt ASP.NET Core-program, från Styrenheter till Mellanprogram till Filter till dina egna tjänster. I varje fall bör du följa principen explicita beroenden, begära dina beroenden i stället för att skapa dem direkt och utnyttja beroendeinmatningen i hela programmet. Var försiktig med var och hur du direkt instansierar implementeringar, särskilt tjänster och objekt som fungerar med infrastruktur eller har biverkningar. Föredrar att arbeta med abstraktioner som definierats i programkärnan och skickats som argument till hårdkodningsreferenser till specifika implementeringstyper.
Strukturera programmet
Monolitiska program har vanligtvis en enda startpunkt. När det gäller ett ASP.NET Core-webbprogram blir startpunkten webbprojektet ASP.NET Core. Det betyder dock inte att lösningen bara ska bestå av ett enda projekt. Det är användbart att dela upp programmet i olika lager för att följa separationen av problem. När det har delats upp i lager är det bra att gå längre än mappar till separata projekt, vilket kan bidra till bättre inkapsling. Det bästa sättet att uppnå dessa mål med ett ASP.NET Core-program är en variant av den rena arkitektur som beskrivs i kapitel 5. Med den här metoden består programmets lösning av separata bibliotek för användargränssnittet, infrastrukturen och ApplicationCore.
Utöver dessa projekt ingår även separata testprojekt (testning beskrivs i kapitel 9).
Programmets objektmodell och gränssnitt ska placeras i ApplicationCore-projektet. Det här projektet kommer att ha så få beroenden som möjligt (och inga på specifika infrastrukturproblem), och de andra projekten i lösningen refererar till det. Affärsentiteter som behöver bevaras definieras i ApplicationCore-projektet, liksom tjänster som inte är direkt beroende av infrastruktur.
Implementeringsinformation, till exempel hur beständighet utförs eller hur meddelanden kan skickas till en användare, behålls i infrastrukturprojektet. Det här projektet refererar till implementeringsspecifika paket som Entity Framework Core, men bör inte visa information om dessa implementeringar utanför projektet. Infrastrukturtjänster och lagringsplatser bör implementera gränssnitt som definieras i ApplicationCore-projektet och dess beständighetsimplementeringar ansvarar för att hämta och lagra entiteter som definierats i ApplicationCore.
Projektet ASP.NET Core UI ansvarar för eventuella problem på användargränssnittsnivå, men bör inte innehålla information om affärslogik eller infrastruktur. I själva verket bör det helst inte ens ha ett beroende av infrastrukturprojektet, vilket hjälper till att säkerställa att inget beroende mellan de två projekten introduceras av misstag. Detta kan uppnås med hjälp av en DI-container från tredje part som Autofac, som gör att du kan definiera DI-regler i modulklasser i varje projekt.
En annan metod för att avkoda programmet från implementeringsinformation är att ha mikrotjänster för programanrop, kanske distribuerade i enskilda Docker-containrar. Detta ger ännu större uppdelning av problem och avkoppling än att utnyttja DI mellan två projekt, men har ytterligare komplexitet.
Funktionsorganisation
Som standard organiserar ASP.NET Core-program sin mappstruktur så att de innehåller kontrollanter och vyer och ofta ViewModels. Kod på klientsidan som stöd för dessa strukturer på serversidan lagras vanligtvis separat i mappen wwwroot. Stora program kan dock stöta på problem med den här organisationen, eftersom arbete med en viss funktion ofta kräver att du hoppar mellan dessa mappar. Detta blir allt svårare när antalet filer och undermappar i varje mapp växer, vilket resulterar i en hel del rullning genom Solution Explorer. En lösning på det här problemet är att ordna programkod efter funktion i stället för efter filtyp. Det här organisationsformatet kallas vanligtvis funktionsmappar eller funktionssektorer (se även: Lodräta sektorer).
ASP.NET Core MVC stöder områden för detta ändamål. Med hjälp av områden kan du skapa separata uppsättningar med kontrollanter och vyer (samt eventuella associerade modeller) i varje områdesmapp. Bild 7–1 visar en exempelmappstruktur med hjälp av Områden.
Bild 7-1. Exempelområdesorganisation
När du använder Områden måste du använda attribut för att dekorera dina kontrollanter med namnet på det område som de tillhör:
[Area("Catalog")]
public class HomeController
{}
Du måste också lägga till områdesstöd för dina vägar:
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?}");
});
Förutom det inbyggda stödet för Områden kan du också använda din egen mappstruktur och konventioner i stället för attribut och anpassade vägar. På så sätt kan du ha funktionsmappar som inte innehåller separata mappar för vyer, kontrollanter osv., vilket gör hierarkin plattare och gör det enklare att se alla relaterade filer på en enda plats för varje funktion. För API:er kan mappar användas för att ersätta kontrollanter och varje mapp kan innehålla alla API-slutpunkter och deras associerade DTU:er.
ASP.NET Core använder inbyggda konventionstyper för att styra dess beteende. Du kan ändra eller ersätta dessa konventioner. Du kan till exempel skapa en konvention som automatiskt hämtar funktionsnamnet för en viss kontrollant baserat på dess namnområde (som vanligtvis korrelerar med mappen där kontrollanten finns):
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;
}
}
Du anger sedan den här konventionen som ett alternativ när du lägger till stöd för MVC i ditt program i ConfigureServices
(eller i 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 använder också en konvention för att hitta vyer. Du kan åsidosätta den med en anpassad konvention så att vyer finns i dina funktionsmappar (med hjälp av funktionsnamnet som tillhandahålls av FeatureConvention ovan). Du kan lära dig mer om den här metoden och ladda ned ett arbetsexempel från artikeln MSDN Magazine, Feature Slices for ASP.NET Core MVC.
API:er och Blazor program
Om ditt program innehåller en uppsättning webb-API:er, som måste skyddas, bör dessa API:er helst konfigureras som ett separat projekt från ditt Visa- eller Razor Pages-program. Att separera API:er, särskilt offentliga API:er, från webbprogrammet på serversidan har ett antal fördelar. Dessa program har ofta unika distributions- och belastningsegenskaper. Det är också mycket troligt att de använder olika säkerhetsmekanismer, med standardformulärbaserade program som utnyttjar cookiebaserad autentisering och API:er som troligen använder tokenbaserad autentisering.
Dessutom Blazor bör program, oavsett om de använder Blazor Server eller BlazorWebAssembly, skapas som separata projekt. Programmen har olika körningsegenskaper och säkerhetsmodeller. De kommer sannolikt att dela vanliga typer med webbprogrammet på serversidan (eller API-projektet), och dessa typer bör definieras i ett gemensamt delat projekt.
Tillägget av ett BlazorWebAssembly administratörsgränssnitt till eShopOnWeb krävde att flera nya projekt skulle läggas till. Själva BlazorWebAssembly projektet, BlazorAdmin
. En ny uppsättning offentliga API-slutpunkter, som används av BlazorAdmin
och konfigureras för att använda tokenbaserad autentisering, definieras i PublicApi
projektet. Och vissa delade typer som används av båda dessa projekt behålls i ett nytt BlazorShared
projekt.
Man kan fråga sig varför lägga till ett separat BlazorShared
projekt när det redan finns ett gemensamt ApplicationCore
projekt som kan användas för att dela alla typer som krävs av både PublicApi
och BlazorAdmin
? Svaret är att det här projektet innehåller hela programmets affärslogik och därför är mycket större än nödvändigt och dessutom mycket mer sannolikt måste hållas säkert på servern. Kom ihåg att alla bibliotek som refereras av BlazorAdmin
laddas ned till användarnas webbläsare när de läser in Blazor programmet.
Beroende på om man använder mönstret Backends-For-Frontends (BFF) kanske de API:er som används av BlazorWebAssembly appen inte delar sina typer med 100 % med Blazor. I synnerhet kan ett offentligt API som är avsett att användas av många olika klienter definiera sina egna typer av begäranden och resultat i stället för att dela dem i ett klientspecifikt delat projekt. I eShopOnWeb-exemplet antas att PublicApi
projektet i själva verket är värd för ett offentligt API, så inte alla dess typer av begäranden och svar kommer från BlazorShared
projektet.
Övergripande frågor
I takt med att programmen växer blir det allt viktigare att ta hänsyn till övergripande problem för att eliminera duplicering och upprätthålla konsekvens. Några exempel på övergripande problem i ASP.NET Core-program är autentisering, modellverifieringsregler, cachelagring av utdata och felhantering, även om det finns många andra. ASP.NET Core MVC-filter kan du köra kod före eller efter vissa steg i pipelinen för bearbetning av begäranden. Ett filter kan till exempel köras före och efter modellbindningen, före och efter en åtgärd, eller före och efter en åtgärds resultat. Du kan också använda ett auktoriseringsfilter för att styra åtkomsten till resten av pipelinen. Siffror 7–2 visar hur körning av begäranden flödar via filter, om det är konfigurerat.
Bild 7-2. Begär körning via filter och pipeline för begäranden.
Filter implementeras vanligtvis som attribut, så du kan tillämpa dem på kontrollanter eller åtgärder (eller till och med globalt). När de läggs till på det här sättet åsidosätter eller bygger filter som anges på kontrollantnivå, som i sig åsidosätter globala filter. Attributet kan till exempel [Route]
användas för att bygga upp vägar mellan kontrollanter och åtgärder. På samma sätt kan auktorisering konfigureras på kontrollantnivå och sedan åsidosättas av enskilda åtgärder, vilket visas i följande exempel:
[Authorize]
public class AccountController : Controller
{
[AllowAnonymous] // overrides the Authorize attribute
public async Task<IActionResult> Login() {}
public async Task<IActionResult> ForgotPassword() {}
}
Den första metoden, Login, använder [AllowAnonymous]
filtret (attributet) för att åsidosätta auktorisera filteruppsättningen på kontrollantnivå. Åtgärden ForgotPassword
(och andra åtgärder i klassen som inte har ett AllowAnonymous-attribut) kräver en autentiserad begäran.
Filter kan användas för att eliminera duplicering i form av vanliga felhanteringsprinciper för API:er. En typisk API-princip är till exempel att returnera ett NotFound-svar på begäranden som refererar till nycklar som inte finns och ett BadRequest
svar om modellverifieringen misslyckas. I följande exempel visas dessa två principer i praktiken:
[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();
}
Tillåt inte att dina åtgärdsmetoder blir belamrade med villkorsstyrd kod som den här. Hämta i stället principerna till filter som kan tillämpas efter behov. I det här exemplet kan modellverifieringskontrollen, som ska ske när ett kommando skickas till API:et, ersättas med följande attribut:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
Du kan lägga till i ValidateModelAttribute
projektet som ett NuGet-beroende genom att inkludera paketet Ardalis.ValidateModel . För API:er kan du använda ApiController
attributet för att framtvinga det här beteendet utan att behöva ett separat ValidateModel
filter.
På samma sätt kan ett filter användas för att kontrollera om en post finns och returnera en 404 innan åtgärden körs, vilket eliminerar behovet av att utföra dessa kontroller i åtgärden. När du har tagit fram vanliga konventioner och organiserat din lösning för att separera infrastrukturkod och affärslogik från användargränssnittet bör MVC-åtgärdsmetoderna vara extremt tunna:
[HttpPut("{id}")]
[ValidateAuthorExists]
public async Task<IActionResult> Put(int id, [FromBody]Author author)
{
await _authorRepository.UpdateAsync(author);
return Ok();
}
Du kan läsa mer om att implementera filter och ladda ned ett arbetsexempel från artikeln MSDN Magazine, Real-World ASP.NET Core MVC Filters.
Om du upptäcker att du har ett antal vanliga svar från API:er baserat på vanliga scenarier som valideringsfel (felaktig begäran), resurs som inte hittades och serverfel, kan du överväga att använda en resultatabstraktion . Resultatabstraktionen returneras av tjänster som används av API-slutpunkter, och kontrollantåtgärden eller slutpunkten skulle använda ett filter för att översätta dessa till IActionResults
.
Referenser – Strukturera program
- Områden
https://learn.microsoft.com/aspnet/core/mvc/controllers/areas- MSDN Magazine – Funktionssegment för ASP.NET Core MVC
https://learn.microsoft.com/archive/msdn-magazine/2016/september/asp-net-core-feature-slices-for-asp-net-core-mvc- Filter
https://learn.microsoft.com/aspnet/core/mvc/controllers/filters- MSDN Magazine – Real World ASP.NET Core MVC Filter
https://learn.microsoft.com/archive/msdn-magazine/2016/august/asp-net-core-real-world-asp-net-core-mvc-filters- Resultat i eShopOnWeb
https://github.com/dotnet-architecture/eShopOnWeb/wiki/Patterns#result
Säkerhet
Att skydda webbprogram är ett stort ämne, med många saker att tänka på. På den mest grundläggande nivån innebär säkerheten att se till att du vet vem en viss begäran kommer från och sedan se till att begäran bara har åtkomst till resurser som den bör. Autentisering är en process för att jämföra autentiseringsuppgifter som tillhandahålls med en begäran till dem i ett betrott datalager, för att se om begäran ska behandlas som kommer från en känd entitet. Auktorisering är processen för att begränsa åtkomsten till vissa resurser baserat på användaridentitet. Ett tredje säkerhetsproblem är att skydda begäranden från avlyssning från tredje part, för vilka du åtminstone bör se till att SSL används av ditt program.
Identitet
ASP.NET Core Identity är ett medlemskapssystem som du kan använda för att stödja inloggningsfunktioner för ditt program. Den har stöd för lokala användarkonton samt stöd för extern inloggningsprovider från leverantörer som Microsoft-konto, Twitter, Facebook, Google med mera. Förutom ASP.NET Core Identity kan ditt program använda Windows-autentisering eller en identitetsprovider från tredje part som Identity Server.
ASP.NET Core Identity ingår i nya projektmallar om alternativet Enskilda användarkonton har valts. Den här mallen innehåller stöd för registrering, inloggning, externa inloggningar, bortglömda lösenord och ytterligare funktioner.
Bild 7-3. Välj Enskilda användarkonton för att ha identiteten förkonfigurerad.
Identitetsstöd konfigureras i Program.cs eller Startup
, och omfattar konfiguration av tjänster samt mellanprogram.
Konfigurera identitet i Program.cs
I Program.cs konfigurerar du tjänster från instansen WebHostBuilder
och när appen har skapats konfigurerar du dess mellanprogram. De viktigaste punkterna att notera är anropet till AddDefaultIdentity
för nödvändiga tjänster och de UseAuthentication
och-anrop UseAuthorization
som lägger till obligatoriskt mellanprogram.
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();
Konfigurera identitet i appstart
// 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();
Det är viktigt att och UseAuthentication
UseAuthorization
visas före MapRazorPages
. När du konfigurerar identitetstjänster ser du ett anrop till AddDefaultTokenProviders
. Detta har inget att göra med token som kan användas för att skydda webbkommunikation, utan refererar i stället till leverantörer som skapar uppmaningar som kan skickas till användare via SMS eller e-post för att de ska kunna bekräfta sin identitet.
Du kan lära dig mer om att konfigurera tvåfaktorautentisering och aktivera externa inloggningsprovidrar från de officiella ASP.NET Core-dokumenten.
Autentisering
Autentisering är en process för att avgöra vem som har åtkomst till systemet. Om du använder ASP.NET Core Identity och konfigurationsmetoderna som visas i föregående avsnitt konfigureras automatiskt vissa standardinställningar för autentisering i programmet. Du kan dock också konfigurera dessa standardvärden manuellt eller åsidosätta de som anges av AddIdentity. Om du använder Identity konfigureras cookiebaserad autentisering som standardschema.
I webbaserad autentisering finns det vanligtvis upp till fem åtgärder som kan utföras under autentiseringen av en klient i ett system. Dessa är:
- Autentisera. Använd informationen som tillhandahålls av klienten för att skapa en identitet som de kan använda i programmet.
- Utmaning. Den här åtgärden används för att kräva att klienten identifierar sig.
- Förbjuda. Informera klienten om att de är förbjudna att utföra en åtgärd.
- Logga in. Spara den befintliga klienten på något sätt.
- Logga ut. Ta bort klienten från beständighet.
Det finns ett antal vanliga tekniker för att utföra autentisering i webbprogram. Dessa kallas system. Ett visst schema definierar åtgärder för vissa eller alla ovanstående alternativ. Vissa system stöder endast en delmängd av åtgärder och kan kräva ett separat schema för att utföra dem som inte stöds. Till exempel stöder OpenId-Anslut-schemat (OIDC) inte inloggning eller utloggning, men är ofta konfigurerat för att använda Cookie-autentisering för denna beständighet.
I ditt ASP.NET Core-program kan du konfigurera såväl som valfria specifika scheman för var och en DefaultAuthenticateScheme
av de åtgärder som beskrivs ovan. Till exempel DefaultChallengeScheme
och DefaultForbidScheme
. Samtal AddIdentity konfigurerar ett antal aspekter av programmet och lägger till många nödvändiga tjänster. Den innehåller även det här anropet för att konfigurera autentiseringsschemat:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});
Dessa scheman använder cookies för beständighet och omdirigering till inloggningssidor för autentisering som standard. Dessa scheman är lämpliga för webbprogram som interagerar med användare via webbläsare, men som inte rekommenderas för API:er. I stället använder API:er vanligtvis en annan form av autentisering, till exempel JWT-ägartoken.
Webb-API:er används av kod, till exempel HttpClient
i .NET-program och motsvarande typer i andra ramverk. Dessa klienter förväntar sig ett användbart svar från ett API-anrop eller en statuskod som anger vad, om något, problem har uppstått. Dessa klienter interagerar inte via en webbläsare och renderar inte eller interagerar inte med någon HTML som ett API kan returnera. Det är därför inte lämpligt för API-slutpunkter att omdirigera sina klienter till inloggningssidor om de inte autentiseras. Ett annat system är lämpligare.
Om du vill konfigurera autentisering för API:er kan du konfigurera autentisering som följande, som används av PublicApi
projektet i referensprogrammet 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
};
});
Det är möjligt att konfigurera flera olika autentiseringsscheman i ett enda projekt, men det är mycket enklare att konfigurera ett enda standardschema. Därför avgränsar referensprogrammet eShopOnWeb sina API:er i sitt eget projekt, PublicApi
, separat från huvudprojektet Web
som innehåller programmets vyer och Razor Pages.
Autentisering i Blazor appar
Blazor Serverprogram kan använda samma autentiseringsfunktioner som andra ASP.NET Core-program. BlazorWebAssembly program kan dock inte använda de inbyggda identitets- och autentiseringsprovidrar eftersom de körs i webbläsaren. BlazorWebAssembly program kan lagra användarautentiseringsstatus lokalt och kan komma åt anspråk för att avgöra vilka åtgärder användarna ska kunna utföra. Alla autentiserings- och auktoriseringskontroller bör dock utföras på servern oavsett logik som BlazorWebAssembly implementeras i appen, eftersom användarna enkelt kan kringgå appen och interagera direkt med API:erna.
Referenser – autentisering
- Autentiseringsåtgärder och standardinställningar
https://stackoverflow.com/a/52493428- Autentisering och auktorisering för SPA:er
https://learn.microsoft.com/aspnet/core/security/authentication/identity-api-authorization- ASP.NET Grundläggande Blazor autentisering och auktorisering
https://learn.microsoft.com/aspnet/core/blazor/security/- Säkerhet: Autentisering och auktorisering i ASP.NET webbformulär och Blazor
https://learn.microsoft.com/dotnet/architecture/blazor-for-web-forms-developers/security-authentication-authorization
Auktorisering
Den enklaste formen av auktorisering innebär att begränsa åtkomsten till anonyma användare. Den här funktionen kan uppnås genom att använda [Authorize]
attributet på vissa kontrollanter eller åtgärder. Om roller används kan attributet utökas ytterligare för att begränsa åtkomsten till användare som tillhör vissa roller, enligt följande:
[Authorize(Roles = "HRManager,Finance")]
public class SalaryController : Controller
{
}
I det här fallet skulle användare som tillhör antingen rollerna HRManager
eller Finance
(eller båda) ha åtkomst till SalaryController. Om du vill kräva att en användare tillhör flera roller (inte bara en av flera) kan du använda attributet flera gånger och ange en obligatorisk roll varje gång.
Att ange vissa uppsättningar roller som strängar i många olika styrenheter och åtgärder kan leda till oönskad upprepning. Definiera som minst konstanter för dessa strängliteraler och använd konstanterna var du än behöver för att ange strängen. Du kan också konfigurera auktoriseringsprinciper som kapslar in auktoriseringsregler och sedan ange principen i stället för enskilda roller när du tillämpar [Authorize]
attributet:
[Authorize(Policy = "CanViewPrivateReport")]
public IActionResult ExecutiveSalaryReport()
{
return View();
}
Med hjälp av principer på det här sättet kan du separera de typer av åtgärder som begränsas från de specifika roller eller regler som gäller för den. Om du senare skapar en ny roll som behöver ha åtkomst till vissa resurser kan du bara uppdatera en princip i stället för att uppdatera varje lista med roller för varje [Authorize]
attribut.
Anspråk
Anspråk är namnvärdepar som representerar egenskaper för en autentiserad användare. Du kan till exempel lagra användarnas personalnummer som ett anspråk. Anspråk kan sedan användas som en del av auktoriseringsprinciper. Du kan skapa en princip med namnet "EmployeeOnly" som kräver förekomsten av ett anspråk med namnet "EmployeeNumber"
, enligt följande exempel:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
});
}
Den här principen kan sedan användas med [Authorize]
attributet för att skydda alla kontrollanter och/eller åtgärder enligt beskrivningen ovan.
Skydda webb-API:er
De flesta webb-API:er bör implementera ett tokenbaserat autentiseringssystem. Tokenautentisering är tillståndslös och utformad för att vara skalbar. I ett tokenbaserat autentiseringssystem måste klienten först autentisera med autentiseringsprovidern. Om det lyckas utfärdas klienten en token, vilket helt enkelt är en kryptografiskt meningsfull teckensträng. Det vanligaste formatet för token är JSON Web Token eller JWT (uttalas ofta "jot"). När klienten sedan behöver utfärda en begäran till ett API lägger den till den här token som en rubrik i begäran. Servern validerar sedan token som hittades i begärandehuvudet innan begäran slutförs. Bild 7–4 visar den här processen.
Bild 7-4. Tokenbaserad autentisering för webb-API:er.
Du kan skapa en egen autentiseringstjänst, integrera med Azure AD och OAuth eller implementera en tjänst med hjälp av ett verktyg med öppen källkod som IdentityServer.
JWT-token kan bädda in anspråk om användaren, som kan läsas på klienten eller servern. Du kan använda ett verktyg som jwt.io för att visa innehållet i en JWT-token. Lagra inte känsliga data som lösenord eller nycklar i JTW-token, eftersom innehållet är enkelt att läsa.
När du använder JWT-token med SPA eller BlazorWebAssembly program måste du lagra token någonstans på klienten och sedan lägga till den i varje API-anrop. Den här aktiviteten utförs vanligtvis som en rubrik, vilket visas i följande kod:
// AuthService.cs in BlazorAdmin project of eShopOnWeb
private async Task SetAuthorizationHeader()
{
var token = await GetToken();
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
När du har anropat metoden ovan kommer begäranden som görs med _httpClient
att ha token inbäddad i begäranderubrikerna, vilket gör att API:et på serversidan kan autentisera och auktorisera begäran.
Anpassad säkerhet
Varning
Som en allmän regel bör du undvika att implementera dina egna anpassade säkerhetsimplementeringar.
Var särskilt försiktig med att "rulla din egen" implementering av kryptografi, användarmedlemskap eller tokengenereringssystem. Det finns många tillgängliga kommersiella alternativ och alternativ med öppen källkod, som nästan säkert kommer att ha bättre säkerhet än en anpassad implementering.
Referenser – säkerhet
- Översikt över Säkerhetsdokument
https://learn.microsoft.com/aspnet/core/security/- Framtvinga SSL i en ASP.NET Core-app
https://learn.microsoft.com/aspnet/core/security/enforcing-ssl- Introduktion till identitet
https://learn.microsoft.com/aspnet/core/security/authentication/identity- Introduktion till auktorisering
https://learn.microsoft.com/aspnet/core/security/authorization/introduction- Autentisering och auktorisering för API-appar i Azure App Service
https://learn.microsoft.com/azure/app-service-api/app-service-api-authentication- Identitetsserver
https://github.com/IdentityServer
Klientkommunikation
Förutom att hantera sidor och svara på begäranden om data via webb-API:er kan ASP.NET Core-appar kommunicera direkt med anslutna klienter. Den här utgående kommunikationen kan använda en mängd olika transporttekniker, det vanligaste är WebSockets. ASP.NET Core SignalR är ett bibliotek som gör det enkelt att lägga till kommunikationsfunktioner i realtid från server till klient i dina program. SignalR stöder en mängd olika transporttekniker, inklusive WebSockets, och abstraherar bort många av implementeringsinformationen från utvecklaren.
Klientkommunikation i realtid, oavsett om du använder WebSockets direkt eller andra tekniker, är användbara i en mängd olika programscenarier. Vissa exempel inkluderar:
Program för livechattrum
Övervaka program
Uppdateringar av jobbstatus
Meddelanden
Program för interaktiva formulär
När du skapar klientkommunikation i dina program finns det vanligtvis två komponenter:
Anslutningshanteraren på serversidan (SignalR Hub, WebSocketManager WebSocketHandler)
Bibliotek på klientsidan
Klienter är inte begränsade till webbläsare – mobilappar, konsolappar och andra interna appar kan också kommunicera med SignalR/WebSockets. Följande enkla program ekar allt innehåll som skickas till ett chattprogram till konsolen som en del av ett WebSocketManager-exempelprogram:
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();
}
}
Överväg hur dina program kommunicerar direkt med klientprogram och fundera på om kommunikation i realtid skulle förbättra appens användarupplevelse.
Referenser – klientkommunikation
- ASP.NET Core SignalR
https://github.com/dotnet/aspnetcore/tree/main/src/SignalR- WebSocket Manager
https://github.com/radu-matei/websocket-manager
Domändriven design – Ska du använda den?
Domändriven design (DDD) är en flexibel metod för att skapa programvara som fokuserar på affärsdomänen. Det lägger stor vikt vid kommunikation och interaktion med affärsdomänexperter som kan relatera till utvecklarna hur det verkliga systemet fungerar. Om du till exempel skapar ett system som hanterar aktieaffärer kan din domänexpert vara en erfaren aktiemäklare. DDD är utformat för att hantera stora, komplexa affärsproblem och är ofta inte lämpligt för mindre, enklare program, eftersom investeringen i att förstå och modellera domänen inte är värd det.
När du skapar programvara enligt en DDD-metod bör ditt team (inklusive icke-tekniska intressenter och deltagare) utveckla ett allmänt språk för problemutrymmet. Det vill säga samma terminologi bör användas för det verkliga konceptet som modelleras, programvaruekvivalenten och eventuella strukturer som kan finnas för att bevara konceptet (till exempel databastabeller). De begrepp som beskrivs i det allestädes närvarande språket bör därför utgöra grunden för din domänmodell.
Din domänmodell består av objekt som interagerar med varandra för att representera systemets beteende. Dessa objekt kan ingå i följande kategorier:
Entiteter som representerar objekt med en identitetstråd. Entiteter lagras vanligtvis i beständighet med en nyckel som de senare kan hämtas med.
Aggregeringar, som representerar grupper av objekt som ska bevaras som en enhet.
Värdeobjekt, som representerar begrepp som kan jämföras baserat på summan av deras egenskapsvärden. Till exempel DateRange som består av ett start- och slutdatum.
Domänhändelser, som representerar saker som händer i systemet och som är av intresse för andra delar av systemet.
En DDD-domänmodell bör kapsla in komplext beteende i modellen. I synnerhet entiteter bör inte bara vara samlingar med egenskaper. När domänmodellen saknar beteende och bara representerar systemets tillstånd, sägs det vara en anemisk modell, som är oönskad i DDD.
Utöver dessa modelltyper använder DDD vanligtvis en mängd olika mönster:
Lagringsplats för abstrakt beständighetsinformation.
Factory, för att kapsla in komplexa objekt.
Tjänster för att kapsla in komplext beteende och/eller information om infrastrukturimplementering.
Kommando för att avkoda utfärdande kommandon och köra själva kommandot.
Specifikation för inkapsling av frågeinformation.
DDD rekommenderar också användning av den rena arkitekturen som beskrevs tidigare, vilket möjliggör lös koppling, inkapsling och kod som enkelt kan verifieras med hjälp av enhetstester.
När bör du tillämpa DDD
DDD passar bra för stora program med betydande affärskomplexitet (inte bara teknisk). Programmet bör kräva kunskap från domänexperter. Det bör finnas ett betydande beteende i själva domänmodellen, som representerar affärsregler och interaktioner utöver att bara lagra och hämta det aktuella tillståndet för olika poster från datalager.
När bör du inte tillämpa DDD
DDD omfattar investeringar i modellering, arkitektur och kommunikation som kanske inte är berättigade för mindre program eller program som i stort sett bara är CRUD (skapa/läsa/uppdatera/ta bort). Om du väljer att närma dig ditt program efter DDD, men upptäcker att domänen har en anemisk modell utan beteende, kan du behöva tänka om. Antingen behöver ditt program kanske inte DDD, eller så kan du behöva hjälp med att omstrukturera ditt program för att kapsla in affärslogik i domänmodellen i stället för i databasen eller användargränssnittet.
En hybridmetod skulle vara att endast använda DDD för de transaktionella eller mer komplexa områdena i programmet, men inte för enklare CRUD- eller skrivskyddade delar av programmet. Du behöver till exempel inte begränsningarna för en aggregering om du frågar efter data för att visa en rapport eller för att visualisera data för en instrumentpanel. Det är helt acceptabelt att ha en separat, enklare läsmodell för sådana krav.
Referenser – domändriven design
- DDD på vanlig engelska (StackOverflow-svar)
https://stackoverflow.com/questions/1222392/can-someone-explain-domain-driven-design-ddd-in-plain-english-please/1222488#1222488
Distribution
Det finns några steg i processen för att distribuera ditt ASP.NET Core-program, oavsett var det finns. Det första steget är att publicera programmet, vilket kan göras med hjälp av dotnet publish
CLI-kommandot. Det här steget kompilerar programmet och placerar alla filer som behövs för att köra programmet till en angiven mapp. När du distribuerar från Visual Studio utförs det här steget automatiskt. Publiceringsmappen innehåller .exe och .dll filer för programmet och dess beroenden. Ett fristående program innehåller också en version av .NET-körningen. ASP.NET Core-program innehåller även konfigurationsfiler, statiska klienttillgångar och MVC-vyer.
ASP.NET Core-program är konsolprogram som måste startas när servern startar och startas om om programmet (eller servern) kraschar. En processhanterare kan användas för att automatisera den här processen. De vanligaste processcheferna för ASP.NET Core är Nginx och Apache i Linux och IIS eller Windows Service i Windows.
Förutom en processhanterare kan ASP.NET Core-program använda en omvänd proxyserver. En omvänd proxyserver tar emot HTTP-begäranden från Internet och vidarebefordrar dem till Kestrel efter viss preliminär hantering. Omvända proxyservrar ger ett säkerhetslager för programmet. Kestrel stöder inte heller värd för flera program på samma port, så tekniker som värdhuvuden kan inte användas med den för att aktivera värd för flera program på samma port och IP-adress.
Bild 7-5. ASP.NET i Kestrel bakom en omvänd proxyserver
Ett annat scenario där en omvänd proxy kan vara till hjälp är att skydda flera program med hjälp av SSL/HTTPS. I det här fallet behöver bara den omvända proxyn ha SSL konfigurerat. Kommunikationen mellan den omvända proxyservern och Kestrel kan ske via HTTP, enligt bild 7–6.
Bild 7-6. ASP.NET som finns bakom en HTTPS-skyddad omvänd proxyserver
En allt populärare metod är att vara värd för ditt ASP.NET Core-program i en Docker-container, som sedan kan hanteras lokalt eller distribueras till Azure för molnbaserad värd. Docker-containern kan innehålla programkoden som körs på Kestrel och distribueras bakom en omvänd proxyserver, enligt ovan.
Om du är värd för ditt program i Azure kan du använda Microsoft Azure Application Gateway som en dedikerad virtuell installation för att tillhandahålla flera tjänster. Förutom att fungera som en omvänd proxy för enskilda program kan Application Gateway även erbjuda följande funktioner:
HTTP-belastningsutjämning
SSL-avlastning (endast SSL till Internet)
SSL från slutpunkt till slutpunkt
Routning för flera platser (konsolidera upp till 20 platser på en enda Application Gateway)
Brandvägg för webbaserade program
Stöd för Websocket
Avancerad diagnostik
Läs mer om distributionsalternativ för Azure i kapitel 10.
Referenser – distribution
- Översikt över värd- och distribution
https://learn.microsoft.com/aspnet/core/publishing/- När du ska använda Kestrel med en omvänd proxy
https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel#when-to-use-kestrel-with-a-reverse-proxy- Värd för ASP.NET Core-appar i Docker
https://learn.microsoft.com/aspnet/core/publishing/docker- Introduktion till Azure Application Gateway
https://learn.microsoft.com/azure/application-gateway/application-gateway-introduction