ASP.NET Core – Come aggiungere il proprio filtro di azione

ASP.NET Core – Come aggiungere il proprio filtro di azione

I filtri di azione ti consentono di esaminare le richieste subito prima che vengano instradate a un metodo di azione (e le risposte subito dopo che sono state restituite dal metodo di azione).

Il modo più semplice per aggiungere il proprio filtro di azione in ASP.NET Core consiste nel sottoclasse ActionFilterAttribute e quindi sovrascrivere i metodi appropriati a seconda se si desidera esaminare la richiesta, il risultato o entrambi.

Ecco un esempio che sovrascrive OnActionExecuting() in modo che possa esaminare la richiesta:

using Microsoft.AspNetCore.Mvc.Filters;

public class RequestLogger : ActionFilterAttribute
{
	public override void OnActionExecuting(ActionExecutingContext context)
	{
		Console.WriteLine($"Request {context.HttpContext.Request.Method} {context.HttpContext.Request.Path} routed to {context.Controller.GetType().Name}");

		base.OnActionExecuting(context);
	}
}
Code language: C# (cs)

Quindi applica il filtro dell'azione a metodi di azione e controller specifici oppure applicalo a tutti i controller. Questo lo sta aggiungendo a un metodo di azione specifico:

[ApiController]
[Route("[controller]")]
public class HealthStatusController : ControllerBase
{
	[HttpGet()]
	[RequestLogger()]
	public IActionResult Get()
	{
		return Ok();
	}
}
Code language: C# (cs)

Quando arriva una richiesta, passa attraverso questo filtro di azione RequestLogger e lo invia alla console:

Request GET /healthstatus/ routed to HealthStatusControllerCode language: plaintext (plaintext)

In questo articolo, mostrerò come applicare filtri di azione ai tre diversi livelli (azione, controller e globale). Spiegherò come il framework crea istanze di filtro delle azioni per impostazione predefinita (e come utilizzare invece la registrazione dell'attivazione del tipo quando è necessario il supporto per la sicurezza del thread o l'inserimento delle dipendenze). Alla fine, mostrerò diversi esempi di filtri di azioni personalizzati.

Applica un filtro di azione ai diversi livelli:azione, controller e globale

Puoi applicare filtri di azione a uno o più metodi di azione specifici:

[HttpGet()]
[RequestLogger()]
public IActionResult Get()
Code language: C# (cs)

Puoi aggiungere il filtro delle azioni al controller per applicarlo a tutte le azioni nel controller:

[ApiController]
[Route("[controller]")]
[RequestLogger()]
public class HealthStatusController : ControllerBase
{	
	[HttpGet()]
	public IActionResult Get()
	{
		return Ok();
	}

	[HttpPost("SetResponse/{status}")]
	public IActionResult SetResponse(HealthStatus status)
	{
		return Ok();
	}
}
Code language: C# (cs)

Infine, puoi applicarlo a livello globale aggiungendolo in Startup.ConfigureServices:

public class Startup
{
	//rest of class
	
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddControllers(options => options.Filters.Add(new RequestLogger()));

		//rest of method
	}
}
Code language: C# (cs)

Aggiungendolo a livello globale si applica a tutti i metodi di azione in tutti i controller. Nota:l'unico motivo per utilizzare un filtro azione globale invece di una funzione middleware è se sono necessarie le informazioni fornite dal contesto dell'azione (ad esempio quale controller verrà utilizzato).

Come il framework crea istanze del filtro delle azioni

Normalmente quando si aggiungono servizi in ASP.NET Core, è necessario registrarlo e specificare se si tratta di un singleton, transitorio o con ambito. Con i filtri azione, aggiungi semplicemente l'attributo del filtro azione (ad esempio [SomeActionFilter]) o aggiungi il filtro globale usando new().

Quando si utilizza questo approccio di registrazione predefinito, il framework crea una singola istanza per registrazione. Ciò comporta l'utilizzo della stessa istanza per più richieste, il che può causare problemi se non sei a conoscenza di questo comportamento.

Per illustrare questo punto, considera la seguente classe di filtro azione che registra il suo ID istanza:

public class RequestLogger : ActionFilterAttribute
{
	public readonly string Id = Guid.NewGuid().ToString();
	public override void OnActionExecuting(ActionExecutingContext context)
	{
		Console.WriteLine($"Id={Id} Request {context.HttpContext.Request.Method} {context.HttpContext.Request.Path} routed to {context.Controller.GetType().Name}");

		base.OnActionExecuting(context);
	}
}
Code language: C# (cs)

