Share via



Août 2016

Volume 31, numéro 8

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

ASP.NET Core - Filtres réels ASP.NET Core et ASP.NET MVC

Par Steve Smith

Les filtres sont agreat, fonctionnalité souvent sous-exploités d’ASP.NET MVC et ASP.NET MVC de base. Ils permettent de raccorder le pipeline MVC action appel, ce qui les rend très utile pour extraire des tâches répétitives communes hors de vos actions. Souvent, une application aura une stratégie standard qui s’applique à la façon dont il gère certaines conditions, en particulier ceux qui pourraient générer des codes d’état HTTP particuliers. Ou il peut effectuer la gestion des erreurs ou la journalisation au niveau de l’application de manière spécifique, dans chaque action. Ces types de stratégies représentent les problèmes transversaux, et si possible, vous souhaitez suivre le principe de ne pas répéter vous-même (sec) et les extraire dans une abstraction commune. Ensuite, vous pouvez appliquer cette abstraction globalement ou lieu au sein de votre application. Filtres fournissent un excellent moyen pour y parvenir.

Qu’en est-il des logiciels intermédiaires ?

Dans le numéro de juin 2016, j’ai décrit comment middleware de base d’ASP.NET vous permet de contrôler le pipeline de requête dans vos applications (msdn.magazine.com/mt707525). Cela ressemble étrangement que filtres faire dans votre application ASP.NET MVC de base. La différence entre les deux est le contexte. Base d’ASP.NET MVC est implémenté via middleware. (MVC lui-même n’est pas intergiciel (middleware), mais elle configure lui-même pour être la destination par défaut pour le routage middleware). Base d’ASP.NET MVC inclut de nombreuses fonctionnalités telles que la liaison de modèle de négociation de contenu et la mise en forme de réponse. Les filtres existent dans le contexte de MVC, afin qu’ils puissent accéder à ces fonctionnalités au niveau du MVC et les abstractions. Intergiciel (middleware), en revanche, existe à un niveau inférieur et n’a aucune connaissance directe de MVC ou ses fonctionnalités.

Si vous disposez de fonctionnalités que vous souhaitez exécuter à un niveau inférieur et il ne dépendant pas MVC contextuelle, envisagez d’utiliser middleware. Si vous avez tendance à avoir beaucoup de logique commune dans les actions de contrôleur, filtres peuvent constituer un moyen pour vous à sec leur configuration pour les rendre plus facile à maintenir et à tester.

Types de filtres

Une fois le middleware MVC reprend, il appelle dans un large éventail de filtres à différents moments dans son pipeline appel d’action.

Les filtres de premier qui s’exécutent sont des filtres d’autorisation. Si la demande n’est pas autorisée, le filtre court-circuite immédiatement le reste du pipeline.

Dans la ligne sont ressource filtres, (après l’autorisation) le filtre premier et dernier pour traiter une demande. Filtres de ressources exécuter le code au début d’une demande, ainsi qu’à la fin, juste avant qu’il quitte le pipeline MVC. Un bon cas d’utilisation d’un filtre de ressource est sortie mise en cache. Le filtre peut examiner le cache et renvoyer le résultat mis en cache au début du pipeline. Si le cache n’est pas encore rempli, le filtre permet d’ajouter la réponse de l’action dans le cache à la fin du pipeline.

Filtres d’action s’exécuter juste avant et après que les actions sont exécutées. Elles s’exécutent après la liaison de modèle, afin qu’ils ont accès aux paramètres liées au modèle qui sera envoyé à l’action, ainsi que l’état de validation de modèle.

Actions de renvoyer les résultats. Filtres de résultat s’exécuter juste avant et après que les résultats sont exécutées. Ils peuvent ajouter des comportements à afficher ou l’exécution du module de formatage.

Enfin, les filtres d’exception sont utilisés pour gérer les exceptions non interceptées et appliquer des stratégies globales à ces exceptions au sein de l’application.

Dans cet article, je me concentrerai sur les filtres d’action.

Filtre d’étendue

Filtres peuvent être appliqués globalement ou au niveau de chaque contrôleur ou d’action. Filtres qui sont implémentées comme attributs peuvent être ajoutés en général à tout niveau, avec filtres globaux qui affectent toutes les actions, les filtres d’attribut contrôleur affecte toutes les actions au sein de ce contrôleur et filtres d’attribut action appliquer à simplement cette action. Lorsque plusieurs filtres s’appliquent à une action, leur ordre est déterminé par une propriété de classement et ensuite comment ils sont limités à l’action en question. Les filtres avec le même ordre s’exécutent en dehors de, ce qui signifie que tout d’abord global, puis contrôleur et des filtres puis au niveau des actions sont exécutées. Après l’exécution de l’action, l’ordre est inversé, donc le niveau de l’action de filtre s’exécute, puis le filtre au niveau du contrôleur, puis le filtre global.

