Как мне вернуть NotFound () IHttpActionResult с сообщением об ошибке или исключении?

98

Я возвращаю NotFound IHttpActionResult, если что-то не найдено в моем действии GET WebApi. Вместе с этим ответом я хочу отправить собственное сообщение и / или сообщение об исключении (если есть). Тока ApiController«ы NotFound()способ не обеспечивает перегрузку , чтобы передать сообщение.

Есть ли способ сделать это? или мне придется писать свой кастом IHttpActionResult?

Аджай Джадхав
источник
Вы хотите возвращать одно и то же сообщение для всех результатов Not Found?
Николай Самтеладзе
@NikolaiSamteladze Нет, в зависимости от ситуации это могло быть другое сообщение.
Аджай Джадхав

Ответы:

84

Вам нужно будет написать собственный результат действия, если вы хотите настроить форму ответного сообщения.

Мы хотели предоставить наиболее распространенные формы ответных сообщений из коробки для таких вещей, как простые пустые сообщения 404, но мы также хотели, чтобы эти результаты были как можно более простыми; Одно из основных преимуществ использования результатов действий состоит в том, что они значительно упрощают модульное тестирование вашего метода действия. Чем больше свойств мы добавляем к результатам действия, тем больше вещей необходимо учитывать вашему модульному тесту, чтобы убедиться, что метод действия выполняет то, что вы ожидаете.

Мне часто нужна возможность предоставить собственное сообщение, поэтому не стесняйтесь регистрировать ошибку, чтобы мы рассмотрели возможность поддержки этого результата действия в будущей версии: https://aspnetwebstack.codeplex.com/workitem/list/advanced

Однако в результатах действий есть одна приятная вещь: вы всегда можете довольно легко написать свои собственные, если хотите сделать что-то немного другое. Вот как вы могли бы это сделать в своем случае (если вы хотите, чтобы сообщение об ошибке было в текстовом / обычном формате; если вам нужен 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». часть (которая, к сожалению, требуется при вызове метода расширения):

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.");
    }
}
dmatson
источник
1
Я написал точно такую ​​же реализацию IHttpActionResult, но не специально для результата NotFound. Вероятно, это будет работать для всех «HttpStatusCodes». Мой CustomActionResult код выглядит примерно так это и мой Controler в 'Get () действий выглядит следующим образом : «общественность IHttpActionResult Get () {возвращение CustomNotFoundResult ( " Meessage к Return"); } 'Кроме того, я зарегистрировал ошибку в CodePlex, чтобы рассмотреть ее в будущем выпуске.
Аджай Джадхав
Я использую ODataControllers, и мне пришлось использовать this.NotFound ("бла");
Jerther 05
1
Очень хороший пост, но я бы просто не рекомендовал совет по наследованию. Моя команда решила сделать именно это давным-давно и из-за этого сильно раздула классы. Я совсем недавно реорганизовал все это в методы расширения и отошел от цепочки наследования. Я бы серьезно порекомендовал людям внимательно подумать, когда им следует использовать подобное наследование. Обычно композиция намного лучше, потому что она намного более независима.
julealgon
6
Эта функциональность должна была быть готовой к работе. Включение необязательного параметра ResponseBody не должно влиять на модульные тесты.
Теодор Зографос,
230

Вот однострочный вариант для возврата IHttpActionResult NotFound с простым сообщением:

return Content(HttpStatusCode.NotFound, "Foo does not exist.");
Энтони Ф
источник
24
Люди должны проголосовать за этот ответ. Это приятно и просто!
Джесс
2
Имейте в виду, что это решение не устанавливает статус заголовка HTTP на «404 Not Found».
Каспер Халвас Дженсен,
4
@KasperHalvasJensen Код статуса http с сервера - 404, вам нужно что-то еще?
Энтони Ф,
4
@AnthonyF Вы правы. Я использовал Controller.Content (...). Шоуд использовал ApiController.Content (...) - Мое плохо.
Каспер Халвас Дженсен
Спасибо,
дружище
28

Вы можете использовать, ResponseMessageResultесли хотите:

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

да, если вам нужны намного более короткие версии, я думаю, вам нужно реализовать свой собственный результат действия.

Киран Чалла
источник
Я выбрал этот метод, поскольку он казался изящным. Я просто определил пользовательское сообщение в другом месте и выделил код возврата с отступом.
ozzy432836 07
Мне это нравится больше, чем Content, потому что он фактически возвращает объект, который я могу проанализировать с помощью свойства Message, как и стандартный метод BadRequest.
user1568891
7

Вы можете использовать свойство ReasonPhrase класса HttpResponseMessage

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
Дмитрий Руденко
@DmytroRudenko: результаты действий были введены для улучшения тестируемости. Выбрасывая здесь HttpResponseException, вы рискуете этим. Также здесь у нас нет никаких исключений, но OP ищет отправку сообщения.
Kiran Challa
Хорошо, если вы не хотите использовать NUint для тестирования, вы можете написать свою собственную реализацию NotFoundResult и переписать его ExecuteAsync для возврата данных вашего сообщения. И верните экземпляр этого класса в результате вызова действия.
Дмитрий Руденко
1
Обратите внимание, что теперь вы можете передавать код состояния напрямую, например, HttpResponseException (HttpStatusCode.NotFound)
Марк Совул
3

Вы можете создать собственный согласованный результат контента, как предложено 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

Я решил это, просто взяв 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);
    }
}
Деметр
источник
1

Если вы наследуете от базы NegotitatedContentResult<T>, как уже упоминалось, и вам не нужно преобразовывать свой content(например, вы просто хотите вернуть строку), вам не нужно переопределять ExecuteAsyncметод.

Все, что вам нужно сделать, это предоставить соответствующее определение типа и конструктор, который сообщает базе, какой код состояния HTTP нужно вернуть. Все остальное просто работает.

Вот примеры для NotFoundи InternalServerError:

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>чтобы увидеть все это делает. В этом нет ничего особенного.

ползунок
источник
1

Мне нужно было создать IHttpActionResultэкземпляр в теле IExceptionHandlerкласса, чтобы установить ExceptionHandlerContext.Resultсвойство. Однако я также хотел установить custom ReasonPhrase.

Я обнаружил, что ResponseMessageResultможно обернуть a 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" });
        }
    }
}
Джоно Джоб
источник
0

Я знаю, что PO запрашивает текст сообщения, но другой вариант просто вернуть 404 - заставить метод возвращать IHttpActionResult и использовать функцию StatusCode

    public async Task<IHttpActionResult> Get([FromUri]string id)
    {
       var item = await _service.GetItem(id);
       if(item == null)
       {
           StatusCode(HttpStatusCode.NotFound);
       }
       return Ok(item);
    }
Майкул Хименес
источник
0

В ответах здесь отсутствует небольшая проблема с историей разработчика. ApiControllerКласс еще обнажая NotFound()метод , который разработчики могут использовать. Это приведет к тому, что ответ 404 будет содержать неконтролируемое тело результата.

Я представляю здесь несколько частей кода « лучшего метода ApiController NotFound », который обеспечит метод , менее подверженный ошибкам, который не требует от разработчиков знания «лучшего способа отправки 404».

  • создать класс, наследующий отApiController вызываемогоApiController
    • Я использую эту технику, чтобы запретить разработчикам использовать исходный класс
  • переопределить его NotFoundметод, чтобы разработчики могли использовать первый доступный API
  • если вы хотите препятствовать этому, отметьте это как [Obsolete("Use overload instead")]
  • добавьте еще, protected NotFoundResult NotFound(string message)что вы хотите поощрить
  • проблема: результат не поддерживает ответ телом. решение: наследовать и использовать NegotiatedContentResult. см. прилагаемый лучше класс NotFoundResult .
SandRock
источник