Loupe

Initialisation asynchrone en ASP.NET Core, revu et corrigé

L'initialisation d'une application ASP.NET Core n'est pas toujours évidente à réaliser proprement. Il y a des endroits clairement définis pour enregistrer les services (la méthode Startup.ConfigureServices) et pour construire le pipeline de middleware (la méthode Startup.Configure), mais pas pour effectuer d'autres tâches d'initialisation (par exemple précharger des données, initialiser un service externe, etc.).

Utiliser un middleware : pas une si bonne idée

Il y a deux mois, j'ai publié un article à propos de l'initialisation asynchrone en ASP.NET Core à l'aide d'un middleware dédié. A l'époque j'étais assez content de cette solution, mais j'ai finalement pris conscience que ce n'était en fait pas une très bonne approche. En effet, utiliser un middleware pour cela a un inconvénient majeur : même si l'initialisation n'est faite qu'une seule fois, le middleware reste dans le pipeline et est appelé à chaque requête, ce qui a un coût non négligeable. Clairement, on ne veut pas que l'initialisation impacte les performances pendant toute la durée de vie de l'application, donc il ne faut pas le faire dans le pipeline de traitement des requêtes.

Une meilleure approche : la méthode Main

Il y a une partie de toutes les applications ASP.NET Core qu'on a tendance à négliger, car elle est habituellement générée par un template et on a rarement besoin de la modifier : la classe Program. Elle ressemble en général à ceci :

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

En gros, elle construit un web host et l'exécute immédiatement. Cependant, rien ne nous empêche de faire quelque chose avec l'hôte avant de l'exécuter. En fait, c'est même un assez bon endroit pour effectuer l'initialisation :

    public static void Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();
        /* Effectuer l'initialisation ici */
        host.Run();
    }

Et en bonus, l'hôte expose un IServiceProvider (host.Services), configuré avec les services enregistrés dans Startup.ConfigureServices, ce qui nous donne accès à tout ce dont on pourrait avoir besoin pour initialiser l'application.

Mais attendez une minute… n'ai-je pas parlé d'initialisation asynchrone dans le titre ? Eh bien, depuis C# 7.1, il est possible de rendre asynchrone la méthode Main. Pour activer ça, il suffit de définir la propriété LangVersion à la valeur 7.1 ou plus dans votre projet (ou latest pour toujours bénéficier des dernières fonctionnalités du langage). Vous pouvez ensuite changer le type de retour de la méthode en Task ou Task<int>, et la rendre asynchrone avec le mot-clé async.

Emballez, c'est pesé

On pourrait simplement résoudre les services dont on a besoin pour l'initialisation et les appeler directement depuis la méthode Main, mais ce ne serait pas très propre ; il vaudrait mieux avoir une classe "initialiseur" qui reçoit par injection les dépendances dont elle a besoin. Cette classe serait enregistrée dans Startup.Configuration et appelée depuis la méthode Main.

Après avoir utilisé cette approche dans deux projets différents, j'ai créé une petite bibliothèque pour faciliter les choses : AspNetCore.AsyncInitialization. Elle s'utilise comme ceci :

  1. Créer une classe qui implémente l'interface IAsyncInitializer :

    public class MyAppInitializer : IAsyncInitializer
    {
        public MyAppInitializer(IFoo foo, IBar bar)
        {
            ...
        }
    
        public async Task InitializeAsync()
        {
            /* Effectuer l'initialisation ici */
        }
    }
    

    Cette classe peut prendre comme dépendance n'importe quel service enregistré dans la méthode Startup.ConfigureServices.

  2. Enregistrer cette classe dans Startup.ConfigureServices, en utilisant la méthode d'extension AddAsyncInitializer :

    services.AddAsyncInitializer<MyAppInitializer>();
    

    Il est possible d'enregistrer plusieurs initialiseurs si besoin, ce qui permet de séparer différentes tâches d'initialisation.

  3. Appeler la méthode d'extension InitAsync sur le web host dans la méthode Main :

    public static async Task Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();
        await host.InitAsync();
        host.Run();
    }
    

    Cela exécutera tous les initialiseurs enregistrés.

Et voilà, un moyen simple et propre d'initialiser votre application !

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus