Les middlewares dans une application ASP.NET Core

Les middlewares correspondent à des portions de code qui peuvent être exécutées lorsqu’une requête HTTP est reçue par une application ASP.NET Core. Ces portions de code sont exécutées successivement. Lorsqu’un middleware écrit une réponse correspondant à la requête, les middlewares suivants ne sont plus exécutés. Le terme middleware était déjà utilisé avec Owin et ASP.NET MVC (utilisant le framework .NET et spécifique aux plateformes Windows). Ils correspondent au même concept en ASP.NET Core (utilisant .NET Core et multi-plateforme). Le terme middleware est utilisé car il s’agit de portions de code placées entre la partie recevant les requêtes et le code métier se trouvant, par exemple, dans les controllers.

Ainsi lorsqu’une requête HTTP parvient à l’application web, les portions de code correspondant aux middlewares vont être exécutées successivement jusqu’à ce qu’un des middlewares écrive la réponse. L’appel successif des différents middlewares s’appelle un pipeline de middlewares. Les middlewares sont ordonnés dans le pipeline et ils sont exécutés dans le même ordre.

Le grand intérêt des middlewares est qu’ils offrent une grande flexiblité puisqu’ils sont tous capables de répondre à une requête ou d’effectuer un traitement spécifique sur la requête comme par exemple:

  • Effectuer des traitements d’authentification,
  • Logguer des informations concernant la requête et/ou la réponse correspondante,
  • Gérer les exceptions éventuelles,
  • Etc…

Avec ASP.NET Core, il est possible d’évoquer l’exécution des middlewares en utilisant différentes méthodes:

  • Appeler une portion de code sous forme d’un delegate ou
  • Appeler du code se trouvant dans une classe spécifique.

Dans un 1er temps, on expliquera les différentes méthodes pour définir un middleware en utilisant des delegate. Dans un 2e temps, on explicitera la méthode pour configurer un middleware se trouvant dans une classe particulière. Enfin, on indiquera quelques middlewares usuels.

Comme pour les articles précédents concernant ASP.NET Core, le but de cet article est de complémenter la documentation officielle (cf. ASP.NET Core Middleware) en passant en revue tous les éléments d’implémentation concernant les middlewares.

Pour illustrer les différents éléments de configuration, on peut se servir d’un exemple simple d’une API ASP.NET Core comportant quelques controller de façon à effectuer des requêtes HTTP:

Middlewares sous forme de “delegate”

Les middlewares de ce type se configurent dans la fonction StartUp.Configure().

Prérequis: installation de lognet

En préembule, on effectue l’installation de log4net car les exemples dans cet article l’utilise. On peut l’installer en effectuant les étapes suivantes:

  1. Exécutant la ligne suivante:
    user@debian:~/% dotnet add webapi_example/WebApi.csproj package log4net
  2. On ajoute un fichier de configuration nommé log4net.config avec le contenu suivant:
    <log4net> 
        <appender name="Console" type="log4net.Appender.ConsoleAppender"> 
            <layout type="log4net.Layout.PatternLayout"> 
                <!-- Pattern to output the caller's file name and line number --> 
                <conversionPattern value="%date %5level [%thread] - %message%newline" /> 
            </layout> 
        </appender> 
         
        <appender name="RollingFile" type="log4net.Appender.RollingFileAppender"> 
            <file value="Logs/webapi.log" /> 
            <appendToFile value="true" /> 
            <maximumFileSize value="100KB" /> 
            <maxSizeRollBackups value="2" /> 
    
            <layout type="log4net.Layout.PatternLayout"> 
                <conversionPattern value="%date %level %thread %logger - %message%newline" /> 
            </layout> 
        </appender> 
         
        <root> 
            <level value="DEBUG" /> 
            <appender-ref ref="Console" /> 
            <appender-ref ref="RollingFile" /> 
        </root> 
    </log4net>
    

    Cette configuration permettra de généer des fichiers de log dans le répertoire Logs/.

  3. On prends en compte la configuration en ajoutant les lignes suivantes dans StartUp.Configure():
    var configFile = Path.Combine(env.ContentRootPath, "log4net.config"); 
    var repository = LogManager.GetRepository(Assembly.GetEntryAssembly()); 
    XmlConfigurator.Configure(repository, new FileInfo(configFile));
    

