Layout di distribuzione per le app ospitate Blazor WebAssembly ASP.NET Core

Questo articolo illustra come abilitare le distribuzioni ospitate Blazor WebAssembly in ambienti che bloccano il download e l'esecuzione di file DLL (Dynamic Link Library).

Nota

Queste indicazioni illustrano gli ambienti che impediscono ai client di scaricare ed eseguire DLL. In .NET 8 o versioni successive Blazor usa il formato di file Webcil per risolvere il problema. Per altre informazioni, vedere Ospitare e distribuire ASP.NET Core Blazor WebAssembly. La creazione di bundle in più parti con il pacchetto NuGet sperimentale descritto in questo articolo non è supportata per Blazor le app in .NET 8 o versioni successive. Per altre informazioni, vedere Migliorare il pacchetto Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle per definire un formato di bundle personalizzato (dotnet/aspnetcore #36978). È possibile usare le indicazioni contenute in questo articolo per creare un pacchetto NuGet multipart bundling per .NET 8 o versione successiva.

Blazor WebAssembly Le app richiedono librerie a collegamento dinamico (DLL) per funzionare, ma alcuni ambienti impediscono ai client di scaricare ed eseguire DLL. In un subset di questi ambienti, la modifica dell'estensione di file DLL (.dll) è sufficiente per ignorare le restrizioni di sicurezza, ma i prodotti di sicurezza sono spesso in grado di analizzare il contenuto dei file che attraversano la rete e bloccare o mettere in quarantena i file DLL. Questo articolo descrive un approccio per abilitare Blazor WebAssembly le app in questi ambienti, in cui viene creato un file di bundle multipart dalle DLL dell'app in modo che le DLL possano essere scaricate insieme ignorando le restrizioni di sicurezza.

Un'app ospitata Blazor WebAssembly può personalizzare i file pubblicati e la creazione di pacchetti di DLL dell'app usando le funzionalità seguenti:

  • Inizializzatori JavaScript che consentono di personalizzare il Blazor processo di avvio.
  • Estendibilità di MSBuild per trasformare l'elenco dei file pubblicati e definire Blazor le estensioni di pubblicazione. Blazor Le estensioni di pubblicazione sono file definiti durante il processo di pubblicazione che forniscono una rappresentazione alternativa per il set di file necessari per eseguire un'app pubblicata Blazor WebAssembly . In questo articolo viene creata un'estensione Blazor di pubblicazione che produce un bundle multipart con tutte le DLL dell'app compresse in un singolo file in modo che le DLL possano essere scaricate insieme.

L'approccio illustrato in questo articolo funge da punto di partenza per gli sviluppatori per definire le proprie strategie e processi di caricamento personalizzati.

Avviso

Qualsiasi approccio adottato per aggirare una restrizione di sicurezza deve essere considerato attentamente per le implicazioni di sicurezza. È consigliabile esaminare ulteriormente l'argomento con i professionisti della sicurezza di rete dell'organizzazione prima di adottare l'approccio in questo articolo. Le alternative da considerare includono:

  • Abilitare appliance di sicurezza e software di sicurezza per consentire ai client di rete di scaricare e usare i file esatti richiesti da un'app Blazor WebAssembly .
  • Passare dal Blazor WebAssembly modello di hosting al Blazor Server modello di hosting, che gestisce tutto il codice C# dell'app nel server e non richiede il download delle DLL ai client. Blazor Server offre anche il vantaggio di mantenere privato il codice C# senza richiedere l'uso delle app per le API Web per la privacy del codice C# con Blazor WebAssembly le app.

Pacchetto NuGet sperimentale e app di esempio

L'approccio descritto in questo articolo viene usato dal pacchetto sperimentaleMicrosoft.AspNetCore.Components.WebAssembly.MultipartBundle (NuGet.org) per le app destinate a .NET 6 o versioni successive. Il pacchetto contiene destinazioni MSBuild per personalizzare l'output Blazor di pubblicazione e un inizializzatore JavaScript per l'uso di un caricatore di risorse di avvio personalizzato, ognuna delle quali è descritta in dettaglio più avanti in questo articolo.

Codice sperimentale (include l'origine di riferimento del pacchetto NuGet e CustomPackagedApp l'app di esempio)

Avviso

Le funzionalità sperimentali e di anteprima vengono fornite allo scopo di raccogliere commenti e suggerimenti e non sono supportate per l'uso in produzione.

Più avanti in questo articolo, la sezione Personalizzare il Blazor WebAssembly processo di caricamento tramite un pacchetto NuGet con le relative tre sottosezioni fornisce spiegazioni dettagliate sulla configurazione e sul codice nel Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle pacchetto. Le spiegazioni dettagliate sono importanti per comprendere quando si crea una strategia personalizzata e un processo di caricamento personalizzato per Blazor WebAssembly le app. Per usare il pacchetto NuGet pubblicato, sperimentale e non supportato senza personalizzazione come dimostrazione locale, seguire questa procedura:

  1. Usare una soluzione ospitata Blazor WebAssemblyesistente o creare una nuova soluzione dal Blazor WebAssembly modello di progetto usando Visual Studio o passando l'opzione-ho|--hosted al dotnet new comando (dotnet new blazorwasm -ho). Per altre informazioni, vedere Strumenti per ASP.NET Core Blazor.

  2. Client Nel progetto aggiungere il pacchetto sperimentaleMicrosoft.AspNetCore.Components.WebAssembly.MultipartBundle.

    Nota

    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). Confermare le versioni corrette del pacchetto all'indirizzo NuGet.org.

  3. Server Nel progetto aggiungere un endpoint per gestire il file di bundle (app.bundle). Il codice di esempio è disponibile nella sezione Servire il bundle dall'app server host di questo articolo.

  4. Pubblicare l'app nella configurazione della versione.

