Condividi tramite



Agosto 2016

Volume 31 Numero 8

Il presente articolo è stato tradotto automaticamente.

ASP.NET Core - Filtri MVC di ASP.NET Core reali

Da Smith Steve

I filtri sono agreat, spesso sottoutilizzati funzionalità di ASP.NET MVC e Core ASP.NET MVC. Forniscono un modo per effettuare l'hook nella pipeline di chiamata azione MVC, che li rende ideale per effettuare il pull comuni attività ripetitive fuori le azioni. Un'applicazione hanno spesso un criterio standard che viene applicato per la modalità di gestione di determinate condizioni, specialmente quelle che potrebbero generare codici di stato HTTP specifici. O eseguire la gestione degli errori o la registrazione a livello di applicazione in modo specifico, in ogni azione. Questi tipi di criteri rappresentano questioni trasversali e, se possibile, si desidera seguano il principio non ripetere manualmente (sorgente) ed estrarre in un'astrazione comune. Quindi, è possibile applicare questa astrazione a livello globale o se del caso, all'interno dell'applicazione. I filtri rappresentano un'ottima soluzione per ottenere questo risultato.

Per quanto riguarda Middleware?

Nel numero di giugno 2016, descritto come middleware ASP.NET di base consente di controllare la pipeline di richieste nelle applicazioni (msdn.microsoft.com/mt707525). Che sembra essere potrebbe apparire filtri le operazioni eseguibili nell'applicazione ASP.NET MVC di base. La differenza tra i due è contesto. Componenti di base di ASP.NET MVC è implementata tramite middleware. (MVC stessa non middleware, ma configurato automaticamente per essere la destinazione predefinita per il routing middleware). Componenti di base di ASP.NET MVC include numerose funzionalità come modello di associazione, negoziazione dei contenuti e la formattazione di risposta. I filtri presenti all'interno del contesto di MVC, in modo che abbiano accesso a tali funzionalità a livello di MVC e astrazioni. Middleware, al contrario, esiste un livello inferiore e non dispone di alcuna informazione diretto di MVC o le funzionalità.

Se si dispone di funzionalità che si desidera eseguire in un livello inferiore e non dipendono dal contesto a livello di MVC, è possibile utilizzare middleware. Se si sentono molta logica comune in azioni controller, filtri potrebbero fornire un modo per consentire a asciutto i backup per renderli più semplici da gestire e testare.

Tipi di filtri

Una volta assume il middleware MVC, effettua una chiamata a una gamma di filtri in momenti diversi all'interno di relativa pipeline di chiamata di azione.

I filtri prima di eseguono sono filtri di autorizzazione. Se la richiesta non è autorizzata, il filtro provoca un corto circuito il resto della pipeline immediatamente.

Quindi nella riga sono filtri delle risorse, ovvero (dopo l'autorizzazione) sia il filtro e il cognome di gestire una richiesta. Filtri delle risorse possono eseguire codice all'inizio di una richiesta, nonché alla fine, appena prima che la pipeline MVC. La cache di un buon caso di utilizzo per un filtro di risorse è output. Il filtro può controllare la cache e restituire il risultato memorizzato nella cache all'inizio della pipeline. Se la cache non è ancora popolata, il filtro può aggiungere la risposta dall'azione nella cache alla fine della pipeline.

I filtri azione vengono eseguiti prima e dopo l'esecuzione di azioni. Vengono eseguiti dopo l'associazione di modelli viene eseguita, in modo che abbiano accesso ai parametri di modello di associazione che verrà inviato per l'azione, nonché lo stato di convalida del modello.

Azioni restituiscono risultati. I filtri di risultato vengono eseguiti prima e dopo l'esecuzione di risultati. Possono aggiungere il comportamento per visualizzare o l'esecuzione del formattatore.

Infine, vengono utilizzati filtri eccezioni per gestire le eccezioni non rilevate e applicare criteri globali per queste eccezioni all'interno dell'app.

In questo articolo, mi concentrerò su filtri azione.

Filtro ambito

È possibile applicare filtri a livello globale o a livello di controller o nessun'azione singola. Filtri che vengono implementati come attributi possono essere aggiunti a qualsiasi livello, in genere con i filtri globali che interessano tutte le azioni, i filtri di attributo controller che interessano tutte le azioni all'interno del controller e filtri azione applicare solo tale azione. Quando più filtri si applicano a un'azione, l'ordine viene innanzitutto determinato da una proprietà di ordine e la seconda da come vengono sta nell'ambito dell'operazione in questione. I filtri con lo stesso ordine eseguire esterno a interno, vale a dire prima globale, quindi controller e i filtri a livello di azione quindi vengono eseguiti. Dopo l'azione viene eseguita, l'ordine è invertito, in modo a livello di operazione filtro viene eseguito, quindi il filtro a livello di controller e il filtro globale.

Filtri che non sono implementati come attributi possono essere ancora applicati al controller o azioni utilizzando il tipo TypeFilterAttribute. Questo attributo accetta il tipo di filtro per l'esecuzione come un parametro del costruttore. Ad esempio, per applicare il CustomActionFilter a un metodo di azione singola, si scriverebbe:

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

Il TypeFilterAttribute funziona con contenitore di servizi integrati dell'applicazione per verificare eventuali dipendenze esposte dal CustomActionFilter vengono popolate in fase di esecuzione.

UN'API ASCIUTTA

Ho creato una semplice API che fornisce una base per illustrare alcuni esempi in cui i filtri possono migliorare la progettazione di un'applicazione ASP.NET MVC di base, creazione, lettura, aggiornamento, eliminazione (CRUD) ed segue alcune regole standard per la gestione delle richieste non valide. Poiché le API di protezione è un argomento specifico, sono lasciati intenzionalmente che all'esterno dell'ambito di questo esempio.

L'applicazione di esempio espone un'API per la gestione degli autori, che sono tipi semplici con solo un paio di proprietà. L'API utilizza le convenzioni basate su verbi HTTP standard per ottenere tutti gli autori, ottenere un autore dall'ID, creare un nuovo autore, un autore di modificare ed eliminare un autore. Accetta un IAuthorRepository tramite inserimento delle dipendenze (DI) per astrarre l'accesso ai dati. (Vedere il mese di maggio articolo all'indirizzo msdn.com/magazine/mt703433 per ulteriori informazioni sull'inserimento di dipendenze.) Sia l'implementazione del controller e il repository vengono implementati in modo asincrono.

L'API segue due criteri:

  1. Se non esiste tale ID, le richieste di API che specificano un ID autore particolare riceveranno una risposta 404.
  2. Le richieste di API che forniscono autore non valido di un'istanza del modello (ModelState.IsValid = = false) restituirà una richiesta non valida con gli errori del modello elencati.

Figura 1 viene illustrata l'implementazione di questa API con queste regole sul posto.

Figura 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();
  }
}

