Charger des assemblys en mode différé dans Blazor WebAssembly ASP.NET Core

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

Les performances de démarrage de l’application Blazor WebAssembly peuvent être améliorées en chargeant les assemblys d’application créés par le développeur uniquement s’ils sont requis. Cela s’appelle le chargement différé.

Les premières sections de cet article portent sur la configuration des applications. Pour une démonstration pratique, consultez la section Exemple complet à la fin de cet article.

Cet article s’applique uniquement aux applications Blazor WebAssembly. Le chargement différé d’assembly ne bénéficie pas aux applications côté serveur, car les applications rendues par le serveur ne téléchargent pas d’assemblys sur le client.

Le chargement différé ne doit pas être utilisé pour les principaux assemblys de CLR, qui peuvent être supprimés lors de la publication et indisponibles sur le client lorsque l’application se charge.

Espace réservé d’extension de fichier ({FILE EXTENSION}) pour les fichiers d’assembly

Les fichiers d’assembly utilisent le format d’empaquetage Webcil pour les assemblies .NET avec une extension de fichier .wasm.

Tout au long de l’article, l’espace réservé {FILE EXTENSION} représente « wasm ».

Les fichiers d’assembly sont basés sur des bibliothèques de liens dynamiques (Dynamic-Link Libraries/DLL) avec une extension de fichier .dll.

Tout au long de l’article, l’espace réservé {FILE EXTENSION} représente « dll ».

Configuration du fichier projet

Marquez les assemblys pour le chargement différé dans le fichier projet (.csproj) de l’application en utilisant l’élément BlazorWebAssemblyLazyLoad. Utilisez le nom de l’assembly avec l’extension de fichier. Le framework Blazor empêche le chargement de l’assembly au lancement de l’application.

<ItemGroup>
  <BlazorWebAssemblyLazyLoad Include="{ASSEMBLY NAME}.{FILE EXTENSION}" />
</ItemGroup>

L’espace réservé {ASSEMBLY NAME} est le nom de l’assembly et l’espace réservé {FILE EXTENSION} est l’extension de fichier. L’extension de fichier est obligatoire.

Incluez un élément BlazorWebAssemblyLazyLoad pour chaque assembly. Si un assembly possède des dépendances, incluez une entrée BlazorWebAssemblyLazyLoad pour chaque dépendance.

Configuration du composant Router

L’infrastructure Blazor enregistre automatiquement un service singleton pour le chargement différé des assemblys dans les applications Blazor WebAssembly coté client, LazyAssemblyLoader. La méthode LazyAssemblyLoader.LoadAssembliesAsync :

  • Uses l’interopérabilité JS pour récupérer (fetch) les assemblys via un appel réseau.
  • Charge les assemblys dans le runtime qui s’exécutent sur WebAssembly dans le navigateur.

Remarque

L’aide concernant les solutionsBlazor WebAssemblyhébergées se trouve dans la section Charger des assemblys en mode différé dans une solution Blazor WebAssembly hébergée.

Le composant Router de Blazor désigne les assemblys dans lesquels Blazor recherche des composants routables. Il est également responsable du rendu du composant pour la route empruntée par l’utilisateur. La méthode OnNavigateAsync du composant Router est utilisée conjointement avec le chargement différé pour charger les assemblys appropriés pour les points de terminaison demandés par un utilisateur.

La logique est implémentée dans OnNavigateAsync pour déterminer quels assemblys charger avec LazyAssemblyLoader. Les options de structuration de la logique sont les suivantes :

  • Vérifications conditionnelles dans la méthode OnNavigateAsync.
  • Table de recherche qui mappe les routes aux noms d’assemblys, qui est soit injectée dans le composant soit implémentée dans le bloc @code.

Dans l’exemple suivant :

  • L’espace de noms pour Microsoft.AspNetCore.Components.WebAssembly.Services est spécifié.
  • Le service LazyAssemblyLoader est injecté (AssemblyLoader).
  • L’espace réservé {PATH} est le chemin où la liste d’assembly doit se charger. L’exemple utilise une vérification conditionnelle qui contrôle qu’un seul chemin charge un ensemble unique d’assemblys.
  • L’espace réservé {LIST OF ASSEMBLIES} est la liste séparée par des virgules des chaînes de nom de fichier d’assembly, avec leurs extensions de fichier (par exemple, "Assembly1.{FILE EXTENSION}", "Assembly2.{FILE EXTENSION}").

App.razor:

@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger

<Router AppAssembly="typeof(App).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger

<Router AppAssembly="typeof(Program).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}

Remarque

L’exemple précédent ne présente pas le contenu de la balise Razor (...) du composant Router. Pour une démonstration avec le code complet, consultez la section Exemple complet de cet article.

