Blazor .net 6 : tour d'horizon des nouveautés

La feuille de route de Microsoft prévoit désormais une sortie de version majeure de .NET tous les ans et 2021 ne fait pas exception.

En effet, .NET 6 est prévu pour novembre de cette année et il s’agira d’une version LTS (Long Term Support).

Cependant, un des paramètres essentiels à anticiper concerne la nécessité d’utiliser Visual Studio 2022 pour programmer avec .NET 6 puisque Daniel Roth a confirmé qu’il n’y aura pas de rétrocompatibilité prévue pour Visual Studio 2019.

Cette nouvelle version possède bien sûr son lot de nouveautés, celles dédiées à Blazor nous intéresseront plus particulièrement dans cet article qui ambitionne d’en faire un tour d’horizon.

Hot reload

Pour utiliser le Hot Relaod avec Blazor de manière efficace et transparente, il va falloir lancer le projet sans passer par le debugger de Visual Studio. Nous allons à la place utiliser une invite de commande et lancer le projet à l’aide de dotnet watch.

Cette commande va démarrer le navigateur et afficher le projet Blazor en cours. On peut suivre les étapes de démarrage dans l’invite de commande.

A partir de là, on peut modifier le code dans la solution.

Dès lors que le fichier modifié est sauvegardé, le changement est détecté et automatiquement appliqué, ce qui permet de visualiser presque en temps réel ses modifications (en effet cela prend environ 2 à 3 secondes).

Il devient possible d’ajouter, par exemple, un nouveau composant comme le counter sur la page d’accueil et de visualiser ce changement dans le navigateur. Dans ce cas, l’application va se reconstruire et afficher la nouvelle page.

L’utilisation de dotnet watch pour le hot reload permet de modifier à la fois les pages Razor comme le code C#. Nous pouvons donc changer une fonction et constater en temps réel le nouveau fonctionnement.

Les performances .NET 6 dans Blazor

Dans les applications communes en .NET on considère très généralement un seul runtime, le coreclr (parfois même sans se poser la question). Or, en utilisant Blazor, il devient intéressant de se pencher sur la question : en effet, Blazor WebAssembly (Blazor wasm) utilise un runtime différent, nommé mono. Il est pertinent d’indiquer qu’avec Blazor wasm, le runtime est compilé dans l’application. Il faut savoir que mono n’effectue pas que des opérations en Just In Time (JIT) mais qu’il interprète également le Intermediate Language. Cela apporte une certaine sécurité supplémentaire, puisqu’il n’interprète plus le code à la volée.

Alors, pourquoi parlons-nous de cela ? Et bien parce que ce sera le premier sujet à propos des performances de Blazor avec .NET 6.

L’interpréteur de mono a bénéficié d’une refonte, améliorant son « ability to inline » et notamment pour les méthodes qui sont marquées par le tag [MethodImpl(MethodImplOptions.AggressiveInlining)] qui est très utilisé dans les bibliothèques de bas niveau du runtime (pour rappel, le inlining consiste à ce que le compilateur remplace l’appel à une fonction par le corps de la fonction).

Pour mémoire, la gestion d’abandon des threads a disparu avec .NET 5 ; Blazor en bénéficie avec l’arrivée de .NET 6 puisque cette gestion est supprimée dans le traitement des blocs finally dans mono.

Une grosse avancée a été faite en ce qui concerne les Hardware intrinsics qui ont été introduits par .NET Core 3.0.

Mono supporte désormais LLVM pour la génération du code et le support de l’architecture ARM64 est implémenté grâce aux APIs ARM64 AdvSimd. Dans le même registre, les supports de SHA1, SHA256 et AES sont complétés.

On notera l’apparition des Vector64 et Vector 128 ce qui seront nécessairement utiles pour les futures applications Blazor wasm qui utiliseront AOT.
Oh ? AOT !? Et bien oui, sachez que les applications Blazor wasm pourront être entièrement compilées en AOT (ce qui éliminera l’utilisation du JIT).

Webassembly AOT

