Devolver el código de estado http del controlador Web Api

219

Estoy tratando de devolver un código de estado de 304 no modificado para un método GET en un controlador de API web.

La única forma en que tuve éxito fue algo como esto:

public class TryController : ApiController
{
    public User GetUser(int userId, DateTime lastModifiedAtClient)
    {
        var user = new DataEntities().Users.First(p => p.Id == userId);
        if (user.LastModified <= lastModifiedAtClient)
        {
             throw new HttpResponseException(HttpStatusCode.NotModified);
        }
        return user;
    }
}

El problema aquí es que no es una excepción, simplemente no se modifica, por lo que el caché del cliente está bien. También quiero que el tipo de retorno sea un Usuario (como se muestra en todos los ejemplos de API web con GET), no devolver HttpResponseMessage o algo así.

ozba
fuente
¿Estás usando betao construcción nocturna ?
Aliostad
@Aliostad estoy usando beta
ozba
Entonces, ¿qué hay de malo en volver new HttpResponseMessage(HttpStatusCode.NotModified)? ¿No funciona?
Aliostad
@Aliostad No puedo devolver HttpResponseMessage cuando el tipo de retorno es Usuario, no se está compilando (obviamente).
ozba

Respuestas:

251

No sabía la respuesta, así que pregunté al equipo de ASP.NET aquí .

Entonces, el truco es cambiar la firma HttpResponseMessagey usarla Request.CreateResponse.

[ResponseType(typeof(User))]
public HttpResponseMessage GetUser(HttpRequestMessage request, int userId, DateTime lastModifiedAtClient)
{
    var user = new DataEntities().Users.First(p => p.Id == userId);
    if (user.LastModified <= lastModifiedAtClient)
    {
         return new HttpResponseMessage(HttpStatusCode.NotModified);
    }
    return request.CreateResponse(HttpStatusCode.OK, user);
}
Aliostad
fuente
3
No se compila en la versión beta de ASP.NET MVC 4, ya que CreateResponse solo toma el código de estado como parámetro. en segundo lugar, quería una solución sin HttpResponseMessage como valor de retorno, ya que está en desuso: aspnetwebstack.codeplex.com/discussions/350492
ozba
55
En caso de que alguien lo necesite, sería obtener el valor del método del controlador GetUser(request, id, lastModified).TryGetContentValue(out user), donde user(en el caso del ejemplo) es un Userobjeto.
Grinn
44
¿Sigue siendo el método preferido en 2015? MVC 5?
aplastar
44
La versión más moderna devuelve IHttpActionResult - no HttpResponseMessage (2017)
niico
8
Para agregar a la sugerencia de niico, cuando el tipo de retorno es IHttpActionResulty desea devolver el Usuario, simplemente puede hacerlo return Ok(user). Si necesita devolver otro código de estado (por ejemplo, prohibido), puede hacerlo return this.StatusCode(HttpStatusCode.Forbidden).
Dibujó el
68

También puede hacer lo siguiente si desea conservar la firma de la acción como Usuario recurrente:

public User GetUser(int userId, DateTime lastModifiedAtClient) 

Si desea devolver algo distinto de 200eso, debe incluir un mensaje HttpResponseExceptionen su acción y pasar el HttpResponseMessagemensaje que desea enviar al cliente.

Henrik Frystyk Nielsen
fuente
99
Esta es una solución mucho más elegante (aunque respuesta incompleta). ¿Por qué todos prefieren hacerlo de la manera difícil?
nagytech
44
@Geoist stackoverflow.com/questions/1282252/… . Lanzar una excepción es costoso.
tia
10
Sí, si está diseñando una API ocupada, usar una excepción para comunicar el caso más común NotModifiedes realmente un desperdicio. Si todas sus API hicieron esto, entonces su servidor convertirá principalmente vatios a excepciones.
Luke Puplett el
2
@nagytech porque no puede devolver un mensaje de error personalizado si arroja un error (como una respuesta 400) ... también arrojar excepciones es una tontería para algo que espera que haga el código. Caro y se registrará cuando no sea necesario que lo sean. En realidad no son excepciones.
Rocklan
40

