Condividi tramite


Sviluppare app ASP.NET Core MVC

Suggerimento

Questo contenuto è un estratto dell'eBook, Architect Modern Web Applications with ASP.NET Core and Azure, disponibile in .NET Docs o come PDF scaricabile gratuito che può essere letto offline.

Progettare applicazioni Web moderne con ASP.NET Core e anteprima della copertina di Azure eBook.

Non è importante riuscirci al primo tentativo. È fondamentale farlo bene l'ultima volta". - Andrew Hunt e David Thomas

ASP.NET Core è un framework open source multipiattaforma per la creazione di applicazioni Web moderne ottimizzate per il cloud. Le app ASP.NET Core sono leggere e modulari, con supporto integrato per l'inserimento delle dipendenze, consentendo una maggiore testabilità e manutenibilità. In combinazione con MVC, che supporta la creazione di API Web moderne oltre alle app basate sulla visualizzazione, ASP.NET Core è un framework potente con cui creare applicazioni Web aziendali.

MVC e Razor Pages

ASP.NET Core MVC offre molte funzionalità utili per la creazione di API e app basate sul Web. Il termine MVC è "Model-View-Controller", un modello di interfaccia utente che suddivide le responsabilità di rispondere alle richieste degli utenti in diverse parti. Oltre a seguire questo modello, è anche possibile implementare funzionalità nelle app ASP.NET Core come Razor Pages.

Le pagine Razor sono integrate in ASP.NET Core MVC e usano le stesse funzionalità per il routing, l'associazione di modelli, i filtri, l'autorizzazione e così via. Tuttavia, invece di avere cartelle e file separati per controller, modelli, viste e così via e usando il routing basato su attributi, Razor Pages vengono posizionati in una singola cartella ("/Pages"), in base alla posizione relativa in questa cartella e gestire le richieste con gestori anziché le azioni del controller. Di conseguenza, quando si lavora con Razor Pages, tutti i file e le classi necessari sono in genere raggruppati, non distribuiti in tutto il progetto Web.

Altre informazioni su come vengono applicati MVC, Razor Pages e i modelli correlati nell'applicazione di esempio eShopOnWeb.

Quando si crea una nuova app ASP.NET Core, è necessario avere a mente un piano per il tipo di app che si vuole compilare. Quando si crea un nuovo progetto, nell'IDE o usando il comando dell'interfaccia della dotnet new riga di comando, è possibile scegliere tra diversi modelli. I modelli di progetto più comuni sono Empty, Web API, App Web e App Web (Model-View-Controller). Anche se è possibile prendere questa decisione solo quando si crea un progetto per la prima volta, non è una decisione irrevocabile. Il progetto Web API utilizza controller standard Model-View-Controller, manca solo la componente Views per impostazione predefinita. Analogamente, il modello di app Web predefinito usa Razor Pages e quindi manca anche una cartella Views. È possibile aggiungere una cartella Views a questi progetti in un secondo momento per supportare il comportamento basato sulla visualizzazione. API Web e modelli: i progettiView-Controller non includono una cartella Pages per impostazione predefinita, ma è possibile aggiungerne una in un secondo momento per supportare il comportamento basato su Razor Pages. È possibile considerare questi tre modelli come il supporto di tre diversi tipi di interazione utente predefinita: dati (API Web), basati su pagine e basati sulla visualizzazione. Tuttavia, è possibile combinare e associare uno o tutti questi modelli all'interno di un singolo progetto, se lo si desidera.

Perché Razor Pages?

Razor Pages è l'approccio predefinito per le nuove applicazioni Web in Visual Studio. Razor Pages offre un modo più semplice per creare funzionalità dell'applicazione basate su pagine, ad esempio moduli non SPA. L'uso di controller e viste era comune nelle applicazioni avere controller molto grandi che funzionavano con molte dipendenze e modelli di viste diversi e restituivano molte viste diverse. Ciò ha comportato una maggiore complessità e spesso ha comportato controller che non seguivano il principio di responsabilità singola o i principi aperti/chiusi in modo efficace. Razor Pages risolve questo problema incapsulando la logica lato server per una determinata "pagina" logica in un'applicazione Web con il markup Razor. Una pagina Razor che non ha logica sul lato server può essere costituita solo da un file Razor(ad esempio, "Index.cshtml"). Tuttavia, la maggior parte delle pagine Razor non semplici avrà una classe modello di pagina associata, che per convenzione è denominata come il file Razor con un'estensione ".cs", ad esempio "Index.cshtml.cs").

Un modello di pagina di Razor Page combina le responsabilità di un controller MVC e di un modello di visualizzazione. Invece di gestire le richieste con i metodi di azione del controller, vengono eseguiti gestori di modelli di pagina come "OnGet()", eseguendo il rendering della pagina associata per impostazione predefinita. Razor Pages semplifica il processo di creazione di singole pagine in un'app ASP.NET Core, pur fornendo tutte le funzionalità architetturali di ASP.NET Core MVC. Sono una buona scelta predefinita per le nuove funzionalità basate su pagine.

Quando usare MVC

Se si creano API Web, il modello MVC ha più senso rispetto al tentativo di usare Razor Pages. Se il progetto esporrà solo gli endpoint API Web, è consigliabile iniziare idealmente dal modello di progetto API Web. In caso contrario, è facile aggiungere controller ed endpoint API associati a qualsiasi app ASP.NET Core. Usare l'approccio MVC basato sulla visualizzazione se si esegue la migrazione di un'applicazione esistente da ASP.NET MVC 5 o versioni precedenti a ASP.NET Core MVC e si vuole farlo con il minimo sforzo. Dopo aver effettuato la migrazione iniziale, è possibile valutare se è opportuno adottare Razor Pages per le nuove funzionalità o anche come migrazione all'ingrosso. Per altre informazioni sulla conversione di app .NET 4.x in .NET 8, vedere Conversione di app ASP.NET esistenti in ASP.NET Core eBook.

Se si sceglie di compilare l'app Web usando razor Pages o visualizzazioni MVC, l'app avrà prestazioni simili e includerà il supporto per inserimento delle dipendenze, filtri, associazione di modelli, convalida e così via.

Mappatura delle richieste alle risposte

Essenzialmente, le app di ASP.NET Core mappano le richieste in ingresso verso le risposte in uscita. A basso livello, questo mapping viene eseguito con il middleware, e semplici app ASP.NET Core e microservizi possono essere costituiti esclusivamente da middleware personalizzati. Quando si usa ASP.NET Core MVC, è possibile lavorare a un livello leggermente superiore, pensando in termini di route, controller e azioni. Ogni richiesta in ingresso viene confrontata con la tabella di routing dell'applicazione e, se viene trovata una route corrispondente, viene chiamato il metodo di azione associato (appartenente a un controller) per gestire la richiesta. Se non viene trovata alcuna route corrispondente, viene chiamato un gestore errori (in questo caso, restituendo un risultato NotFound).

