ASP.NET Core 10 – Le novità!

Bentornati qui sul mio blog,

oggi approfondiamo gli aspetti più importanti che sono emersi nella .NET Conf per la versione 10 (LTS) del framework di Microsoft riferito al mondo web.

Ci saranno diversi articoli, nello specifico questo è dedicato interamente alle novità di ASP.NET Core 10, mentre ce ne saranno altri focalizzati su C#, Entity framework, ecc

Per ulteriori le informazioni potete consultare la pagina ufficiale microsoft

Gestione della memoria

Ogni anno, durante gli eventi di annuncio di .NET, vengono enunciati significativi miglioramenti di performance di consumo delle risorse. Anche a questo giro i miglioramenti non sono mancati ed in questo contesto è stato migliorata la gestione automatica della memoria (Automatic memory pool eviction) in applicazioni di lunga esecuzione: succedeva a volte che venissero mantenuti in memoria anche oggetti non più utilizzati e questa nuova versione del framework risolve questa problematica.

Blazor

Questa sezione allungherà di molto questo articolo, in quanto sono state introdotte tante piccole novità legato a Blazor. In un mondo dominato da Node.js, ce la farà Blazor ad emergere? Non lo so, ma l’obiettivo di casa Cupertino è quella di creare un ecosistema completo, dal frontend fino ad arrivare all’orchestrazione del backend tramite Aspire.

Non perdiamoci in chiacchere e vediamo cosa c’è di nuovo!

Comportamenti generali

Script come risorse statiche

Prima di .NET 10 i file javascript erano inglobati all’interno delle DLL generate durante la fase di compilazione. Con .NET 10 invece sono stati esclusi da questo meccanismo e sono esterni alle DLL e trattati come comuni file javascript (compressi automaticamente e con fingerprint per randomizzare il nome del file)

Pre-caricamento delle risorse statiche

Tutti i link presenti nell’header della pagina verranno pre-scaricati nel browser, prima ancora che la pagina iniziale sia scaricata e gestita, permettendo una migliore gestione del caricamento delle pagine. Quanto appena descritto vale per le applicazioni con Server-Side-Rendering.

Nel mondo delle applicazioni WebAssembly invece, le risorse hanno ora una priorità maggiore di download (e di caching) rispetto agli altri asset, in due situazioni:

  • Quando si utilizza il tag rel="preload" nel link della risorsa.
    • <link rel="preload" [...] />
  • Quando la property OverrideHtmlAssetPlaceholders (nel file .csproj) è impostata a true
    • <PropertyGroup> <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders> </PropertyGroup>

Form e componenti

Validazione

E’ stata migliorata la validazione, ora estesa anche agli oggetti annidati e alle liste di elementi. Per attivarlo bisognerà aggiungere la validazione nel program.cs (builder.Services.AddValidation()) e aggiungere il tag [ValidatableType] sulla classe che vogliamo validare. Esempio:

C#
//Program.cs
builder.Services.AddValidation();

...

[ValidatableType]
public class BeltAccessories {
  public int Id { get; set; }
  public Lightsaber LightSaber { get; set; }
}

public class Lightsaber
{
    [Required(ErrorMessage = "ID of lightsaber is required.")]
    public Guid Id { get; set; }

    [Range(0,10000)]
    public int NumberOfFights { get; set; } = 0;
 
    //Ignore validation for this specific item
    [SkipValidation]
    public KyberCristal mainCristal { get; set; }
}

In più possiamo ora costruirci un metodo custom di validazione, andando ad estendere l’interfaccia IValidatableObject.Validate (posizionandola anche in un assembly differente rispetto a quello dell’app, magari in una libreria aziendale di validatori da condividere tra più progetti).

Campi nascosti

Beh, era anche ora! 😅 Anziché scrivere un campo nascosto utilizzando il tag html, possiamo ora usare il tag <InputHidden/>

C#
<EditForm Model="BeltAccessories" OnValidSubmit="Submit" FormName="BeltAccessoriesForm">
  [...]
  <InputHidden id="my-belt-id" @bind-Value="Id" />
  <button type="submit">Submit</button>
</EditForm>

Sessione

Nelle versioni precedenti di Blazor, la sessione utente si perdeva in diverse situazioni, per esempio quando la connessione saltava per lunghi periodi di tempo, la tab del browser “dimenticata” andava in risparmio energetico (tab throttling), quando sullo smartphone cambiamo app, ecc.

Questa nuova versione risolve questo comportamento (solo in modalità Server-Side-Rendering) e permette di mantenere più a lungo la sessione, ripristinando quello che si stava facendo anche in queste situazioni.

Persistenza dei dati

In .NET 8 è stata introdotta la “navigazione migliorata”(enhanced navigation) in modalità Server-Side-Rendering, con un effetto finale molto simile alla navigazione di una Single Page Application. In modalità SSR ogni pagina deve prima essere gestita dal server e poi ritornata al client, mentre in questa modalità la pagina viene ricaricata solo parzialmente e dove necessario.