En MVC 5, las cosas se pusieron más fáciles:

return new StatusCodeResult(HttpStatusCode.NotModified, this);
Jon Bates
fuente
3
¿No se puede especificar un mensaje?
aplastar
1
Usar un mensaje es en realidad la respuesta aceptada. Esto es solo un poco terser
Jon Bates
39

Cambie el método de la API GetXxx para devolver HttpResponseMessage y luego devolver una versión con tipo para la respuesta completa y la versión sin tipo para la respuesta NotModified.

    public HttpResponseMessage GetComputingDevice(string id)
    {
        ComputingDevice computingDevice =
            _db.Devices.OfType<ComputingDevice>()
                .SingleOrDefault(c => c.AssetId == id);

        if (computingDevice == null)
        {
            return this.Request.CreateResponse(HttpStatusCode.NotFound);
        }

        if (this.Request.ClientHasStaleData(computingDevice.ModifiedDate))
        {
            return this.Request.CreateResponse<ComputingDevice>(
                HttpStatusCode.OK, computingDevice);
        }
        else
        {
            return this.Request.CreateResponse(HttpStatusCode.NotModified);
        }
    }

* Los datos de ClientHasStale son mi extensión para verificar los encabezados ETag e IfModifiedSince.

El marco MVC aún debe serializar y devolver su objeto.

NOTA

Creo que la versión genérica se está eliminando en alguna versión futura de la API web.

Luke Puplett
fuente
44
Esta fue la respuesta exacta que estaba buscando, aunque como un tipo de retorno Task <HttpResponseMessage <T>>. ¡Gracias!
xeb
1
@xeb: sí, vale la pena llamarlo. Más información sobre async aquí asp.net/mvc/tutorials/mvc-4/…
Luke Puplett
14

Odio encontrar artículos viejos, pero este es el primer resultado para esto en la búsqueda de Google y tuve un gran problema con este problema (incluso con el apoyo de ustedes). Así que aquí no pasa nada ...

Espero que mi solución ayude a aquellos que también estaban confundidos.

namespace MyApplication.WebAPI.Controllers
{
    public class BaseController : ApiController
    {
        public T SendResponse<T>(T response, HttpStatusCode statusCode = HttpStatusCode.OK)
        {
            if (statusCode != HttpStatusCode.OK)
            {
                // leave it up to microsoft to make this way more complicated than it needs to be
                // seriously i used to be able to just set the status and leave it at that but nooo... now 
                // i need to throw an exception 
                var badResponse =
                    new HttpResponseMessage(statusCode)
                    {
                        Content =  new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "application/json")
                    };

                throw new HttpResponseException(badResponse);
            }
            return response;
        }
    }
}

y luego simplemente heredar de BaseController

[RoutePrefix("api/devicemanagement")]
public class DeviceManagementController : BaseController
{...

y luego usándolo

[HttpGet]
[Route("device/search/{property}/{value}")]
public SearchForDeviceResponse SearchForDevice(string property, string value)
{
    //todo: limit search property here?
    var response = new SearchForDeviceResponse();

    var results = _deviceManagementBusiness.SearchForDevices(property, value);

    response.Success = true;
    response.Data = results;

    var statusCode = results == null || !results.Any() ? HttpStatusCode.NoContent : HttpStatusCode.OK;

    return SendResponse(response, statusCode);
}
Kenneth Garza
fuente
1
Brillante. Me ahorró un montón de tiempo.
gls123
10

.net core 2.2 que devuelve el código de estado 304. Esto está usando un ApiController.

    [HttpGet]
    public ActionResult<YOUROBJECT> Get()
    {
        return StatusCode(304);
    }

Opcionalmente, puede devolver un objeto con la respuesta