Blazor WebAssembly supporte maintenant la compilation « ahead-of-time » (AOT). Cela signifie que vous pouvez compiler votre code .NET directement en WebAssembly ce qui améliore grandement les performances d’exécution.

Actuellement, une application Blazor WebAssembly s’exécute en utilisant un interpréteur .NET IL implémenté dans le WebAssembly. Le code .NET étant interprété, cela signifie qu’il s’exécute plus lentement que lors d’une exécution .NET classique. La compilation .NET WebAssembly AOT corrige ces problèmes de performance en compilant directement le code .NET en WebAssembly.

Pour profiter de la compilation .NET WebAssembly AOT, il est nécessaire d’installer un outil de compilation additionnel disponible en option dans le SDK .NET. Pour l’installer, il suffit de taper dans une invite de commande :

dotnet workload install microsoft-net-sdk-blazorwebassembly-aot

Et dans le projet, il faut activer la compilation WebAssembly AOT en ajoutant la propriété :

<RunAOTCompilation>true</RunAOTCompilation>

La compilation WebAssembly AOT ne fonctionne que lorsque le projet est publié. Il faut donc publier le projet en mode release pour en profiter.
dotnet publish -c Release

Il faut noter malgré tout que l’utilisation de la compilation WebAssembly AOT a une conséquence sur le poids. L’application sera plus grosse et cela peut aller jusqu’à 2 fois le poids de l’application utilisant .NET IL.
Ainsi, l’utilisation de cette option dépendra aussi du type d’application déployée et sera surtout pertinente pour les applications faisant une utilisation intensive du CPU.

Error boundaries

Il s’agit d’un nouvel élément qui offre une façon pratique de gérer les exceptions au sein de la hiérarchie des composants. Pour l’utiliser, il suffit d’utiliser le nouveau composent ErrorBoundary pour envelopper le contenu pour lequel vous souhaitez avoir une personnalisation de l’exception.

<div class="content px-4">
        <ErrorBoundary>
            <MyComponent />
        </ErrorBoundary>
    </div>

Tant qu’aucune exception n’est levée, le composent affichera son contenu enfant. Mais dès qu’une exception arrive, il affiche une erreur spécifique. Par défaut, le composant affiche une div vide avec la classe CSS blazor-error-boundary.
Il est possible de personnaliser l’erreur en utilisant la propriété ErrorContent.

<ErrorBoundary>
    <ChildContent>
        <MyComponent />
    </ChildContent>
    <ErrorContent>
        <p class="my-error">Voici une erreur personnalisée.</p>
    </ErrorContent>
</ErrorBoundary>

Il s’agit d’un moyen efficace pour gérer et personnaliser l’affichage des erreurs au sein de Blazor.

Persistance de l'état pendant le pré-rendu

Une application Blazor peut être pré-rendue à partir du serveur afin d’accélérer le temps de chargement perçu lors de la première utilisation. Le HTML est ainsi affiché directement pendant que le reste de la configuration se fait en arrière-plan. Malheureusement, l’état utilisé pendant ce pré rendu est perdu et doit être recréé lorsque l’application est complètement chargée. Si un état est configuré de manière asynchrone, l’interface peut alors clignoter lorsque le pré-rendu est remplacé par l’affichage final.

Pour régler ce problème, une nouvelle balise <preserve-component-state /> a été ajoutée. Il faut ensuite ajouter le service ComponentApplicationState dans son composant pour préserver son état.

L’évènement ComponentApplicationState.OnPersisting est déclenché quand un état doit être conservé dans la page pré-rendue.
Voici un Example montrant comment le composant FetchData peut être persisté.
Dans le fichier _Host.cshtml :

<body>
    <component type="typeof(App)" render-mode="ServerPrerendered" />
    ...
    @* Persist the component state after all component invocations *@
    <persist-component-state />
</body>

Et dans le fichier FetchData.razor :

@page "/fetchdata"
@implements IDisposable
@inject ComponentApplicationState ApplicationState

...

