Juin 2016

Volume 31, numéro 6

Cet article a fait l'objet d'une traduction automatique.

ASP.NET - Utiliser un intergiciel personnalisé pour détecter et corriger les erreurs 404 dans les applications ASP.NET Core

Par Steve Smith

Si vous avez déjà perdu quelque chose sur une école ou d’un parc d’attraction, vous avez peut-être eu la chance de l’obtention par la vérification perdus et Retrouvés de l’emplacement. Dans les applications Web, les utilisateurs effectuent régulièrement des demandes pour les chemins d’accès qui ne sont pas gérées par le serveur, qui génèrent des codes de réponse introuvable 404 (et parfois humoristiques pages expliquant le problème à l’utilisateur). En règle générale, il est à l’utilisateur à trouver ce qu’ils cherchent leurs propres, soit par le biais des propositions répétées ou éventuellement à l’aide d’un moteur de recherche. Toutefois, avec un peu d’intergiciel (middleware), vous pouvez ajouter « perdus et retrouvés » à votre application ASP.NET Core qui permettra aux utilisateurs de trouver les ressources dont vous avez besoin pour.

Nouveautés d’intergiciel (middleware) ?

La documentation ASP.NET définit intergiciel (middleware) en tant que « les composants qui sont regroupés dans un pipeline d’application pour gérer les demandes et réponses ». La plus simple, intergiciel (middleware) est un délégué de la demande, qui peut être représenté comme une expression lambda, comme celle-ci :

app.Run(async context => {
  await context.Response.WriteAsync(“Hello world”);
});

Si votre application se compose de simplement ce petit un intergiciel (middleware), il renvoie « Hello world » à chaque demande. Car il ne fait pas référence à la partie suivante de l’intergiciel (middleware), cet exemple est dit à arrêter le pipeline, rien n’est définie après qu’elle est exécutée. Toutefois, parce que c’est la fin du pipeline évidemment vous ne peut pas « entourer » dans le middleware supplémentaire. Par exemple, vous pouvez ajouter un intergiciel (middleware) qui ajoute un en-tête à la réponse précédente :

app.Use(async (context, next) =>
{
  context.Response.Headers.Add("Author", "Steve Smith");
  await next.Invoke();
});
app.Run(async context =>
{
  await context.Response.WriteAsync("Hello world ");
});

L’appel de l’application. Utilisez encapsule l’appel à l’application. L’exécution, l’appel à l’aide de suivant. Appeler. Lorsque vous écrivez votre propre intergiciel (middleware), vous pouvez choisir si vous voulez lui permet d’effectuer des opérations avant, après ou avant et après le middleware suivant dans le pipeline. Vous pouvez également court-circuiter le pipeline en choisissant de ne pas appeler ensuite. Je vous montrerai comment cela peut vous aider à créer votre correction 404 intergiciel (middleware).

Si vous utilisez le modèle de base de MVC par défaut, vous ne trouverez ce code de bas niveau délégué intergiciel (middleware) dans votre fichier de démarrage initial. Il est recommandé que vous encapsuler middleware dans ses propres classes et fournissez des méthodes d’extension (nommés UseMiddlewareName) qui peuvent être appelées à partir du démarrage. Le middleware ASP.NET intégré suit cette convention, comme le montrent ces appels :

if (env.IsDevelopment())
{
  app.UseDeveloperExceptionPage();
}
app.UseStaticFiles()
app.UseMvc();

L’ordre de vos logiciels intermédiaires est important. Dans le code précédent, l’appel à UseDeveloperExceptionPage (qui est configurée uniquement lorsque l’application s’exécute dans un environnement de développement) doit habiller (donc ajouté avant) n’importe quel autre intergiciel (middleware) qui peut produire une erreur.

Dans une classe de son propre

Je ne veux pas encombrer ma classe de démarrage avec les expressions lambda et application détaillée de mon middleware. Tout comme avec l’intergiciel intégré, je veux mon intergiciel (middleware) à ajouter au pipeline avec une ligne de code. Je prévois également que mon middleware auront besoin de services injectés à l’aide de l’injection de dépendance (DI), qui est facilement effectue une fois le logiciel intermédiaire est refactorisé dans sa propre classe. (Voir mon article à l’adresse msdn.com/magazine/mt703433 pour plus d’informations sur l’injection de dépendances dans ASP.NET Core.)

