如何使用错误消息或异常返回NotFound() IHttpActionResult?

126

当WebApi的GET操作未找到内容时,我将返回一个NotFound IHttpActionResult。除此之外,我想发送自定义消息和/或异常消息(如果有)。当前的ApiControllerNotFound()方法不提供传递消息的重载选项。

有没有什么办法可以做到这一点?还是我必须编写自己的自定义IHttpActionResult


你想为所有的“未找到”结果返回相同的消息吗? - Nikolai Samteladze
@NikolaiSamteladze 不,这取决于情况可能会有不同的消息。 - Ajay Jadhav
13个回答

284

以下是一行代码,用于返回一个具有简单消息的IHttpActionResult NotFound:

return Content(HttpStatusCode.NotFound, "Foo does not exist.");

26
大家应该投赞成票给这个答案,它简单易懂且不错! - Jess
3
请注意,该解决方案不会将HTTP头状态设置为“404 Not Found”。 - Kasper Jensen
7
@KasperHalvasJensen 从服务器返回的HTTP状态码为404,你需要更多信息吗? - Anthony F
6
你说得对。我之前使用了 Controller.Content(...),应该使用 ApiController.Content(...) - 我的错误。 - Kasper Jensen
谢谢,伙计。这正是我在寻找的。 - Kaptein Babbalas
这个答案应该有更多的赞,传递的对象仍然被序列化。 功能非常好。 - DiamondDrake

89

如果您想自定义响应消息的格式,您需要编写自己的操作结果。

我们希望为一些常见的响应消息形状提供开箱即用的支持,例如简单的空404,但我们也希望保持这些结果尽可能简单;使用操作结果的主要优点之一是它使您的操作方法更容易进行单元测试。我们在操作结果上放置的属性越多,您的单元测试需要考虑的事情就越多,以确保操作方法正在按预期运行。

通常我希望能够提供自定义消息的功能,因此请随时向我们记录错误以考虑在未来版本中支持该操作结果: https://aspnetwebstack.codeplex.com/workitem/list/advanced

不过,操作结果的一个好处是如果您想做一些略微不同的事情,那么您总是可以很容易地编写自己的操作结果。以下是在您的情况下如何实现它的方式(假设您希望错误消息采用text/plain格式;如果您想使用JSON,则需要使用不同的内容稍微修改一下):

public class NotFoundTextPlainActionResult : IHttpActionResult
{
    public NotFoundTextPlainActionResult(string message, HttpRequestMessage request)
    {
        if (message == null)
        {
            throw new ArgumentNullException("message");
        }

        if (request == null)
        {
            throw new ArgumentNullException("request");
        }

        Message = message;
        Request = request;
    }

    public string Message { get; private set; }

    public HttpRequestMessage Request { get; private set; }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute());
    }

    public HttpResponseMessage Execute()
    {
        HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.NotFound);
        response.Content = new StringContent(Message); // Put the message in the response body (text/plain content).
        response.RequestMessage = Request;
        return response;
    }
}

public static class ApiControllerExtensions
{
    public static NotFoundTextPlainActionResult NotFound(this ApiController controller, string message)
    {
        return new NotFoundTextPlainActionResult(message, controller.Request);
    }
}

然后,在您的操作方法中,您可以像这样执行:

public class TestController : ApiController
{
    public IHttpActionResult Get()
    {
        return this.NotFound("These are not the droids you're looking for.");
    }
}
如果您使用自定义控制器基类(而不是直接从ApiController继承),则还可以消除“this.”部分(在调用扩展方法时不幸是必需的):
如果您使用自定义控制器基类(而不是直接继承自ApiController),还可以消除“this.”部分(在调用扩展方法时不幸是必需的)。
public class CustomApiController : ApiController
{
    protected NotFoundTextPlainActionResult NotFound(string message)
    {
        return new NotFoundTextPlainActionResult(message, Request);
    }
}

public class TestController : CustomApiController
{
    public IHttpActionResult Get()
    {
        return NotFound("These are not the droids you're looking for.");
    }
}

1
我编写了一个与'IHttpActionResult'完全相似的实现,但不是针对'NotFound'结果。这可能适用于所有'HttpStatusCodes'。我的CustomActionResult代码看起来像这样我的控制器的'Get()'操作如下:'public IHttpActionResult Get() { return CustomNotFoundResult("Meessage to Return."); }'此外,我在CodePlex上记录了一个错误,以在将来的版本中考虑此问题。 - Ajay Jadhav
我使用ODataControllers,我不得不使用this.NotFound("blah"); - Jerther
1
非常好的帖子,但我想反对继承的建议。我的团队很久以前就决定这样做了,结果类变得非常臃肿。最近我将所有内容重构为扩展方法,并远离继承链。我强烈建议人们仔细考虑何时应该使用这种继承方式。通常,组合更好,因为它更加解耦。 - julealgon
7
这个功能本应该是开箱即用的。包括一个可选的“ResponseBody”参数不应影响单元测试。 - Theodore Zographos

29

如果您喜欢的话,可以使用 ResponseMessageResult

var myCustomMessage = "your custom message which would be sent as a content-negotiated response"; 
return ResponseMessage(
    Request.CreateResponse(
        HttpStatusCode.NotFound, 
        myCustomMessage
    )
);

如果你需要更短的版本,那么我想你需要实现自定义操作结果。


我选择了这种方法,因为它看起来很整洁。我只是在其他地方定义了自定义消息并缩进了返回代码。 - Andy
我喜欢这个比Content更好,因为它实际上返回一个对象,我可以解析它并且有一个像标准的BadRequest方法一样的Message属性。 - user1568891