Le app ASP.NET Core MVC possono usare route convenzionali, route basate su attributi o entrambe. Le route convenzionali vengono definite nel codice, specificando convenzioni di routing usando la sintassi come nell'esempio seguente:

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

In questo esempio è stata aggiunta una route denominata "default" alla tabella di routing. Definisce un modello di route con segnaposto per controller, actione id. Il segnaposto controller e il segnaposto action hanno il valore predefinito specificato (Home e Index, rispettivamente), e il segnaposto id è facoltativo (in virtù di un "?" applicato ad esso). La convenzione definita qui indica che la prima parte di una richiesta deve corrispondere al nome del controller, alla seconda parte all'azione e, se necessario, una terza parte rappresenterà un parametro ID. Le route convenzionali vengono in genere definite in un'unica posizione per l'applicazione, ad esempio in Program.cs in cui è configurata la pipeline del middleware della richiesta.

Le route degli attributi vengono applicate direttamente ai controller e alle azioni, invece di essere specificate a livello globale. Questo approccio ha il vantaggio di renderle molto più individuabili quando si esamina un metodo specifico, ma significa che le informazioni di routing non vengono mantenute in un'unica posizione nell'applicazione. Con le rotte con attributi, è possibile specificare facilmente più percorsi per una determinata azione, nonché combinare percorsi tra controller e azioni. Per esempio:

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

Le route possono essere specificate in [HttpGet] e attributi simili, evitando la necessità di aggiungere attributi [Route] separati. Le route degli attributi possono anche usare token per ridurre la necessità di ripetere i nomi di controller o azioni, come illustrato di seguito:

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

Razor Pages non usa il routing degli attributi. È possibile specificare informazioni aggiuntive sul modello di route per una pagina Razor come parte della relativa @page direttiva:

@page "{id:int}"

Nell'esempio precedente la pagina in questione corrisponde a una route con un parametro integer id . Ad esempio, la pagina Products.cshtml che si trova nella radice di /Pages risponde alle richieste come quella seguente:

/Products/123

Dopo che una determinata richiesta è stata abbinata a una route, ma prima della chiamata al metodo di azione, ASP.NET Core MVC eseguirà l'associazione di modelli e la convalida del modello nella richiesta. L'associazione di modelli è responsabile della conversione dei dati HTTP in ingresso nei tipi .NET specificati come parametri del metodo di azione da chiamare. Ad esempio, se il metodo di azione prevede un int id parametro, l'associazione di modelli tenterà di fornire questo parametro da un valore fornito come parte della richiesta. A tale scopo, l'associazione di modelli cerca i valori in un modulo pubblicato, i valori nella route stessa e i valori della stringa di query. Supponendo che venga trovato un id valore, verrà convertito in un numero intero prima di essere passato al metodo di azione.

Dopo l'associazione del modello ma prima di chiamare il metodo di azione, viene eseguita la convalida del modello. La convalida del modello usa attributi facoltativi nel tipo di modello e consente di garantire che l'oggetto modello fornito sia conforme a determinati requisiti di dati. Alcuni valori possono essere specificati come obbligatori o limitati a una determinata lunghezza o intervallo numerico e così via. Se gli attributi di convalida vengono specificati ma il modello non è conforme ai requisiti, la proprietà ModelState.IsValid sarà false e il set di regole di convalida non riuscite sarà disponibile per l'invio al client che effettua la richiesta.

Se si usa la convalida del modello, assicurarsi di verificare sempre che il modello sia valido prima di eseguire comandi di modifica dello stato, per assicurarsi che l'app non sia danneggiata da dati non validi. È possibile usare un filtro per evitare la necessità di aggiungere codice per questa convalida in ogni azione. ASP.NET filtri MVC Core offrono un modo per intercettare gruppi di richieste, in modo che i criteri comuni e le problematiche trasversali possano essere applicati su base mirata. I filtri possono essere applicati a singole azioni, interi controller o a livello globale per un'applicazione.

Per le API Web, ASP.NET Core MVC supporta la negoziazione del contenuto, consentendo alle richieste di specificare la modalità di formattazione delle risposte. In base alle intestazioni fornite nella richiesta, le azioni che restituiscono dati formatteranno la risposta in formato XML, JSON o in un altro formato supportato. Questa funzionalità consente di usare la stessa API da parte di più client con requisiti di formato dati diversi.

I progetti API Web devono prendere in considerazione l'uso dell'attributo [ApiController] , che può essere applicato a singoli controller, a una classe controller di base o all'intero assembly. Questo attributo aggiunge il controllo automatico della convalida del modello e qualsiasi azione con un modello non valido restituirà badRequest con i dettagli degli errori di convalida. L'attributo richiede anche che tutte le azioni abbiano una route di attributi, anziché usare una route convenzionale e restituisca informazioni più dettagliate su ProblemDetails in risposta agli errori.

Mantenere sotto controllo i controller

Per le applicazioni basate su pagine, Razor Pages fanno un ottimo lavoro nel prevenire che i controller diventino troppo grandi. A ogni singola pagina sono assegnati i propri file e classi dedicati solo ai relativi gestori. Prima dell'introduzione di Razor Pages, molte applicazioni incentrate sulla visualizzazione avrebbero classi controller di grandi dimensioni responsabili di molte azioni e visualizzazioni diverse. Queste classi aumentano naturalmente per avere molte responsabilità e dipendenze, rendendo più difficile gestirle. Se si rileva che i controller basati su visualizzazione sono troppo grandi, è consigliabile effettuare il refactoring per usare Razor Pages o introdurre un modello come un mediatore.

Il modello di progettazione mediator viene usato per ridurre l'accoppiamento tra le classi, consentendo al tempo stesso la comunicazione tra di esse. In ASP.NET applicazioni MVC core, questo modello viene spesso usato per suddividere i controller in parti più piccole usando i gestori per eseguire il lavoro dei metodi di azione. Il popolare pacchetto NuGet MediatR viene spesso usato per eseguire questa operazione. In genere, i controller includono molti metodi di azione diversi, ognuno dei quali può richiedere determinate dipendenze. Il set di tutte le dipendenze richieste da qualsiasi azione deve essere passato al costruttore del controller. Quando si usa MediatR, l'unica dipendenza che un controller avrà in genere è un'istanza del mediator. Ogni azione usa quindi l'istanza mediator per inviare un messaggio, elaborato da un gestore. Il gestore è specifico di una singola azione e pertanto richiede solo le dipendenze richieste da tale azione. Di seguito è riportato un esempio di controller che usa 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
}