Étant donné que j’utilise Visual Studio, je peux ajouter intergiciel (middleware) à l’aide d’ajouter un nouvel élément et en choisissant le modèle de classe de Middleware. Figure 1 affiche le contenu par défaut, ce modèle de produit, y compris une méthode d’extension pour l’ajout de l’intergiciel au pipeline via UseMiddleware.

Modèle de classe de l’intergiciel (middleware) de la figure 1

public class MyMiddleware
{
  private readonly RequestDelegate _next;
  public MyMiddleware(RequestDelegate next)
  {
    _next = next;
  }
  public Task Invoke(HttpContext httpContext)
  {
    return _next(httpContext);
  }
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class MyMiddlewareExtensions
{
  public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
  {
    return builder.UseMiddleware<MyMiddleware>();
  }
}

En règle générale, je vais ajouter async à la signature de méthode Invoke et modifiez son corps pour :

await _next(httpContext);

Cela rend l’appel asynchrone.

Une fois que j’ai créé une classe distincte middleware, déplacer ma logique de délégué à la méthode Invoke. Puis remplacer l’appel de la configurer avec un appel à la méthode d’extension UseMyMiddleware. Exécution de l’application à ce stade, il doit vérifier que l’intergiciel se comporte toujours comme avant, et la classe de configuration est beaucoup plus facile à suivre lorsqu’il se compose d’une série d’instructions de UseSomeMiddleware.

Détection et enregistrement 404 introuvable réponses

Dans ASP.NET application, si une demande est faite, qui ne correspond pas à tous les gestionnaires, la réponse inclut un StatusCode 404 la valeur. Je peux créer un peu de middleware qui recherchent ce code de réponse (après l’appel de _next) et prendre des mesures pour enregistrer les détails de la demande :

await _next(httpContext);
if (httpContext.Response.StatusCode == 404)
{
  _requestTracker.Record(httpContext.Request.Path);
}

Je souhaite être en mesure d’effectuer le suivi des erreurs 404 combien un chemin d’accès particulier a été, je peux résoudre les plus courants et tirer le meilleur parti de Mes actions correctives. Pour cela, je crée un service appelé RequestTracker instances d’enregistrements de 404 requêtes en fonction de leur chemin d’accès. RequestTracker est passé dans mon middleware injection de dépendance, comme indiqué dans Figure 2.

Figure 2 l’Injection de dépendance transmettant RequestTracker intergiciel (middleware)

public class NotFoundMiddleware
{
  private readonly RequestDelegate _next;
  private readonly RequestTracker _requestTracker;
  private readonly ILogger _logger;
  public NotFoundMiddleware(RequestDelegate next,
    ILoggerFactory loggerFactory,
    RequestTracker requestTracker)
  {
    _next = next;
    _requestTracker = requestTracker;
    _logger = loggerFactory.CreateLogger<NotFoundMiddleware>();
  }
}

Pour ajouter NotFoundMiddleware à mon pipeline, j’appelle la méthode d’extension UseNotFoundMiddleware. Toutefois, car elle dépend maintenant un service personnalisé est configuré dans le conteneur de services, je dois également garantir que le service est enregistré. Créer une méthode d’extension sur IServiceCollection appelée AddNotFoundMiddleware et de l’appeler cette méthode dans ConfigureServices de démarrage :

public static IServiceCollection AddNotFoundMiddleware(
  this IServiceCollection services)
{
  services.AddSingleton<INotFoundRequestRepository,
    InMemoryNotFoundRequestRepository>();
  return services.AddSingleton<RequestTracker>();
}

Dans ce cas, ma méthode AddNotFoundMiddleware garantit qu'une instance de ma RequestTracker est configurée comme un Singleton dans le conteneur de services, afin qu’il soit disponible pour injecter dans le NotFoundMiddleware lorsqu’il est créé. Il associe également une implémentation de la mémoire de INotFoundRequestRepository, ce qui le RequestTracker utilise pour conserver ses données.

Étant donné que de requêtes simultanées pour le même chemin d’accès manquant, le code peuvent se Figure 3 utilise un verrou simple afin de garantir les instances en double de NotFoundRequest ne sont pas ajoutées et les nombres sont incrémentés correctement.

Figure 3 RequestTracker

public class RequestTracker
{
  private readonly INotFoundRequestRepository _repo;
  private static object _lock = new object();
  public RequestTracker(INotFoundRequestRepository repo)
  {
    _repo = repo;
  }
  public void Record(string path)
  {
    lock(_lock)
    {
      var request = _repo.GetByPath(path);
      if (request != null)
      {
        request.IncrementCount();
      }
      else
      {
        request = new NotFoundRequest(path);
        request.IncrementCount();
        _repo.Add(request);
      }
    }
  }
  public IEnumerable<NotFoundRequest> ListRequests()
  {
    return _repo.List();
  }
  // Other methods
}

Affichage introuvable demandes

Maintenant que je dispose d’un moyen d’enregistrer les erreurs 404, j’ai besoin d’un affichage des données. Pour ce faire, je vais créer un autre composant petit middleware qui affiche une page pour afficher tous les NotFoundRequests enregistrées, classés par procédure autant de fois qu’ils avez s’est produite. Ce middleware vérifie si la demande en cours correspond à un chemin d’accès particulier et ignore (et passer) les demandes qui ne correspondent pas le chemin d’accès. Pour la correspondance des chemins d’accès, le middleware renvoie une page avec une table contenant les demandes introuvables, triés par leur fréquence. À partir de là, l’utilisateur sera en mesure d’assigner des demandes individuelles un chemin corrigé, qui sera utilisé par les requêtes ultérieures au lieu de renvoyer une erreur 404.

Figure 4 montre combien il est simple pour que le contrôle NotFoundPageMiddleware pour un certain chemin d’accès et de mettre à jour en fonction des valeurs de chaîne de requête à l’aide de ce même chemin d’accès. Pour des raisons de sécurité, accès au chemin NotFoundPageMiddleware doit être limité pour les utilisateurs administrateurs.

Figure 4 NotFoundPageMiddleware

public async Task Invoke(HttpContext httpContext)
{
  if (!httpContext.Request.Path.StartsWithSegments("/fix404s"))
  {
    await _next(httpContext);
    return;
  }
  if (httpContext.Request.Query.Keys.Contains("path") &&
      httpContext.Request.Query.Keys.Contains("fixedpath"))
  {
    var request = _requestTracker.GetRequest(httpContext.Request.Query["path"]);
    request.SetCorrectedPath(httpContext.Request.Query["fixedpath"]);
    _requestTracker.UpdateRequest(request);
  }
  Render404List(httpContext);
}

Rédaction, le middleware est codé en dur pour écouter le chemin d’accès /fix404s. Il est conseillé que configurable, de sorte que différentes applications peuvent spécifier le chemin d’accès qu’ils souhaitent. La liste des diaporamas de demandes toutes les demandes classés par nombre erreurs 404 rendue elles avez enregistrées, indépendamment de si un chemin d’accès corrigée a été configuré. Il serait difficile d’améliorer le logiciel intermédiaire pour vous proposer une partie du filtrage. Autres fonctionnalités intéressantes peuvent enregistrer des informations plus détaillées, afin que vous pouvez voir les redirections étaient plus populaires ou erreurs 404 ont généralement au cours des sept derniers jours, mais ils restent en guise d’exercice pour le lecteur (ou de la Communauté open source).

Figure 5 montre un exemple de l’aspect de la page rendue.

La Page de correctif erreurs 404
Figure 5 la Page correction erreurs 404

Ajout d’Options

J’aimerais pouvoir spécifier un autre chemin d’accès de la page de résoudre les erreurs 404 dans différentes applications. La meilleure façon de le faire consiste à créer une classe d’Options et la transmettre l’intergiciel (middleware) à l’aide de l’injection de dépendances. Pour ce middleware, je crée une classe, NotFoundMiddlewareOptions, qui inclut une propriété appelée chemin d’accès avec une valeur par défaut /fix404s. Je peux passer cela dans le NotFoundPageMiddleware à l’aide de l’interface IOptions < T > et puis définissez un champ local à la propriété Value de ce type. Puis référence my chaîne magique pour /fix404s peut être mis à jour :

if (!httpContext.Request.Path.StartsWithSegments(_options.Path))

Résolution des erreurs 404

Lorsqu’une demande arrive qui correspond à un NotFoundRequest qui a un CorrectedUrl, la NotFoundMiddleware doit modifier la requête à utiliser le CorrectedUrl. Pour ce faire, vous pouvez simplement mettre à jour la propriété de chemin d’accès de la demande :

string path = httpContext.Request.Path;
string correctedPath = _requestTracker.GetRequest(path)?.CorrectedPath;
if(correctedPath != null)
{
  httpContext.Request.Path = correctedPath; // Rewrite the path
}
await _next(httpContext);

Avec cette implémentation, n’importe quelle URL corrigée fonctionnera comme si sa demande avait été effectuée directement le chemin d’accès corrigée. Ensuite, le pipeline de requête continue, maintenant, le chemin d’accès de réécriture. Cela peut être ou non le comportement souhaité ; tout d’abord, les listes de moteur de recherche peuvent être sujettes à avoir indexé sur plusieurs URL de contenu en double. Cette approche peut entraîner des dizaines d’URL tout mappage vers le même chemin d’application sous-jacente. Pour cette raison, il est souvent préférable de corriger les erreurs 404 à l’aide d’une redirection permanente (code d’état 301).

Si je modifie le middleware pour envoyer une redirection, je peux avoir le court-circuit middleware dans ce cas, car il n’est pas nécessaire pour exécuter le reste du pipeline si j’ai décidé de que je vais juste pour retourner un 301 :

if(correctedPath != null)
{
  httpContext.Response. Redirect(httpContext.Request.PathBase + correctedPath +
    httpContext.Request.QueryString, permanent: true);
  return;
}
await _next(httpContext);

Vous ne devez ne pas pour définir les chemins d’accès corrigées résultant dans des boucles de redirection infinie.

Dans l’idéal, la NotFoundMiddleware doit prendre en charge les chemin d’accès de réécriture et effectue une redirection permanente. Je peux implémenter cela à l’aide de mon NotFoundMiddlewareOptions, ce qui permet une ou l’autre pour toutes les demandes, ou je peux modifier CorrectedPath sur le chemin d’accès NotFoundRequest, et inclut le chemin d’accès et le mécanisme à utiliser. Pour l’instant je vais simplement mettre à jour la classe d’options pour prendre en charge le comportement et passez IOptions < NotFoundMiddleOptions > dans le NotFoundMiddleware simplement que je fais déjà pour le NotFoundPageMiddleware. Avec les options sélectionnées, la logique de redirection/réécriture devient :

if(correctedPath != null)
{
  if (_options.FixPathBehavior == FixPathBehavior.Redirect)
  {
    httpContext.Response.Redirect(correctedPath, permanent: true);
    return;
  }
  if(_options.FixPathBehavior == FixPathBehavior.Rewrite)
  {
    httpContext.Request.Path = correctedPath; // Rewrite the path
  }
}

À ce stade, la classe NotFoundMiddlewareOptions a deux propriétés, y compris un enum :

public enum FixPathBehavior
{
  Redirect,
  Rewrite
}
public class NotFoundMiddlewareOptions
{
  public string Path { get; set; } = "/fix404s";
  public FixPathBehavior FixPathBehavior { get; set; } 
    = FixPathBehavior.Redirect;
}

Configuration d’intergiciel (middleware)

Une fois que vous avez les Options définies pour votre intergiciel (middleware), vous passez une instance de ces options à votre intergiciel (middleware) lors de la configuration de démarrage. Vous pouvez également lier les options à votre configuration. Configuration ASP.NET est très souple et vous pouvez liée aux variables d’environnement, les fichiers de paramètres ou inspirer par programme. Quelle que soit l’où la configuration est définie, les Options peuvent être liées à la configuration avec une seule ligne de code :

services.Configure<NotFoundMiddlewareOptions>(
  Configuration.GetSection("NotFoundMiddleware"));

Tout cela en place, je peux configurer mon comportement NotFoundMiddleware en mettant à jour appsettings.json (la configuration que j’utilise dans ce cas) :

"NotFoundMiddleware": {
  "FixPathBehavior": "Redirect",
  "Path": "/fix404s"
}

Notez que conversion des valeurs JSON basé sur chaîne dans le fichier de paramètres à l’énumération pour FixPathBehavior est effectuée automatiquement par l’infrastructure.

Persistance

Jusqu’ici, tout fonctionne correctement, mais malheureusement ma liste d’erreurs 404 et leurs chemins d’accès corrigés sont stockés dans une collection en mémoire. Cela signifie que chaque fois que mon application redémarre, toutes ces données sont perdues. Il peut être parfaitement à mon application périodiquement réinitialiser son nombre d’erreurs 404, donc je peux avoir une idée que de celles qui sont actuellement les plus courantes, mais je certainement sans perdre les chemins d’accès corrigées, que j’ai défini.

Heureusement, j’ai configuré le RequestTracker pour s’appuient sur une abstraction pour la persistance (INotFoundRequestRepository), il est relativement facile d’ajouter la prise en charge pour stocker les résultats dans une base de données à l’aide d’Entity Framework Core (EF). De plus, je peux facilitent pour les applications individuelles choisir s’ils souhaitent utiliser Entity Framework ou la configuration en mémoire (idéal pour tester des choses) en fournissant des méthodes d’assistance distincte.

La première chose pour utiliser Entity Framework pour enregistrer et récupérer NotFoundRequests est un DbContext. Je ne veux pas compter sur l’une qui peut avoir configuré l’application, afin d’en créer une pour le NotFoundMiddleware :

public class NotFoundMiddlewareDbContext : DbContext
{
  public DbSet<NotFoundRequest> NotFoundRequests { get; set; }
  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<NotFoundRequest>().HasKey(r => r.Path);
  }
}

