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.
Middlewares sous forme de “delegate”
IApplicationBuilder.Use()
Avec async/await
Exécuter un “middleware” dans une fonction séparée
Exemple de “pipeline de middlewares”
Exemple en stoppant l’exécution dans le “pipeline”
IApplicationBuilder.Run()
Exemple de “pipeline de middlewares” avec IApplicationBuilder.Run()
IApplicationBuilder.Map()
IApplicationBuilder.MapWhen()
Middlewares sous forme de classes
IApplicationBuilder.UseMiddleware()
Exemple avec une “factory”
Injection de dépendances
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:
- Exemple sans Swagger: cet exemple se trouve dans le repository GitHub webapi_example. Pour exécuter ce projet, il faut au préalable installer le SDK .NET Core. Avec cet exemple, on peut effectuer les requêtes avec Postman.
- Exemple avec Swagger: un autre exemple ajoute au code précédent Swagger de façon à faciliter les requêtes. Le code de cet exemple se trouve dans la branche swagger du repository GitHub webapi_example. Pour davantage d’informations sur Swagger voir Documenter une API Web ASP.NET Core avec Swagger.
Middlewares sous forme de “delegate”
Les middlewares de ce type se configurent dans la fonction StartUp.Configure()
.
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:
- Exécutant la ligne suivante:
user@debian:~/% dotnet add webapi_example/WebApi.csproj package log4net
- 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/
. - 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;
});
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.");
});
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.
Pour exécuter cet exemple:
- Il faut cloner le repository GitHub suivant: https://github.com/msoft/webapi_example/tree/swagger.
- 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."); }
- On exécute le projet en exécutant successivement les instructions suivantes:
user@debian:~/webapi_example% dotnet build user@debian:~/webapi_example% dotnet run
- 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 fonctionGET /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.
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>();
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:
- 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); } }
- 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>(); // ... }
- 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>(); // ... }
- 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).
- 1ère requête:
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:
- On considère l’interface
ILoggerFactoryService
qui l’on définit de cette façon:public interface ILoggerFactoryService { ILog GetNewLogger(Type callerType); }
- On définit ensuite la classe
LoggerFactoryService
satisfaisant l’interfaceILoggerFactoryService
. Cette classe permet de créer un logger:internal class LoggerFactoryService : ILoggerFactoryService { public ILog GetNewLogger(Type callerType) { return LogManager.GetLogger(callerType); } }
- 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>(); // ... }
- 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 - On ajoute le middleware au pipeline dans la méthode
Startup.Configure()
en utilisantapp.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(); }
- A l’exécution, on peut voir que
ILoggerFactoryService
est bien injecté dans le constructeur deHealthCheckMiddleware
.
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() |
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() |
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.
- Episode 008 – Middlewares – ASP.NET Core: From 0 to overkill: https://blog.codingmilitia.com/2018/12/05/aspnet-008-from-zero-to-overkill-middlewares
- Stackoverflow.com: How to create a response message and add content string to it in ASP.NET 5 / MVC 6: https://stackoverflow.com/questions/33178983/how-to-create-a-response-message-and-add-content-string-to-it-in-asp-net-5-mvc
- Using Middleware in ASP.NET Core to Log Requests and Responses: https://exceptionnotfound.net/using-middleware-to-log-requests-and-responses-in-asp-net-core/
- Custom middleware with dependency injection in ASP.NET Core : https://blog.dudak.me/2014/custom-middleware-with-dependency-injection-in-asp-net-core/
- ASP.NET Core Middleware: https://docs.microsoft.com/en-gb/aspnet/core/fundamentals/middleware/index?tabs=aspnetcore2x&view=aspnetcore-2.2
- Write custom ASP.NET Core middleware: https://docs.microsoft.com/en-gb/aspnet/core/fundamentals/middleware/write?view=aspnetcore-2.2
- Factory-based middleware activation in ASP.NET Core: https://docs.microsoft.com/en-gb/aspnet/core/fundamentals/middleware/extensibility?view=aspnetcore-2.2
- ASP.NET Core middleware and authorization: https://www.tpeczek.com/2019/01/aspnet-core-middleware-and-authorization.html
- Middleware in ASP.NET Core – Handling requests: https://codingblast.com/asp-net-core-middleware/
- Exploring the cookie authentication middleware in ASP.NET Core: https://andrewlock.net/exploring-the-cookieauthenticationmiddleware-in-asp-net-core/
- How ASP.NET Core 1.0 Middleware is different from HttpModule: https://www.talkingdotnet.com/asp-net-core-middleware-is-different-from-httpmodule/
- Various ASP.NET Core Diagnostics Middleware: https://www.talkingdotnet.com/aspnet-core-diagnostics-middleware-error-handling/
- ASP.NET Core Web API exception handling: https://exceptionshub.com/asp-net-core-web-api-exception-handling.html
- Comprendre le pipeline de middleware d’ASP.NET Core: https://thomaslevesque.fr/2018/03/27/comprendre-le-pipeline-de-middleware-dasp-net-core/
- Custom Errors in ASP.NET Core and MVC 6: https://weblogs.asp.net/imranbaloch/custom-errors-aspnet5-mvc6