@code {
    protected override async Task OnInitializedAsync()
    {
        ApplicationState.OnPersisting += PersistForecasts;
        if (!ApplicationState.TryRedeemPersistedState("fetchdata", out var data))
        {
            forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
        }
        else
        {
            var options = new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                PropertyNameCaseInsensitive = true,
            };
            forecasts = JsonSerializer.Deserialize<WeatherForecast[]>(data, options);
        }
    }

    private Task PersistForecasts()
    {
        ApplicationState.PersistAsJson("fetchdata", forecasts);
        return Task.CompletedTask;
    }

    void IDisposable.Dispose()
    {
        ApplicationState.OnPersisting -= PersistForecasts;
    }
}

Réduction de la taille de téléchargement

SignalR, MessagePack, et Blazor Server scripts sont significativement plus petit.

Par ailleurs, la taille d’une application Blazor WebAssembly a été considérablement réduite.

Paramètres requis

Il est possible de spécifier qu’un composant Blazor a des paramètres requis en utilisant l’attribut [EditorRequired].

Exemple :
[EditorRequired]
[Parameter]
public string Title { get; set; }

Si l’utilisateur ne précise pas le paramètre, il aura un avertissement et le composant sera souligné. Attention toutefois, il s’agit d’un attribut apportant une aide lors de la conception et il ne garantit pas à l’exécution que la valeur du paramètre sera non nulle.

Support du SVG

Il est maintenant possible d’utiliser la syntaxe Razor, dont les composants Blazor, dans un élément SVG foreignObject.

<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
    <rect x="0" y="0" rx="10" ry="10" width="200" height="200" stroke="black" fill="none" />
    <foreignObject x="20" y="20" width="160" height="160">
        <p>@message</p>
    </foreignObject>
</svg>

@code {
    string message = "Wow, it's so nice that this text wraps like it's HTML...because that's what it is!";
}

Modifiez le contenu <head> HTML
Blazor prend désormais en charge la modification de l’élément <head> depuis un composant. Il devient possible de modifier le titre et d’ajouter des éléments <meta>.

Pour modifier le titre d’une page, il faut utiliser le composant PageTitle. Le composant HeadContent permet quant à lui d’interagir avec les autres éléments de <head>.

<PageTitle>@title</PageTitle>

<HeadContent>
    <meta name="description" content="@description">
</HeadContent>

@code {
    private string description = "Description set by component";
    private string title = "Title set by component";
}

Pour activer cette fonctionnalité, il est nécessaire d’ajouter le composant HeadOutlet.
builder.RootComponents.Add<HeadOutlet>("head::after");

Blazor streaming interop from javascript to .NET

Blazor peut maintenant diffuser des données directement depuis Javascript vers .NET. Les flux sont appelés avec la nouvelle interface IJSStreamReference.

Javascript :
function jsToDotNetStreamReturnValue() {
    return new Uint8Array(10000000);
}

C# :
  var dataReference = await JSRuntime.InvokeAsync<IJSStreamReference>("jsToDotNetStreamReturnValue");
  using var dataReferenceStream = await dataReference.OpenReadStreamAsync(maxAllowedSize: 10_000_000);

  // Write JS Stream to disk
  var outputPath = Path.Combine(Path.GetTempPath(), "file.txt");
  using var outputFileStream = File.OpenWrite(outputPath);
  await dataReferenceStream.CopyToAsync(outputFileStream);

Téléversement de fichiers plus gros plus rapidement

En utilisant la nouvelle façon de diffuser des données entre JavaScript et .NET, il est maintenant possible de téléverser des fichiers d’une taille supérieure à 2GB avec le composant InputFile. Le transfert est également plus rapide grâce à l’utilisation d’un flux de byte[] directement sans utiliser un encodage Base64.

Arguments d'événement personnalisés

En plus du support d’évènements personnalisés dans Blazor, il est aussi possible de passer des données au gestionnaire d’évènements pour les évènements personnalisés.

Par exemple, il est possible de faire un évènement lorsque l’on colle le contenu du presse-papier avec le texte collé par l’utilisateur.
Pour faire cela, il faut déclarer un évènement avec un nom personnalisé et une classe .NET qui contiendra les arguments pour cet évènement.