Filtres ne sont pas implémentés comme attributs peuvent être toujours appliqués aux contrôleurs ou les actions à l’aide du type TypeFilterAttribute. Cet attribut accepte le type du filtre à exécuter en tant que paramètre de constructeur. Par exemple, pour appliquer le CustomActionFilter à une méthode d’action unique, vous écririez :

[TypeFilter(typeof(CustomActionFilter))]
public IActionResult SomeAction()
{
  return View();
}

Le TypeFilterAttribute fonctionne avec le conteneur de services intégrés de l’application pour vous assurer toutes les dépendances exposées par le CustomActionFilter sont remplis au moment de l’exécution.

UNE API SEC

Pour illustrer quelques exemples où les filtres peuvent améliorer la conception d’une application ASP.NET MVC Core, j’ai créé une API simple qui fournit la base créer, lire, mettre à jour, supprimer des fonctionnalités (CRUD) et suit quelques règles standards pour la gestion des demandes non valides. Sécurisation des API étant sa propre rubrique, je qui laisse intentionnellement sort du cadre de cet exemple.

Mon exemple d’application expose une API de gestion des auteurs, qui sont des types simples avec quelques propriétés. L’API utilise les conventions basée sur le verbe HTTP pour obtenir tous les auteurs, obtenir un auteur par ID, créez un nouvel auteur, modifier un auteur et de supprimer un auteur. Il accepte un IAuthorRepository via l’injection de dépendance (DI) pour l’accès aux données. (Voir mon article à l’adresse msdn.com/magazine/mt703433 pour plus d’informations sur l’injection de dépendance.) L’implémentation de contrôleur et le référentiel sont implémentées de façon asynchrone.

L’API suit deux stratégies :

  1. Requêtes d’API qui spécifient l’ID d’un auteur particulier recevra une réponse 404 si cet ID n’existe pas.
  2. Requêtes d’API qui fournissent un auteur non valide d’instance de modèle (ModelState.IsValid == false) renvoie un BadRequest avec les erreurs de modèle répertoriés.

Figure 1 illustre l’implémentation de cette API avec ces règles en place.

Figure 1 AuthorsController

[Route("api/[controller]")]
public class AuthorsController : Controller
{
  private readonly IAuthorRepository _authorRepository;
  public AuthorsController(IAuthorRepository authorRepository)
  {
    _authorRepository = authorRepository;
  }
  // GET: api/authors
  [HttpGet]
  public async Task<List<Author>> Get()
  {
    return await _authorRepository.ListAsync();
  }
  // GET api/authors/5
  [HttpGet("{id}")]
  public async Task<IActionResult> Get(int id)
  {
    if ((await _authorRepository.ListAsync()).All(a => a.Id != id))
    {
      return NotFound(id);
    }
    return Ok(await _authorRepository.GetByIdAsync(id));
  }
  // POST api/authors
  [HttpPost]
  public async Task<IActionResult> Post([FromBody]Author author)
  {
    if (!ModelState.IsValid)
    {
      return BadRequest(ModelState);
    }
    await _authorRepository.AddAsync(author);
    return Ok(author);
  }
  // PUT api/authors/5
  [HttpPut("{id}")]
  public async Task<IActionResult> Put(int id, [FromBody]Author author)
  {
    if ((await _authorRepository.ListAsync()).All(a => a.Id != id))
    {
      return NotFound(id);
    }
    if (!ModelState.IsValid)
    {
       return BadRequest(ModelState);
    }
    author.Id = id;
    await _authorRepository.UpdateAsync(author);
    return Ok();
  }
  // DELETE api/values/5
  [HttpDelete("{id}")]
  public async Task<IActionResult> Delete(int id)
  {
    if ((await _authorRepository.ListAsync()).All(a => a.Id != id))
    {
      return NotFound(id);
    }
    await _authorRepository.DeleteAsync(id);
    return Ok();
  }
  // GET: api/authors/populate
  [HttpGet("Populate")]
  public async Task<IActionResult> Populate()
  {
    if (!(await _authorRepository.ListAsync()).Any())
    {
      await _authorRepository.AddAsync(new Author()
      {
        Id = 1,
        FullName = "Steve Smith",
        TwitterAlias = "ardalis"
      });
      await _authorRepository.AddAsync(new Author()
      {
        Id = 2,
        FullName = "Neil Gaiman",
        TwitterAlias = "neilhimself"
      });
    }
    return Ok();
  }
}