Personalizzare il Blazor WebAssembly processo di caricamento tramite un pacchetto NuGet

Avviso

Le linee guida contenute in questa sezione con le relative tre sottosezioni riguardano la creazione di un pacchetto NuGet da zero per implementare la propria strategia e il processo di caricamento personalizzato. Il pacchetto sperimentaleMicrosoft.AspNetCore.Components.WebAssembly.MultipartBundle (NuGet.org) per .NET 6 e 7 si basa sulle indicazioni riportate in questa sezione. Quando si usa il pacchetto fornito in una dimostrazione locale dell'approccio di download del bundle multipart, non è necessario seguire le indicazioni riportate in questa sezione. Per indicazioni su come usare il pacchetto fornito, vedere la sezione Pacchetto NuGet sperimentale e app di esempio.

Blazorle risorse dell'app vengono compresse in un file bundle multipart e caricate dal browser tramite un inizializzatore JavaScript (JS) personalizzato. Per un'app che utilizza il pacchetto con l'inizializzatore JS , l'app richiede solo che il file di bundle venga servito quando richiesto. Tutti gli altri aspetti di questo approccio vengono gestiti in modo trasparente.

Sono necessarie quattro personalizzazioni per il caricamento di un'app pubblicata Blazor predefinita:

  • Attività MSBuild per trasformare i file di pubblicazione.
  • Un pacchetto NuGet con destinazioni MSBuild che si associa al Blazor processo di pubblicazione, trasforma l'output e definisce uno o più Blazor file di estensione di pubblicazione (in questo caso, un singolo bundle).
  • Inizializzatore JS per aggiornare il callback del Blazor WebAssembly caricatore di risorse in modo che carichi il bundle e fornisca all'app i singoli file.
  • Helper nell'app host Server per assicurarsi che il bundle venga servito ai client su richiesta.

Creare un'attività MSBuild per personalizzare l'elenco dei file pubblicati e definire nuove estensioni

Creare un'attività MSBuild come classe C# pubblica che può essere importata come parte di una compilazione MSBuild e che può interagire con la compilazione.

Per la classe C# sono necessari gli elementi seguenti:

Nota

Il pacchetto NuGet per gli esempi in questo articolo è denominato in base al pacchetto fornito da Microsoft, Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Per indicazioni sulla denominazione e la produzione di un pacchetto NuGet personalizzato, vedere gli articoli nuGet seguenti:

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Build.Framework" Version="{VERSION}" />
    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="{VERSION}" />
  </ItemGroup>

</Project>

Determinare le versioni più recenti dei pacchetti per i {VERSION} segnaposto in NuGet.org:

Per creare l'attività MSBuild, creare una classe C# pubblica che Microsoft.Build.Utilities.Task estende (non System.Threading.Tasks.Task) e dichiarare tre proprietà:

  • PublishBlazorBootStaticWebAsset: elenco di file da pubblicare per l'app Blazor .
  • BundlePath: percorso in cui viene scritto il bundle.
  • Extension: le nuove estensioni di pubblicazione da includere nella compilazione.