Fatta questa premessa veniamo al discorso della persistenza dei dati: durante l’enhanced navigation è stata implementata la possibilità di persistere i dati, una sorta di cache di sessione per mantenere alcuni stati o alcuni oggetti caricati. Questo comportamento si verifica, di default, solamente al primo caricamento della pagina ma può essere cambiato a nostro piacimento, agendo sul parametro “RestoreBehavior” del data annotation PersistentState:

C#
//Questa lista viene salvata e persistita, sono dati che cambiano poco, in questo modo evitiamo di rigenerarli ogni volta
[PersistentState(AllowUpdates = true)]
public Jedi[]? AvailableJedi{ get; set; }

//Nessun dato pre-renderizzato quando si apre la pagina
[PersistentState(RestoreBehavior = RestoreBehavior.SkipInitialValue)]
public BecomeJediForm[]? BecomeJediForm{ get; set; }

//Dati persistiti ma ricaricati ad ogni caricamento di pagina
[PersistentState(RestoreBehavior = RestoreBehavior.SkipLastSnapshot)]
public BecomeJediForm[]? BecomeJediFormExample{ get; set; }

protected override async Task OnInitializedAsync()
{
    Forecasts ??= await JediService.GetavailablesJedis();
}

Oltre a questi approcci già out-of-the-box possiamo andare ad estendere il funzionamento e gestirlo con una nostra callback richiamando PersistentComponentState.RegisterOnRestoring.

Questo approccio può essere utile per salvare lo stato di compilazione di una form, elementi di un carrello o liste calcolate specifiche per l’utente.

Blazor WebAssembly (WASM)

Quando parliamo di WASM intendiamo la modalità WebAssembly di Blazor, che permette di eseguire l’applicazione scritta in C# direttamente nel browser. Oltre all’applicazione, viene scaricato anche un runtime .NET e non è necessario nessun backend per funzionare.

Hot Reload

La più grande novità di WebAssembly è sicuramente il supporto all’Hot Reload (la possibilità di modificare il codice della pagina e di vederne gli affetti live, senza stoppare e riavviare l’applicazione).

Nei nuovi progetti sarà già abilitata di default per la configurazione di Debug, nel caso volessimo modificarla allora basterà agire su questa impostazione nel progetto .csproj (impostando true/false)

project.csproj
<PropertyGroup>
  <WasmEnableHotReload>true</WasmEnableHotReload>
</PropertyGroup>

Variabile d’ambiente

Continuando a parlare di cambiamenti, un altro di quelli introdotti è che l’appsettings.json non è più utilizzato per impostare la variabile d’ambiente, ora dovremo andare ad impostarlo nel file .csproj

Project.csproj
...
<WasmApplicationEnvironmentName>Staging</WasmApplicationEnvironmentName>
...

Fingerprint javascript

Con la versione 9 di .NET era stato introdotto il fingerprint dei file javascript (il nome del file viene randomizzato), analogamente a quanto già succede su altri framework. Con la versione 10 questo meccanismo è stato reso disponibile anche per le applicazioni WebAssembly tramite un tag, da inserire nella pagina index.html della directory wwwroot e una property nel file .csproj

wwwroot/index.html
<body>
    //...
    
    <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
    <script src="js/my-script#[.{fingerprint}].js"></script>
    
    //...
</body>

</html>
Project.csproj
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
  </PropertyGroup>
</Project>

Quickgrid

Quickgrid è un componente tabellare introdotto sperimentalmente con .NET 7, il suo scopo è di visualizzare i dati in modo performante, supportando anche l’interfaccia IQuerable per manipolare dati in arrivo dal database.

Essendo un componente relativamente nuovo c’è qualche novità in merito. La prima è che hanno aggiunto la possibilità di specificare una classe css direttamente sulla riga della tabella, tramite il parametro RowClass.

C#
<QuickGrid [...] RowClass="GetCustomCssClassFunction">
    //...
</QuickGrid>

@code {
    private string GetCustomCssClassFunction(MyGridItem lightsaber) =>
        lightsaber.IsAssigned? "lsbr-assigned" : "lsbr-not-assigned";
}

Un’altra modifica introdotta è relativa al menù delle impostazioni delle colonne, ora è possibile andare a nasconderlo usando la funzione HideColumnOptionsAsync()

OpenApi

Supporto OpenApi 3.1

Lo standard OpenAPI è una specifica utilizzata per descrivere, progettare e documentare le API REST. E’ un file (YAML o più comunemente Json) che descrive la struttura delle API, i loro endpoint, i parametri, l’autenticazione, ecc.

Il passaggio dalla versione 3.0 alla versione 3.1 introduce delle migliorie significative, con una completa compatibilità con l’ultima versione di JSON Schema, risolvendo il problema della versione 3.0 tra “cosa è permesso in JSON Schema” e “cosa è permesso in OpenAPI”.

Quando andiamo ad aggiungere OpenApi ai nostri servizi, possiamo ora specificare la versione delle api

C#
builder.Services.AddOpenApi(options => {
     options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1; 
});