Comme vous pouvez le voir, il existe une quantité raisonnable de logique en double dans ce code, en particulier de la façon introuvables et BadRequest les résultats sont retournés. Puis-je remplacer rapidement les vérifications de validation/BadRequest modèle avec un filtre d’action simple :

public class ValidateModelAttribute : ActionFilterAttribute
{
  public override void OnActionExecuting(ActionExecutingContext context)
    {
    if (!context.ModelState.IsValid)
    {
      context.Result = new BadRequestObjectResult(context.ModelState);
    }
  }
}

Cet attribut peut ensuite être appliqué à ces actions à effectuer la validation de modèle en ajoutant [ValidateModel] à la méthode d’action. Notez que de définir la propriété Result sur la ActionExecutingContext sera court-circuit la demande. Dans ce cas, il n’est aucune raison de ne pas appliquer l’attribut à chaque action, donc je vais l’ajouter au contrôleur plutôt qu’à chaque action.

Vérifie l’existence de l’auteur est un peu plus complexe, car elle repose sur le IAuthorRepository qui est passé dans le contrôleur de l’injection de dépendances. Il est assez simple créer un attribut de filtre d’action qui accepte un paramètre de constructeur, mais, malheureusement, attributs attendent ces paramètres pour indiquer où elles sont déclarées. Je ne peux pas fournir l’instance de référentiel dans lequel l’attribut est appliqué ; Je veux qu’il injectée par le conteneur de services en cours d’exécution.

Heureusement, l’attribut TypeFilter fournit la prise en charge de l’injection de dépendances que nécessite ce filtre. Je peux simplement appliquer l’attribut TypeFilter aux actions et spécifier le type ValidateAuthorExistsFilter :

[TypeFilter(typeof(ValidateAuthorExistsFilter))]

Bien que cela fonctionne, il n’est pas mon approche préférée, car il est moins lisible et les développeurs qui souhaitent pour appliquer l’une de plusieurs filtres d’attribut commun ne trouveront pas le ValidateAuthorExistsAttribute via IntelliSense. Une approche que je préfère est de sous-classer la TypeFilterAttribute, donnez-lui un nom approprié et placer l’implémentation de filtre dans une classe privée à l’intérieur de cet attribut. Figure 2 illustre cette approche. Le travail réel est effectué par la classe ValidateAuthorExistsFilterImpl privée, dont le type est passé constructeur de la TypeFilterAttribute.

Figure 2 ValidateAuthorExistsAttribute

public class ValidateAuthorExistsAttribute : TypeFilterAttribute
{
  public ValidateAuthorExistsAttribute():base(typeof
    (ValidateAuthorExistsFilterImpl))
  {
  }
  private class ValidateAuthorExistsFilterImpl : IAsyncActionFilter
  {
    private readonly IAuthorRepository _authorRepository;
    public ValidateAuthorExistsFilterImpl(IAuthorRepository authorRepository)
    {
      _authorRepository = authorRepository;
    }
    public async Task OnActionExecutionAsync(ActionExecutingContext context,
      ActionExecutionDelegate next)
    {
      if (context.ActionArguments.ContainsKey("id"))
      {
        var id = context.ActionArguments["id"] as int?;
        if (id.HasValue)
        {
          if ((await _authorRepository.ListAsync()).All(a => a.Id != id.Value))
          {
            context.Result = new NotFoundObjectResult(id.Value);
            return;
          }
        }
      }
      await next();
    }
  }
}

Notez que l’attribut a accès aux arguments passés à l’action, en tant que partie du paramètre ActionExecutingContext. Ainsi, le filtre vérifier si un paramètre id est présent et obtenir sa valeur avant de vérifier si un auteur existe avec cet Id. Notez également que le ValidateAuthorExistsFilterImpl privé est un filtre async. Avec ce modèle, il existe une méthode à implémenter et travail peut être effectué avant ou après l’exécution de l’action à exécuter avant ou après l’appel suivant. Toutefois, si vous êtes court-circuit le filtre en définissant un contexte. Le résultat, vous devez retourner sans appeler ensuite (sinon vous obtiendrez une exception).