Deux méthodes permettent de rajouter des middlewares sous forme de delegate:

  • IApplicationBuilder.Use()
  • IApplicationBuilder.Map()

IApplicationBuilder.Use()

Cette méthode permet d’enregistrer un middleware dans le pipeline en passant par un delegate. La spécificité de IApplicationBuilder.Use() est d’écrire du code avant et après avoir invoquer le middleware suivant dans le pipeline.

On l’utilise dans la méthode Startup.Configure():

public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
{ 
    // ...
    app.Use((ntext, next) => { 
        // Code du middleware avant d'invoquer le middleware suivant

        next.Invoke(); // Permet d'appeler le middleware suivant 

        // Code exécuté après avoir invoqué le middleware suivant
        return Task.CompletedTask; 
    }); 

    // ... 
}

Par exemple pour logguer un message avant et après exécution du middleware suivant, on peut écrire:

this.logger = LogManager.GetLogger(typeof(Startup)); 
app.Use((context, next) => { 
    logger.Info("Invoking next middleware..."); 

    next.Invoke(); // Appelle le middleware suivant 

    logger.Info("Invoked."); 

    return Task.CompletedTask; 
});
Le middleware MVC n’appelle pas les middlewares suivants dans le pipeline

Si on configure un middleware après app.UseMvc(), il ne sera jamais appelé:

public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
{ 
    // ... 

    app.UseMvc();

    // Ce middleware ne sera jamais appelé 
    app.Use((context, next) => { 
        next.Invoke(); 

        return Task.CompletedTask; 
    }); 
}

Il faut configurer le middleware avant app.UseMvc() car MVC est un middleware qui n’appelle pas de middleware suivant:

public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
{ 
    // ... 

    app.Use((context, next) => { 
        next.Invoke(); 

        return Task.CompletedTask; 
    }); 

    app.UseMvc();
}

Avec async/await

La notation précédente permet d’appeler un middleware de façon synchrone. On peut utiliser une notation plus adaptée avec async/await.

Par exemple:

app.Use(async (context, next) => { 
    logger.Info("Invoking next middleware..."); 

    await next.Invoke(); // Appelle le middleware suivant 

    logger.Info("Invoked."); 
});
Ne pas répondre à une requête plusieurs fois

Si on écrit la réponse à une requête, on ne peut pas l’écrire une 2e fois. L’écriture de la réponse se fait sous forme d’un stream. Une exception est lancée si on tente d’écrire une réponse à plusieurs reprises.

Par exemple si on écrit:

app.Use(async (context, next) => { 
    // Appelle le middleware suivant qui est MVC 

    await next.Invoke(); // MVC écrit une réponse une 1ère fois 

    // Permet d'envoyer une réponse vide 
    byte[] data = Encoding.UTF8.GetBytes("{}"); 
    context.Response.ContentType = "application/json"; 

    // Une réponse est écrite une 2e fois 
    await context.Response.Body.WriteAsync(data, 0, data.Length); 
}); 

app.UseMvc();

On obtient une exception de ce type car on écrit plusieurs fois une réponse à une requête:

fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1] 
      An unhandled exception has occurred while executing the request. 
System.InvalidOperationException: Headers are read-only, response has already started. 

Il faut donc être vigilant dans l’ordre d’exécution des middlewares dans le pipeline et savoir si un middleware dont l’exécution a déjà été effectué a déjà écrit une réponse.

Pour vérifier si une réponse est en cours d’écriture ou si elle a déjà été écrite, on peut utiliser la propriété:

IHttpContext.Response.HasStarted

Exécuter un “middleware” dans une fonction séparée

Au lieu d’utiliser un delegate dans le corps de la méthode Startup.Configure(), on peut aussi exécuter une méthode séparée:

public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
{ 
    // ... 

    app.Use(this.LoggingMiddlewareAsync); 

    // ... 
} 

private async Task LoggingMiddlewareAsync(HttpContext context, Func<Task> next) 
{ 
    this.logger.Info("Executing custom middleware..."); 

    await next.Invoke(); 

    this.logger.Info("Custom middleware executed."); 
} 

Exemple de “pipeline de middlewares”

Dans un pipeline de middleware, les middlewares sont appelés successivement dans l’ordre dans lequel ils ont été configurés:

  • Ordre des requests: lorsqu’un requête arrive, elle traverse tous les middlewares jusqu’à ce que l’un d’entre eux réponde.
  • Ordre des responses: lorsqu’une réponse est effectuée, les middlewares sont invoqués dans l’ordre inverse.

On se propose de montrer un exemple de pipeline de middlewares de façon à voir l’enchainement des appels. Dans cet exemple, les middlewares LoggingMiddlewareAsync1, LoggingMiddlewareAsync2, LoggingMiddlewareAsync3 et MVC sont executés successivement. Seul le middleware MVC écrit la réponse.

Exemple de pipeline de middlewares

Pour exécuter cet exemple:

  1. Il faut cloner le repository GitHub suivant: https://github.com/msoft/webapi_example/tree/swagger.
  2. Dans la méthode Startup.Configure() on configure les middlewares de cette façon:
    public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
    { 
        // Configuration de log4net 
        var configFile = Path.Combine(env.ContentRootPath, "log4net.config"); 
        var repository = LogManager.GetRepository(Assembly.GetEntryAssembly()); 
        XmlConfigurator.Configure(repository, new FileInfo(configFile)); 
    
        this.logger = LogManager.GetLogger(typeof(Startup)); 
    
        // Configuration de Swagger 
        app.UseSwagger(); 
    
        app.UseSwaggerUI(c => 
        { 
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "Pizza API V1"); 
        }); 
    
        // On configure 3 middlewares 
        app.Use(this.LoggingMiddlewareAsync1); 
        app.Use(this.LoggingMiddlewareAsync2); 
        app.Use(this.LoggingMiddlewareAsync3);
    
        // Ajout du middleware MVC 
        app.UseMvc();  
    } 
    

    Les middlewares sont implémentés de la même façon:

    private async Task LoggingMiddlewareAsync1(HttpContext context, Func<Task> next) 
    { 
        this.logger.Info("Executing 1st custom middleware..."); 
        await next.Invoke(); 
        this.logger.Info("1st custom middleware executed."); 
    }
    
    private async Task LoggingMiddlewareAsync2(HttpContext context, Func<Task> next) 
    { 
        this.logger.Info("Executing 2nd custom middleware..."); 
        await next.Invoke(); 
        this.logger.Info("2nd custom middleware executed."); 
    } 
    
    private async Task LoggingMiddlewareAsync3(HttpContext context, Func<Task> next) 
    { 
        this.logger.Info("Executing 3rd custom middleware..."); 
        await next.Invoke(); 
        this.logger.Info("3rd custom middleware executed."); 
    }
    
  3. On exécute le projet en exécutant successivement les instructions suivantes:
    user@debian:~/webapi_example% dotnet build
    user@debian:~/webapi_example% dotnet run
    
  4. Il faut se connecter à l’adresse http://localhost:5000/swagger/index.html avec un browser pour atteindre l’interface de Swagger.

    Si on exécute des méthodes du controller PizzaOrder comme par exemple la fonction GET /api/PizzaOrder, on peut voir les messages logs suivants:

    2019-04-13 01:52:49,674  INFO [9] - Executing 1st custom middleware... 
    2019-04-13 01:52:49,731  INFO [9] - Executing 2nd custom middleware... 
    2019-04-13 01:52:49,739  INFO [9] - Executing 3rd custom middleware... 
    2019-04-13 01:52:49,962  INFO [9] - 3rd custom middleware executed. 
    2019-04-13 01:52:49,963  INFO [9] - 2nd custom middleware executed. 
    2019-04-13 01:52:49,963  INFO [9] - 1st custom middleware executed. 
    