Oltre a questo ora ASP.NET supporta anche la generazione del documento OpenAPI in YAML. Per farlo basterà semplicemente cambiare il l’estensione del file nel metodo .MapOpenApi() (nel program.cs)

Program.cs
if (app.Environment.IsDevelopment()) {   
    app.MapOpenApi("/openapi/{documentName}.yaml"); 
}

Descrizione della response

E’ stata aggiunta la possibilità di descrivere il significato di un endpoint tramite gli attributi [Produces], [ProducesResponseType], and [ProducesDefaultResponseType], descrizione che verrà poi esportata nel documento OpenAPI.

C#
[HttpGet(Name = "get-status")]
[ProducesResponseType<AstroDroidStatus>(StatusCodes.Status200OK,Description = "Status of mechanical astrodroid")]
public AstroDroidStatus Get()
{
    ...
}

AOT

Le applicazioni AOT sono applicazioni compilate nativamente per l’ambiente di distribuzione, con un enorme vantaggio in termini di dimensioni e performance. E’ stato aggiunto il supporto di OpenAPI nel template webapiaot

Minimal API

Le minima API si stanno imponendo come il nuovo approccio per scrivere gli endpoint in modo più semplice, snello e pulito, centralizzando tutta la logica nei servizi, evitando la tentazione di inserire logica di business nei classici endpoint.

Validazione

E’ ora possibile validare i parametri di input delle nostre API tramite attributi. Il primo passo consiste nell’attivare la funzionalità nel program.cs:

Program.cs
builder.Services.AddValidation();

Successivamente possiamo andare ad utilizzare i nostri attributi:

C#
[HttpPost(Name = "lightsaber")]
public AstroDroidActionResponse ThrowLightsaber(
    [Required] string securityCode,
    [Range(1, 100)] int ThrowForce
    )
)
{
    ...
}

La stessa logica si applica anche nel caso in cui il parametro sia una classe o record, con vari data annotation sulle property: passando l’oggetto di tipo ‘Lightsaber‘ ad una minima API verranno validate tutte le property secondo quanto impostato.

C#
public class Lightsaber
{
    [Required]
    public Guid Id { get; set; }

    [Range(0,10000)]
    public int NumberOfFights { get; set; } = 0;

    [Required]
    public KyberCristal mainCristal { get; set; }
}

//controller
[HttpPost]
public ResponseResult Add(Lightsaber lightsaber)
)
{
    ...
}

Oltre alle validazioni classiche possiamo anche creare degli attributi di validazione custom implementando l’interfaccia IValidatableObject.

⚠️Attenzione

La validazione delle API è stata spostata nel package Microsoft.Extensions.Validation

Property null su oggetti complessi

Quando passiamo un oggetto complesso tramite il data annotation [FromForm], viene ora mappato come null qualiasi valore vuoto associato ad una property nullable. Nell’esempio qui sotto il campo ValidUntil verrà mappato a null.

C#
app.MapPost("/messages", ([FromForm] Message newMessage) => TypedResults.Ok(newMessage));

public class Message
{
  public string Message { get; set; }
  public string Author { get; set; }
  public DateOnly? ValidUntil { get; set; } //--> Mappta come null
}

/*

  payload:
  {
    'title': 'Aiutami Obi One Kenobi, sei la mia unica speranza',
    'author': 'Leila'
    'validUntil': ''
  }
  
*/

Metriche

Sono state migliorate le metriche in generale, sotto diversi aspetti:

  • Eventi di AuthN and AuthZ
  • ASP.NET Core Identity
  • Memory eviction
  • Blazor app (per un miglior tracciamento e osservabilità di applicazioni Blazor)

Sicurezza

Una delle più grandi (e richieste) novità riguarda l’annoso redirect verso l’endpoint di login, che si verificava quando si contattava senza cookie un endpoint API protetto da autenticazione cookie.

Essendo appunto in un contesto API non aveva senso ottenere un redirect, ora invece possiamo andare a scegliere di ritornare 403 o 401. E’ comunque possibile ri-abilitare il comportamento precedente, anche se è ipotizzabile che verrà tolta questa possibilità con le prossime release del framework.

Passkey

Le passkey sono un nuovo approccio per accedere ai siti web in sostituzione delle care e vecchie password e funzionano con una coppia di chiavi (pubblica e privata): loggarsi con l’impronta digitale è un esempio di utilizzo di una passkey, anzichè usare la password.

In .NET 10 è stato introdotto il supporto a questo metodo di accesso, basato sugli standard AuthN e FIDO2, ed è disponibile fin da subito. Se volete vedere come funziona potete fare riferimento al template di progetto di Blazor Web App.

Conclusioni

In questo articolo abbiamo visto le novità del mondo web che sono state introdotte con .NET 10. Se questo articolo vi è piaciuto ricordatevi che ce ne sono altri dedicati agli altri ambiti di novità del framework.

Alla prossima!

Condividi questo articolo
Shareable URL
Post precedente

C# 14

Prosimo post

EF Core 10: .leftJoin() e .rightJoin() in LINQ

Leggi il prossimo articolo