Une fois que j’ai dbContext, je dois implémenter l’interface de référentiel. Créer un EfNotFoundRequestRepository, qui demande une instance de la NotFoundMiddlewareDbContext dans son constructeur et l’assigne à un champ privé, _dbContext. L’implémentation de méthodes individuelles est par exemple simple :

public IEnumerable<NotFoundRequest> List()
{
  return _dbContext.NotFoundRequests.AsEnumerable();
}
public void Update(NotFoundRequest notFoundRequest)
{
  _dbContext.Entry(notFoundRequest).State = EntityState.Modified;
  _dbContext.SaveChanges();
}

À ce stade, il reste qu’à associer le DbContext EF de stockage dans le conteneur des services de l’application. Cela est effectué dans une nouvelle méthode d’extension (et renommer la méthode d’extension d’origine pour indiquer qu’il est utilisé pour la version en mémoire) :

public static IServiceCollection AddNotFoundMiddlewareEntityFramework(
  this IServiceCollection services, string connectionString)
{
    services.AddEntityFramework()
      .AddSqlServer()
      .AddDbContext<NotFoundMiddlewareDbContext>(options =>
        options.UseSqlServer(connectionString));
  services.AddSingleton<INotFoundRequestRepository,
    EfNotFoundRequestRepository>();
  return services.AddSingleton<RequestTracker>();
}