Nell'azione MyOrders la chiamata a Send un GetMyOrders messaggio viene gestita da questa classe:

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

Il risultato finale di questo approccio è che i controller siano molto più piccoli e incentrati principalmente sul routing e sull'associazione di modelli, mentre i singoli gestori sono responsabili delle attività specifiche necessarie per un determinato endpoint. Questo approccio può essere ottenuto anche senza MediatR usando il pacchetto NuGet ApiEndpoints, che tenta di portare ai controller API gli stessi vantaggi offerti da Razor Pages ai controller basati sulla visualizzazione.

Riferimenti: mappatura delle richieste alle risposte

Gestione delle dipendenze

ASP.NET Core offre un supporto integrato per e utilizza internamente una tecnica nota come iniezione delle dipendenze. L'inserimento delle dipendenze è una tecnica che consente l'accoppiamento libero tra parti diverse di un'applicazione. L'accoppiamento più debole è auspicabile perché semplifica l'isolamento delle parti dell'applicazione, consentendo test o sostituzione. Rende inoltre meno probabile che una modifica in una parte dell'applicazione abbia un impatto imprevisto in un'altra posizione nell'applicazione. L'iniezione delle dipendenze si basa sul principio di inversione delle dipendenze ed è spesso fondamentale per realizzare il principio aperto/chiuso. Quando si valuta il funzionamento dell'applicazione con le relative dipendenze, tenere presente l'odore del codice di cling statico e ricordare l'aforismo "new is glue".

L'adesione statica si verifica quando le tue classi effettuano chiamate a metodi statici o accedono a proprietà statiche che hanno effetti collaterali o dipendenze dall'infrastruttura. Ad esempio, se si dispone di un metodo che chiama un metodo statico, che a sua volta scrive in un database, il metodo è strettamente associato al database. Tutto ciò che interrompe la chiamata al database interromperà il metodo. Il test di tali metodi è notoriamente difficile, poiché tali test richiedono librerie di simulazione commerciale per simulare le chiamate statiche o possono essere testate solo con un database di test sul posto. Le chiamate statiche che non hanno alcuna dipendenza dall'infrastruttura, in particolare quelle completamente senza stato, vanno bene da chiamare e non hanno alcun impatto sull'accoppiamento o sulla testabilità (a parte l'accoppiamento del codice alla chiamata statica stessa).

Molti sviluppatori comprendono i rischi dell'aderenza statica e dello stato globale, ma continueranno a legare strettamente il loro codice a implementazioni specifiche tramite l'istanziazione diretta. "New is glue" è destinato a ricordare questo accoppiamento, e non una condanna generale dell'uso della new parola chiave. Come per le chiamate al metodo statico, le nuove istanze di tipi che non hanno dipendenze esterne in genere non associano strettamente il codice ai dettagli dell'implementazione o rendono più difficile il test. Ogni volta che una classe viene istanziata, prenditi un momento per considerare se ha senso codificare in modo fisso quell'istanza in quella posizione specifica, o se sarebbe un approccio progettuale migliore richiedere quell'istanza come dipendenza.

Dichiarare le dipendenze

ASP.NET Core si basa sulla presenza di metodi e classi che dichiarano le relative dipendenze, richiedendole come argomenti. ASP.NET le applicazioni vengono in genere configurate in Program.cs o in una Startup classe.

Annotazioni

La configurazione completa delle app in Program.cs è l'approccio predefinito per le app .NET 6 (e versioni successive) e Visual Studio 2022. I modelli di progetto sono stati aggiornati per iniziare a usare questo nuovo approccio. I progetti ASP.NET Core possono comunque utilizzare la classe Startup, se lo si desidera.

Configurare i servizi in Program.cs

Per le app molto semplici, è possibile collegare le dipendenze direttamente nel file Program.cs usando un oggetto WebApplicationBuilder. Dopo aver aggiunto tutti i servizi necessari, il generatore viene usato per creare l'app.

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

Configurare i servizi in Startup.cs

Il Startup.cs è configurato per supportare l'inserimento delle dipendenze in diversi punti. Se si usa una Startup classe, è possibile assegnargli un costruttore e richiedere dipendenze tramite di essa, come indicato di seguito:

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

La Startup classe è interessante in quanto non esistono requisiti di tipo espliciti per esso. Non eredita da una classe base speciale Startup , né implementa alcuna interfaccia specifica. È possibile assegnare o meno un costruttore e specificare il numero di parametri nel costruttore desiderato. All'avvio dell'host web configurato per l'applicazione, verrà chiamata la classe Startup (se è stato indicato di usarla) e verrà utilizzata l'iniezione delle dipendenze per popolare qualsiasi dipendenza richiesta dalla classe Startup. Naturalmente, se si richiedono parametri non configurati nel contenitore di servizi usato da ASP.NET Core, si otterrà un'eccezione, ma se ci si attiene alle dipendenze che il contenitore conosce, è possibile richiedere qualsiasi cosa si voglia.

L'inserimento delle dipendenze è integrato nelle app ASP.NET Core direttamente dall'inizio, quando si crea l'istanza Startup. Non si arresta lì per la classe Startup. È anche possibile richiedere dipendenze nel Configure metodo :

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

}

Il metodo ConfigureServices è l'eccezione a questo comportamento; deve accettare solo un parametro di tipo IServiceCollection. Non è in realtà necessario supportare l'inserimento delle dipendenze, perché da un lato è responsabile dell'aggiunta di oggetti al contenitore dei servizi e dall'altro ha accesso a tutti i servizi attualmente configurati tramite il IServiceCollection parametro . È quindi possibile usare le dipendenze definite nella raccolta di ASP.NET Servizi di base in ogni parte della Startup classe, richiedendo il servizio necessario come parametro o usando IServiceCollection in ConfigureServices.

Annotazioni

Se è necessario assicurarsi che determinati servizi siano disponibili per la Startup classe, è possibile configurarli usando un IWebHostBuilder e il relativo ConfigureServices metodo all'interno della CreateDefaultBuilder chiamata.

La classe Startup è un modello per la struttura di altre parti dell'applicazione core ASP.NET, dai controller al middleware ai filtri ai propri servizi. In ogni caso, è necessario seguire il principio delle dipendenze esplicite, richiedendo le dipendenze anziché crearle direttamente e sfruttando l'inserimento delle dipendenze in tutta l'applicazione. Fai attenzione a dove e come creare direttamente istanze di implementazioni, soprattutto servizi e oggetti che funzionano con l'infrastruttura o hanno effetti secondari. Preferisci lavorare con astrazioni definite nel nucleo della tua applicazione e passate come argomenti, rispetto a codificare riferimenti a tipi di implementazione specifici.