La classe di esempio BundleBlazorAssets seguente è un punto di partenza per un'ulteriore personalizzazione:

  • Execute Nel metodo viene creato il bundle dai tre tipi di file seguenti:
    • File JavaScript (dotnet.js)
    • File WASM (dotnet.wasm)
    • DLL dell'app (.dll)
  • Viene creato un multipart/form-data bundle. Ogni file viene aggiunto al bundle con le rispettive descrizioni tramite l'intestazione Content-Disposition e l'intestazione Content-Type.
  • Dopo aver creato il bundle, il bundle viene scritto in un file.
  • La compilazione è configurata per l'estensione. Il codice seguente crea un elemento di estensione e lo aggiunge alla Extension proprietà . Ogni elemento di estensione contiene tre parti di dati:
    • Percorso del file di estensione.
    • Percorso URL relativo alla radice dell'app Blazor WebAssembly .
    • Nome dell'estensione, che raggruppa i file prodotti da una determinata estensione.

Dopo aver raggiunto gli obiettivi precedenti, l'attività MSBuild viene creata per personalizzare l'output di Blazor pubblicazione. Blazor si occupa di raccogliere le estensioni e assicurarsi che le estensioni vengano copiate nel percorso corretto nella cartella di output di pubblicazione (ad esempio, bin\Release\net6.0\publish). Le stesse ottimizzazioni (ad esempio, la compressione) vengono applicate ai file Blazor JavaScript, WASM e DLL applicati ad altri file.

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks/BundleBlazorAssets.cs:

using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks
{
    public class BundleBlazorAssets : Task
    {
        [Required]
        public ITaskItem[]? PublishBlazorBootStaticWebAsset { get; set; }

        [Required]
        public string? BundlePath { get; set; }

        [Output]
        public ITaskItem[]? Extension { get; set; }

        public override bool Execute()
        {
            var bundle = new MultipartFormDataContent(
                "--0a7e8441d64b4bf89086b85e59523b7d");

            foreach (var asset in PublishBlazorBootStaticWebAsset)
            {
                var name = Path.GetFileName(asset.GetMetadata("RelativePath"));
                var fileContents = File.OpenRead(asset.ItemSpec);
                var content = new StreamContent(fileContents);
                var disposition = new ContentDispositionHeaderValue("form-data");
                disposition.Name = name;
                disposition.FileName = name;
                content.Headers.ContentDisposition = disposition;
                var contentType = Path.GetExtension(name) switch
                {
                    ".js" => "text/javascript",
                    ".wasm" => "application/wasm",
                    _ => "application/octet-stream"
                };
                content.Headers.ContentType = 
                    MediaTypeHeaderValue.Parse(contentType);
                bundle.Add(content);
            }

            using (var output = File.Open(BundlePath, FileMode.OpenOrCreate))
            {
                output.SetLength(0);
                bundle.CopyToAsync(output).ConfigureAwait(false).GetAwaiter()
                    .GetResult();
                output.Flush(true);
            }

            var bundleItem = new TaskItem(BundlePath);
            bundleItem.SetMetadata("RelativePath", "app.bundle");
            bundleItem.SetMetadata("ExtensionName", "multipart");

            Extension = new ITaskItem[] { bundleItem };

            return true;
        }
    }
}

Creare un pacchetto NuGet per trasformare automaticamente l'output di pubblicazione

Generare un pacchetto NuGet con destinazioni MSBuild incluse automaticamente quando si fa riferimento al pacchetto:

  • Creare un nuovo Razor progetto di libreria di classi (RCL).
  • Creare un file di destinazioni seguendo le convenzioni NuGet per importare automaticamente il pacchetto nei progetti che usano. Ad esempio, creare build\net6.0\{PACKAGE ID}.targets, dove {PACKAGE ID} è l'identificatore del pacchetto del pacchetto.
  • Raccogliere l'output dalla libreria di classi contenente l'attività MSBuild e verificare che l'output sia compresso nella posizione corretta.
  • Aggiungere il codice MSBuild necessario per connettersi alla Blazor pipeline e richiamare l'attività MSBuild per generare il bundle.

L'approccio descritto in questa sezione usa solo il pacchetto per distribuire destinazioni e contenuto, che è diverso dalla maggior parte dei pacchetti in cui il pacchetto include una DLL di libreria.

Avviso

Il pacchetto di esempio descritto in questa sezione illustra come personalizzare il Blazor processo di pubblicazione. Il pacchetto NuGet di esempio è destinato all'uso solo come dimostrazione locale. L'uso di questo pacchetto nell'ambiente di produzione non è supportato.

