Как вернуть BadRequest из контроллера? - PullRequest
0 голосов
/ 18 октября 2018

У меня есть форма, позволяющая пользователю обновить свой профиль.Когда форма отправляется, возникает запрос ajax:

$.ajax({
    url: url,
    data: formData,
    type: 'POST',
    success: function (response) {
        alert(true);
    },
    error: function (jqXHR, textStatus, errorThrown) {
        //Handle error
    }
});

внутри запроса ajax. Мне нужно проверить, произошла ли ошибка, если да, на основе сгенерированной ошибки я хочу отобразить другое сообщение об исключении.

Теперь основная проблема состоит в том, что вызываемый метод возвращает ViewModel обновленного пользователя, что-то вроде:

publi class UserController : Controller 
{
    private readonly IUserRepository _repo;

    public UserController(IUserRepository repo)
    {
        _repo = repo;
    }
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UpdateUser(UserProfileViewModel updateUser)
{
    if(ModelState.IsValid)
    {
       updateUser = await _repo.UpdateUserAsync(updateUser);
    }

    return RedirectToAction("Profile");
}

Контроллер имеет внедрение зависимостей IUserRepository, которое фактически обрабатываетлогика для обновления пользователя, например:

public async Task<User> UpdateUserAsync(UserProfileViewModel updatedUser)
{
     if(updatedUser.FirstName == "")
        throw new Exception("FirstName not filled");
}

, как видно из приведенного выше примера, если FirstName не заполнено, то выдается исключение.

Я хочу избежать использования исключения;после некоторых исследований, которые я обнаружил BadRequest(), проблема в том, что BadRequest, кажется, отсутствует в AspNetCore, кажется доступным только в версии API.

У кого-то есть хороший способ справиться с этим?

Ответы [ 2 ]

0 голосов
/ 18 октября 2018

@ poke и @agua от Марса совершенно правы;Вы должны использовать проверку модели.Если, однако, ваша проверка немного сложнее, и вы должны обрабатывать ее в своем сервисе, вы можете использовать этот шаблон.Во-первых, создайте класс, который представляет не только данные, но и показатель успеха службы в получении данных.

public class Result<T>
{
  public bool HasError { get; set; }
  public T Data { get; set; }
}

Это выше очень упрощенно;вам, вероятно, понадобится дополнительная информация.

Затем в вашем сервисе измените подпись, чтобы она возвращала Result<T>, и добавьте код для создания соответствующей:

public async Task<Result<User>> UpdateUserAsync(UserProfileViewModel updatedUser) {
  if (updatedUser.FirstName == "")
    return new Result<User> {
      HadError = true,
      Data = (User)null
    };
  // do something to save
  return new Result<User> {
    HadError = false,
    Data = updatedUser
  };
}

И затем обновитеваше действие, чтобы получить Result<T> и вернуть соответствующий результат:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UpdateUser(UserProfileViewModel updateUser) {
  var result = new Result<User> {
    HadError = true,
    Data = (User) null
  };
  if (ModelState.IsValid) {
    result = await _repo.UpdateUserAsync(updateUser);
  }

  return result.HadError ? BadRequest() : RedirectToAction("Profile");
}

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

0 голосов
/ 18 октября 2018

Как сказал poke , пользователь может использовать подтверждение модели , украсив UserProfileViewModel.FirstName атрибутом Required:

public class UserProfileViewModel
{
    [Required]
    public string Name { get; set; }
}

Я добавляю фильтры вмоя конфигурация для факторизации кода.
Один для проверки модели:

/// <summary>
/// Check api model state filter
/// </summary>
public class ApiCheckModelStateFilter : IActionFilter
{
    private readonly PathString _apiPathString = PathString.FromUriComponent("/api");

    /// <summary>
    /// Called after the action executes, before the action result.
    /// </summary>
    /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext" />.</param>
    public void OnActionExecuted(ActionExecutedContext context)
    {
    }

    /// <summary>
    /// Called before the action executes, after model binding is complete.
    /// </summary>
    /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext" />.</param>
    /// <exception cref="InvalidOperationException"></exception>
    public void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.HttpContext.Request.Path.StartsWithSegments(_apiPathString))
        {
            return;
        }

        var state = context.ModelState;
        if (!state.IsValid)
        {
            var message = string.Join("; ", state.Values
                                    .SelectMany(x => x.Errors)
                                    .Select(x => x.ErrorMessage));

            throw new InvalidOperationException(message);
        }
    }
}