8
您可以使用 HttpResponseMessage 类的 ReasonPhrase 属性。
catch (Exception exception)
{
  throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)
  {
    ReasonPhrase = exception.Message
  });
}

谢谢。好的...这应该可以工作,但是我将不得不在每个操作中自己构建HttpResponseException。为了使代码更简洁,我想知道是否可以使用任何WebApi 2功能(就像现成的**NotFount()Ok()**方法),并将ReasonPhrase消息传递给它。 - Ajay Jadhav
你可以创建自己的扩展方法NotFound(Exception exception),它将抛出正确的HttpResponseException。 - Dmytro Rudenko
@DmytroRudenko:引入操作结果是为了提高可测试性。如果在这里抛出HttpResponseException,您将会损害它。此外,在这里我们没有任何异常,但OP正在寻找发送消息的方法。 - Kiran
好的,如果您不想使用NUint进行测试,您可以编写自己的NotFoundResult实现,并重写其ExecuteAsync方法以返回您的消息数据。并将此类的实例作为您操作调用的结果返回。 - Dmytro Rudenko
1
请注意,现在您可以直接传递状态码,例如:HttpResponseException(HttpStatusCode.NotFound)。 - Mark Sowul
如果异常消息包含换行符,代码将无法正常工作。 - pcl

5

在 ASP.NET Core 中的一行代码:

Return StatusCode(404, "Not a valid request.");

4
您可以像d3m3t3er建议的那样创建自定义的协商内容结果。然而,我会继承。另外,如果您只需要返回NotFound,您不需要从构造函数初始化http状态。
public class NotFoundNegotiatedContentResult<T> : NegotiatedContentResult<T>
{
    public NotFoundNegotiatedContentResult(T content, ApiController controller)
        : base(HttpStatusCode.NotFound, content, controller)
    {
    }

    public override Task<HttpResponseMessage> ExecuteAsync(
        CancellationToken cancellationToken)
    {
        return base.ExecuteAsync(cancellationToken).ContinueWith(
            task => task.Result, cancellationToken);
    }
}

2
我需要在一个 IExceptionHandler 类的主体中创建一个 IHttpActionResult 实例,以设置 ExceptionHandlerContext.Result 属性。然而,我还想设置自定义的 ReasonPhrase

我发现可以使用 ResponseMessageResult 来包装一个 HttpResponseMessage(这允许轻松设置 ReasonPhrase)。

例如:

public class MyExceptionHandler : ExceptionHandler
{
    public override void Handle(ExceptionHandlerContext context)
    {
        var ex = context.Exception as IRecordNotFoundException;
        if (ex != null)
        {
            context.Result = new ResponseMessageResult(new HttpResponseMessage(HttpStatusCode.NotFound) { ReasonPhrase = $"{ex.EntityName} not found" });
        }
    }
}

2

我通过简单地派生OkNegotiatedContentResult并覆盖结果响应消息中的HTTP代码来解决了这个问题。该类允许您使用任何HTTP响应代码返回内容主体。

public class CustomNegotiatedContentResult<T> : OkNegotiatedContentResult<T>
{
    public HttpStatusCode HttpStatusCode;

    public CustomNegotiatedContentResult(
        HttpStatusCode httpStatusCode, T content, ApiController controller)
        : base(content, controller)
    {
        HttpStatusCode = httpStatusCode;
    }

    public override Task<HttpResponseMessage> ExecuteAsync(
        CancellationToken cancellationToken)
    {
        return base.ExecuteAsync(cancellationToken).ContinueWith(
            task => { 
                // override OK HTTP status code with our own
                task.Result.StatusCode = HttpStatusCode;
                return task.Result;
            },
            cancellationToken);
    }
}

2
另一个不错的选择是使用不同的内置结果类型:NotFoundObjectResult(message)

1
如果你继承自基类NegotitatedContentResult<T>,并且不需要转换你的content(例如,你只想返回一个字符串),那么你不需要重写ExecuteAsync方法。 你只需要提供一个适当的类型定义和一个构造函数,告诉基类要返回哪个HTTP状态码。其他所有的都会自动工作。
以下是NotFoundInternalServerError的示例:
public class NotFoundNegotiatedContentResult : NegotiatedContentResult<string>
{
    public NotFoundNegotiatedContentResult(string content, ApiController controller)
        : base(HttpStatusCode.NotFound, content, controller) { }
}

public class InternalServerErrorNegotiatedContentResult : NegotiatedContentResult<string>
{
    public InternalServerErrorNegotiatedContentResult(string content, ApiController controller)
        : base(HttpStatusCode.InternalServerError, content, controller) { }
}

然后您可以为ApiController创建相应的扩展方法(如果您有基类,则可以在其中完成):

public static NotFoundNegotiatedContentResult NotFound(this ApiController controller, string message)
{
    return new NotFoundNegotiatedContentResult(message, controller);
}

public static InternalServerErrorNegotiatedContentResult InternalServerError(this ApiController controller, string message)
{
    return new InternalServerErrorNegotiatedContentResult(message, controller);
}

然后它们就像内置方法一样工作。您可以调用现有的 NotFound(),也可以调用您的新自定义 NotFound(myErrorMessage)

当然,如果您希望摆脱自定义类型定义中的“硬编码”字符串类型并使其通用化,则可能需要考虑 ExecuteAsync 问题,这取决于您的 <T> 实际上是什么。

您可以查看 NegotiatedContentResult<T>源代码 以了解它所做的所有事情。它没有太多内容。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接