Nota

Il pacchetto NuGet per gli esempi in questo articolo è denominato in base al pacchetto fornito da Microsoft, Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Per indicazioni sulla denominazione e la produzione di un pacchetto NuGet personalizzato, vedere gli articoli nuGet seguenti:

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.csproj:

<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <NoWarn>NU5100</NoWarn>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <Description>
      Sample demonstration package showing how to customize the Blazor publish 
      process. Using this package in production is not supported!
    </Description>
    <IsPackable>true</IsPackable>
    <IsShipping>true</IsShipping>
    <IncludeBuildOutput>false</IncludeBuildOutput>
  </PropertyGroup>

  <ItemGroup>
    <None Update="build\**" 
          Pack="true" 
          PackagePath="%(Identity)" />
    <Content Include="_._" 
             Pack="true" 
             PackagePath="lib\net6.0\_._" />
  </ItemGroup>

  <Target Name="GetTasksOutputDlls" 
          BeforeTargets="CoreCompile">
    <MSBuild Projects="..\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.csproj" 
             Targets="Publish;PublishItemsOutputGroup" 
             Properties="Configuration=Release">
      <Output TaskParameter="TargetOutputs" 
              ItemName="_TasksProjectOutputs" />
    </MSBuild>
    <ItemGroup>
      <Content Include="@(_TasksProjectOutputs)" 
               Condition="'%(_TasksProjectOutputs.Extension)' == '.dll'" 
               Pack="true" 
               PackagePath="tasks\%(_TasksProjectOutputs.TargetPath)" 
               KeepMetadata="Pack;PackagePath" />
    </ItemGroup>
  </Target>

</Project>

Nota

La <NoWarn>NU5100</NoWarn> proprietà nell'esempio precedente elimina l'avviso sugli assembly inseriti nella tasks cartella . Per altre informazioni, vedere Avviso NuGet NU5100.

Aggiungere un .targets file per collegare l'attività MSBuild alla pipeline di compilazione. In questo file vengono raggiunti gli obiettivi seguenti:

  • Importare l'attività nel processo di compilazione. Si noti che il percorso della DLL è relativo alla posizione finale del file nel pacchetto.
  • La ComputeBlazorExtensionsDependsOn proprietà associa la destinazione personalizzata alla Blazor WebAssembly pipeline.
  • Acquisire la Extension proprietà nell'output dell'attività e aggiungerla a BlazorPublishExtension per indicare Blazor l'estensione. Il richiamo dell'attività nella destinazione produce il bundle. L'elenco dei file pubblicati viene fornito dalla Blazor WebAssembly pipeline nel PublishBlazorBootStaticWebAsset gruppo di elementi. Il percorso del bundle viene definito usando IntermediateOutputPath (in genere all'interno della obj cartella). In definitiva, il bundle viene copiato automaticamente nel percorso corretto nella cartella di output di pubblicazione , ad esempio bin\Release\net6.0\publish.

Quando si fa riferimento al pacchetto, viene generato un bundle dei file durante la Blazor pubblicazione.

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/build/net6.0/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.targets:

<Project>
  <UsingTask 
    TaskName="Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.BundleBlazorAssets" 
    AssemblyFile="$(MSBuildThisProjectFileDirectory)..\..\tasks\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.dll" />

  <PropertyGroup>
    <ComputeBlazorExtensionsDependsOn>
      $(ComputeBlazorExtensionsDependsOn);_BundleBlazorDlls
    </ComputeBlazorExtensionsDependsOn>
  </PropertyGroup>

  <Target Name="_BundleBlazorDlls">
    <BundleBlazorAssets
      PublishBlazorBootStaticWebAsset="@(PublishBlazorBootStaticWebAsset)"
      BundlePath="$(IntermediateOutputPath)bundle.multipart">
      <Output TaskParameter="Extension" 
              ItemName="BlazorPublishExtension"/>
    </BundleBlazorAssets>
  </Target>

</Project>

Bootstrap Blazor automatico dal bundle

Il pacchetto NuGet sfrutta gli inizializzatori JavaScript (JS) per avviare automaticamente un'app Blazor WebAssembly dal bundle invece di usare singoli file DLL. JS gli inizializzatori vengono usati per modificare il Blazorcaricatore di risorse di avvio e usare il bundle.