Strutturazione dell'applicazione

Le applicazioni monolitiche in genere hanno un singolo punto di ingresso. Nel caso di un'applicazione Web ASP.NET Core, il punto di ingresso sarà il progetto Web ASP.NET Core. Tuttavia, ciò non significa che la soluzione sia costituita da un solo progetto. È utile suddividere l'applicazione in livelli diversi per seguire la separazione delle problematiche. Una volta suddivisi in livelli, è utile superare il concetto di cartelle separando i progetti, il che può contribuire a ottenere un migliore incapsulamento. L'approccio migliore per raggiungere questi obiettivi con un'applicazione ASP.NET Core è una variante dell'architettura pulita illustrata nel capitolo 5. Seguendo questo approccio, la soluzione dell'applicazione comprenderà librerie separate per l'interfaccia utente, l'infrastruttura e ApplicationCore.

Oltre a questi progetti, sono inclusi anche progetti di test separati (il test è discusso nel capitolo 9).

Il modello a oggetti e le interfacce dell'applicazione devono essere inserite nel progetto ApplicationCore. Questo progetto avrà il minor numero possibile di dipendenze (e nessuna per problemi di infrastruttura specifici) e gli altri progetti nella soluzione vi faranno riferimento. Le entità aziendali che devono essere mantenute vengono definite nel progetto ApplicationCore, perché sono servizi che non dipendono direttamente dall'infrastruttura.

I dettagli di implementazione, ad esempio il modo in cui viene eseguita la persistenza o il modo in cui le notifiche possono essere inviate a un utente, vengono mantenute nel progetto Infrastruttura. Questo progetto farà riferimento a pacchetti specifici dell'implementazione, ad esempio Entity Framework Core, ma non deve esporre dettagli su queste implementazioni all'esterno del progetto. I servizi e i repository dell'infrastruttura devono implementare interfacce definite nel progetto ApplicationCore e le relative implementazioni di persistenza sono responsabili del recupero e dell'archiviazione delle entità definite in ApplicationCore.

Il progetto dell'interfaccia utente principale ASP.NET è responsabile di eventuali problemi a livello di interfaccia utente, ma non deve includere la logica di business o i dettagli dell'infrastruttura. In realtà, idealmente non dovrebbe nemmeno avere una dipendenza dal progetto Infrastruttura, che consentirà di garantire che non venga introdotta accidentalmente alcuna dipendenza tra i due progetti. A tale scopo, è possibile usare un contenitore di inserimento delle dipendenze di terze parti, ad esempio Autofac, che consente di definire le regole di inserimento delle dipendenze nelle classi Module in ogni progetto.

Un altro approccio per separare l'applicazione dai dettagli di implementazione consiste nell'avere i microservizi di chiamata dell'applicazione, forse distribuiti in singoli contenitori Docker. Ciò garantisce una maggiore separazione delle responsabilità e un disaccoppiamento rispetto a sfruttare l'iniezione delle dipendenze (DI) tra due progetti, ma presenta ulteriori complessità.

Organizzazione delle funzionalità

Per impostazione predefinita, le applicazioni ASP.NET Core organizzano la struttura delle cartelle in modo da includere Controllers, Views, e spesso ViewModels. Il codice lato client per supportare queste strutture lato server viene in genere archiviato separatamente nella cartella wwwroot. Tuttavia, le applicazioni di grandi dimensioni possono riscontrare problemi con questa organizzazione, poiché l'uso di una determinata funzionalità spesso richiede il passaggio tra queste cartelle. Ciò diventa sempre più difficile man mano che aumenta il numero di file e sottocartelle in ogni cartella, con conseguente notevole scorrimento in Esplora soluzioni. Una soluzione a questo problema consiste nell'organizzare il codice dell'applicazione in base alla funzionalità anziché in base al tipo di file. Questo stile dell'organizzazione viene in genere definito cartelle di funzionalità o sezioni di funzionalità (vedere anche: Sezioni verticali).

ASP.NET Core MVC supporta le aree a questo scopo. Usando le aree, è possibile creare set separati di cartelle Controller e Visualizzazioni (nonché qualsiasi modello associato) in ogni cartella Area. La figura 7-1 mostra una struttura di cartelle di esempio, usando Aree.

Organizzazione dell'area di esempio

Figura 7-1. Organizzazione dell'area di esempio

Quando si usano Aree, è necessario usare gli attributi per decorare i controller con il nome dell'area a cui appartengono:

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

È necessario anche aggiungere supporto dell'area alle tue rotte.

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

Oltre al supporto predefinito per Aree, è anche possibile usare la propria struttura di cartelle e convenzioni al posto di attributi e route personalizzate. Ciò consente di avere cartelle di funzionalità che non includono cartelle separate per Visualizzazioni, Controller e così via, mantenendo la gerarchia più flat e semplificando la visualizzazione di tutti i file correlati in un'unica posizione per ogni funzionalità. Per le API, le cartelle possono essere usate per sostituire i controller e ogni cartella può contenere tutti gli endpoint API e i relativi DTO associati.

ASP.NET Core usa tipi di convenzione predefiniti per controllarne il comportamento. È possibile modificare o sostituire queste convenzioni. Ad esempio, è possibile creare una convenzione che otterrà automaticamente il nome della funzionalità per un determinato controller in base allo spazio dei nomi (che in genere è correlato alla cartella in cui si trova il controller):

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

È quindi necessario specificare questa convenzione come opzione quando si aggiunge il supporto per MVC all'applicazione in ConfigureServices (o 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 usa anche una convenzione per individuare le visualizzazioni. È possibile eseguirne l'override con una convenzione personalizzata in modo che le visualizzazioni si trovino nelle cartelle delle funzionalità (usando il nome della funzionalità fornito da FeatureConvention, sopra). Per altre informazioni su questo approccio, scaricare un esempio funzionante dall'articolo msdn Magazine, Feature Slices for ASP.NET Core MVC.

API e Blazor applicazioni

Se l'applicazione include un set di API Web, che devono essere protette, queste API devono essere configurate idealmente come progetto separato dall'applicazione View o Razor Pages. La separazione delle API, in particolare le API pubbliche, dall'applicazione Web sul lato server offre numerosi vantaggi. Queste applicazioni hanno spesso caratteristiche di distribuzione e caricamento univoco. È anche molto probabile che adottino meccanismi diversi per la sicurezza, con applicazioni basate su form standard che sfruttano l'autenticazione basata su cookie e le API che probabilmente usano l'autenticazione basata su token.

Inoltre, Blazor le applicazioni, che usano Blazor Server o BlazorWebAssembly, devono essere compilate come progetti separati. Le applicazioni hanno caratteristiche di runtime diverse e modelli di sicurezza. È probabile che condividano tipi comuni con l'applicazione Web lato server (o il progetto API) e questi tipi devono essere definiti in un progetto condiviso comune.