Autre point à retenir à propos des filtres est qu’ils ne doit pas inclure n’importe quel état au niveau de l’objet, tel qu’un champ sur un IActionFilter (en particulier celui implémenté en tant qu’attribut) définies pendant le OnActionExecuting et ensuite lu ou modifié dans OnActionExecuted. Si vous trouvez la nécessité d’effectuer ce genre de logique, vous pouvez éviter ce type d’état en passant à un IAsyncActionFilter, qui peut simplement utiliser des variables locales de la méthode OnActionExecutionAsync.

Après avoir un décalage de validation de modèle et vérification de l’existence d’enregistrements à partir de dans les actions de contrôleur aux filtres courants, ce qui a été l’effet sur mon contrôleur ? Pour comparaison, Figure 3 indique Authors2Controller, qui effectue la même logique que AuthorsController, mais s’appuie sur ces deux filtres pour son comportement de stratégie courantes.

Figure 3 Authors2Controller

[Route("api/[controller]")]
[ValidateModel]
public class Authors2Controller : Controller
{
  private readonly IAuthorRepository _authorRepository;
  public Authors2Controller(IAuthorRepository authorRepository)
  {
    _authorRepository = authorRepository;
  }
  // GET: api/authors2
  [HttpGet]
  public async Task<List<Author>> Get()
  {
    return await _authorRepository.ListAsync();
  }
  // GET api/authors2/5
  [HttpGet("{id}")]
  [ValidateAuthorExists]
  public async Task<IActionResult> Get(int id)
  {
    return Ok(await _authorRepository.GetByIdAsync(id));
  }
  // POST api/authors2
  [HttpPost]
  public async Task<IActionResult> Post([FromBody]Author author)
  {
    await _authorRepository.AddAsync(author);
    return Ok(author);
  }
  // PUT api/authors2/5
  [HttpPut("{id}")]
  [ValidateAuthorExists]
  public async Task<IActionResult> Put(int id, [FromBody]Author author)
  {
    await _authorRepository.UpdateAsync(author);
    return Ok();
  }
  // DELETE api/authors2/5
  [HttpDelete("{id}")]
  [ValidateAuthorExists]
  public async Task<IActionResult> Delete(int id)
  {
    await _authorRepository.DeleteAsync(id);
    return Ok();
  }
}

Notez que deux choses concernant ce contrôleur refactorisé. Tout d’abord, il est plus courte et plus claire. Ensuite, il n’existe aucune condition dans une des méthodes. La logique commune de l’API a été acheminée complètement dans des filtres qui sont appliquées le cas échéant, afin que le travail du contrôleur est aussi simple que possible.

Mais vous tester ?

Déplacer la logique de votre contrôleur en attributs est très utile pour réduire la complexité du code et en appliquant un comportement cohérent. Malheureusement, si vous exécutez des tests unitaires directement sur vos méthodes d’action, vos tests ne vont pas avoir le comportement de l’attribut ou le filtre appliqué. Ceci est normal et bien sûr vous effectuer des tests unitaires vos filtres indépendamment des méthodes d’action individuelles pour vous assurer qu’ils fonctionnent comme prévu. Mais que se passe-t-il si vous devez vous assurer non seulement que vos filtres fonctionnent, mais qu’ils sont correctement configurés et appliqués aux méthodes d’action individuelles ? Que se passe-t-il si vous souhaitez refactoriser du code de l’API vous l’avez déjà tirer parti des filtres que je viens de, et vous voulez être sûr de que l’API comporte toujours correctement lorsque vous avez terminé ? Qui appelle pour les tests d’intégration. Heureusement, ASP.NET Core inclut une excellente prise en charge de tester l’intégration rapide et simple.

Mon exemple d’application est configuré pour utiliser un DbContext en mémoire d’Entity Framework Core, mais même si elle a été à l’aide de SQL Server, j’aurais pu basculer facilement à l’aide d’un magasin en mémoire pour mes tests d’intégration. Ceci est important, car elle améliore la vitesse de ces tests, considérablement et facilite leur configuration, car aucune infrastructure n’est requise.

La classe qui effectue la plupart du gros travail pour les tests d’intégration dans ASP.NET Core est la classe TestServer, disponible dans le package Microsoft.AspNetCore.TestHost. Vous configurez le TestServer est identique à la façon dont vous configurez votre application Web dans le point d’entrée Program.cs, à l’aide d’un WebHostBuilder. Dans mes tests, j’ai choisi d’utiliser la même classe de démarrage comme dans mon exemple d’application Web et je spécifie qu’il s’exécute dans l’environnement de test. Cela déclenche des exemples de données au démarrage de site :

