Reduce/remove ASP.NET 核心中的重复业务逻辑检查
Reduce/remove repetitive business logic checks in ASP.NET Core
有没有办法 reduce/remove 在业务层不断重复用户访问检查(或其他一些检查)?
让我们考虑以下示例:具有一个实体的简单 CRUD 应用程序 BlogPost
:
public class BlogPost
{
public int Id { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public int AuthorId { get; set; }
}
在修改或删除实体之前的 PUT/DELETE 请求中,我需要检查发出请求的用户是否是 BlogPost 的作者,因此允许他 delete/edit 它。
所以无论是UpdateBlogPost
还是DeleteBlogPost
的虚数BlogPostService
我都得这样写:
var blogPostInDb = _blogPostRepository.GetBlogPost();
if(blogPostInDb == null)
{
// throw exception or do whatever is needed
}
if(blogPostInDb.AuthorId != _currentUser.Id)
{
// throw exception etc...
}
这种代码对于 Update
和 Delete
方法以及将来可能添加的其他方法都是相同的,并且对于所有实体都是相同的。
有什么方法可以减少或完全消除这种重复?
我仔细考虑并提出了以下解决方案,但它们并不完全令我满意。
第一个解
使用过滤器。我们可以创建一些自定义过滤器,如 [EnsureEntityExists]
和 [EnsureUserCanManageEntity]
,但通过这种方式,我们在 API 层中传播了一些业务逻辑,并且它不够灵活,因为我们需要为创建这样的过滤器每个实体。也许可以使用反射来制作某种通用过滤器。
另外 这种方法还有另一个问题,假设我们制作了这样的过滤器来检查我们的规则。我们从数据库中获取实体,进行检查,抛出异常和所有这些东西,然后让控制器方法执行。但是在服务层,我们需要再次获取实体,所以我们要往返 db 两次。也许我想多了这个问题,考虑到可以应用缓存这一事实,进行 2 次往返就可以了。
第二种解法
因为我使用的是 CQRS(或至少某种类型的),所以我有 MediatR library and I can make use of Pipeline Behaviors,甚至通过变异 TRequest
将获取的实体进一步传递到管道中(我不想这样做).此解决方案需要一些通用接口,以便所有请求能够检索实体的 ID。往返问题也适用于此。
public interface IBlogPostAccess
{
public int Id { get; set; }
}
public class ChangeBlogPostCommand: IRequest, IBlogPostAccess
{
// ...
}
public class DeleteBlogPostCommand: IRequest, IBlogPostAccess
{
// ...
}
public class BlogPostAccessBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IBlogPostAccess
{
// all nessesary stuff injected via DI
public BlogPostAccessBehavior()
{
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var blogPostInDb = _blogPostRepository.GetBlogPost(request.Id);
if(blogPostInDb == null)
{
// throw exception or do whatever is needed
}
if(blogPostInDb.AuthorId != _currentUser.Id)
{
// throw exception etc...
}
return await next();
}
}
第三种解法
创建类似请求上下文服务的东西。以一种非常简单的方式,它将是一个字典,将在我们可以存储数据的请求中持久存在(在本例中,我们在 filter/pipeline 中获取的 BlogPost)。这看起来很蹩脚,让我想起 ASP.NET MVC 中的 ViewBag
。
第四解
它比解决方案更有效,但我们可以使用 GuardClause
或扩展方法来减少 if
语句的嵌套。
同样,也许我对这个问题想得太多了,或者这根本不是问题,或者那是设计问题。任何帮助,想法表示赞赏。
如果您担心许多数据库调用,您可以尝试使用类似 LazyCache 的方法缓存每个请求返回的对象 https://github.com/alastairtree/LazyCache
我不建议跨请求缓存...
对于代码组织,我建议将授权逻辑提取到一个单独的方法中,并在每个请求中调用该方法。好处是如果逻辑发生变化,只需要更新一处。
例如这样的事情:
bool canEdit(userId){
var user = getUserByUserId(userId);
if(user.IsAdmin) return true;
//depending on where this method lives might have access to blogpost here
if(_blogPost.AuthorId == userId) return true;
return false;
}
有没有办法 reduce/remove 在业务层不断重复用户访问检查(或其他一些检查)?
让我们考虑以下示例:具有一个实体的简单 CRUD 应用程序 BlogPost
:
public class BlogPost
{
public int Id { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public int AuthorId { get; set; }
}
在修改或删除实体之前的 PUT/DELETE 请求中,我需要检查发出请求的用户是否是 BlogPost 的作者,因此允许他 delete/edit 它。
所以无论是UpdateBlogPost
还是DeleteBlogPost
的虚数BlogPostService
我都得这样写:
var blogPostInDb = _blogPostRepository.GetBlogPost();
if(blogPostInDb == null)
{
// throw exception or do whatever is needed
}
if(blogPostInDb.AuthorId != _currentUser.Id)
{
// throw exception etc...
}
这种代码对于 Update
和 Delete
方法以及将来可能添加的其他方法都是相同的,并且对于所有实体都是相同的。
有什么方法可以减少或完全消除这种重复?
我仔细考虑并提出了以下解决方案,但它们并不完全令我满意。
第一个解
使用过滤器。我们可以创建一些自定义过滤器,如 [EnsureEntityExists]
和 [EnsureUserCanManageEntity]
,但通过这种方式,我们在 API 层中传播了一些业务逻辑,并且它不够灵活,因为我们需要为创建这样的过滤器每个实体。也许可以使用反射来制作某种通用过滤器。
另外 这种方法还有另一个问题,假设我们制作了这样的过滤器来检查我们的规则。我们从数据库中获取实体,进行检查,抛出异常和所有这些东西,然后让控制器方法执行。但是在服务层,我们需要再次获取实体,所以我们要往返 db 两次。也许我想多了这个问题,考虑到可以应用缓存这一事实,进行 2 次往返就可以了。
第二种解法
因为我使用的是 CQRS(或至少某种类型的),所以我有 MediatR library and I can make use of Pipeline Behaviors,甚至通过变异 TRequest
将获取的实体进一步传递到管道中(我不想这样做).此解决方案需要一些通用接口,以便所有请求能够检索实体的 ID。往返问题也适用于此。
public interface IBlogPostAccess
{
public int Id { get; set; }
}
public class ChangeBlogPostCommand: IRequest, IBlogPostAccess
{
// ...
}
public class DeleteBlogPostCommand: IRequest, IBlogPostAccess
{
// ...
}
public class BlogPostAccessBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IBlogPostAccess
{
// all nessesary stuff injected via DI
public BlogPostAccessBehavior()
{
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var blogPostInDb = _blogPostRepository.GetBlogPost(request.Id);
if(blogPostInDb == null)
{
// throw exception or do whatever is needed
}
if(blogPostInDb.AuthorId != _currentUser.Id)
{
// throw exception etc...
}
return await next();
}
}
第三种解法
创建类似请求上下文服务的东西。以一种非常简单的方式,它将是一个字典,将在我们可以存储数据的请求中持久存在(在本例中,我们在 filter/pipeline 中获取的 BlogPost)。这看起来很蹩脚,让我想起 ASP.NET MVC 中的 ViewBag
。
第四解
它比解决方案更有效,但我们可以使用 GuardClause
或扩展方法来减少 if
语句的嵌套。
同样,也许我对这个问题想得太多了,或者这根本不是问题,或者那是设计问题。任何帮助,想法表示赞赏。
如果您担心许多数据库调用,您可以尝试使用类似 LazyCache 的方法缓存每个请求返回的对象 https://github.com/alastairtree/LazyCache
我不建议跨请求缓存...
对于代码组织,我建议将授权逻辑提取到一个单独的方法中,并在每个请求中调用该方法。好处是如果逻辑发生变化,只需要更新一处。
例如这样的事情:
bool canEdit(userId){
var user = getUserByUserId(userId);
if(user.IsAdmin) return true;
//depending on where this method lives might have access to blogpost here
if(_blogPost.AuthorId == userId) return true;
return false;
}