Come si può vedere, è un po' di logica duplicato in questo codice, in particolare in modo vengono restituiti risultati non trovato e richiesta non valida. È possibile sostituire rapidamente i controlli di convalida/richiesta non valida del modello con un filtro azioni semplice:

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

Questo attributo può quindi essere applicato a tali azioni che devono eseguire la convalida del modello aggiungendo [ValidateModel] al metodo di azione. Si noti che l'impostazione della proprietà Result sul ActionExecutingContext verrà corto circuito di richiesta. In questo caso, non vi è alcun motivo per non applicare l'attributo a ogni azione, quindi verrà aggiunto al controller anziché per ogni azione.

Per verificare se esiste l'autore è un po' più complicato, perché questo comportamento si basa su IAuthorRepository che viene passato al controller DI. È abbastanza semplice per creare un attributo di filtro di azione che accetta un parametro di costruttore, ma, Sfortunatamente, gli attributi prevedono questi parametri devono essere fornite in cui sono dichiarate. Impossibile includere l'istanza di repository in cui è applicato l'attributo; Si desidera venga inserito in fase di esecuzione per il contenitore dei servizi.

Fortunatamente, l'attributo TypeFilter fornirà il supporto DI che questo filtro è necessario. Semplicemente è possibile applicare l'attributo TypeFilter alle azioni e specificare il tipo di ValidateAuthorExistsFilter:

[TypeFilter(typeof(ValidateAuthorExistsFilter))]

Durante questa procedura è valida, non è l'approccio consigliato, perché è meno leggibile e gli sviluppatori che desiderano per applicare uno dei diversi filtri attributo comune non saranno possibile trovare il ValidateAuthorExistsAttribute tramite IntelliSense. Un approccio che prediligono è per creare una sottoclasse di TypeFilterAttribute, assegnargli un nome appropriato e inserire l'implementazione del filtro in una classe privata all'interno di questo attributo. Figura 2 illustra questo approccio. Il lavoro effettivo viene eseguito dalla classe ValidateAuthorExistsFilterImpl privata, il cui tipo viene passato nel costruttore del TypeFilterAttribute.

Figura 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();
    }
  }
}

Si noti che l'attributo dispone di accesso per gli argomenti passati all'azione, come parte del parametro ActionExecutingContext. In questo modo il filtro controllare se è presente un parametro di id e ottenere il relativo valore prima di verificare l'esistenza di un autore con tale ID. Si noterà inoltre che ValidateAuthorExistsFilterImpl privato è un filtro asincrono. Con questo modello, esiste un solo metodo per implementare e lavoro può essere eseguito prima o dopo l'azione viene eseguita eseguendolo prima o dopo la chiamata successiva. Tuttavia, se si sta corto circuito di filtro impostando un contesto. Risultato, è necessario restituire senza chiamare successivamente (in caso contrario si otterrà un'eccezione).

Un altro aspetto da tenere presente sui filtri è che non includono qualsiasi stato a livello di oggetto, ad esempio un campo in un IActionFilter (in particolare quello implementato come un attributo) che ha impostato durante OnActionExecuting e quindi letti o modificati in OnActionExecuted. Se si rileva la necessità di eseguire questo tipo di logica, è possibile evitare questo tipo di stato passando a un IAsyncActionFilter, che è possibile utilizzare semplicemente le variabili locali all'interno del metodo OnActionExecutionAsync.

Dopo lo spostamento di convalida del modello e la verifica dell'esistenza di record all'interno di azioni del controller ai filtri comuni, ciò che è stato l'effetto sul mio controller? Per il confronto, Figura 3 Mostra Authors2Controller, che esegue la stessa logica AuthorsController, ma si basa su questi due filtri per il relativo comportamento dei criteri comuni.

Figura 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();
  }
}