var builder = new WebHostBuilder()
  .UseStartup<Startup>()
  .UseEnvironment("Testing");
var server = new TestServer(builder);
var client = server.CreateClient();

Dans ce cas, le client est un System.Net.Http.HttpClient standard, ce qui vous permet d’effectuer des demandes au serveur, comme s’il était sur le réseau. Mais étant donné que toutes les demandes sont effectuées en mémoire, les tests sont extrêmement rapide et robuste.

Pour mes tests, j’utilise xUnit, qui inclut la possibilité d’exécuter plusieurs tests avec différents jeux de données pour une méthode de test donnée. Pour vérifier que mes classes AuthorsController et Authors2Controller se comportent de façon identique, j’utilise cette fonctionnalité pour spécifier les deux contrôleurs à chaque test. Figure 4 montre plusieurs tests de la méthode Put.

Figure 4 les auteurs de placer des Tests

[Theory]
[InlineData("authors")]
[InlineData("authors2")]
public async Task ReturnsNotFoundForId0(string controllerName)
{
  var authorToPost = new Author() { Id = 0, FullName = "test",
    TwitterAlias = "test" };
  var jsonContent = new StringContent(JsonConvert.SerializeObject(authorToPost),
     Encoding.UTF8, "application/json");
  var response = await _client.PutAsync($"/api/{controllerName}/0", jsonContent);
  Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
  var stringResponse = await response.Content.ReadAsStringAsync();
  Assert.Equal("0", stringResponse);
}
[Theory]
[InlineData("authors")]
[InlineData("authors2")]
public async Task ReturnsBadRequestGivenNoAuthorName(string controllerName)
{
  var authorToPost = new Author() {Id=1, FullName = "", TwitterAlias = "test"};
  var jsonContent = new StringContent(
    JsonConvert.SerializeObject(authorToPost),
     Encoding.UTF8, "application/json");
  var response = await _client.PutAsync($"/api/{controllerName}/1", jsonContent);
  Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
  var stringResponse = await response.Content.ReadAsStringAsync();
  Assert.Contains("FullName", stringResponse);
  Assert.Contains("The FullName field is required.", stringResponse);
}
[Theory]
[InlineData("authors")]
[InlineData("authors2")]
public async Task ReturnsOkGivenValidAuthorData(string controllerName)
{
  var authorToPost = new Author() {
    Id=1,FullName = "John Doe",
    TwitterAlias = "johndoe" };
  var jsonContent = new StringContent(JsonConvert.SerializeObject(authorToPost),
     Encoding.UTF8, "application/json");
  var response = await _client.PutAsync($"/api/{controllerName}/1", jsonContent);
  response.EnsureSuccessStatusCode();
}

Notez que ces tests d’intégration ne nécessitent pas une base de données ou une connexion Internet ou un serveur Web en cours d’exécution. Ils sont presque comme rapide et simple que des tests unitaires, mais plus important encore, ils vous permettent de tester vos applications ASP.NET via le pipeline de requête entière, pas comme une méthode isolée au sein d’une classe de contrôleur. Je recommande néanmoins d’écriture où vous pouvez et recourir à des tests d’intégration pour le comportement vous ne pouvez pas les tests unitaires, mais il est agréable d’avoir un moyen de hautes performances pour exécuter des tests d’intégration dans ASP.NET Core les tests unitaires.

Étapes suivantes

Les filtres sont un vaste sujet, je n’avais place pour quelques exemples dans cet article. Vous pouvez consulter la documentation officielle sur docs.asp.net pour en savoir plus sur les filtres et test des applications ASP.NET Core.

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


Steve Smith est un formateur indépendant, mentor et consultant, ainsi que MVP ASP.NET.  Il a contribué à des dizaines d’articles à la documentation officielle de la base de ASP.NET (docs.asp.net) et aide les équipes à être rapidement opérationnel avec ASP.NET Core. Contactez-le à l’adresse ardalis.com et le suivre sur Twitter : également appelé @ardalis.


Merci à l'experte technique Microsoft suivante d'avoir relu cet article : Doug drapeaux
Drapeaux de Doug est un développeur qui travaille sur l’équipe ASP.Net de Microsoft.