Condividi tramite


Ospitare e distribuire app lato serverBlazor

Annotazioni

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 10 di questo articolo.

Avvertimento

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 10 di questo articolo.

Questo articolo spiega come ospitare e distribuire app server-side Blazor (Blazor Web Apps e applicazioni Blazor Server) utilizzando ASP.NET Core.

Valori di configurazione dell'host

Le app lato Blazor server possono accettare valori di configurazione host generici.

Distribuzione

Usando un modello di hosting lato server, Blazor viene eseguito nel server dall'interno di un'app ASP.NET Core. Gli aggiornamenti dell'interfaccia utente, la gestione degli eventi e le chiamate JavaScript vengono gestiti tramite una SignalR connessione.

È necessario un server Web in grado di ospitare un'app ASP.NET Core. Visual Studio include un modello di progetto app sul lato server. Per ulteriori informazioni sui modelli di progetto , consultare la struttura del progetto ASP.NET Core .

Pubblicare un'app nella configurazione Release e rilasciare il contenuto della cartella bin/Release/{TARGET FRAMEWORK}/publish, in cui il segnaposto {TARGET FRAMEWORK} è il framework di destinazione.

Scalabilità

Quando si considera la scalabilità verticale di un singolo server, è probabile che la memoria disponibile per un'applicazione sia la prima risorsa che l'app esaurisce man mano che aumentano le richieste degli utenti. La memoria disponibile nel server influisce su:

  • Numero di circuiti attivi che un server può supportare.
  • Latenza dell'interfaccia utente nel client.

Per indicazioni sulla creazione di app lato Blazor server sicure e scalabili, vedere le risorse seguenti:

Ogni circuito usa circa 250 KB di memoria per un'app di tipo Hello World minima. Le dimensioni di un circuito dipendono dal codice dell'app e dai requisiti di manutenzione dello stato associati a ogni componente. Ti consigliamo di misurare le richieste di risorse durante lo sviluppo per l'app e l'infrastruttura, ma la baseline seguente può essere un punto di partenza nella pianificazione della destinazione di distribuzione: se prevedi che l'app supporti 5.000 utenti simultanei, valuta la possibilità di budget di almeno 1,3 GB di memoria server per l'app (o circa 273 KB per utente).

Blazor WebAssembly precaricamento delle risorse statiche

Il componente ResourcePreloader nel contenuto di testa del componente App (App.razor) viene usato per riferire a Blazor risorse statiche. Il componente viene inserito dopo il tag URL di base (<base>):

<ResourcePreloader />

Un Razor componente viene usato invece di <link> elementi perché:

  • Il componente consente all'URL di base (valore dell'attributo del tag <base>) di identificare correttamente la radice dell'app href all'interno di un'app ASP.NET Core.
  • La funzionalità può essere rimossa rimuovendo il ResourcePreloader tag del componente dal App componente. Ciò è utile nei casi in cui l'app usa un loadBootResource callback per modificare gli URL.

Configurazione SignalR

SignalRLe condizioni di hosting e ridimensionamento si applicano alle Blazor app che usano SignalR.

Per ulteriori informazioni su SignalR nelle app Blazor, incluse le linee guida sulla configurazione, vedere le linee guida di ASP.NET Core BlazorSignalR.

Trasporti

Blazor funziona meglio quando si usano WebSocket come SignalR trasporto a causa di una latenza inferiore, maggiore affidabilità e maggiore sicurezza. Long Polling viene usato da SignalR quando WebSockets non sono disponibili o quando l'app è configurata in modo esplicito per utilizzare Long Polling.

Viene visualizzato un avviso della console se viene utilizzato long polling:

Impossibile connettersi tramite WebSocket, utilizzando il trasporto di fallback Long Polling. Ciò può essere dovuto a una VPN o a un proxy che blocca la connessione.

Errori di distribuzione e connessione globali

Raccomandazioni per le distribuzioni globali nei data center geografici:

  • Distribuire l'app nelle aree in cui risiede la maggior parte degli utenti.
  • Prendere in considerazione l'aumento della latenza per il traffico in tutti i continenti. Per controllare l'aspetto dell'interfaccia utente di riconnessione, consultare la guida di ASP.NET CoreBlazorSignalR.
  • Considerare l'uso del Servizio SignalR Azure.

Servizio app di Azure

L'hosting su Azure App Service richiede la configurazione per i WebSocket e l'affinità di sessione, nota anche come affinità di Application Request Routing (ARR).

Annotazioni

Un'app Blazor su Azure App Service non richiede il servizio Azure SignalR.