Exemple en stoppant l’exécution dans le “pipeline”

On modifie l’exemple précédent en introduisant un nouveau middleware qui va stopper l’exécution du pipeline. Le middleware StoppingMiddlewareAsync va stopper l’exécution du pipeline en écrivant une réponse à la requête et en n’appelant pas le middleware suivant.

Exemple de middleware interceptant une requête

En modifiant le code précédent, on obtient:

public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
{ 
    // ... 
    app.Use(this.LoggingMiddlewareAsync1); 
    app.Use(this.StoppingMiddlewareAsync);
    app.Use(this.LoggingMiddlewareAsync2); 
    app.Use(this.LoggingMiddlewareAsync3); 

    // ...
}

Avec:

private async Task StoppingMiddlewareAsync(HttpContext context, Func<Task> next) 
{ 
    this.logger.Info("Invoking stopping middleware..."); 

    var emptyJsonString = "{}"; 
    context.Response.ContentType = new System.Net.Http.Headers
        .MediaTypeHeaderValue("application/json").ToString(); 
    await context.Response.WriteAsync(emptyJsonString, Encoding.UTF8); 

    this.logger.Info("Stopping middleware invoked."); 
}

StoppingMiddlewareAsync() n’exécute pas le middleware suivant et écrit une réponse vide. Les middlewares LoggingMiddlewareAsync2, LoggingMiddlewareAsync3 et MVC ne seront pas appelés. On peut le voir en regardant les logs générés:

2019-04-13 02:02:36,491  INFO [5] - Executing 1st custom middleware... 
2019-04-13 02:02:36,520  INFO [5] - Invoking stopping middleware... 
2019-04-13 02:02:36,529  INFO [5] - Stopping middleware invoked. 
2019-04-13 02:02:36,529  INFO [5] - 1st custom middleware executed. 

IApplicationBuilder.Run()

Cette méthode permet d’enregistrer un middleware dans le pipeline. Contrairement à IApplicationBuilder.Use(), IApplicationBuilder.Run() ne donne pas la possibilité d’appeler le middleware suivant.

On peut utiliser IApplicationBuilder.Run() de cette façon:

public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
{ 
    // ... 

    app.Run(async context => { 
        await // ... 
    }); 

    // ... 
} 

Par exemple:

app.Run(async context => { 
    context.Response.ContentType = new System.Net.Http.Headers
        .MediaTypeHeaderValue("application/json").ToString(); 
    await context.Response.WriteAsync("{}", Encoding.UTF8); 
}); 

Exemple de “pipeline de middlewares” avec IApplicationBuilder.Run()

Comme il n’est pas possible d’appeler le middleware suivant avec IApplicationBuilder.Run(), l’exécution du pipeline s’arrête. Si on reprends l’exemple précédent et qu’on modifie l’ajout des middlewares de cette façon:

app.Use(this.LoggingMiddlewareAsync1); 
app.Run(this.StoppingMiddlewareAsync); 
app.Use(this.LoggingMiddlewareAsync2); 
app.Use(this.LoggingMiddlewareAsync3); 

Avec:

private async Task StoppingMiddlewareAsync(HttpContext context) 
{ 
    this.logger.Info("Invoking stopping middleware..."); 

    context.Response.ContentType = new System.Net.Http.Headers
        .MediaTypeHeaderValue("application/json").ToString(); 
    await context.Response.WriteAsync("{}", Encoding.UTF8); 

    this.logger.Info("Stopping middleware invoked."); 
} 

Si on effectue une requête, on peut voir que les messages de logs indiquent que les middlewares suivant StoppingMiddlewareAsync n’ont pas été exécutés:

2019-04-13 02:27:01,261  INFO [5] - Executing 1st custom middleware... 
2019-04-13 02:27:01,327  INFO [5] - Invoking stopping middleware... 
2019-04-13 02:27:01,366  INFO [5] - Stopping middleware invoked. 
2019-04-13 02:27:01,368  INFO [5] - 1st custom middleware executed.

IApplicationBuilder.Map()

Cette méthode permet de rajouter une condition concernant l’URL de la requête pour appeler un middleware. Ainsi le middleware sera appelé seulement si la condition est vraie. La condition porte sur le chemin de la requête, si l’URL de la requête contient un chemin particulier alors le middleware sera exécuté.

Dans la méthode Startup.Configure():

public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
{ 
    // ... 

    app.Map(<condition URL>, builder => { 
        // Configuration des middlewares à exécuter 
    }); 

    // ... 
}

L’intérêt de IApplicationBuilder.Map() est de pouvoir choisir d’exzcuter des branches différentes du pipeline.

Par exemple, si on souhaite exécuter le middleware LoggingMiddlewareAsync1 quand l’URL de la requête contient "/api/PizzaFlavour", on écrit:

app.Map("/api/PizzaFlavour", builder => { 
    builder.Use(this.LoggingMiddlewareAsync1); 
}); 

Avec:

private async Task LoggingMiddlewareAsync1(HttpContext context, Func<Task> next) 
{ 
    this.logger.Info("Executing 1st custom middleware..."); 
    await next.Invoke(); 
    this.logger.Info("1st custom middleware executed."); 
} 

Ainsi si on exécute GET /api/PizzaOrder, le middleware LoggingMiddlewareAsync1 n’est pas appelé:

Request starting HTTP/1.1 GET http://localhost:5000/api/PizzaOrder 

Si on exécute GET /api/PizzaFlavour, le middleware est appelé et on obtient:

Request starting HTTP/1.1 GET http://localhost:5000/api/PizzaFlavour 
2019-04-13 02:43:28,284  INFO [6] - Executing 1st custom middleware... 
2019-04-13 02:43:28,313  INFO [6] - 1st custom middleware executed. 

IApplicationBuilder.MapWhen()

Cette méthode permet d’indiquer une condition pour exécuter la configuration de middlewares. La condition est indiquée sous forme d’une expression lambda utilisant le contexte de la requête.

Dans la méthode Startup.Configure():