L'aggiunta di un'interfaccia BlazorWebAssembly amministratore a eShopOnWeb richiede l'aggiunta di diversi nuovi progetti. Il progetto BlazorWebAssembly stesso, BlazorAdmin. Nel progetto BlazorAdmin viene definito un nuovo set di endpoint API pubblici, utilizzati da PublicApi e configurati per l'autenticazione basata su token. Alcuni tipi condivisi usati da entrambi questi progetti vengono mantenuti in un nuovo BlazorShared progetto.

Si potrebbe chiedere, perché aggiungere un progetto separato BlazorShared quando esiste già un progetto comune ApplicationCore che può essere usato per condividere qualsiasi tipo richiesto da e PublicApiBlazorAdmin? La risposta è che questo progetto include tutta la logica di business dell'applicazione ed è quindi molto più grande del necessario e molto più probabile che debba essere mantenuto sicuro nel server. Tenere presente che qualsiasi libreria a cui si fa BlazorAdmin riferimento verrà scaricata nei browser degli utenti quando carica l'applicazione Blazor .

A seconda che si usi il modello Back-end-For-Frontends (BFF), le API utilizzate dall'app BlazorWebAssembly potrebbero non condividere i tipi 100% con Blazor. In particolare, un'API pubblica che deve essere usata da molti client diversi può definire i propri tipi di richiesta e risultato, anziché condividerli in un progetto condiviso specifico del client. Nell'esempio eShopOnWeb si presuppone che il PublicApi progetto sia, in effetti, l'hosting di un'API pubblica, quindi non tutti i tipi di richiesta e risposta provengono dal BlazorShared progetto.

Aspetti trasversali

Man mano che le applicazioni aumentano, diventa sempre più importante tenere conto delle problematiche trasversali per eliminare la duplicazione e mantenere la coerenza. Alcuni esempi di problemi trasversali nelle applicazioni ASP.NET Core sono l'autenticazione, le regole di convalida del modello, la memorizzazione nella cache di output e la gestione degli errori, anche se esistono molti altri. ASP.NET filtri MVC core consentono di eseguire codice prima o dopo determinati passaggi nella pipeline di elaborazione delle richieste. Ad esempio, un filtro può essere eseguito prima e dopo l'associazione di modelli, prima e dopo un'azione o prima e dopo il risultato di un'azione. È anche possibile usare un filtro di autorizzazione per controllare l'accesso al resto della pipeline. Le figure da 7 a 2 illustrano il flusso dell'esecuzione delle richieste tramite filtri, se configurati.

La richiesta viene elaborata tramite filtri di autorizzazione, filtri risorse, associazione di modelli, filtri azione, esecuzione di azioni e conversione dei risultati dell'azione, filtri eccezioni, filtri dei risultati ed esecuzione dei risultati. In uscita, la richiesta viene elaborata solo dai filtri dei risultati e dai filtri delle risorse prima di diventare una risposta inviata al client.

Figura 7-2. Eseguire le richieste tramite filtri e pipeline di richieste.

I filtri vengono in genere implementati come attributi, in modo da poterli applicare ai controller o alle azioni (o anche a livello globale). Se aggiunti in questo modo, i filtri specificati a livello di azione eseguono l'override o si basano sui filtri specificati a livello di controller, che a loro volta eseguono l'override dei filtri globali. Ad esempio, l'attributo [Route] può essere usato per creare route tra controller e azioni. Analogamente, l'autorizzazione può essere configurata a livello di controller e quindi sottoposta a override da singole azioni, come illustrato nell'esempio seguente:

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

Il primo metodo, Login, usa il filtro [AllowAnonymous] (attributo) per effettuare l'override del filtro Authorize impostato a livello di controller. L'azione ForgotPassword (e qualsiasi altra azione nella classe che non ha un attributo AllowAnonymous) richiederà una richiesta autenticata.

I filtri possono essere usati per eliminare la duplicazione sotto forma di criteri comuni di gestione degli errori per le API. Ad esempio, un criterio API tipico consiste nel restituire una risposta NotFound alle richieste che fanno riferimento a chiavi che non esistono e una BadRequest risposta se la convalida del modello ha esito negativo. L'esempio seguente illustra questi due criteri in azione:

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

Non consentire ai metodi di azione di diventare ingombrati con codice condizionale come questo. Anziché, convertire i criteri in filtri che possono essere applicati secondo necessità. In questo esempio, il controllo di convalida del modello, che deve verificarsi ogni volta che viene inviato un comando all'API, può essere sostituito dall'attributo seguente:

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

È possibile aggiungere il ValidateModelAttribute al progetto come dipendenza NuGet, includendo il pacchetto Ardalis.ValidateModel. Per le API, è possibile usare l'attributo ApiController per applicare questo comportamento senza la necessità di un filtro separato ValidateModel .

Analogamente, è possibile usare un filtro per verificare se esiste un record e restituire un valore 404 prima dell'esecuzione dell'azione, eliminando la necessità di eseguire questi controlli nell'azione. Dopo aver estratto le convenzioni comuni e organizzato la soluzione per separare il codice dell'infrastruttura e la logica di business dall'interfaccia utente, i metodi di azione MVC devono essere estremamente sottili:

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

Per altre informazioni sull'implementazione dei filtri e sul download di un esempio funzionante, vedere l'articolo msdn Magazine Real-World ASP.NET filtri MVC principali.

Se si ritiene di avere una serie di risposte comuni dalle API basate su scenari comuni, ad esempio errori di convalida (richiesta non valida), risorse non trovate ed errori del server, è consigliabile usare un'astrazione dei risultati . L'astrazione dei risultati verrebbe restituita dai servizi utilizzati dagli endpoint API e l'azione o l'endpoint del controller userebbe un filtro per convertirli in IActionResults.

Riferimenti: strutturazione delle applicazioni

Sicurezza

La protezione delle applicazioni Web è un argomento di grandi dimensioni, con molte considerazioni. Al livello più semplice, la sicurezza comporta la verifica dell'provenienza di una determinata richiesta e quindi la garanzia che la richiesta abbia accesso solo alle risorse da cui deve accedere. L'autenticazione è il processo di confronto delle credenziali fornite con una richiesta a quelle in un archivio dati attendibile, per verificare se la richiesta deve essere considerata come proveniente da un'entità nota. L'autorizzazione è il processo di limitazione dell'accesso a determinate risorse in base all'identità utente. Un terzo problema di sicurezza è la protezione delle richieste di intercettazione da parte di terze parti, per cui è necessario almeno assicurarsi che SSL venga usato dall'applicazione.

Identità