Remarque

Depuis le lancement d’ASP.NET Core 5.0.1 et pour les éventuelles versions 5.x supplémentaires, le composant Router comprend le paramètre PreferExactMatches, qui est défini sur @true. Pour plus d’informations, consultez Migrer de ASP.NET Core 3.1 vers 5.0.

Assemblys qui incluent des composants routables

Quand la liste d’assemblys inclut des composants routables, la liste d’assemblys d’un chemin donné est transmise à la collection AdditionalAssemblies du composant Router.

Dans l’exemple suivant :

  • List<Assembly> dans lazyLoadedAssemblies transmet la liste d’assemblys à AdditionalAssemblies. Le framework recherche des routes dans les assemblys et met à jour la collection de routes s’il en trouve de nouvelles. Pour accéder au type Assembly, l’espace de noms pour System.Reflection est inclus en haut du fichier App.razor.
  • L’espace réservé {PATH} est le chemin où la liste d’assembly doit se charger. L’exemple utilise une vérification conditionnelle qui contrôle qu’un seul chemin charge un ensemble unique d’assemblys.
  • L’espace réservé {LIST OF ASSEMBLIES} est la liste séparée par des virgules des chaînes de nom de fichier d’assembly, avec leurs extensions de fichier (par exemple, "Assembly1.{FILE EXTENSION}", "Assembly2.{FILE EXTENSION}").

App.razor:

@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(App).Assembly" 
    AdditionalAssemblies="lazyLoadedAssemblies" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new();

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
                   lazyLoadedAssemblies.AddRange(assemblies);
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(Program).Assembly" 
    AdditionalAssemblies="lazyLoadedAssemblies" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new List<Assembly>();

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
                   lazyLoadedAssemblies.AddRange(assemblies);
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}

Remarque

L’exemple précédent ne présente pas le contenu de la balise Razor (...) du composant Router. Pour une démonstration avec le code complet, consultez la section Exemple complet de cet article.

Remarque

Depuis le lancement d’ASP.NET Core 5.0.1 et pour les éventuelles versions 5.x supplémentaires, le composant Router comprend le paramètre PreferExactMatches, qui est défini sur @true. Pour plus d’informations, consultez Migrer de ASP.NET Core 3.1 vers 5.0.

Pour plus d’informations, consultez Routage et navigation ASP.NET Core Blazor.

Interaction utilisateur avec du contenu <Navigating>

Pendant le chargement d’assemblys, ce qui peut prendre plusieurs secondes, le composant Router peut indiquer à l’utilisateur qu’une transition de page se produit avec la propriété Navigating du routeur.

Pour plus d’informations, consultez Routage et navigation ASP.NET Core Blazor.

Gérer les annulations dans OnNavigateAsync

L’objet NavigationContext passé au rappel de OnNavigateAsync contient un CancellationToken qui est défini quand un nouvel événement de navigation se produit. Le rappel OnNavigateAsync doit se lever une exception quand le jeton d’annulation est défini pour éviter de continuer à exécuter le rappel OnNavigateAsync dans une navigation obsolète.

Pour plus d’informations, consultez Routage et navigation ASP.NET Core Blazor.

Événements OnNavigateAsync et fichiers d’assembly renommés

Le chargeur de ressources se fie aux noms d’assembly définis dans le fichier blazor.boot.json. Si des assemblys sont renommés, les noms d’assembly utilisés dans un rappel OnNavigateAsync et les noms d’assembly présents dans le fichier blazor.boot.json ne sont pas synchronisés.

Pour rectifier cela :

  • Vérifiez si l’application s’exécute dans l’environnement Production au moment de déterminer les noms d’assembly à utiliser.
  • Stockez les noms d’assembly renommés dans un fichier distinct et lisez dans ce fichier pour déterminer le nom d’assembly à utiliser avec le service LazyAssemblyLoader et le rappel OnNavigateAsync.

Charger les assemblys en mode différé dans une solution Blazor WebAssembly hébergée

L’implémentation du chargement différé du framework prend en charge le chargement différé avec prérendu dans une solutionBlazor WebAssembly hébergée. Pendant le prérendu, tous les assemblys, y compris ceux marqués pour le chargement différé, sont supposés être chargés. Inscrivez manuellement le service LazyAssemblyLoader dans le projet Server.

Dans la partie supérieure du fichier Program.cs du projet Server, ajoutez l’espace de noms pour Microsoft.AspNetCore.Components.WebAssembly.Services :

using Microsoft.AspNetCore.Components.WebAssembly.Services;

Dans le fichier Program.cs du projet Server, inscrivez le service :

