c# - net - ¿Cómo establecer encabezados personalizados cuando se usa IHttpActionResult?
web api rest c# (6)
En ASP.NET Web API 2, IHttpActionResult
ofrece mucho valor para simplificar el código del controlador y soy reacio a dejar de usarlo, pero me he topado con un problema.
Necesito establecer un ETag en una respuesta saliente, y no puedo encontrar ninguna propiedad que me dé acceso a los encabezados de la respuesta. En este momento estoy usando el método de ayuda Ok<T>(T content)
del ApiController
, que devuelve un objeto OkNegotiatedContentResult<T>
. Eso no parece tener nada expuesto que me permita modificar los encabezados.
¿Me estoy perdiendo algo, o realmente no hay forma de hacerlo mientras IHttpActionResult
tipos stock IHttpActionResult
? Consideré un manejador de mensajes, pero luego tendría que descubrir cómo pasar el ETag fuera de la acción (los ETags se generan de manera diferente para diferentes acciones, por lo que no se trata de crear un controlador genérico para todas las acciones).
Me gustaría evitar tener que usar el HttpResponseMessage crudo, pero por el momento eso parece difícil.
Aquí está mi implementación simple sin ActionFilterAttributes y es similar a la respuesta de AlexACD. Mi solución usa ResponseMessageResult que implementa la interfaz IHttpActionResult.
HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
responseMessage.Headers.Add("Headername", "Value");
ResponseMessageResult response = new ResponseMessageResult(responseMessage);
return response;
Aquí hay una solución que uso en mi código común de la biblioteca Web API 2 que puede soportar fácilmente establecer cualquier encabezado, o cualquier otra propiedad en el HttpResponseMessage
provisto en ExecuteAsync
sin estar vinculado a ninguna implementación derivada de NegotiatedContentResult
específica:
public class FlexibleNegotiatedContentResult<T> : NegotiatedContentResult<T>
{
private readonly Action<HttpResponseMessage> _responseMessageDelegate;
public FlexibleNegotiatedContentResult(HttpStatusCode statusCode, T content, IContentNegotiator contentNegotiator, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters)
: base(statusCode, content, contentNegotiator, request, formatters)
{
}
public FlexibleNegotiatedContentResult(HttpStatusCode statusCode, T content, ApiController controller, Action<HttpResponseMessage> responseMessageDelegate = null)
: base(statusCode, content, controller)
{
_responseMessageDelegate = responseMessageDelegate;
}
public override async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
HttpResponseMessage responseMessage = await base.ExecuteAsync(cancellationToken);
if (_responseMessageDelegate != null)
{
_responseMessageDelegate(responseMessage);
}
return responseMessage;
}
}
y un uso de ejemplo:
new FlexibleNegotiatedContentResult<string>(HttpStatusCode.Created, "Entity created!", controller, response => response.Headers.Location = new Uri("https://myapp.com/api/entity/1"));
Esto se puede lograr con un ActionFilterAttribute, que examinará la respuesta después de la función del controlador pero antes de que se apague, luego puede establecer el atributo en el método del controlador para agregar esta información, aquí está mi implementación a continuación:
public class EnableETag : ActionFilterAttribute
{
/// <summary>
/// NOTE: a real production situation, especially when it involves a web garden
/// or a web farm deployment, the tags must be retrieved from the database or some other place common to all servers.
/// </summary>
private static ConcurrentDictionary<string, EntityTagHeaderValue> etags = new ConcurrentDictionary<string, EntityTagHeaderValue>();
public override void OnActionExecuting(HttpActionContext context)
{
var request = context.Request;
if (request.Method == HttpMethod.Get)
{
var key = GetKey(request);
ICollection<EntityTagHeaderValue> etagsFromClient = request.Headers.IfNoneMatch;
if (etagsFromClient.Count > 0)
{
EntityTagHeaderValue etag = null;
if (etags.TryGetValue(key, out etag) && etagsFromClient.Any(t => t.Tag == etag.Tag))
{
context.Response = new HttpResponseMessage(HttpStatusCode.NotModified);
SetCacheControl(context.Response);
}
}
}
}
public override void OnActionExecuted(HttpActionExecutedContext context)
{
var request = context.Request;
var key = GetKey(request);
EntityTagHeaderValue etag;
if (!etags.TryGetValue(key, out etag) || request.Method == HttpMethod.Put ||
request.Method == HttpMethod.Post)
{
etag = new EntityTagHeaderValue("/"" + Guid.NewGuid().ToString() + "/"");
etags.AddOrUpdate(key, etag, (k, val) => etag);
}
context.Response.Headers.ETag = etag;
SetCacheControl(context.Response);
}
private string GetKey(HttpRequestMessage request)
{
return request.RequestUri.ToString();
}
/// <summary>
/// Defines the time period to hold item in cache (currently 10 seconds)
/// </summary>
/// <param name="response"></param>
private void SetCacheControl(HttpResponseMessage response)
{
response.Headers.CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(10),
MustRevalidate = true,
Private = true
};
}
}
}
Para su escenario, necesitaría crear un IHttpActionResult
personalizado. A continuación se muestra un ejemplo en el que se deriva de OkNegotiatedContentResult<T>
ya que ejecuta Content-Negotiation
y establece el código de estado Ok
.
public class CustomOkResult<T> : OkNegotiatedContentResult<T>
{
public CustomOkResult(T content, ApiController controller)
: base(content, controller) { }
public CustomOkResult(T content, IContentNegotiator contentNegotiator, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters)
: base(content, contentNegotiator, request, formatters) { }
public string ETagValue { get; set; }
public override async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
HttpResponseMessage response = await base.ExecuteAsync(cancellationToken);
response.Headers.ETag = new EntityTagHeaderValue(this.ETagValue);
return response;
}
}
Controlador :
public class ValuesController : ApiController
{
public IHttpActionResult Get()
{
return new CustomOkResult<string>(content: "Hello World!", controller: this)
{
ETagValue = "You ETag value"
};
}
}
Tenga en cuenta que también puede derivar de NegotiatedContentResult<T>
, en cuyo caso deberá proporcionar el código de estado usted mismo. Espero que esto ayude.
Puedes encontrar el código fuente de OkNegotiatedContentResult<T>
y NegotiatedContentResult<T>
, que como puedes imaginar son realmente simples.
Puede crear un HttpResponseMessage
, agregar encabezados según sea necesario y luego crear ResponseMessageResult
desde él:
HttpResponseMessage response =new HttpResponseMessage(HttpStatusCode.OK);
response.Headers.Add("MyHeader", "MyHeaderValue");
return ResponseMessage(response);
public static class HttpExtentions
{
public static IHttpActionResult AddHeader(this IHttpActionResult action,
string headerName, IEnumerable<string> headerValues)
{
return new HeaderActionResult(action, headerName, headerValues);
}
public static IHttpActionResult AddHeader(this IHttpActionResult action,
string headerName, string header)
{
return AddHeader(action, headerName, new[] {header});
}
private class HeaderActionResult : IHttpActionResult
{
private readonly IHttpActionResult action;
private readonly Tuple<string, IEnumerable<string>> header;
public HeaderActionResult(IHttpActionResult action, string headerName,
IEnumerable<string> headerValues)
{
this.action = action;
header = Tuple.Create(headerName, headerValues);
}
public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
var response = await action.ExecuteAsync(cancellationToken);
response.Headers.Add(header.Item1, header.Item2);
return response;
}
}
}