И еще один для управления кодом состояния в зависимости от исключения:

/// <summary>
/// Api exception filter
/// </summary>
public class ApiExceptionFilter : IExceptionFilter, IAsyncExceptionFilter
{
    private readonly PathString _apiPathString = PathString.FromUriComponent("/api");
    private readonly ILogger<ApiExceptionFilter> _logger;

    /// <summary>
    /// Initialize a new instance of <see cref="ApiExceptionFilter"/>
    /// </summary>
    /// <param name="logger">A logger</param>
    public ApiExceptionFilter(ILogger<ApiExceptionFilter> logger)
    {
        _logger = logger;
    }
    /// <summary>
    /// Called after an action has thrown an <see cref="T:System.Exception" />.
    /// </summary>
    /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ExceptionContext" />.</param>
    /// <returns>
    /// A <see cref="T:System.Threading.Tasks.Task" /> that on completion indicates the filter has executed.
    /// </returns>
    public Task OnExceptionAsync(ExceptionContext context)
    {
        Process(context);
        return Task.CompletedTask;
    }

    /// <summary>
    /// Called after an action has thrown an <see cref="T:System.Exception" />.
    /// </summary>
    /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ExceptionContext" />.</param>
    public void OnException(ExceptionContext context)
    {
        Process(context);
    }

    private void Process(ExceptionContext context)
    {
        var e = context.Exception;
        _logger.LogError(e, e.Message);

        if (!context.HttpContext.Request.Path.StartsWithSegments(_apiPathString))
        {
            return;
        }
        else if (e is EntityNotFoundException)
        {
            context.Result = WriteError(HttpStatusCode.NotFound, e);
        }
        else if (e is InvalidOperationException)
        {
            context.Result = WriteError(HttpStatusCode.BadRequest, e);
        }
        else if (e.GetType().Namespace == "Microsoft.EntityFrameworkCore")
        {
            context.Result = WriteError(HttpStatusCode.BadRequest, e);
        }
        else
        {
            context.Result = WriteError(HttpStatusCode.InternalServerError, e);
        }                
    }

    private IActionResult WriteError(HttpStatusCode statusCode, Exception e)
    {
        var result = new ApiErrorResult(e.Message, e)
        {
            StatusCode = (int)statusCode,               
        };

        return result;
    }
}

Возвращает ApiErrorResultс сообщением об ошибке в фразе причины:

/// <summary>
/// Api error result
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.ObjectResult" />
public class ApiErrorResult : ObjectResult
{
    private readonly string _reasonPhrase;

    /// <summary>
    /// Initializes a new instance of the <see cref="ApiErrorResult"/> class.
    /// </summary>
    /// <param name="reasonPhrase">The reason phrase.</param>
    /// <param name="value">The value.</param>
    public ApiErrorResult(string reasonPhrase, object value) : base(value)
    {
        _reasonPhrase = reasonPhrase;
    }

    /// <inheritdoc />
    public override async Task ExecuteResultAsync(ActionContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        var reasonPhrase = _reasonPhrase;
        reasonPhrase = reasonPhrase.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)[0];
        context.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = reasonPhrase;
        await base.ExecuteResultAsync(context);
    }
}

Фильтры для чеков установлены в методе Startup ConfigureServices:

    /// <summary>
    /// This method gets called by the runtime. Use this method to add services to the container.
    /// </summary>
    /// <param name="services">A service collection</param>
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(configure =>
            {
                var filters = configure.Filters;
                filters.Add<ApiExceptionFilter>();
                filters.Add<ApiCheckModelStateFilter>();
            })
            .AddJsonOptions(configure =>
            {
                configure.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
            })
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);            
    }

Таким образом, мне не нужно проверять, если модельдопустимо в методах контроллеров:

[HttpPost]
[ValidateAntiForgeryToken]
public Task<IActionResult> UpdateUser(UserProfileViewModel updateUser) => _repo.UpdateUserAsync(updateUser);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...