builder.Services.AddScoped<LazyAssemblyLoader>();

Dans la partie supérieure du fichier Startup.cs du projet Server, ajoutez l’espace de noms pour Microsoft.AspNetCore.Components.WebAssembly.Services :

using Microsoft.AspNetCore.Components.WebAssembly.Services;

Dans le fichier Startup.ConfigureServices (Startup.cs) du projet Server, inscrivez le service :

services.AddScoped<LazyAssemblyLoader>();

Exemple complet

La démonstration de cette section effectue les opérations suivantes :

  • Elle crée un assembly de contrôles de robot (GrantImaharaRobotControls.{FILE EXTENSION}) en tant que bibliothèque de classes Razor (RCL) qui inclut un composant Robot (Robot.razor avec un modèle de route /robot).
  • Elle charge l’assembly de la bibliothèque RCL en mode différé pour assurer le rendu de son composant Robot quand l’URL /robot est demandée par l’utilisateur.

Créer une application autonome Blazor WebAssembly pour illustrer le chargement différé de l’assembly d’une bibliothèque de classes Razor. Nommez le projet LazyLoadTest.

Ajouter un projet de bibliothèque de classes ASP.NET Core à la solution :

  • Visual Studio : faites un clic droit sur le fichier solution dans l’Explorateur de solutions et sélectionnez Ajouter>Nouveau projet. Dans la boîte de dialogue des nouveaux types de projet, sélectionnez Razor Bibliothèque de classes. Nommez le projet GrantImaharaRobotControls. Ne cochez pas la case Prendre en charge les pages et les vues.
  • Visual Studio Code/Interface CLI .NET : Exécutez dotnet new razorclasslib -o GrantImaharaRobotControls depuis une invite de commandes. L’option -o|--output crée un dossier et nomme le projet GrantImaharaRobotControls.

L’exemple de composant présenté plus loin dans cette section utilise un formulaire Blazor. Dans le projet RCL, ajoutez le package Microsoft.AspNetCore.Components.Forms au projet.

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.

Créez une classe HandGesture dans la RCL avec une méthode ThumbUp qui est censée faire lever le pouce à un robot. La méthode accepte un argument pour l’axe, Left ou Right, en tant que enum. La méthode retourne true en cas de réussite.

HandGesture.cs:

using Microsoft.Extensions.Logging;

namespace GrantImaharaRobotControls;

public static class HandGesture
{
    public static bool ThumbUp(Axis axis, ILogger logger)
    {
        logger.LogInformation("Thumb up gesture. Axis: {Axis}", axis);

        // Code to make robot perform gesture

        return true;
    }
}

public enum Axis { Left, Right }
using Microsoft.Extensions.Logging;

namespace GrantImaharaRobotControls
{
    public static class HandGesture
    {
        public static bool ThumbUp(Axis axis, ILogger logger)
        {
            logger.LogInformation("Thumb up gesture. Axis: {Axis}", axis);

            // Code to make robot perform gesture

            return true;
        }
    }

    public enum Axis { Left, Right }
}

Ajoutez le composant suivant à la racine du projet RCL. Le composant permet à l’utilisateur d’envoyer une demande de lever de pouce gauche ou droit.

Robot.razor:

@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm FormName="RobotForm" Model="robotModel" OnValidSubmit="HandleValidSubmit">
    <InputRadioGroup @bind-Value="robotModel.AxisSelection">
        @foreach (var entry in Enum.GetValues<Axis>())
        {
            <InputRadio Value="entry" />
            <text>&nbsp;</text>@entry<br>
        }
    </InputRadioGroup>

    <button type="submit">Submit</button>
</EditForm>

<p>
    @message
</p>

@code {
    private RobotModel robotModel = new() { AxisSelection = Axis.Left };
    private string? message;

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

        message = $"ThumbUp returned {result} at {DateTime.Now}.";
    }

    public class RobotModel
    {
        public Axis AxisSelection { get; set; }
    }
}
@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm Model="robotModel" OnValidSubmit="HandleValidSubmit">
    <InputRadioGroup @bind-Value="robotModel.AxisSelection">
        @foreach (var entry in Enum.GetValues<Axis>())
        {
            <InputRadio Value="entry" />
            <text>&nbsp;</text>@entry<br>
        }
    </InputRadioGroup>

    <button type="submit">Submit</button>
</EditForm>

<p>
    @message
</p>

