在使用 C# 的 REST API 开发中,我遇到了一个问题:如果控制器使用的服务返回错误,但该错误取决于有关某些内容的业务规则,我应该如何以及在哪里确定在HTTP 响应?
我的意思是,这应该是服务的责任,还是控制器应该根据服务结果确定返回什么状态码?
现在,在我的团队中,我们让服务通过来自名为 Response 的类的通用包装对象返回状态代码和消息以及有效负载。因此,当我们想要返回用户列表时,该服务会执行类似于
的操作if (users?.Any() ?? true)
return new Response()
{
Message = "There are no users.",
Status = HttpStatusCodes.Status204NoContent
}
return new Response()
{
Message = "Users retrieved successfully!",
Status = HttpStatusCodes.Status200OK,
Data = users
}
但是,尽管这种方法可行,但对我来说看起来不太好,因为从 ServiceA 到 ServiceB 调用一个方法很奇怪,因为有时 HTTP 状态代码不必要地组合在一起。
一种替代方法是将一些结果对象从服务返回到控制器,但在中间件中进行一些数据处理,将结果对象转换为包含状态代码的有效响应对象。
所以可能是这样的:
// Result object returned by services
public class Result<T>
{
public string Message { get; }
public ResultReason Reason { get; }
public T Data { get; set; }
}
public enum ResultReason
{
BusinessRuleViolation, ItemNotFound, Success, InvalidParameter
}
// Response object returned by a middleware as an HTTP response body
public class Response<T>
{
public string Message { get; }
public int StatusCode { get; }
public T Data { get; set; }
}
public static class ResponseMapper
{
public static Response<T> ToResponse<T>(this Result<T> result)
{
return new Response()
{
Data = result.Data,
Message = result.Message,
StatusCode = result.Reason == ResultReason.BusinessRuleViolation ? 422
: result.Reason == ResultReason.ItemNotFound ? 204
: result.Reason == ResultReason.Success ? 200
: result.Reason == ResultReason.InvalidParameter ? 400
}
}
}
如果你们能够带来来自 DDD、简洁架构、GoF 设计模式、简洁代码或类似内容的概念和想法,我会非常高兴:)
注意:上面的例子不一定有效,我只是为了演示目的而写的
您可以使用包Middleware中的问题详细信息中间件。有关问题详情您可以查看微软文档问题详情服务
基本思想是,您可以在域层中拥有全局自定义异常,您可以使用问题详细信息中间件将其映射到业务规则特定异常。
因此,您将将此中间件添加到 asp net core 项目中的管道中。此外,您可以简单地配置您的例外,如下所示(示例用例)
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services)
{
services.AddProblemDetails(problemDetailsOptions =>
{
problemDetailsOptions.MapToStatusCodeWithErrorMessage<SomeCustomException>(StatusCodes.Status409Conflict);
});
return services;
}
private static void MapToStatusCodeWithErrorMessage<TException>(
this ProblemDetailsOptions options,
int statusCode,
LogLevel logLevel = LogLevel.Debug)
where TException : Exception
{
ExceptionsLogLevels.Add(typeof(TException).FullName!, logLevel);
options.Map<TException>(
exception =>
{
var problemDetails = StatusCodeProblemDetails.Create(statusCode);
problemDetails.Detail = exception.Message;
return problemDetails;
});
}
然后,您可以以最小的 API 将其添加到配置服务中的管道(如果您使用旧方式),您必须在程序 cs 文件中执行此操作。
services.AddCustomProblemDetails();
以及配置方法内部
app.UseProblemDetails();
现在,您基本上可以在服务层中抛出业务领域特定的自定义异常,并且您的控制器非常精简。
上面的示例基于您将使用我在此答案中链接的中间件包的假设:)
在服务中使用此结果类。
public class Result<T>
{
public bool IsSuccess { get; private set; }
public bool IsFailure { get; private set; }
public T? Value { get; private set; }
public string[]? Errors { get; private set; }
private Result() { }
public static Result<T> Success(T value) => new Result<T>() { IsSuccess = true, IsFailure = false, Value = value };
public static Result<T> Failure(params string[] errors) => new Result<T>() { IsSuccess = false, IsFailure = true, Errors = errors };
}
服务层可以从不支持 http 的其他功能中使用。然后在外层你可以决定返回什么http代码。