将业务逻辑与控制器分离的最佳方法



规则是控制器不应该有业务逻辑,而应该将其委托给服务。但是当我们这样做时,我们无法处理所有可能的情况并返回适当的HTTP响应。

让我们看一个例子。假设我们正在构建某种社交网络,我们需要创建一个端点来对帖子进行评级(喜欢或不喜欢(。

首先,让我们看一个将逻辑委托给服务的示例,这是我们的控制器操作:

public IActionResult Rate(long postId, RatingType ratingType)
{
var user = GetCurrentUser();
PostRating newPostRating = _postsService.Rate(postId, ratingType, user);
return Created(newPostRating);
}

你认为这有问题吗?如果没有带有给定 id 的帖子,我们将如何返回未找到的响应?如果用户无权对帖子进行评分,我们将如何返回禁止的回复?

PostsService.Rate只能返回一个新的PostRating,但其他情况呢?好吧,我们可以抛出一个异常,我们需要创建很多自定义异常,以便我们可以将它们映射到适当的 HTTP 响应。我不喜欢为此使用例外,我认为有更好的方法来处理这些情况而不是例外。因为我认为帖子不存在和用户没有权限的情况根本不是例外,它们只是正常情况,就像成功评级帖子一样。

我建议,改为在控制器中处理该逻辑。因为在我看来,无论如何,这应该是控制器的责任,在提交操作之前检查所有权限。所以这就是我会这样做的方式:

public IActionResult Rate(long postId, RatingType ratingType)
{
var user = GetCurrentUser();
var post = _postsRepository.GetByIdWithRatings(postId);
if (post == null)
return NotFound();
if (!_permissionService.CanRate(user, post))
return Forbidden();
PostRating newPostRating = new PostRating 
{
Post = post,
Author = user,
Type = ratingType
};
_postRatingsRepository.Save(newPostRating);
return Created(newPostRating);
}

在我看来,这是应该这样做的方式,但我敢打赌,有人会说这对控制器来说逻辑太多了,或者你不应该在其中使用存储库。

如果您不喜欢在控制器中使用存储库,那么将获取或保存帖子的方法放在哪里?在役?因此,会有PostsService.GetByIdWithRatingsPostsService.Save什么都不做,只会打电话给PostsRepository.GetByIdWithRatingsPostsRepository.Save。这是不必要的,只会导致样板代码。

更新: 也许有人会说使用PostsService检查权限,然后调用PostsService.Rate。这很糟糕,因为它涉及更多不必要的数据库访问。例如,它可能是这样的:

public IActionResult Rate(long postId, RatingType ratingType)
{
var user = GetCurrentUser();
if(_postsService.Exists(postId))
return NotFound();
if(!_postsService.CanUserRate(user, postId))        
return Forbidden();
PostRating newPostRating = _postsService.Rate(postId, ratingType, user);
return Created(newPostRating);
}

我什至需要进一步解释为什么这很糟糕吗?

有许多方法可以解决这个问题,但最接近"最佳实践"方法的方法可能是使用结果类。例如,如果您的服务方法创建了一个评级,然后返回它创建的评级,则您返回一个对象,该对象封装了评级以及其他相关信息,例如成功状态、错误消息(如果有(等。

public class RateResult
{
public bool Succeeded { get; internal set; }
public PostRating PostRating { get; internal set; }
public string[] Errors { get; internal set; }
}

然后,您的控制器代码将变为:

public IActionResult Rate(long postId, RatingType ratingType)
{
var user = GetCurrentUser();
var result = _postsService.Rate(postId, ratingType, user);
if (result.Succeeded)
{
return Created(result.PostRating);
}
else
{
// handle errors
}
}

我(刚才(所做的是创建新的类ApiResult

public class ApiResult
{
public int StatusCode { get; private set; } = 200;
public string RouteName { get; private set; }
public object RouteValues { get; private set; }
public object Content { get; private set; }
public void Ok(object content = null)
{
this.StatusCode = 200;
this.Content = content;
}
public void Created(string routeName, object routeValues, object content)
{
this.StatusCode = 201;
this.RouteName = routeName;
this.RouteValues = routeValues;
this.Content = content;
}
public void BadRequest(object content = null)
{
this.StatusCode = 400;
this.Content = content;
}
public void NotFound(object content = null)
{
this.StatusCode = 404;
this.Content = content;
}
public void InternalServerError(object content = null)
{
this.StatusCode = 500;
this.Content = content;
}
}

以及具有单个方法的控制器基类TranslateApiResult

public abstract class CommonControllerBase : ControllerBase
{
protected IActionResult TranslateApiResult(ApiResult result)
{
if (result.StatusCode == 201)
{
return CreatedAtAction(result.RouteName, result.RouteValues, result.Content);
}
else
{
return StatusCode(result.StatusCode, result.Content);
}
}
}

现在在控制器中,我做到了:

[ApiController]
[Route("[controller]/[action]")]
public class MyController : CommonControllerBase
{
private readonly IMyApiServcie _service;
public MyController (
IMyApiServcie service)
{
_service = service;
}
[HttpGet]
public async Task<IActionResult> GetData()
{
return TranslateApiResult(await _service.GetData());
}
}

在服务中注入存储库和其他依赖项:

public class MyApiServcie : IMyApiServcie 
{
public async Task<ApiResult> GetData()
{
var result = new ApiResult();
// do something here
result.Ok("success");
return result;
}
}

现在,在Service之前Api前缀的原因是,此服务并不意味着是包含所有逻辑的最终服务。

在这一点上,我将业务逻辑拆分为不同的域,以便服务(或外观(最终没有 Api 前缀,只是为了区分,即CarService.最好这些服务不会知道与 API 响应、状态等相关的任何内容。不过,这取决于您如何实现它。

最新更新