@code {
    private RobotModel robotModel = new() { AxisSelection = Axis.Left };
    private string? message;

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

        message = $"ThumbUp returned {result} at {DateTime.Now}.";
    }

    public class RobotModel
    {
        public Axis AxisSelection { get; set; }
    }
}
@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm Model="robotModel" OnValidSubmit="HandleValidSubmit">
    <InputRadioGroup @bind-Value="robotModel.AxisSelection">
        @foreach (var entry in (Axis[])Enum
            .GetValues(typeof(Axis)))
        {
            <InputRadio Value="entry" />
            <text>&nbsp;</text>@entry<br>
        }
    </InputRadioGroup>

    <button type="submit">Submit</button>
</EditForm>

<p>
    @message
</p>

@code {
    private RobotModel robotModel = new RobotModel() { AxisSelection = Axis.Left };
    private string message;

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

        message = $"ThumbUp returned {result} at {DateTime.Now}.";
    }

    public class RobotModel
    {
        public Axis AxisSelection { get; set; }
    }
}

Dans le projet LazyLoadTest, créez une référence de projet pour la RCL GrantImaharaRobotControls :

  • Visual Studio : faites un clic droit sur le projet LazyLoadTest, puis sélectionnez Ajouter>Référence de projet pour ajouter une référence de projet pour la RCL GrantImaharaRobotControls.
  • Visual Studio Code/Interface CLI .NET : Exécutez dotnet add reference {PATH} dans un interpréteur de commandes depuis le dossier du projet. L’espace réservé {PATH} est le chemin du projet RCL.

Spécifiez l’assembly de la RCL pour le chargement différé dans le fichier projet (.csproj) de l’application LazyLoadTest :

<ItemGroup>
    <BlazorWebAssemblyLazyLoad Include="GrantImaharaRobotControls.{FILE EXTENSION}" />
</ItemGroup>

Le composant suivant Router illustre le chargement de l’assembly GrantImaharaRobotControls.{FILE EXTENSION} quand l’utilisateur accède à /robot. Remplacez le composant par défaut App de l’application par le composant App suivant.

Pendant les transitions de page, un message stylise est présenté à l’utilisateur avec l’élément <Navigating>. Pour plus d’informations, consultez la section Interaction utilisateur avec le contenu <Navigating>.

L’assembly est affecté à AdditionalAssemblies, ce qui amène le routeur à rechercher des composants routables dans l’assembly, où il trouve le composant Robot. La route du composant Robot est ajoutée à la collection de routes de l’application. Pour plus d’informations, consultez l’article Routage et navigation Blazor ASP.NET Core et la section Assemblys qui incluent des composants routables de cet article.

App.razor:

@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(App).Assembly"
        AdditionalAssemblies="lazyLoadedAssemblies" 
        OnNavigateAsync="OnNavigateAsync">
    <Navigating>
        <div style="padding:20px;background-color:blue;color:white">
            <p>Loading the requested page&hellip;</p>
        </div>
    </Navigating>
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new();

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            if (args.Path == "robot")
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    new[] { "GrantImaharaRobotControls.{FILE EXTENSION}" });
                lazyLoadedAssemblies.AddRange(assemblies);
            }
        }
        catch (Exception ex)
        {
            Logger.LogError("Error: {Message}", ex.Message);
        }
    }
}
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(Program).Assembly"
        AdditionalAssemblies="lazyLoadedAssemblies" 
        OnNavigateAsync="OnNavigateAsync">
    <Navigating>
        <div style="padding:20px;background-color:blue;color:white">
            <p>Loading the requested page&hellip;</p>
        </div>
    </Navigating>
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new List<Assembly>();

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            if (args.Path == "robot")
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    new[] { "GrantImaharaRobotControls.{FILE EXTENSION}" });
                lazyLoadedAssemblies.AddRange(assemblies);
            }
        }
        catch (Exception ex)
        {
            Logger.LogError("Error: {Message}", ex.Message);
        }
    }
}

Générez et exécutez l'application.

Si le composant Robot de la RCL est demandé au niveau /robot, l’assembly GrantImaharaRobotControls.{FILE EXTENSION} est chargé et le composant Robot est rendu. Vous pouvez inspecter le chargement de l’assembly dans l’onglet Réseau des outils pour développeurs du navigateur.

Résoudre les problèmes

  • Si un rendu inattendu se produit, tel que le rendu d’un composant d’une navigation précédente, vérifiez que le code lève une exception si le jeton d’annulation est défini.
  • Si les assemblys configurés pour le chargement différé se chargent de manière inattendue au démarrage de l’application, vérifiez que l’assembly est marqué pour le chargement différé dans le fichier projet.

Remarque

Le chargement de types à partir d’un assembly chargé en mode différé est connu pour générer un problème. Pour plus d’informations, consultez Blazor WebAssembly lazy loading assemblies not working when using @ref attribute in the component (dotnet/aspnetcore #29342).

Ressources supplémentaires