如何从控制器返回BadRequest?

时间:2018-10-18 14:53:05

标签: asp.net asp.net-core

我有一个允许用户更新其个人资料的表格。提交表单后,会提出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 :(得分:0)

正如poke告诉您的那样,用户可以使用UserProfileViewModel.FirstName属性修饰Required来使用model validation

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);

答案 1 :(得分:0)

来自火星的

@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");
}

同样,对于诸如必需属性之类的东西,还有更好的方法。我发现上述模式在服务中发生错误时非常有用,并且我想将此事实传达给控制器/操作/ UI。