[EventHandler("oncustompaste", typeof(CustomPasteEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
public static class EventHandlers
{
    // This static class doesn't need to contain any members. It's just a place where we can put
    // [EventHandler] attributes to configure event types on the Razor compiler. This affects the
    // compiler output as well as code completions in the editor.
}

public class CustomPasteEventArgs : EventArgs
{
    // Data for these properties will be supplied by custom JavaScript logic
    public DateTime EventTimestamp { get; set; }
    public string PastedData { get; set; }
}

Une fois cela fait, IntelliSense proposera un nouvel évènement appelé @oncustompaste.
@page "/"

<p>Try pasting into the following text box:</p>
<input @oncustompaste="HandleCustomPaste" />
<p>@message</p>

@code {
    string message;

    void HandleCustomPaste(CustomPasteEventArgs eventArgs)
    {
        message = $"At {eventArgs.EventTimestamp.ToShortTimeString()}, you pasted: {eventArgs.PastedData}";
    }
}

Cependant, cela ne suffit pas pour déclencher l’évènement, il faut également ajouter un peu de JavaScript dans le fichier index.html ou  _Host.cshtml.

<!-- You should add this directly after the <script> tag for blazor.server.js or blazor.webassembly.js -->
<script>
    Blazor.registerCustomEventType('custompaste', {
        browserEventName: 'paste',
        createEventArgs: event => {
            // This example only deals with pasting text, but you could use arbitrary JavaScript APIs
            // to deal with users pasting other types of data, such as images
            return {
                eventTimestamp: new Date(),
                pastedData: event.clipboardData.getData('text')
            };
        }
    });
</script>

Cela dira au navigateur que chaque fois qu’un évènement “coller” a lieu, il doit aussi déclencher un évènement custompaste et passer les arguments définis.

Génération automatique de composant Blazor

Il est possible de générer un composant Blazor en créant le tag dans une page puis faire un clic droit et générer le composant.

Il est également possible de créer automatiquement la classe partielle à partir de la balise code du fichier Razor.

Blazor peut désormais déduire des paramètres de type générique à partir de composants parents

Avec Blazor, il est possible d’utiliser des composants imbriqués. Nous avons alors un composant parent qui possède des composants enfants. Lors de l’utilisation d’unparamètre générique, il était jusqu’à présent obligatoire de préciser pour chaque composant ce paramètre générique. Par exemple lors de son utilisation dans une grid, il fallait spécifier Grid<TItem> puis spécifier à nouveau dans les colonnes Column<TItem>. Ce qui donne en termes de code :

<BlazorDataGrid Items="@forecasts" ShowTotalResult="true" TheadClass="thead-dark" Translation="@translate"
                ShowPageSelector="true" PageSelector="@PageSelector" Editable="false" RowSelector="true">
    <BlazorDataGridColumn>
        <DataGridColumn Items="@forecasts" ColumnName="Date" Filter="true" Format="dd/MM/yyyy"><strong>Date</strong></DataGridColumn>
        <DataGridColumn Items="@forecasts" ColumnName="TemperatureC" DisplayColumnName="TemperatureC" Filter="true"></DataGridColumn>
        <DataGridColumn Items="@forecasts" ColumnName="TemperatureF" DisplayColumnName="TemperatureF" DropdownFilter="true" ReadOnly="true"></DataGridColumn>
        <DataGridColumn Items="@forecasts" ColumnName="Summary" DisplayColumnName="Summary" Filter="true"></DataGridColumn>
        <DataGridColumn Items="@forecasts" ColumnName="" DisplayColumnName="Test" Filter="false"></DataGridColumn>
        <DataGridColumn Items="@forecasts" ColumnName="" DisplayColumnName="Test clic" Filter="false"></DataGridColumn>
    </BlazorDataGridColumn>
    <GridRow>
        <Cell Items="@forecasts" Content="{{Date}}" />
        <Cell Items="@forecasts" Content="<strong>{{TemperatureC}}</strong>" ValidationPattern="^[-]?\d+$" LabelError="@translate["labelError"]" />
        <Cell Items="@forecasts" Content="{{TemperatureF}}" />
        <Cell Items="@forecasts" Content="<a href='https://nullrefexception.com/'>{{Summary}}</a>" />
        <Cell Items="@forecasts" Context="cellcontext">
            <a href='https://nullrefexception.com/'>@cellcontext.Summary</a>
        </Cell>
        <Cell Items="@forecasts" Context="cellcontext">
            <button style="cursor:pointer" class="btn btn-primary" @onclick="@(() => TestGetContext(cellcontext))">@cellcontext.TemperatureC</button>
        </Cell>
    </GridRow>
</BlazorDataGrid>

Nous conviendrons que c’est un peu fastidieux de devoir spécifier pour chaque colonne les paramètres génériques de la grid. Il est maintenant possible de transmettre le paramètre générique aux composants enfants en cascadant le paramètre. Il faut que les paramètres parents et enfants portent le même nom.

@attribute [CascadingTypeParameter(nameof(TItem))]

Nous pouvons dès lors simplifier l’écriture de la grid en ne spécifiant plus le paramètre générique au niveau des enfants.

<BlazorDataGrid Items="@forecasts" ShowTotalResult="true" TheadClass="thead-dark" Translation="@translate"
                ShowPageSelector="true" PageSelector="@PageSelector" Editable="false" RowSelector="true">
    <BlazorDataGridColumn>
        <DataGridColumn ColumnName="Date" Filter="true" Format="dd/MM/yyyy"><strong>Date</strong></DataGridColumn>
        <DataGridColumn ColumnName="TemperatureC" DisplayColumnName="TemperatureC" Filter="true"></DataGridColumn>
        <DataGridColumn ColumnName="TemperatureF" DisplayColumnName="TemperatureF" DropdownFilter="true" ReadOnly="true"></DataGridColumn>
        <DataGridColumn ColumnName="Summary" DisplayColumnName="Summary" Filter="true"></DataGridColumn>
        <DataGridColumn ColumnName="" DisplayColumnName="Test" Filter="false"></DataGridColumn>
        <DataGridColumn ColumnName="" DisplayColumnName="Test clic" Filter="false"></DataGridColumn>
    </BlazorDataGridColumn>
    <GridRow>
        <Cell Content="{{Date}}" />
        <Cell Content="<strong>{{TemperatureC}}</strong>" ValidationPattern="^[-]?\d+$" LabelError="@translate["labelError"]" />
        <Cell Content="{{TemperatureF}}" />
        <Cell Content="<a href='https://nullrefexception.com/'>{{Summary}}</a>" />
        <Cell Context="cellcontext">
            <a href='https://nullrefexception.com/'>@cellcontext.Summary</a>
        </Cell>
        <Cell Context="cellcontext">
            <button style="cursor:pointer" class="btn btn-primary" @onclick="@(() => TestGetContext(cellcontext))">@cellcontext.TemperatureC</button>
        </Cell>
    </GridRow>
</BlazorDataGrid>

La version 5 de ma Datagrid pour Blazor (actuellement en béta) utilise d’ailleurs ce nouveau mécanisme pour simplifier son écriture.

Contraintes de type générique

Dans une page Razor, il est possible de définir un paramètre générique avec la directive @typeparam. Avec .NET 6, il est maintenant possible d’ajouter une contrainte en utilisant la syntaxe C# classique : @typeparam TEntity where TEntity : IEntity

Rendu de composant dynamique

.NET6 permet de créer des composants Blazor dynamique de la même manière que l’on utilise une variable de type dynamique en .NET.

 <DynamicComponent Type="@someType" />

Les paramètres peuvent être transmis au composant en utilisant un tableau.

<DynamicComponent Type="@someType" Parameters="@myDictionaryOfParameters" />

@code {
    Type someType = ...
    IDictionary<string, object> myDictionaryOfParameters = ...
}

Récupération de paramètres depuis une query string.

Depuis la preview 7 de .NET6, il est possible de spécifier que certains paramètres sont à récupérer dans la query string. Il faut ajouter l’attribut [SupplyParameterFromQuery] en plus de l’attribut classique [Parameter].


Exemple :

[Parameter]
[SupplyParameterFromQuery]
public int? Page { get; set; }

Avec l’url suivante https://localhost:5001/page=3, la valeur 3 sera alors affectée à Page.
Il est possible d’utiliser cet attribut avec les types String, bool, DateTime, decimal, double, float, Guid, int, long et leur variant nullable à l’exception de string.

Les tableaux de ces différents types sont également autorisés.

Support de la sélection multiple dans l’élément select.

Il s’agit d’une fonctionnalité très appréciée et qui manquait lors des précédentes release. Lors de l’utilisation d’un composant <select>, il est maintenant possible de spécifier l’attribut multiple. Cela permet de choisir plusieurs éléments dans la liste. Dans ces conditions, l’évènement onchange va alors fournir un tableau avec les éléments sélectionnés via ChangeEventArgs. En conséquence, il est possible d’utiliser un tableau comme valeur pour la liaison lorsque l’attribut multiple est spécifié.

Et lors de l’utilisation de <InputSelect>, l’attribut multiple est automatiquement déduit lorsque la valeur liée est un tableau.

MAUI & Blazor

Pour commencer, il faut définir ce qu’est une hybrid app.
Il s’agit de faire une utilisation du code Blazor pour des applications natives.

  • Réutilisation du développement web (code et compétences)
  • Accès à toutes les fonctionnalités natives de l’appareil.
  • Mélange de d’interface native et de web
  • Réduit le temps de développement des applications.

Le code .Net est exécuté directement dans l’application native et exécute des composants Blazor localement. Le rendu du DOM est envoyé dans un contrôleur de vue web. Tous les événements qui se déclenchent dans cette vue sont envoyés dans le code .NET puis les modifications sont retournées dans le contrôleur qui affiche la vue. Tout s’exécute dans l’application.

.NET Multi-platform App UI (MAUI) est une évolution de xamarin form mais étendue à plus de plateformes (comme le desktop par exemple).
Ces principales caractéristiques sont :

  • Cross platform.
  • Utilisation des interfaces natives de chaque plateforme
  • Un seul projet système, une seule base de code
  • Déploiement sur plusieurs appareil, mobile et desktop
  • Disponible avec .NET 6

C’est le principe des Blazor hybrid apps qui sont utilisées au sein de .NET MAUI.
Le contrôleur BlazorWebView est intégré dans MAUI. On retrouve donc :

  • Réutilisation des composants UI entre native et web
  • Mix & match web and native UI
  • Accès direct aux fonctionnalités native des appareils
  • Applications Cross-platform mobile & desktop. (A la sortie de .NET 6, le focus sera mis sur le desktop)

On peut donc utiliser des composants Blazor tels quels au sein d’une application .NET MAUI. Tous les composants déjà développés dans le cadre d’autres projets peuvent donc facilement être réutilisés.

.NET MAUI possède le composant natif BlazorWebView qui permet de faire le rendu des composants Blazor. Cela permet un vrai gain de productivité.
BlazorWebView est également disponible pour Winforms & WPF, ce qui permettra d’utiliser aussi les composant Blazor avec ces technologies.

Mobile Blazor Bindings

Vous connaissez certainement Blazor pour la création d’applications web, avec toute la puissance qui lui incombe. Mais Blazor ne s’arrête pas là ; en effet, il existe un projet expérimental dans le dépôt dotnet, nommé MobileBlazorBindings. Mais alors, késako ?

Et bien c’est tout simplement donner la possibilité aux développeurs de créer des applications lourdes ou mobiles en utilisant C# et .NET. Ça doit vous parler ça, C# et .NET pour des applications lourdes ou mobiles ? Et bien c’est normal puisque c’est ce que fait Xamarin, et c’est là toute la subtilité : Mobile Blazor Bindings se présente comme une abstraction de haut niveau de Xamarin. Il utilise la syntaxe des pages Razor pour définir les composants, autant sur le front, que sur le back-end.

On y retrouve ainsi tous les composants de Xamarin.Forms, que l’on peut utiliser à sa guise, le rendu étant lui toujours laissé à la charge de Xamarin.
En tant que projet expérimental, il n’est pas encore disponible en standalone de manière native et nécessite Edge Canary et le SDK .NET Core 3.1. De même, il faut installer le template grâce à une commande dotnet.

La solution se présente sous cette forme, on y retrouve bien les différents projets selon la plateforme visée.

Cependant on constate la présence d’un projet Blazor. Voyons ce que contient la page principale :

C’est bien un fichier Razor classique qui contient un counter !
Connaissant l’engouement et la montée en puissance de Blazor, ce projet reste donc à surveiller de près !

Micro FrontEnd avec Blazor

Tout d’abord, définissons ensemble ce que nous appellerons le micro-frontend. Le micro-frontend est une architecture dans laquelle plusieurs composants coopèrent tout en étant hautement indépendants. Il facile de faire le parallèle avec l’architecture micro services. Chacun des composants a sa propre logique, ils sont tous isolés les uns des autres et peuvent être développés par des équipes différentes.

Pour que cette architecture fonctionne, il faut envisager une application principale, nommée la “App Shell” qui appellera tous les composants indépendants. Ainsi, pour récupérer les composants faisant partie de l’application, il faut utiliser ce que l’on nomme un compositeur. En .NET il est par exemple possible d’utiliser la bibliothèque Piral.

Dans notre exemple, nous utiliserons des composants Razor sous forme de Razor Class Library (RCL) qui représenteront les composantes du micro-frontend, l’app shell sera un projet Blazor wasm, et le compositeur sera fait grâce aux dépendances internes de C#. Puis nous mettrons en place le lazy loading, afin de rendre l’expérience plus réaliste.

Il est important de préciser que l’implémentation de la démo qui vous est faite servira d’introduction et ne représente pas une solution efficace en production.

Voici donc l’architecture de notre solution :

Chacune des features possède un ou plusieurs composants, qui seront utilisés dans l’app shell. Rien d’extravagant, seulement les composants basiques qui sont créés avec Blazor.

Intéressons-nous donc à l’import de ces composants dans l’app shell. Comme nous l’avons précisé, nous utilisons les références internes de la solution comme compositeur pour cet exemple. Vous devez donc ajouter les références de vos RCL dans l’app shell : ajoutez-les à la main dans le csproj, ou bien via la GUI.

Une fois que vos références sont ajoutées, vous pourrez bénéficier des composants en les important à votre guise :
AppShell / _Imports.razor

Mais souvenez-vous, nous allons utiliser le lazy loading pour cela, il faut commencer par indiquer quelle ressource sera à exécuter en lazy loading, pour cela, ajoutez la référence dans votre csproj

Puis importez le package de gestion des assemblies, ajouter donc

dans votre _Imports.razor.
Le Lazy Loading servira également de compositeur, en vous donnant la possibilité de ne charger vos composants qu’au moment où ils sont nécessaires, mais surtout il vous abroge de l’import de vos composants dans votre _Imports.razor !

Illustrons donc la solution dans le App.razor :

On ajoute deux nouvelles lignes à notre router:

  • AdditionalAssemblies qui contiendra les assemblies des composants que l’on chargera à la volée.
  • OnNavigateAsync qui exécutera une fonction lorsque l’on souhaite naviguer dans l’application.

Dans notre cas, on cherchera à vérifier si l’on veut naviguer vers nos composants et si tel est le cas, on ajoutera l’assembly correspondante.
Et bien voilà ! Vous avez une application qui respecte les principes du micro front-end, et tout ça avec Blazor ! On constate bien que chacune des Features est strictement isolée, et qu’elle peut être modifiée indépendamment de l’application complète et sans impacter le reste.

Il y a bien entendu des manières bien plus efficaces et pragmatiques d’implémenter cette architecture, et vous en trouverez pléthore sur internet. Sachez toutefois que le micro front-end ne se contente pas d’utiliser une seule technologie, vous pouvez tout à fait mixer les frameworks et utiliser chaque brique dans votre app shell, cependant cela requiert notamment un CI plus complexe.

Tester Blazor avec bUnit

Contrairement à ce que le célèbre adage nous dit, non, tester, ce n’est pas douter. Et plus encore, la certitude n’évite pas le danger.
Alors, dans Blazor, on fait comment ?

Et bien Microsoft ne propose pas de framework de test officiel pour Blazor, il nous faut donc nous tourner vers des projets open source, et dans le cas de Blazor, on trouve assez vite bUnit qui est placé sous licence MIT.

BUnit fonctionne avec les frameworks de tests classiques, tels que nUnit ou bien xUnit (ne les citons pas tous, nous n’en finirions pas) et c’est pour cette raison qu’il faut envisager bUnit comme un fournisseur de contexte aux tests.

Allons un peu plus loin dans notre analyse : en utilisant bUnit pour le rendu des composants, vous pourrez aisément passer des paramètres, injecter un service et avoir accès simple au DOM du composant. Le rendu d’un composant se fait à partir de la classe TestContext de bUnit, il en résulte un objet IRenderedComponent qu’il sera facile de manipuler.

Pour écrire vos tests, vous aurez le choix entre le faire dans un fichier classique “.cs” ou bien dans un fichier Razor “.razor” ce qui facilite l’écriture du code HTML à tester ; cependant l’éditeur Razor de Visual Studio 2019 n’intègre pas encore toutes les fonctionnalités disponibles dans un fichier C# classique, et quelques bugs de formatage sont à prévoir.

Tout d’abord, il vous faut :

  • créer un projet de tests, avec le framework qui vous conviendra le mieux (pour ma part j’utilise xUnit)
  • installer le package NuGet bUnit. Attention, il faut penser à changer le SdK dans son nouveau projet, pointez vers Microsoft.NET.Sdk.Razor.
  • pensez à ajouter la dépendance vers le RCL que vous souhaitez tester.

Vous devez obtenir un fichier csproj proche de cela :

A noter qu’il est possible d’installer le projet de test via le template bUnit, mais cela fonctionne à l’heure actuelle exclusivement avec xUnit.
Prenons l’approche d’un test écrit dans un fichier C#, nous testons un composant qui n’intègre pas de logique :

Le fait d’étendre la classe TestContext de bUnit nous donne accès au contexte de test de bUnit.

Maintenant, voyons si l’on ajoute un peu de logique à un composant. Pour cela nous utiliserons le composant Counter qui est automatiquement généré lors de la création d’un projet Blazor.

On constate que l’on a cliqué une fois sur le bouton et qu’en effet le composant affiche bien la bonne valeur.

Bien entendu, vous pourrez aller bien plus loin dans vos tests, comme précisé plus haut dans l’article grâce à notre introduction sur le sujet.
Vous pourrez par exemple donner une valeur précise à un paramètre, injecter des services, mocker vos données ou encore gérer les retours asynchrones.

En bref, cette bibliothèque est très complète, et son utilisation est sans appel bénéfique à nos développements !

JSInterop

Une fonctionnalité de .NET (arrivée avec .NET Core 3.1) très intéressante dans l’utilisation de Blazor est le JSInterop. JSinterop est le mécanisme permettant d’interagir entre C# et JavaScript. En effet, il offre la possibilité d’appeler des fonctions C# dans le JavaScript et vice-versa.
Attention toutefois à ne pas commettre deux erreurs importantes :

  • Utiliser la balise <script> dans un composant Razor (.razor), car cette balise ne peut pas être mise à jour dynamiquement par Blazor.
  • Modifier un composant dont le rendu a été fait par Blazor avec du JavaScript. Blazor garde en mémoire un arbre du DOM de ce qu’il a rendu, et si ce dernier est modifié sans passer par Blazor, cela peut poser des problèmes à l’usage.

En ce qui concerne l’isolation JavaScript, Blazor intègre le standard des modules JavaScript. On retrouve donc nos fichiers JavaScript dans la partie wwwroot du projet Blazor.

L’intérêt de cette fonctionnalité au sein de Blazor se retrouve essentiellement dans l’utilisation de bibliothèques javascript, ou bien de frameworks CSS tels que jQuery, Bootstrap ou encore Materialize. Mais on peut en retrouver une certaine utilité pour l’utilisation plus poussée de framework JavaScript, comme React.