ASP.NET Core Identity è un sistema di appartenenza che è possibile usare per supportare la funzionalità di accesso per l'applicazione. Include il supporto per gli account utente locali e il supporto del provider di accesso esterno da provider come Account Microsoft, Twitter, Facebook, Google e altro ancora. Oltre a ASP.NET Core Identity, l'applicazione può usare l'autenticazione di Windows o un provider di identità di terze parti, ad esempio Identity Server.

ASP.NET Core Identity è incluso nei nuovi modelli di progetto se è selezionata l'opzione Account utente singoli. Questo modello include il supporto per la registrazione, l'accesso, gli account di accesso esterni, le password dimenticate e altre funzionalità.

Selezionare Account utente singoli per avere identità preconfigurata

Figura 7-3. Selezionare Account utente singoli per avere identità preconfigurata.

Il supporto delle identità è configurato in Program.cs o Startupe include la configurazione dei servizi e il middleware.

Configurare l'identità in Program.cs

In Program.cs, configuri i servizi dall'istanza WebHostBuilder, e successivamente, dopo aver creato l'app, configuri il suo middleware. I punti chiave da notare sono la chiamata a AddDefaultIdentity per i servizi necessari e le chiamate a UseAuthentication e UseAuthorization che aggiungono il middleware necessario.

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

Configurazione dell'identità all'avvio dell'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();

È importante che UseAuthentication e UseAuthorization vengano visualizzati prima MapRazorPagesdi . Quando si configurano i servizi di identità, si noterà una chiamata a AddDefaultTokenProviders. Questo non ha nulla a che fare con i token che possono essere usati per proteggere le comunicazioni Web, ma si riferisce invece ai provider che creano richieste che possono essere inviate agli utenti tramite SMS o posta elettronica per confermare la propria identità.

Per altre informazioni sulla configurazione dell'autenticazione a due fattori e sull'abilitazionedei provider di accesso esterni , vedere la documentazione ufficiale di ASP.NET Core.

Autenticazione

L'autenticazione è il processo di determinazione dell'accesso al sistema. Se si usa ASP.NET Core Identity e i metodi di configurazione illustrati nella sezione precedente, verranno configurate automaticamente alcune impostazioni predefinite di autenticazione nell'applicazione. Tuttavia, è anche possibile configurare queste impostazioni predefinite manualmente o eseguire l'override di quelle impostate da AddIdentity. Se si usa Identity, configura l'autenticazione basata su cookie come schema predefinito.

Nell'autenticazione basata sul Web, in genere sono presenti fino a cinque azioni che possono essere eseguite durante l'autenticazione di un client di un sistema. Si tratta di:

  • Eseguire l'autenticazione. Usare le informazioni fornite dal client per creare un'identità da usare all'interno dell'applicazione.
  • Sfida. Questa azione viene usata per richiedere al client di identificarsi.
  • Proibire. Informare il cliente che gli è vietato effettuare un'azione.
  • Accesso. Mantenere il client esistente in qualche modo.
  • Disconnettersi. Rimuovere il client dalla persistenza.

Esistono diverse tecniche comuni per l'esecuzione dell'autenticazione nelle applicazioni Web. Questi sono detti schemi. Uno schema specifico definirà le azioni per alcune o tutte le opzioni precedenti. Alcuni schemi supportano solo un sottoinsieme di azioni e possono richiedere uno schema separato per eseguire quelle che non supportano. Ad esempio, il sistema OpenId-Connect (OIDC) non supporta l'accesso o la disconnessione, ma è comunemente configurato per utilizzare l'autenticazione tramite cookie per la persistenza della sessione.

Nell'applicazione ASP.NET Core è possibile configurare un DefaultAuthenticateScheme oggetto e schemi specifici facoltativi per ognuna delle azioni descritte in precedenza. Ad esempio, DefaultChallengeScheme e DefaultForbidScheme. La chiamata AddIdentity configura diversi aspetti dell'applicazione e aggiunge molti servizi necessari. Include anche questa chiamata per configurare lo schema di autenticazione:

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

Questi schemi usano cookie per la persistenza e il reindirizzamento alle pagine di accesso per l'autenticazione per impostazione predefinita. Questi schemi sono appropriati per le applicazioni Web che interagiscono con gli utenti tramite Web browser, ma non sono consigliate per le API. Le API useranno in genere un'altra forma di autenticazione, ad esempio token di tipo bearer JWT.

Le API Web vengono utilizzate dal codice, ad esempio HttpClient nelle applicazioni .NET e nei tipi equivalenti in altri framework. Questi client si aspettano una risposta utilizzabile da una chiamata API o un codice di stato che indichi quale problema, se presente, si è verificato. Questi client non interagiscono tramite un browser e non eseguono il rendering né l'interazione con l'HTML che un'API potrebbe restituire. Pertanto, non è appropriato per gli endpoint API reindirizzare i client alle pagine di accesso se non sono autenticate. Un altro schema è più appropriato.

Per configurare l'autenticazione per le API, è possibile configurare l'autenticazione come quella seguente, usata dal PublicApi progetto nell'applicazione di riferimento 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
        };
    });

Sebbene sia possibile configurare più schemi di autenticazione diversi all'interno di un singolo progetto, è molto più semplice configurare un singolo schema predefinito. Per questo motivo, tra gli altri, l'applicazione di riferimento eShopOnWeb separa le API nel proprio progetto, PublicApi, separato dal progetto principale Web che include le visualizzazioni dell'applicazione e Razor Pages.

Autenticazione nelle Blazor app

Blazor Le applicazioni server possono sfruttare le stesse funzionalità di autenticazione di qualsiasi altra applicazione ASP.NET Core. Blazor WebAssembly le applicazioni non possono usare i provider di identità e autenticazione predefiniti, tuttavia, poiché vengono eseguiti nel browser. Blazor WebAssembly le applicazioni possono archiviare lo stato di autenticazione utente in locale e possono accedere alle attestazioni per determinare quali azioni gli utenti devono essere in grado di eseguire. Tuttavia, tutti i controlli di autenticazione e autorizzazione devono essere eseguiti sul server indipendentemente da qualsiasi logica implementata all'interno dell'app BlazorWebAssembly , poiché gli utenti possono facilmente ignorare l'app e interagire direttamente con le API.

Riferimenti - Autenticazione

Autorizzazione

La forma più semplice di autorizzazione prevede la limitazione dell'accesso agli utenti anonimi. Questa funzionalità può essere ottenuta applicando l'attributo [Authorize] a determinati controller o azioni. Se vengono usati ruoli, l'attributo può essere ulteriormente esteso per limitare l'accesso agli utenti che appartengono a determinati ruoli, come illustrato di seguito:

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

}