Abilitare quanto segue per la registrazione dell'app nel servizio app Azure:

  • WebSocket per consentire il funzionamento del trasporto WebSocket. L'impostazione predefinita è Disattivata.
  • Affinità di sessione per instradare le richieste di un utente alla stessa istanza di App Service. L'impostazione predefinita è .
  1. Nel portale di Azure, vai all'app Web in Servizi app.
  2. Aprire Configurazione delle impostazioni>.
  3. Impostare Web Sockets su Attivo.
  4. Verificare che l'affinità di sessione sia impostata su .

Servizio di Azure SignalR

Il servizio di Azure SignalR facoltativo funziona insieme all'hub dell'app SignalR per aumentare le prestazioni di un'app lato server a un numero elevato di connessioni simultanee. Inoltre, la copertura globale del servizio e i data center ad alte prestazioni aiutano significativamente a ridurre la latenza a causa della geografia.

Il servizio non è necessario per Blazor le app ospitate nel servizio app Azure o nelle app di Azure Container, ma può essere utile in altri ambienti di hosting:

  • Per agevolare l'espansione delle connessioni su larga scala.
  • Gestire la distribuzione globale.

Il servizio Azure SignalR con SDK v1.26.1 o versione successiva supporta SignalR riconnessione con stato (WithStatefulReconnect).

Se l'app utilizza Long Polling o esegue il fallback su Long Polling invece di ai WebSocket, potrebbe essere necessario configurare l'intervallo di polling massimo (MaxPollIntervalInSeconds, impostazione predefinita: 5 secondi, limite: 1-300 secondi), che definisce l'intervallo di polling massimo consentito per le connessioni di Long Polling nel servizio AzureSignalR. Se la richiesta di polling successiva non arriva entro l'intervallo massimo di polling, il servizio chiude la connessione client.

Per indicazioni su come aggiungere il servizio come dipendenza a una distribuzione di produzione, vedere Pubblicare un'app ASP.NET Core nel servizio app di Azure.

Per altre informazioni, vedere:

App contenitore di Azure

Per un'esplorazione più approfondita del ridimensionamento delle app lato Blazor server nel servizio App Contenitore di Azure, vedere Ridimensionamento delle app di base ASP.NET in Azure. L'esercitazione illustra come creare e integrare i servizi necessari per ospitare app su Azure Container Apps. In questa sezione vengono forniti anche i passaggi di base.

  1. Configurare il servizio Azure Container Apps per l'affinità di sessione seguendo le indicazioni riportate in Affinità di sessione in Azure Container Apps (documentazione di Azure).

  2. Il servizio Protezione dati di base (DP) ASP.NET deve essere configurato per rendere persistenti le chiavi in una posizione centralizzata a cui tutte le istanze del contenitore possono accedere. Le chiavi possono essere archiviate in Archiviazione BLOB di Azure e protette con Azure Key Vault. Il servizio Device Provisioning usa le chiavi per deserializzare i componenti Razor. Per configurare il servizio DP per l'uso di Azure Blob Storage e Azure Key Vault, fare riferimento ai pacchetti NuGet seguenti:

    Annotazioni

    Per indicazioni sull'aggiunta di pacchetti alle app .NET, vedere gli articoli sotto Installare e gestire pacchetti in Flusso di lavoro dell'utilizzo di pacchetti (documentazione di NuGet). Verificare le versioni corrette dei pacchetti in NuGet.org.

  3. Eseguire l'aggiornamento Program.cs con il codice evidenziato seguente:

    using Azure.Identity;
    using Microsoft.AspNetCore.DataProtection;
    using Microsoft.Extensions.Azure;
    
    var builder = WebApplication.CreateBuilder(args);
    var BlobStorageUri = builder.Configuration["AzureURIs:BlobStorage"];
    var KeyVaultURI = builder.Configuration["AzureURIs:KeyVault"];
    
    builder.Services.AddRazorPages();
    builder.Services.AddHttpClient();
    builder.Services.AddServerSideBlazor();
    
    builder.Services.AddAzureClientsCore();
    
    builder.Services.AddDataProtection()
                    .PersistKeysToAzureBlobStorage(new Uri(BlobStorageUri),
                                                    new DefaultAzureCredential())
                    .ProtectKeysWithAzureKeyVault(new Uri(KeyVaultURI),
                                                    new DefaultAzureCredential());
    var app = builder.Build();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    
    app.UseRouting();
    
    app.UseAuthorization();
    
    app.MapRazorPages();
    
    app.Run();
    

    Le modifiche precedenti consentono all'app di gestire il servizio Device Provisioning usando un'architettura centralizzata e scalabile. DefaultAzureCredential individua l'identità gestita dell'app contenitore dopo che il codice è stato distribuito in Azure e la utilizza per connettersi all'archiviazione BLOB e al key vault dell'app.

  4. Per creare l'identità gestita per l'app contenitore e concedergli l'accesso all'archiviazione BLOB e a un key vault, seguire questa procedura:

    1. Nel portale di Azure passare alla pagina di panoramica dell'app contenitore.
    2. Selezionare Service Connector dal riquadro di spostamento sinistro.
    3. Selezionare + Crea nella barra di spostamento superiore.
    4. Nel menu a comparsa Crea connessione immettere i valori seguenti:
      • Contenitore: selezionare l'app contenitore creata per ospitare l'app.
      • Tipo di servizio: selezionare Archiviazione BLOB.
      • Sottoscrizione: selezionare la sottoscrizione proprietaria dell'app contenitore.
      • Nome connessione: immettere un nome di scalablerazorstorage.
      • Tipo di client: selezionare .NET e quindi selezionare Avanti.
    5. Selezionare Identità gestita assegnata dal sistema e selezionare Avanti.
    6. Usare le impostazioni di rete predefinite e selezionare Avanti.
    7. Dopo che Azure convalida le impostazioni, selezionare Crea.

    Ripetere le impostazioni precedenti per la cassaforte delle chiavi. Selezionare il servizio di Key Vault appropriato e inserire la chiave nella scheda Base.