Ora applicalo a più metodi di azione:

[HttpGet()]
[RequestLogger()]
public IActionResult Get()
{
	return Ok();
}

[HttpPost("SetResponse/{status}")]
[RequestLogger()]
public ActionResult SetResponse(HealthStatus status)
{
	return Ok();
}
Code language: C# (cs)

Ora invia più richieste GET:

Id=ea27a176-6a1f-4a25-bd26-6c5b865e2844 Request GET /healthstatus/ routed to HealthStatusController
Id=ea27a176-6a1f-4a25-bd26-6c5b865e2844 Request GET /healthstatus/ routed to HealthStatusControllerCode language: plaintext (plaintext)

Notare che l'ID è lo stesso. Ciò è dovuto al fatto che per il metodo di azione Get() viene utilizzata una singola istanza del filtro dell'azione RequestLogger.

Ora invia più richieste POST:

Id=7be936e7-d147-42af-9316-834cbfb9adb3 Request POST /healthstatus/setresponse/healthy routed to HealthStatusController
Id=7be936e7-d147-42af-9316-834cbfb9adb3 Request POST /healthstatus/setresponse/healthy routed to HealthStatusControllerCode language: plaintext (plaintext)

Si noti che l'id è lo stesso per due richieste POST, ma è diverso dall'id mostrato per le richieste GET. Questo perché viene creata un'istanza per registrazione ([RequestLogger] è stato registrato sui metodi GET e POST, quindi due istanze).

Poiché più richieste utilizzano la stessa istanza, non è thread-safe. Questo è un problema solo se il tuo filtro azione ha campi di istanza/dati condivisi. Per risolvere questo problema, puoi invece utilizzare la registrazione dell'attivazione del tipo (mostrata di seguito).

Utilizzare la registrazione dell'attivazione del tipo per la sicurezza dei thread e l'inserimento delle dipendenze

L'utilizzo dell'attivazione del tipo risolve due problemi con i filtri di azione:

  • Crea una nuova istanza per richiesta, quindi i tuoi filtri di azione possono avere campi di istanza senza che siano thread non sicuri.
  • Ti permette di inserire le dipendenze nel filtro delle azioni.

Per eseguire la registrazione dell'attivazione del tipo, prima aggiungi il filtro azione come servizio in Startup.ConfigureServices():

public class Startup
{
	//rest of class
	
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddScoped<RequestLogger>();
		
		//rest of method
	}
}
Code language: C# (cs)

Quindi, invece di applicare direttamente il filtro dell'azione, utilizza l'attributo [ServiceFilter] e il tipo di filtro dell'azione:

[HttpGet()]
[ServiceFilter(typeof(RequestLogger))]
public IActionResult Get()
{
	return Ok();
}
Code language: C# (cs)

Nota:se stai registrando il filtro azione a livello globale, passa il tipo del filtro azione invece di usare new(), in questo modo:services.AddControllers(options => options.Filters.Add(typeof(RequestLogger) ));

Ora quando le richieste GET vengono inviate, puoi vedere che gli ID sono diversi (perché ci sono più istanze del filtro azione):

Id=233a93b7-99e9-43c1-adfc-4299ff9ac47c Request GET /healthstatus/ routed to HealthStatusController
Id=cbb02112-651c-475e-84e3-de8775387ceb Request GET /healthstatus/ routed to HealthStatusControllerCode language: plaintext (plaintext)

Esegui l'override di OnResultExecuted se desideri controllare HttpContext.Response

Quando un metodo di azione viene eseguito, restituisce un oggetto risultato (come BadRequestResult). Il framework deve eseguire questo risultato per popolare HttpContext.Response. Questo viene fatto dopo OnActionExecuted. Ecco perché se provi a controllare HttpContext.Response in OnActionExecuted, non avrà i valori corretti.

Per controllare l'HttpContext.Response popolato, puoi sovrascrivere OnResultExecuted (o OnResultExecutionAsync).

Ecco un esempio che mostra la differenza tra OnActionExecuted e OnResultExecuted:

public override void OnActionExecuted(ActionExecutedContext context)
{
	Console.WriteLine($"Action executed. Response.StatusCode={context.HttpContext.Response.StatusCode}");
	base.OnActionExecuted(context);
}
public override void OnResultExecuted(ResultExecutedContext context)
{
	Console.WriteLine($"Result executed. Response.StatusCode={context.HttpContext.Response.StatusCode}"); 
	base.OnResultExecuted(context);
}
Code language: C# (cs)