In questo caso, gli utenti appartenenti ai HRManager ruoli o Finance (o entrambi) avranno accesso a SalaryController. Per richiedere che un utente appartenga a più ruoli (non solo uno di più), è possibile applicare l'attributo più volte, specificando un ruolo obbligatorio ogni volta.

La specifica di determinati set di ruoli come stringhe in molti controller e azioni diversi può causare ripetizioni indesiderate. Al minimo, definisci le costanti per questi letterali di stringa e usa le costanti dove è necessario specificare la stringa. È anche possibile configurare i criteri di autorizzazione, che incapsulano le regole di autorizzazione e quindi specificare i criteri anziché i singoli ruoli quando si applica l'attributo [Authorize] :

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

Usando i criteri in questo modo, è possibile separare i tipi di azioni limitate dai ruoli o dalle regole specifiche applicabili. Successivamente, se si crea un nuovo ruolo che deve avere accesso a determinate risorse, è sufficiente aggiornare un criterio anziché aggiornare ogni elenco di ruoli in ogni [Authorize] attributo.

Richieste di rimborso

Le attestazioni sono coppie nome-valore che indicano le proprietà di un utente autenticato. Ad esempio, è possibile archiviare il numero di dipendente degli utenti come attestazione. Le attestazioni possono quindi essere usate come parte dei criteri di autorizzazione. È possibile creare un criterio denominato "EmployeeOnly" che richiede l'esistenza di un'attestazione denominata "EmployeeNumber", come illustrato in questo esempio:

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

Questo criterio può quindi essere usato con l'attributo [Authorize] per proteggere qualsiasi controller e/o azione, come descritto in precedenza.

Protezione delle API Web

La maggior parte delle API Web deve implementare un sistema di autenticazione basato su token. L'autenticazione del token è senza stato e progettata per essere scalabile. In un sistema di autenticazione basato su token, il client deve prima eseguire l'autenticazione con il provider di autenticazione. In caso di esito positivo, il client viene emesso un token, che è semplicemente una stringa crittograficamente significativa di caratteri. Il formato più comune per i token è JSON Web Token o JWT (spesso pronunciato "jot"). Quando il client deve quindi inviare una richiesta a un'API, aggiunge questo token come intestazione nella richiesta. Il server convalida quindi il token trovato nell'intestazione della richiesta prima di completare la richiesta. La figura 7-4 illustra questo processo.

TokenAuth

Figura 7-4. Autenticazione basata su token per le API Web.

È possibile creare un servizio di autenticazione personalizzato, integrarsi con Azure AD e OAuth o implementare un servizio usando uno strumento open source come IdentityServer.

I token JWT possono incorporare attestazioni sull'utente, che possono essere lette nel client o nel server. È possibile usare uno strumento come jwt.io per visualizzare il contenuto di un token JWT. Non archiviare dati sensibili come password o chiavi nei token JTW, perché i relativi contenuti sono facilmente letti.

Quando si usano token JWT con SPA o BlazorWebAssembly applicazioni, è necessario archiviare il token in un punto qualsiasi del client e quindi aggiungerlo a ogni chiamata API. Questa attività viene normalmente svolta come intestazione, come illustrato nel codice seguente:

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

Dopo aver chiamato il metodo precedente, le richieste effettuate con _httpClient avranno il token incorporato nelle intestazioni della richiesta, consentendo all'API lato server di autenticare e autorizzare la richiesta.

Sicurezza personalizzata

Attenzione

Come regola generale, evitare di implementare implementazioni di sicurezza personalizzate.

Prestare particolare attenzione quando si decide di creare da sé un sistema di crittografia, di gestione degli utenti o di generazione di token. Sono disponibili molte alternative commerciali e open source, che quasi certamente avranno una sicurezza migliore rispetto a un'implementazione personalizzata.

Riferimenti - Sicurezza

Comunicazione cliente

Oltre a gestire le pagine e rispondere alle richieste di dati tramite API Web, ASP.NET le app Core possono comunicare direttamente con i client connessi. Questa comunicazione in uscita può usare un'ampia gamma di tecnologie di trasporto, il più comune è WebSocket. ASP.NET Core SignalR è una libreria che semplifica l'aggiunta di funzionalità di comunicazione da server a client in tempo reale alle applicazioni. SignalR supporta un'ampia gamma di tecnologie di trasporto, tra cui WebSocket, e astrae molti dei dettagli di implementazione dello sviluppatore.

Le comunicazioni client in tempo reale, che usano direttamente WebSocket o altre tecniche, sono utili in diversi scenari applicativi. Alcuni esempi includono:

  • Applicazioni per chat room dal vivo

  • Monitoraggio delle applicazioni

  • Aggiornamenti sui progressi del lavoro

  • Notifiche

  • Applicazioni per moduli interattivi

Quando si compila la comunicazione client nelle applicazioni, in genere sono presenti due componenti:

  • Gestione connessione lato server (Hub SignalR, WebSocketManager WebSocketHandler)

  • Libreria lato client

I client non sono limitati ai browser: le app per dispositivi mobili, le app console e altre app native possono anche comunicare tramite SignalR/WebSocket. Il semplice programma seguente restituisce tutto il contenuto inviato a un'applicazione di chat alla console, come parte di un'applicazione di esempio WebSocketManager:

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

Valutare i modi in cui le applicazioni comunicano direttamente con le applicazioni client e valutare se la comunicazione in tempo reale migliorerebbe l'esperienza utente dell'app.

Riferimenti - Comunicazione cliente

Progettazione basata su dominio: è necessario applicarla?

Domain-Driven Design (DDD) è un approccio agile alla creazione di software che sottolinea l'attenzione sul dominio aziendale. Sottolinea l'importanza della comunicazione e dell'interazione con esperti del settore aziendale che possono spiegare agli sviluppatori come funziona il sistema nel mondo reale. Ad esempio, se stai creando un sistema che gestisce le operazioni azionarie, il tuo esperto di dominio potrebbe essere un broker azionario esperto. DDD è progettato per risolvere problemi aziendali complessi di grandi dimensioni e spesso non è appropriato per applicazioni più piccole e più semplici, poiché l'investimento nella comprensione e nella modellazione del dominio non vale la pena.

Quando si compila un software seguendo un approccio DDD, il team (inclusi gli stakeholder non tecnici e i collaboratori) deve sviluppare un linguaggio comune per lo spazio dei problemi. Vale a dire, la stessa terminologia deve essere usata per il concetto reale modellato, l'equivalente software e tutte le strutture che potrebbero esistere per rendere persistente il concetto (ad esempio, tabelle di database). Pertanto, i concetti descritti nel linguaggio universale devono costituire la base per il modello di dominio.