Annotazioni

L'esempio precedente usa DefaultAzureCredential per semplificare l'autenticazione durante lo sviluppo di app distribuite in Azure combinando le credenziali usate negli ambienti di hosting di Azure con le credenziali usate nello sviluppo locale. Quando si passa all'ambiente di produzione, un'alternativa è una scelta migliore, ad esempio ManagedIdentityCredential. Per altre informazioni, vedere Autenticare le app .NET ospitate in Azure nelle risorse di Azure usando un'identità gestita assegnata dal sistema.

IIS

Quando si usa IIS, abilitare:

Per altre informazioni, vedere le linee guida e i collegamenti incrociati delle risorse IIS esterne in Pubblicare un'app ASP.NET Core in IIS.

Kubernetes

Creare una definizione di ingresso usando le seguenti annotazioni di Kubernetes per l'affinità di sessione:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: <ingress-name>
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "affinity"
    nginx.ingress.kubernetes.io/session-cookie-expires: "14400"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "14400"

Linux con Nginx

Seguire le indicazioni per un'app SignalR ASP.NET Core con le modifiche seguenti:

  • Modificare il location percorso da /hubroute (location /hubroute { ... }) al percorso radice / (location / { ... }).
  • Rimuovere la configurazione per il buffering proxy (proxy_buffering off;) perché l'impostazione si applica solo agli eventi inviati dal server (SSE), che non sono rilevanti per Blazor le interazioni client-server dell'app.

Per altre informazioni e indicazioni sulla configurazione, vedere le risorse seguenti:

Linux con Apache

Per ospitare un'app Blazor dietro Apache in Linux, configurare ProxyPass per il traffico HTTP e WebSocket.

Nell'esempio seguente:

  • Kestrel il server è in esecuzione sulla macchina host.
  • L'app rimane in ascolto del traffico sulla porta 5000.
ProxyPreserveHost   On
ProxyPassMatch      ^/_blazor/(.*) http://localhost:5000/_blazor/$1
ProxyPass           /_blazor ws://localhost:5000/_blazor
ProxyPass           / http://localhost:5000/
ProxyPassReverse    / http://localhost:5000/

Abilitare i moduli seguenti:

a2enmod   proxy
a2enmod   proxy_wstunnel

Controllare la presenza di errori webSocket nella console del browser. Errori di esempio:

  • Firefox non riesce a stabilire una connessione al server all'indirizzo ws://the-domain-name.tld/_blazor?id=XXX
  • Errore: Impossibile avviare il trasporto 'WebSockets': Errore: Si è verificato un problema con il trasporto.
  • Errore: Impossibile avviare il trasporto 'LongPolling': TypeError: this.transport non è definito
  • Errore: impossibile connettersi al server con uno dei trasporti disponibili. WebSocket non riuscito
  • Errore: impossibile inviare dati se la connessione non è nello stato "Connesso".

Per altre informazioni e indicazioni sulla configurazione, vedere le risorse seguenti:

Misurare la latenza di rete

JS l'interoperabilità può essere usata per misurare la latenza di rete, come illustrato nell'esempio seguente.

MeasureLatency.razor:

@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}

Per un'esperienza di interfaccia utente ragionevole, è consigliabile una latenza dell'interfaccia utente sostenuta di 250 ms o meno.