Questo genera quanto segue:

Action executed. Response.StatusCode=200
Result executed. Response.StatusCode=400Code language: plaintext (plaintext)

Si noti che il codice di stato in OnActionExecuted è 200. Ciò è dovuto al fatto che BadRequestResult non è stato ancora eseguito. Quindi in OnResultExecuted il codice di stato è 400.

Esempio:richiedi un'intestazione personalizzata nella richiesta

Supponiamo che tu voglia richiedere alle richieste di avere un'intestazione personalizzata specifica per il metodo di azione.

Per applicarlo con un filtro azione, puoi sovrascrivere OnActionExecuting(), controllare l'intestazione della richiesta e impostare il contesto. Risultato:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

public class RequireCustomHeader : ActionFilterAttribute
{
	private readonly string RequiredHeader;
	public RequireCustomHeader(string requiredHeader)
	{
		RequiredHeader = requiredHeader;
	}
	public override void OnActionExecuting(ActionExecutingContext context)
	{
		if (!context.HttpContext.Request.Headers.ContainsKey(RequiredHeader))
		{
			context.Result = new ContentResult()
			{
				StatusCode = (int)System.Net.HttpStatusCode.BadRequest,
				Content = $"Missing required header - {RequiredHeader}"
			};
		}
	}
}
Code language: C# (cs)

Nota:l'impostazione del contesto.Result manda in cortocircuito la richiesta (salta i filtri delle azioni rimanenti e non la indirizza al metodo di azione).

Applicalo a un metodo di azione, passando il nome dell'intestazione della richiesta richiesta:

[ApiController]
[Route("[controller]")]
public class HealthStatusController : ControllerBase
{
	
	[HttpGet()]
	[RequireCustomHeader("HealthApiKey")]
	public IActionResult Get()
	{
		return Ok();
	}
}
Code language: C# (cs)

Quando una richiesta viene inviata senza l'intestazione HealthApiKey, restituisce:

Status: 400 - Bad Request
Body: Missing required header - HealthApiKeyCode language: plaintext (plaintext)

Esempio:aggiungi un'intestazione di risposta

Supponiamo che tu voglia aggiungere un'intestazione di risposta contenente informazioni di debug per aiutarti durante la risoluzione dei problemi della tua API web.

Per farlo con un filtro azione, sovrascrivi OnActionExecuted() e aggiungi l'intestazione della risposta personalizzata:

public class AddDebugInfoToResponse : ActionFilterAttribute
{
	public override void OnActionExecuted(ActionExecutedContext context)
	{
		context.HttpContext.Response.Headers.Add("DebugInfo", context.ActionDescriptor.DisplayName);

		base.OnActionExecuted(context);
	}
}
Code language: C# (cs)

Applica questo filtro di azione:

[ApiController]
[Route("[controller]")]
public class HealthStatusController : ControllerBase
{
	[HttpGet()]
	[AddDebugInfoToResponse()]
	public IActionResult Get()
	{
		return Ok();
	}
}
Code language: C# (cs)

Quando una richiesta viene inviata, restituisce una risposta con le seguenti intestazioni:

Content-Length=0
Date=Tue, 26 Oct 2021 20:31:55 GMT
DebugInfo=WebApi.Controllers.HealthStatusController.Get (WebApi)
Server=Kestrel
Code language: plaintext (plaintext)

Esempio:traccia la durata dell'azione

Supponiamo che tu voglia restituire il tempo trascorso del metodo di azione in un'intestazione di risposta a scopo di monitoraggio.

Il modo più semplice per farlo con un filtro azione è sovrascrivere OnActionExecutionAsync(), utilizzare un cronometro e attendere il metodo di azione:

public class LogStats : ActionFilterAttribute
{
	public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
	{
		var stopwatch = Stopwatch.StartNew();

		var actionExecutedContext = await next();

		stopwatch.Stop();

		actionExecutedContext.HttpContext.Response.Headers.Add("Stats", stopwatch.Elapsed.ToString());
	}
}
Code language: C# (cs)

Applica il filtro azione:

[ApiController]
[Route("[controller]")]
public class HealthStatusController : ControllerBase
{
	[HttpGet()]
	[LogStats()]
	public IActionResult Get()
	{
		return Ok();
	}
}
Code language: C# (cs)

Quando una richiesta viene inviata, restituisce un'intestazione con il tempo trascorso:

Content-Length=0
Date=Tue, 26 Oct 2021 20:45:33 GMT
Server=Kestrel
Stats=00:00:00.0000249
Code language: plaintext (plaintext)