Il modello di dominio comprende oggetti che interagiscono tra loro per rappresentare il comportamento del sistema. Questi oggetti possono rientrare nelle categorie seguenti:

  • Entità, che rappresentano oggetti con un thread di identità. Le entità vengono in genere archiviate in persistenza con una chiave in base alla quale possono essere recuperate in un secondo momento.

  • Aggregazioni, che rappresentano gruppi di oggetti che devono essere salvati in modo permanente come unità.

  • Oggetti valore, che rappresentano concetti che possono essere confrontati sulla base della somma dei relativi valori di proprietà. Ad esempio, DateRange è costituito da una data di inizio e di fine.

  • Eventi di dominio, che rappresentano gli eventi che si verificano all'interno del sistema che sono di interesse per altre parti del sistema.

Un modello di dominio DDD deve incapsulare un comportamento complesso all'interno del modello. Le entità, in particolare, non devono essere semplicemente raccolte di proprietà. Quando il modello di dominio non ha un comportamento e rappresenta semplicemente lo stato del sistema, si dice che sia un modello anemico, che è indesiderato in DDD.

Oltre a questi tipi di modello, DDD usa in genere un'ampia gamma di modelli:

  • Repository, per l'astrazione dei dettagli di persistenza.

  • Factory, per incapsulare la creazione di oggetti complessi.

  • Servizi, per incapsulare comportamenti complessi e/o dettagli dell'implementazione dell'infrastruttura.

  • Comando, per disaccoppiare l'emissione dei comandi e l'esecuzione del comando stesso.

  • Specifica, per incapsulare i dettagli della query.

DDD consiglia anche l'uso dell'architettura pulita illustrata in precedenza, consentendo l'accoppiamento libero, l'incapsulamento e il codice che possono essere verificati facilmente usando unit test.

Quando dovresti applicare DDD

DDD è particolarmente adatto alle applicazioni di grandi dimensioni con una notevole complessità aziendale (non solo tecnica). L'applicazione deve richiedere la conoscenza di esperti di dominio. Deve esserci un comportamento significativo nel modello di dominio stesso, che rappresenta regole business e interazioni oltre all'archiviazione e al recupero dello stato corrente di vari record dagli archivi dati.

Quando non dovresti applicare DDD

DDD prevede investimenti nella modellazione, nell'architettura e nella comunicazione che potrebbero non essere giustificati per applicazioni più piccole o che sono essenzialmente solo CRUD (creazione/lettura/aggiornamento/eliminazione). Se si sceglie di avvicinarsi all'applicazione seguendo la DDD, ma si scopre che il dominio ha un modello anemico senza alcun comportamento, potrebbe essere necessario ripensare l'approccio. L'applicazione potrebbe non avere bisogno di DDD oppure potrebbe essere necessaria assistenza per effettuare il refactoring dell'applicazione per incapsulare la logica di business nel modello di dominio, anziché nel database o nell'interfaccia utente.

Un approccio ibrido consiste nell'usare DDD solo per le aree transazionali o più complesse dell'applicazione, ma non per parti CRUD o di sola lettura più semplici dell'applicazione. Ad esempio, non sono necessari i vincoli di un'aggregazione se si eseguono query sui dati per visualizzare un report o per visualizzare i dati per un dashboard. È perfettamente accettabile avere un modello di lettura separato e più semplice per tali requisiti.

Riferimenti : progettazione Domain-Driven

Distribuzione

Nel processo di distribuzione dell'applicazione ASP.NET Core sono necessari alcuni passaggi, indipendentemente dalla posizione in cui verrà ospitato. Il primo passaggio consiste nel pubblicare l'applicazione, che può essere eseguito usando il comando dell'interfaccia della linea di comando dotnet publish. Questo passaggio compilerà l'applicazione e inserisce tutti i file necessari per eseguire l'applicazione in una cartella designata. Quando si esegue la distribuzione da Visual Studio, questo passaggio viene eseguito automaticamente. La cartella publish contiene .exe e .dll file per l'applicazione e le relative dipendenze. Un'applicazione autonoma includerà anche una versione del runtime .NET. ASP.NET applicazioni principali includeranno anche file di configurazione, asset client statici e visualizzazioni MVC.

ASP.NET Le applicazioni principali sono applicazioni console che devono essere avviate all'avvio del server e riavviate se l'applicazione (o il server) si arresta in modo anomalo. Un process manager può essere usato per automatizzare questo processo. I gestori di processi più comuni per ASP.NET Core sono Nginx e Apache in Linux e IIS o servizio Windows in Windows.

Oltre a un gestore di processi, ASP.NET le applicazioni Core possono usare un server proxy inverso. Un server proxy inverso riceve richieste HTTP da Internet e le inoltra a Kestrel dopo una gestione preliminare. I server proxy inversi forniscono un livello di sicurezza per l'applicazione. Kestrel non supporta anche l'hosting di più applicazioni sulla stessa porta, quindi non è possibile usare tecniche come le intestazioni host per abilitare l'hosting di più applicazioni nella stessa porta e nello stesso indirizzo IP.

Kestrel verso Internet

Figura 7-5. ASP.NET ospitata in Kestrel dietro un server proxy inverso

Un altro scenario in cui un proxy inverso può essere utile è proteggere più applicazioni tramite SSL/HTTPS. In questo caso, solo il proxy inverso deve avere SSL configurato. La comunicazione tra il server proxy inverso e Kestrel può avvenire su HTTP, come illustrato nella figura 7-6.

ASP.NET ospitata dietro un server proxy inverso protetto da HTTPS

Figura 7-6. ASP.NET ospitata dietro un server proxy inverso protetto da HTTPS

Un approccio sempre più diffuso consiste nell'ospitare l'applicazione ASP.NET Core in un contenitore Docker, che può quindi essere ospitata in locale o distribuita in Azure per l'hosting basato sul cloud. Il contenitore Docker potrebbe contenere il codice dell'applicazione, in esecuzione in Kestrel e verrebbe distribuito dietro un server proxy inverso, come illustrato in precedenza.

Se si ospita l'applicazione in Azure, è possibile usare il gateway applicazione di Microsoft Azure come appliance virtuale dedicata per fornire diversi servizi. Oltre a fungere da proxy inverso per singole applicazioni, il gateway applicazione può anche offrire le funzionalità seguenti:

  • Bilanciamento del carico HTTP

  • Scarico SSL (solo SSL per Internet)

  • SSL end-to-end

  • Routing multisito (consolidare fino a 20 siti in un singolo gateway applicativo)

  • Firewall per applicazioni Web

  • Supporto di Websocket

  • Diagnostica avanzata

Altre informazioni sulle opzioni di distribuzione di Azure sono disponibili nel capitolo 10.

Riferimenti - Distribuzione