Notare due cose su questo controller di refactoring. Innanzitutto, è più breve e più chiara. In secondo luogo, non esistono condizionali in uno dei metodi. La logica comune dell'API è stato completamente estratto in, i filtri vengono applicati dove appropriato, in modo che il lavoro del controller è semplice come possibili.

Ma è possibile testare il?

Lo spostamento logica dal controller di in attributi è ideale per ridurre la complessità di codice e applica il comportamento di runtime coerente. Sfortunatamente, se si eseguono unit test direttamente con i metodi di azione, i test non verranno hanno il comportamento di attributo o un filtro applicato. Per motivi strutturali e ovviamente poter eseguire unit test dei filtri indipendenti dei singoli metodi di azione per assicurare che funzionino come previsto. Ma cosa accade se è necessario per garantire non solo che i filtri di lavoro, ma che è correttamente configurati e applicate a singoli metodi di azione? Che cosa accade se si desidera effettuare il refactoring del codice API si dispone già di sfruttare i vantaggi dei filtri che ho appena descritto, e si desidera che l'API continuerà a funzionare correttamente quando hai finito? Che viene chiamato per il test di integrazione. Fortunatamente, ASP.NET di base include un ampio supporto per il test di integrazione semplice e rapido.

Applicazione di esempio è configurato per utilizzare un DbContext di Core in memoria Entity Framework, ma anche se utilizza SQL Server, è possibile passare facilmente a tramite un archivio in memoria per il test di integrazione. Questo è importante, perché notevolmente migliora la velocità delle prove e risulta molto più semplice per l'impostazione, poiché non è necessaria alcuna infrastruttura.

La classe che la maggior parte del lavoro sporco per l'integrazione di test in ASP.NET di base è la classe TestServer, disponibile nel pacchetto Microsoft.AspNetCore.TestHost. Configurare TestServer identico a come configurare app Web nel punto di ingresso Program.cs, utilizzando un WebHostBuilder. Per i miei test, si sceglie di usare la stessa classe di avvio come l'applicazione Web di esempio e Sto specificando che venga eseguito nell'ambiente di Testing. Alcuni dati di esempio verrà attivata quando viene avviato il sito:

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

In questo caso, il client è un System.Net.Http.HttpClient standard, che consente di effettuare richieste al server come se fosse sulla rete. Ma, poiché tutte le richieste vengono effettuate in memoria, i test sono estremamente veloce e affidabile.

Per i test, ho utilizzato xUnit, che include la possibilità di eseguire più test con diversi set di dati per un metodo di test specificato. Per verificare che il AuthorsController Authors2Controller entrambe le classi e si comportano in modo identico, sto utilizzando questa funzionalità per specificare entrambi i controller per ogni test. Figura 4 illustra alcuni test del metodo Put.

Figura 4 autori Put test

[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();
}

Si noti che questi test di integrazione non richiedono un database o una connessione Internet o un server Web in esecuzione. Sono quasi come rapido e semplice come unit test, ma più importante, consentono di testare le applicazioni ASP.NET tramite la pipeline di richieste intero, non solo come metodo di tipo isolato all'interno di una classe controller. Consiglio ancora scrivere unit test in cui è possibile, eseguire il fallback al test di integrazione per il comportamento è possibile eseguire unit test, ma è utile disporre di un modo ad alte prestazioni per l'esecuzione di test di integrazione in ASP.NET di base.

Passaggi successivi

I filtri sono un argomento molto esteso, avevo solo lo spazio per un paio di esempi in questo articolo. È possibile estrarre la documentazione ufficiale su docs.asp.net per ulteriori informazioni su filtri e test sulle applicazioni ASP.NET di base.

Il codice sorgente per questo esempio è disponibile all'indirizzo bit.ly/1sJruw6.


Steve Smith è un formatore indipendente, mentore e consulente, nonché un MVP di ASP.NET.  Ha contribuito decine di articoli per la documentazione ufficiale di ASP.NET di base (docs.asp.net) e consente ai team di accedere rapidamente a lavorare con ASP.NET di base. Contattarlo all'indirizzo ardalis.com e seguirlo su Twitter: noto anche come @ardalis.


Grazie all'esperto tecnico Microsoft seguente per la revisione di questo articolo: Doug granpavesi
Doug granpavesi è uno sviluppatore che lavora nel team di ASP.Net in Microsoft.