public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
{ 
    // ... 

    app.MapWhen( 
        context => {  
            // Code indiquant le contexte d'exécution }, 
        builder => { 
            // Configuration des middlewares à exécuter 
    }); 

    // ... 
}

Par exemple pour configurer un “health check” qui répondrait "OK" si l’URL de la requête commence par "/health", on pourrait écrire:

app.MapWhen( 
    context => context.Request.Path.StartsWithSegments("/health"), 
    builder => { 
        builder.Use(async (context, next) => { 

        context.Response.ContentType = new System.Net.Http.Headers
            .MediaTypeHeaderValue("application/json").ToString(); 

        await context.Response.WriteAsync("{ \"health\": \"OK.\" }", Encoding.UTF8); 
    }); 

Middlewares sous forme de classes

D’autres notations permettent de configurer des middlewares dans des classes séparées. Cette partie indique comment configurer ces middlewares.

IApplicationBuilder.UseMiddleware()

Utiliser IApplicationBuilder.UseMiddleware() permet de configurer un middleware autorisé à exécuter le middleware suivant dans le pipeline (comme pour IApplicationBuilder.Use()). Pour utiliser cette méthode, il faut définir un middleware dans une classe contenant une méthode avec la signature suivante:

public async Task Invoke(HttpContent context) 
{ 
    // ... 
} 

Par exemple, pour définir un middleware permettant de mesurer le temps d’exécution d’une requête par le middleware suivant de cette façon:

public class LoggingMiddleware 
{ 
    private readonly RequestDelegate next; 
    private readonly ILog logger = LogManager
        .GetLogger(typeof(LoggingMiddleware)); 
    private readonly int instanceHashCode; 

    public LoggingMiddleware(RequestDelegate next) 
    { 
        this.next = next; 
        this.instanceHashCode = this.GetHashCode(); 
    } 

    public async Task Invoke(HttpContext context)
    { 
        // Code exécuté avant le middleware suivant
        this.logger.InfoFormat("Executing logging middleware (HashCode:{0})...", 
        this.instanceHashCode); 

        var stopWatch = new Stopwatch(); 
        stopWatch.Start(); 

        await this.next(context); // Appel au middleware suivant 

        // Code exécuté après le middleware suivant
        stopWatch.Stop(); 
        var executionTime = stopWatch.Elapsed; 

        this.logger.InfoFormat("Logging middleware executed ({0} ms) (HashCode:{1}.",  
        executionTime.Milliseconds, this.instanceHashCode); 
    } 
} 

On configure ce middleware dans la méthode Startup.Configure() de cette façon:

app.UseMiddleware<LoggingMiddleware>();
Durée de vie du middleware

Un middleware ajouté de cette façon est instancié une seule fois et toutes les requêtes transitent à travers la même instance. On peut s’en apercevoir en regardant la valeur du “hash code” de la classe dans les logs:

2019-04-13 03:49:09,479  INFO [6] - Executing logging middleware (HashCode:2530563)... 
2019-04-13 03:49:09,855  INFO [6] - Logging middleware executed (309 ms) (HashCode:2530563).

Exemple avec une “factory”

Au lieu de rajouter un middleware directement comme précédemment, il existe une autre méthode permettant de rajouter une factory qui exécutera le code du middleware. L’intérêt d’utiliser une factory est de pouvoir contrôler sa durée de vie. En effet, pour configurer ce type d’objet, il faut d’abord le rajouter au container d’injection de dépendances, ce qui donne une flexibilité puisqu’on peut choisir le type d’enregistrement:

  • Transient (i.e. éphémère): les objets enregistrés de cette façon sont instanciés à chaque fois qu’ils sont injectés.
  • Scoped: la même instance de l’objet sera utilisée dans le cadre d’une même requête HTTP. Ainsi une nouvelle instance est créée pour chaque requête web.
  • Singleton: les objets de ce type sont créés une seule fois et la même instance est utilisée pendant toute la durée de vie.

Pour plus d’informations sur la configuration d’objets dans le container d’injection de dépendances: L’injection de dépendances dans une application ASP.NET Core.

Le 2e intérêt d’utiliser une factory est que le code invoqué du middleware est exécuté au moment de la requête et non au moment d’exécuter la méthode Startup.ConfigureServices(). Cette flexibilité permet, par exemple, d’injecter des objets qui n’existe pas encore dans le container au moment de l’exécution de Startup.ConfigureServices().

Par exemple, pour implémenter une factory permettant de logguer des messages avant et après avoir appelé le middleware suivant dans le pipeline:

  1. On commence par implémenter cette factory en satisfaisant l’interface Microsoft.AspNetCore.Http.IMiddleware:
    public class FactoryActivatedMiddleware : IMiddleware
    { 
        private readonly ILog logger = LogManager.GetLogger(typeof(FactoryActivatedMiddleware)); 
        private readonly int instanceHashCode; 
    
        public FactoryActivatedMiddleware() 
        { 
            this.instanceHashCode = this.GetHashCode(); 
        } 
    
        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        { 
            this.logger.InfoFormat("Executing factory activated logging middleware (HashCode: {0})...", 
            this.instanceHashCode); 
    
            await next(context); 
    
            this.logger.InfoFormat("Factory activated logging middleware executed (HashCode:{0}).", 
            this.instanceHashCode); 
        } 
    }
    
  2. Il faut ensuite l’ajouter au container d’injection de dépendances dans la méthode Startup.ConfigureServices():
    public void ConfigureServices(IServiceCollection services) 
    { 
        services.AddTransient<FactoryActivatedMiddleware>();
         
        // ... 
    } 
    
  3. On ajoute la factory dans le pipeline de middlewares dans la méthode Startup.Configure():
    public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
    { 
        // ... 
    
        app.UseMiddleware<FactoryActivatedMiddleware>();
    
        // ... 
    } 
    
  4. Etant donné que la factory a été enregistrée dans le container d’injection de dépendances avec la méthode IServiceCollection.AddTransient(), chaque nouvelle requête provoque la création d’une nouvelle instance de la factory. Ainsi à chaque requête, le “hash code” de l’instance sera différent puisque l’instance n’est pas la même:
    • 1ère requête:
      2019-04-13 04:14:57,900  INFO [21] - Executing factory activated logging 
          middleware (HashCode: 43694208)... 
      2019-04-13 04:14:57,900  INFO [21] - Factory activated logging 
          middleware executed (HashCode:43694208).
      
    • 2e requête:
      2019-04-13 04:16:52,756  INFO [23] - Executing factory activated logging 
          middleware (HashCode: 18991046)... 
      2019-04-13 04:16:52,757  INFO [23] - Factory activated logging 
          middleware executed (HashCode:18991046). 
      

Injection de dépendances

L’injection de dépendances est supportée pour les middlewares définis dans des classes séparées ou en utilisant une factory. L’injection d’objets peut se faire:

  • A l’instanciation du middleware si on utilise IApplicationBuilder.UseMiddleware() ou
  • A l’invocation du middleware dans une factory si on utilise IApplicationBuilder.UseMiddleware<Type de la factory>().

Par exemple, pour illustrer l’injection d’un objet dans un middleware:

  • On va définir un service permettant de créer des loggers LoggerFactoryService,
  • Enregistrer ce service dans le container d’injection de dépendances
  • Injecter ce service dans un middleware

Ainsi:

  1. On considère l’interface ILoggerFactoryService qui l’on définit de cette façon:
    public interface ILoggerFactoryService 
    { 
        ILog GetNewLogger(Type callerType); 
    } 
    
  2. On définit ensuite la classe LoggerFactoryService satisfaisant l’interface ILoggerFactoryService. Cette classe permet de créer un logger:
    internal class LoggerFactoryService : ILoggerFactoryService 
    { 
        public ILog GetNewLogger(Type callerType) 
        { 
            return LogManager.GetLogger(callerType); 
        } 
    } 
    
  3. On enregistre cette classe dans le container d’injection de dépendances dans la méthode Startup.ConfigureServices():
    public void ConfigureServices(IServiceCollection services) 
    { 
        // ... 
    
        services.AddSingleton<ILoggerFactoryService, LoggerFactoryService>();
    
        // ... 
    }
    
  4. On définit le middleware suivant permettant de répondre à un “health check”:
    public class HealthCheckMiddleware 
    { 
        private readonly RequestDelegate next; 
        private readonly ILog logger; 
        private readonly int instanceHashCode; 
    
        public HealthCheckMiddleware(RequestDelegate next, 
            ILoggerFactoryService loggerFactory) 
        { 
            this.next = next; 
            this.instanceHashCode = this.GetHashCode(); 
            this.logger = loggerFactory.GetNewLogger(typeof(HealthCheckMiddleware)); 
        } 
    
        public async Task Invoke(HttpContext context) 
        { 
            this.logger.InfoFormat("Executing health check middleware (HashCode:{0})...", 
            this.instanceHashCode); 
    
            context.Response.ContentType = new System.Net.Http.Headers
                .MediaTypeHeaderValue("application/json").ToString(); 
            await context.Response.WriteAsync("{ \"health\": \"OK.\" }", Encoding.UTF8); 
    
            this.logger.InfoFormat("Health check middleware executed (HashCode:{0}.",  
            this.instanceHashCode); 
        } 
    }
    

    Ce middleware utilise ILoggerFactoryService pour instancier un nouveau logger. ILoggerFactoryService est injecté par le constructeur dans le middleware

  5. On ajoute le middleware au pipeline dans la méthode Startup.Configure() en utilisant app.MapWhen() de façon à exécuter le middleware quand l’URL de la requête contient "/health":
    public void Configure(IApplicationBuilder app, IHostingEnvironment env) 
    { 
        // ...  
    
        app.MapWhen( 
            context => context.Request.Path.StartsWithSegments("/health"),
            builder => { 
                builder.UseMiddleware<HealthCheckMiddleware>(); 
        });  
    
        app.UseMvc();
    } 
    
  6. A l’exécution, on peut voir que ILoggerFactoryService est bien injecté dans le constructeur de HealthCheckMiddleware.

Cet exemple illustre l’injection de dépendances dans le constructeur du middleware. On peut aussi injecter des objets lors de l’exécution du code du middleware dans une factory.

Quelques middlewares usuels

Dans cette partie, on indique quelques middlewares:

Catégorie Appel du middleware Fonction Package NuGet Namespace
Autre app.UseWelcomePage() Permet d’afficher une page de bienvenu à la racine du service. Microsoft.AspNetCore.App
(pas nécessaire d’ajouter un autre package)
Microsoft.AspNetCore.Builder
Routing app.UseRouter() Permet de configurer le routage des requêtes (voir Le routage en ASP.NET Core en 5 min)
app.UseStaticFiles() Rend accessible des fichiers statiques (comme les fichiers CSS, les images ou des fichiers Javascripts).
app.UseHttpsRedirection() Effectue une redirection des requêtes HTTP vers HTTPS.
app.UseFileServer() Rend accessible tous les fichiers statiques mais ne permet l’exploration de répertoires.
app.UseDirectoryBrowser() Permet d’explorer les répertoires.
app.UseDefaultFiles() Redirige vers des pages index.html si elles sont présentes.
app.UseCors() Permet au browser d’effectuer des appels Cross Origin Resource Sharing.
Gestion d’erreurs app.UseDatabaseErrorPage() Permet de renvoyer le détail d’erreurs provenant d’une base dans le cas où on utilise EntityFramework.
app.UseExceptionHandler() Permet de configurer une page d’erreur personnalisée.
app.UseStatusCodePages()
app.UseStatusCodePagesWithRedirects()
app.UseStatusCodePagesWithReExecute()
Renvoie une réponse par défaut dans le cas où une requête est invalide avec une réponse entre 400 et 600.
app.UseDeveloperExceptionPage() Renvoie le détail d’une erreur dans le cas d’une exception.
Session app.UseSession() Permet l’utilisation de sessions pour garder en mémoire des données utilisateur ou des états de l’application entre plusieurs requêtes HTTP.
Documentation app.UseSwagger()
app.UseSwaggerUI()
Permet de documenter une API (voir Documenter une API Web ASP.NET Core avec Swagger) Swashbuckle.AspNetCore Swashbuckle.AspNetCore.Swagger

Pour conclure…

Comme on a pu le voir, les middlewares apportent une solution facile à mettre en œuvre pour effectuer une multitude de traitements sur les requêtes HTTP: logging, gestion des exceptions, outils de diagnostic, monitoring des performances etc… Ces middlewares apportent une flexibilité que l’on ne peut ignorer lors du développement d’une application ASP.NET Core.
D’autre part l’implémentation des middlewares en ASP.NET Core permet de facilement les configurer. Avec ASP.NET MVC, l’utilisation des middlewares OWIN était moins triviale, l’expérience acquise a probablement servi à améliorer les middlewares ASP.NET Core.
Enfin il faut penser à utiliser les middlewares “built-in” d’ASP.NET Core qui permettent de facilement apporter des fonctionnalités très utiles.

Références

Leave a Reply