    [HttpGet]
    public ActionResult<YOUROBJECT> Get()
    {
        return StatusCode(304, YOUROBJECT); 
    }
Ives.me
fuente
7

Para ASP.NET Web Api 2, esta publicación de MS sugiere cambiar el tipo de retorno del método a IHttpActionResult. A continuación, puede volver construido en IHttpActionResultla aplicación como Ok, BadRequest, etc ( ver aquí ) o devolver su propia implementación.

Para su código, se podría hacer así:

public IHttpActionResult GetUser(int userId, DateTime lastModifiedAtClient)
{
    var user = new DataEntities().Users.First(p => p.Id == userId);
    if (user.LastModified <= lastModifiedAtClient)
    {
        return StatusCode(HttpStatusCode.NotModified);
    }
    return Ok(user);
}
datchung
fuente
3

Otra opción:

return new NotModified();

public class NotModified : IHttpActionResult
{
    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = new HttpResponseMessage(HttpStatusCode.NotModified);
        return Task.FromResult(response);
    }
}
Bora Aydın
fuente
2
public HttpResponseMessage Post(Article article)
{
    HttpResponseMessage response = Request.CreateResponse<Article>(HttpStatusCode.Created, article);

    string uriToTheCreatedItem = Url.Route(null, new { id = article.Id });
    response.Headers.Location = new Uri(Request.RequestUri, uriToTheCreatedItem);

    return response;
}
Jo Smo
fuente
2

Si necesita devolver un IHttpActionResult y desea devolver el código de error más un mensaje, use:

return ResponseMessage(Request.CreateErrorResponse(HttpStatusCode.NotModified, "Error message here"));
Chris Halcrow
fuente
2

No me gusta tener que cambiar mi firma para usar el tipo HttpCreateResponse, por lo que se me ocurrió una pequeña solución extendida para ocultar eso.

public class HttpActionResult : IHttpActionResult
{
    public HttpActionResult(HttpRequestMessage request) : this(request, HttpStatusCode.OK)
    {
    }

    public HttpActionResult(HttpRequestMessage request, HttpStatusCode code) : this(request, code, null)
    {
    }

    public HttpActionResult(HttpRequestMessage request, HttpStatusCode code, object result)
    {
        Request = request;
        Code = code;
        Result = result;
    }

    public HttpRequestMessage Request { get; }
    public HttpStatusCode Code { get; }
    public object Result { get; }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult(Request.CreateResponse(Code, Result));
    }
}

Luego puede agregar un método a su ApiController (o mejor a su controlador base) como este:

protected IHttpActionResult CustomResult(HttpStatusCode code, object data) 
{
    // Request here is the property on the controller.
    return new HttpActionResult(Request, code, data);
}

Luego puede devolverlo como cualquiera de los métodos integrados:

[HttpPost]
public IHttpActionResult Post(Model model)
{
    return model.Id == 1 ?
                Ok() :
                CustomResult(HttpStatusCode.NotAcceptable, new { 
                    data = model, 
                    error = "The ID needs to be 1." 
                });
}
krillgar
fuente
0

Una actualización de @Aliostads responde usando el modo más moderno IHttpActionResultintroducido en Web API 2.

https://docs.microsoft.com/en-us/aspnet/web-api/overview/getting-started-with-aspnet-web-api/action-results#ihttpactionresult

public class TryController : ApiController
{
    public IHttpActionResult GetUser(int userId, DateTime lastModifiedAtClient)
    {
        var user = new DataEntities().Users.First(p => p.Id == userId);
        if (user.LastModified <= lastModifiedAtClient)
        {
            return StatusCode(HttpStatusCode.NotModified);
            // If you would like to return a Http Status code with any object instead:
            // return Content(HttpStatusCode.InternalServerError, "My Message");
        }
        return Ok(user);
    }
}
Ogglas
fuente
0

Prueba esto :

return new ContentResult() { 
    StatusCode = 404, 
    Content = "Not found" 
};
don_mega
fuente