Per creare un JS inizializzatore, aggiungere un JS file con il nome {NAME}.lib.module.js alla wwwroot cartella del progetto del pacchetto, dove il {NAME} segnaposto è l'identificatore del pacchetto. Ad esempio, il file per il pacchetto Microsoft è denominato Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js. Le funzioni beforeWebAssemblyStart esportate e afterWebAssemblyStarted gestiscono il caricamento.

Inizializzatori JS :

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/wwwroot/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js:

const resources = new Map();

export async function beforeWebAssemblyStart(options, extensions) {
  if (!extensions || !extensions.multipart) {
    return;
  }

  try {
    const integrity = extensions.multipart['app.bundle'];
    const bundleResponse = 
      await fetch('app.bundle', { integrity: integrity, cache: 'no-cache' });
    const bundleFromData = await bundleResponse.formData();
    for (let value of bundleFromData.values()) {
      resources.set(value, URL.createObjectURL(value));
    }
    options.loadBootResource = function (type, name, defaultUri, integrity) {
      return resources.get(name) ?? null;
    }
  } catch (error) {
    console.log(error);
  }
}

export async function afterWebAssemblyStarted(blazor) {
  for (const [_, url] of resources) {
    URL.revokeObjectURL(url);
  }
}

Per creare un JS inizializzatore, aggiungere un JS file con il nome {NAME}.lib.module.js alla wwwroot cartella del progetto del pacchetto, dove il {NAME} segnaposto è l'identificatore del pacchetto. Ad esempio, il file per il pacchetto Microsoft è denominato Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js. Le funzioni beforeStart esportate e afterStarted gestiscono il caricamento.

Inizializzatori JS :

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/wwwroot/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js:

const resources = new Map();

export async function beforeStart(options, extensions) {
  if (!extensions || !extensions.multipart) {
    return;
  }

  try {
    const integrity = extensions.multipart['app.bundle'];
    const bundleResponse = 
      await fetch('app.bundle', { integrity: integrity, cache: 'no-cache' });
    const bundleFromData = await bundleResponse.formData();
    for (let value of bundleFromData.values()) {
      resources.set(value, URL.createObjectURL(value));
    }
    options.loadBootResource = function (type, name, defaultUri, integrity) {
      return resources.get(name) ?? null;
    }
  } catch (error) {
    console.log(error);
  }
}

export async function afterStarted(blazor) {
  for (const [_, url] of resources) {
    URL.revokeObjectURL(url);
  }
}

Gestire il bundle dall'app server host

A causa delle restrizioni di sicurezza, ASP.NET Core non gestisce il app.bundle file per impostazione predefinita. Un helper di elaborazione delle richieste è necessario per gestire il file quando viene richiesto dai client.

Nota

Poiché le stesse ottimizzazioni vengono applicate in modo trasparente alle estensioni di pubblicazione applicate ai file dell'app, i app.bundle.gz file di asset compressi e app.bundle.br vengono generati automaticamente alla pubblicazione.

Inserire il codice C# nel Program.cs progetto immediatamente prima della Server riga che imposta il file di fallback su index.html (app.MapFallbackToFile("index.html");) per rispondere a una richiesta per il file di bundle (ad esempio, app.bundle):

app.MapGet("app.bundle", (HttpContext context) =>
{
    string? contentEncoding = null;
    var contentType = 
        "multipart/form-data; boundary=\"--0a7e8441d64b4bf89086b85e59523b7d\"";
    var fileName = "app.bundle";

    var acceptEncodings = context.Request.Headers.AcceptEncoding;

    if (Microsoft.Net.Http.Headers.StringWithQualityHeaderValue
        .StringWithQualityHeaderValue
        .TryParseList(acceptEncodings, out var encodings))
    {
        if (encodings.Any(e => e.Value == "br"))
        {
            contentEncoding = "br";
            fileName += ".br";
        }
        else if (encodings.Any(e => e.Value == "gzip"))
        {
            contentEncoding = "gzip";
            fileName += ".gz";
        }
    }

    if (contentEncoding != null)
    {
        context.Response.Headers.ContentEncoding = contentEncoding;
    }

    return Results.File(
        app.Environment.WebRootFileProvider.GetFileInfo(fileName)
            .CreateReadStream(), contentType);
});

Il tipo di contenuto corrisponde al tipo definito in precedenza nell'attività di compilazione. L'endpoint verifica la presenza delle codifiche del contenuto accettate dal browser e serve il file ottimale, Brotli (.br) o Gzip (.gz).