Disposition du déploiement pour les applications Blazor WebAssembly hébergées sur ASP.NET Core

Cet article explique comment activer les déploiements de Blazor WebAssembly hébergés dans les environnements qui bloquent le téléchargement et l’exécution de fichiers DLL (bibliothèque de liens dynamiques).

Remarque

Ce guide traite les environnements qui empêchent les clients de télécharger et d’exécuter des bibliothèque de liens dynamiques (DLL). Dans .NET 8 ou version ultérieure, Blazor utilise le format de fichier Webcil pour résoudre ce problème. Pour plus d’informations, consultez Héberger et déployer ASP.NET Core Blazor WebAssembly. Le regroupement multipartite à l’aide du package NuGet expérimental décrit dans cet article n’est pas pris en charge pour les applications Blazor dans .NET 8 ou ultérieur. Pour plus d’informations, consultez Améliorer le package Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle pour définir un format de regroupement personnalisé (dotnet/aspnetcore #36978). Vous pouvez utiliser les instructions de cet article pour créer votre propre package NuGet de regroupement multipartite pour .NET 8 ou ultérieur.

Les applications Blazor WebAssembly nécessitent des DLL (bibliothèques de liens dynamiques) pour fonctionner, mais certains environnements empêchent les clients de télécharger et d’exécuter les DLL. Dans un sous-ensemble de ces environnements, le changement de l’extension de nom de fichier des fichiers DLL (.dll) suffit pour contourner les restrictions de sécurité. Toutefois, les produits de sécurité peuvent souvent analyser le contenu des fichiers parcourant le réseau, et bloquer ou mettre en quarantaine les fichiers DLL. Cet article décrit une approche permettant d’activer les applications Blazor WebAssembly dans ces environnements, où un fichier de bundle en plusieurs parties est créé à partir des DLL de l’application pour que ces DLL puissent être téléchargées ensemble en contournant les restrictions de sécurité.

Une application Blazor WebAssembly hébergée peut personnaliser ses fichiers publiés et son package de DLL d’application à l’aide des fonctionnalités suivantes :

  • Initialiseurs JavaScript, qui permettent de personnaliser le processus de démarrage de Blazor.
  • Extensibilité MSBuild pour transformer la liste des fichiers publiés et définir des extensions de publication Blazor. Les extensions de publication Blazor sont des fichiers définis durant le processus de publication. Elles fournissent une autre représentation de l’ensemble des fichiers nécessaires à l’exécution d’une application Blazor WebAssembly publiée. Dans cet article, une extension de publication Blazor est créée pour produire un bundle en plusieurs parties, où toutes les DLL de l’application sont packagées dans un seul fichier afin d’être téléchargées ensemble.

L’approche présentée dans cet article sert de point de départ aux développeurs pour leur permettre de concevoir leurs propres stratégies et processus de chargement personnalisés.

Avertissement

Toute approche adoptée pour contourner une restriction de sécurité doit être soigneusement examinée en fonction de ses implications sur la sécurité. Nous vous recommandons d’approfondir le sujet avec les professionnels de la sécurité réseau de votre organisation avant d’adopter l’approche décrite dans cet article. Les alternatives à prendre en compte sont les suivantes :

  • Activez les appliances de sécurité et les logiciels de sécurité pour permettre aux clients réseau de télécharger et d’utiliser les fichiers exacts nécessaires à une application Blazor WebAssembly.
  • Passez du modèle d’hébergement Blazor WebAssembly au modèle d’hébergement Blazor Server, qui conserve l’ensemble du code C# de l’application sur le serveur et ne nécessite pas le téléchargement de DLL sur les clients. Blazor Server offre également l’avantage de garder le code C# privé sans nécessiter l’utilisation d’applications API web pour la confidentialité du code C# avec les applications Blazor WebAssembly.

Package NuGet expérimental et exemple d’application

L’approche décrite dans cet article est utilisée par le package expérimentalMicrosoft.AspNetCore.Components.WebAssembly.MultipartBundle (NuGet.org) pour les applications ciblant .NET 6 ou ultérieur. Le package contient des cibles MSBuild pour personnaliser la sortie de publication de Blazor ainsi qu’un initialiseur JavaScript pour utiliser un chargeur de ressources de démarrage personnalisé, chacun d’eux étant décrit en détail plus loin dans cet article.

Code expérimental (inclut la source de référence du package NuGet et l’exemple d’application CustomPackagedApp)

Avertissement

Les fonctionnalités expérimentales et les fonctionnalités en préversion sont fournies pour collecter des commentaires. Elles ne sont pas prises en charge en production.

Plus loin dans cet article, la section Personnaliser le processus de chargement de Blazor WebAssembly via un package NuGet avec ses trois sous-sections fournit des explications détaillées sur la configuration et le code dans le package Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Il est important de comprendre les explications détaillées quand vous créez votre propre stratégie et votre propre processus de chargement personnalisé pour les applications Blazor WebAssembly. Pour utiliser le package NuGet publié, expérimental et non pris en charge sans personnalisation en tant que démonstration locale, suivez les étapes ci-dessous :

  1. Utilisez une solutionBlazor WebAssembly hébergée existante, ou créez une solution à partir du modèle de projet Blazor WebAssembly en utilisant Visual Studio ou en passant l’option -ho|--hosted à la commandedotnet new (dotnet new blazorwasm -ho). Pour plus d’informations, consultez Outils pour ASP.NET Core Blazor.

  2. Dans le projet Client, ajoutez le package Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle expérimental.

    Remarque

    Pour obtenir des conseils sur l’ajout de packages à des applications .NET, consultez les articles figurant sous Installer et gérer des packages dans Flux de travail de la consommation des packages (documentation NuGet). Vérifiez les versions du package sur NuGet.org.

  3. Dans le projet Server, ajoutez un point de terminaison pour la mise à disposition du fichier bundle (app.bundle). Vous trouverez un exemple de code dans la section Mettre à disposition le bundle à partir de l’application de serveur hôte au sein de cet article.

  4. Publiez l’application dans la configuration Release.

Personnaliser le processus de chargement de Blazor WebAssembly via un package NuGet

Avertissement

Les conseils d’aide de cette section et de ses trois sous-sections concernent la création d’un package NuGet à partir de zéro pour implémenter votre propre stratégie et votre processus de chargement personnalisé. Le package expérimentalMicrosoft.AspNetCore.Components.WebAssembly.MultipartBundle (NuGet.org) pour .NET 6 et 7 est basé sur les instructions de cette section. Quand vous utilisez le package fourni dans une démonstration locale de l’approche basée sur le téléchargement de bundle en plusieurs parties, vous n’avez pas besoin de suivre les conseils d’aide de cette section. Pour obtenir des conseils d’aide sur l’utilisation du package fourni, consultez la section Package NuGet expérimental et exemple d’application.

Les ressources d’application Blazor sont compressées dans un fichier bundle en plusieurs parties et chargées par le navigateur via un initialiseur JavaScript (JS) personnalisé. Pour une application qui consomme le package avec l’initialiseur JS, celle-ci nécessite uniquement la mise à disposition du fichier bundle sur demande. Tous les autres aspects de cette approche sont gérés de manière transparente.

Quatre personnalisations sont nécessaires pour le mode de chargement d’une application Blazor publiée par défaut :

  • Une tâche MSBuild pour transformer les fichiers de publication.
  • Un package NuGet avec des cibles MSBuild, qui se connecte au processus de publication Blazor, transforme la sortie et définit un ou plusieurs fichiers d’extension de publication Blazor (dans ce cas, un seul bundle).
  • Un initialiseur JS pour mettre à jour le rappel du chargeur de ressources Blazor WebAssembly afin qu’il charge le bundle et fournisse les fichiers individuels à l’application.
  • Un outil d’assistance sur l’application Server hôte pour vérifier que le bundle est mis à disposition des clients sur demande.

Créer une tâche MSBuild pour personnaliser la liste des fichiers publiés et définir de nouvelles extensions

Créez une tâche MSBuild en tant que classe C# publique pouvant être importée dans le cadre d’une compilation MSBuild et pouvant interagir avec la build.

Les éléments suivants sont nécessaires pour la classe C# :

Remarque

Le package NuGet des exemples de cet article est nommé d’après le package fourni par Microsoft, Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Pour obtenir des conseils d’aide sur le nommage et la production de votre propre package NuGet, consultez les articles NuGet suivants :

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>

Déterminez quelles sont les dernières versions de package pour les espaces réservés {VERSION} sur NuGet.org :

Pour créer la tâche MSBuild, créez une classe C# publique qui étend Microsoft.Build.Utilities.Task (et non System.Threading.Tasks.Task), puis déclarez trois propriétés :

  • PublishBlazorBootStaticWebAsset : liste des fichiers à publier pour l’application Blazor.
  • BundlePath : chemin où le bundle est écrit.
  • Extension : nouvelles extensions de publication à inclure dans la build.

L’exemple de classe BundleBlazorAssets suivant est le point de départ d’une personnalisation supplémentaire :

  • Dans la méthode Execute, le bundle est créé à partir des trois types de fichier suivants :
    • Fichiers JavaScript (dotnet.js)
    • Fichiers WASM (dotnet.wasm)
    • DLL d’application (.dll)
  • Un bundle multipart/form-data est créé. Chaque fichier est ajouté au bundle avec ses descriptions respectives via l’en-tête Content-Disposition et l’en-tête Content-Type.
  • Une fois le bundle créé, il est écrit dans un fichier.
  • La build est configurée pour l’extension. Le code suivant crée un élément d’extension et l’ajoute à la propriété Extension. Chaque élément d’extension contient trois sortes de données :
    • Chemin du fichier d’extension.
    • Chemin d’URL relatif à la racine de l’application Blazor WebAssembly.
    • Nom de l’extension, qui regroupe les fichiers produits par une extension donnée.

Une fois les objectifs atteints, la tâche MSBuild est créée pour permettre la personnalisation de la sortie de publication Blazor. Blazor se charge de collecter les extensions et de vérifier qu’elles sont copiées à l’emplacement approprié dans le dossier de sortie de publication (par exemple bin\Release\net6.0\publish). Les fichiers JavaScript, WASM et DLL bénéficient des mêmes optimisations (par exemple la compression) que celles que Blazor applique aux autres fichiers.

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

Créer un package NuGet pour transformer automatiquement la sortie de publication

Générez un package NuGet avec des cibles MSBuild qui sont automatiquement incluses quand le package est référencé :

  • Créez un projet de bibliothèque de classes (RCL) Razor.
  • Créez un fichier de cibles en suivant les conventions NuGet pour importer automatiquement le package dans les projets qui le consomment. Par exemple, créez build\net6.0\{PACKAGE ID}.targets, où {PACKAGE ID} est l’identificateur de package du package.
  • Collectez la sortie de la bibliothèque de classes contenant la tâche MSBuild, puis vérifiez qu’elle est compressée au bon emplacement.
  • Ajoutez le code MSBuild nécessaire à l’attachement au pipeline Blazor, puis appelez la tâche MSBuild pour générer le bundle.

L’approche décrite dans cette section utilise uniquement le package pour remettre des cibles et du contenu, ce qui diffère de la plupart des packages où le package comprend une DLL de bibliothèque.

Avertissement

L’exemple de package décrit dans cette section montre comment personnaliser le processus de publication Blazor. L’exemple de package NuGet est à utiliser à des fins de démonstration locale uniquement. L’utilisation de ce package en production n’est pas prise en charge.

Remarque

Le package NuGet des exemples de cet article est nommé d’après le package fourni par Microsoft, Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Pour obtenir des conseils d’aide sur le nommage et la production de votre propre package NuGet, consultez les articles NuGet suivants :

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>

Remarque

La propriété <NoWarn>NU5100</NoWarn> de l’exemple précédent supprime l’avertissement relatif aux assemblys placés dans le dossier tasks. Pour plus d’informations, consultez l’Avertissement NuGet NU5100.

Ajoutez un fichier .targets pour connecter la tâche MSBuild au pipeline de build. Dans ce fichier, les objectifs suivants sont atteints :

  • Importer la tâche dans le processus de génération. Notez que le chemin de la DLL est relatif à l’emplacement final du fichier dans le package.
  • La propriété ComputeBlazorExtensionsDependsOn attache la cible personnalisée au pipeline Blazor WebAssembly.
  • Capturer la propriété Extension dans la sortie de la tâche, puis l’ajouter à BlazorPublishExtension pour indiquer à Blazor l’existence de l’extension. L’appel de la tâche dans la cible produit le bundle. La liste des fichiers publiés est fournie par le pipeline Blazor WebAssembly dans le groupe d’éléments PublishBlazorBootStaticWebAsset. Le chemin du bundle est défini à l’aide de IntermediateOutputPath (généralement dans le dossier obj). Pour finir, le bundle est copié automatiquement à l’emplacement approprié dans le dossier de sortie de publication (par exemple bin\Release\net6.0\publish).

Quand le package est référencé, il génère un bundle des fichiers Blazor durant la publication.

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>

Démarrer automatiquement Blazor à partir du bundle

Le package NuGet tire parti des initialiseurs JavaScript (JS) pour démarrer automatiquement une application Blazor WebAssembly à partir du bundle, au lieu d’utiliser des fichiers DLL individuels. Les initialiseurs JS permettent de changer le chargeur de ressources de démarrageBlazoret d’utiliser le bundle.

Pour créer un initialiseur JS, ajoutez un fichier JS portant le nom {NAME}.lib.module.js au dossier wwwroot du projet de package, où l’espace réservé {NAME} correspond à l’identificateur de package. Par exemple, le fichier du package Microsoft se nomme Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js. Les fonctions exportées beforeWebAssemblyStart et afterWebAssemblyStarted gèrent le chargement.

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

Pour créer un initialiseur JS, ajoutez un fichier JS portant le nom {NAME}.lib.module.js au dossier wwwroot du projet de package, où l’espace réservé {NAME} correspond à l’identificateur de package. Par exemple, le fichier du package Microsoft se nomme Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js. Les fonctions exportées beforeStart et afterStarted gèrent le chargement.

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

Mettre à disposition le bundle à partir de l’application de serveur hôte

Pour des raisons de sécurité, ASP.NET Core ne met pas à disposition le fichier app.bundle par défaut. Un outil d’assistance aux demandes est nécessaire pour mettre à disposition le fichier quand il est demandé par les clients.

Remarque

Dans la mesure où les mêmes optimisations sont appliquées de manière transparente aux extensions de publication et aux fichiers de l’application, les fichiers de ressources compressés app.bundle.gz et app.bundle.br sont produits automatiquement au moment de la publication.

Placez le code C# dans le Program.cs du projet Server immédiatement avant la ligne qui définit le fichier de secours en tant que index.html (app.MapFallbackToFile("index.html");) pour répondre à une demande de fichier bundle (par exemple 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);
});

Le type de contenu correspond au type défini plus tôt dans la tâche de build. Le point de terminaison vérifie les codages de contenu acceptés par le navigateur et met à disposition le fichier optimal, Brotli (.br) ou Gzip (.gz).