J’ai choisi pour que la chaîne de connexion transmise, plutôt que stockés dans NotFoundMiddlewareOptions, car la plupart des applications ASP.NET qui utilisent EF déjà fournira une chaîne de connexion à celle-ci dans la méthode ConfigureServices. Si vous le souhaitez, la même variable peut être utilisée lors de l’appel des services. AddNotFoundMiddlewareEntityFramework(connectionString).

La dernière chose qu’une application doit effectuer avant de pouvoir utiliser la version d’Entity Framework de ce logiciel intermédiaire est exécutée les migrations afin de que la structure de table de base de données est correctement configurée. Je dois spécifier DbContext des intergiciels lorsque je fais cela, étant donné que l’application (dans le cas présent) a déjà son propre DbContext. La commande, exécutée à partir de la racine du projet, est la suivante :

dotnet ef database update --context NotFoundMiddlewareContext

Si vous obtenez une erreur sur un fournisseur de base de données, assurez-vous que vous appelez des services. AddNotFoundMiddlewareEntityFramework de démarrage.

Étapes suivantes

L’exemple que j’ai présenté ici fonctionne correctement et inclut une implémentation en mémoire et celle qui utilise Entity Framework pour stocker des chemins d’accès fixes et de nombre de requêtes introuvable dans une base de données. La liste des erreurs 404 et la possibilité d’ajouter des chemins d’accès corrigées doivent être sécurisés afin que seuls les administrateurs puissent y accéder. Enfin, l’implémentation actuelle d’Entity Framework n’inclut pas une logique de mise en cache, résultant d’une requête de base de données effectuée avec chaque demande à l’application. Pour des raisons de performances, la mise en cache à l’aide du modèle CachedRepository serait ajouter.

Le code source mis à jour pour cet exemple est disponible à l’adresse bit.ly/1VUcY0J.


Steve Smithest un formateur indépendant, mentor et consultant, ainsi que MVP ASP.NET. Il a participé à des dizaines d’articles à la documentation officielle ASP.NET (docs.asp.net) et aide les équipes à être rapidement opérationnel avec ASP.NET Core. Contactez-le à l’adresse ardalis.com.

Merci à l'experte technique Microsoft suivante d'avoir relu cet article : Chris Ross
Chris Ross est un développeur qui travaille sur l’équipe ASP.NET de Microsoft. À ce stade, son cerveau est effectuée hors middleware.