C# 在Repository/UnitOrWork之上使用服务类时,逻辑不适合存储库的常用数据访问代码应该放在哪里?

C# 在Repository/UnitOrWork之上使用服务类时,逻辑不适合存储库的常用数据访问代码应该放在哪里?,c#,entity-framework,design-patterns,domain-driven-design,repository-pattern,C#,Entity Framework,Design Patterns,Domain Driven Design,Repository Pattern,在我的演讲中,我询问了如何为使用类似EF的ORM框架构建的大型应用程序实现存储库/工作单元模式 我现在无法解决的一个后续问题是将包含业务逻辑的代码放在何处,但其级别仍然较低,足以在应用程序的许多其他部分中普遍使用 例如,这里有一些这样的方法: 获取一个或多个角色中的所有用户 获取用户在可选范围内拥有权限的所有城市 地区 在给定范围内获取给定设备类型的所有测量设备 当前用户具有权限的区域 通过代码查找产品,检查产品是否可见并抛出 如果找不到或不可见,则出现异常 所有这些方法都使用一个工作单元进

在我的演讲中,我询问了如何为使用类似EF的ORM框架构建的大型应用程序实现存储库/工作单元模式

我现在无法解决的一个后续问题是将包含业务逻辑的代码放在何处,但其级别仍然较低,足以在应用程序的许多其他部分中普遍使用

例如,这里有一些这样的方法:

  • 获取一个或多个角色中的所有用户
  • 获取用户在可选范围内拥有权限的所有城市 地区
  • 在给定范围内获取给定设备类型的所有测量设备 当前用户具有权限的区域
  • 通过代码查找产品,检查产品是否可见并抛出 如果找不到或不可见,则出现异常
所有这些方法都使用一个工作单元进行数据访问或操作,并按照其规范接收多个参数。我认为每个人都可以为大型项目中的此类常见任务编写更多的示例。我的问题是我应该把tese方法实现放在哪里?我目前可以看到以下选项

选项1:每个方法都指向自己的服务类
公共类区域服务{
//支持DI构造函数注入
公共区域服务(工作单元){…}
...
public IEnumerable GetCitiesForUser(用户用户,Region=null){…}
...
}
公共类设备服务{
//支持DI构造函数注入
公共设备服务(工作单元){…}
...
public IEnumerable GetDeviceForUser(用户用户,设备类型,区域=null){…}
...
}
我不喜欢的是,如果一个更高级别的应用程序服务需要调用示例3或这些方法,那么它需要实例化3个服务,如果我使用DI,那么我甚至必须将所有3个都放入构造函数中,很容易产生相当多的代码气味

选项2:为这种公共数据访问创建某种外观
公共类DataAccessHelper{
//支持DI构造函数注入
公共DataAccessHelper(IUnitOfWork工作){…}
...
public IEnumerable GetCitiesForUser(用户用户,Region=null){…}
public IEnumerable GetDeviceForUser(用户用户,设备类型,区域=null){…}
public IEnumerable GetUsersInRoles(参数字符串[]roleIds){…}
...
}
我不喜欢它,因为它感觉像是违反了SRP,但是它的使用会更舒适

选项3:为存储库创建扩展方法
公共静态类DataAccessExtensions{
公共静态IEnumerable GetCitiesForUser(此IRepository repo,用户,Region=null){…}
}
这里的
IRepository
是一个带有通用方法的界面,如
Query
Save
,等等。我也不喜欢它,因为它让我觉得我想给存储库提供业务逻辑,这在目前看来是不可取的。然而,它表示这些方法是通用的,并且比我喜欢的服务类级别低


也许还有其他选择?。。。谢谢您的帮助。

如果您说某个域逻辑需要查看3条不同的信息才能做出决定,那么我们需要向其提供这些信息

此外,如果我们说这些不同的片段中的每一个都对域的其他部分有用,那么它们中的每一个都需要有自己的方法。根据您的域/设计,我们可以讨论是否需要将每个查询放在单独的类中

我想指出的一点是,将有一个应用程序服务委托给一个或多个Finder类(您的查询所在的类),这些类只包含查询,然后累积结果并将其作为方法参数传递给域服务

域服务作用于接收到的参数,执行逻辑并返回结果。这样,域服务很容易测试

伪代码

App Service
result1 = finder.query1()
result2 = finder.query2()
result3= yetanotherfinder.query();
domainresult = domainservice.calculate(result1,result2,result3);

存储库属于域,查询不属于域()

您可以定义显式查询和查询处理程序,并使用域外的查询和查询处理程序

public class GetUserStatisticsQuery 
{
    public int UserId { get; set; }
}

public class GetUserStatisticsQueryResult
{
    ...
}

public class GetUserStatisticsQueryHandler : 
    IHandleQuery<GetUserStatisticsQuery, GetUserStatisticsQueryResult> 
{
    public GetUserStatisticsQueryResult Handle(GetUserStatisticsQuery query) 
    {
        ... "SELECT * FROM x" ...
    }
}

var result = _queryExecutor.Execute<GetUserStatisticsQueryResult>(
    new GetUserStatisticsQuery(1));
公共类GetUserStatisticsQuery
{
public int UserId{get;set;}
}
公共类GetUserStatisticsQueryResult
{
...
}
公共类GetUserStatisticsQueryHandler:
IHandleQuery
{
公共GetUserStatisticsQueryResult句柄(GetUserStatisticsQueryQuery)
{
…“从x中选择*”。。。
}
}
var result=\u queryExecutor.Execute(
新的GetUserStatisticsQuery(1));

我添加我的结论作为答案,因为我很快意识到这个问题是相对的,不准确的,很大程度上取决于个人喜好或设计趋势

这些评论和答案帮助我更清楚地看到了这样的事情基本上应该如何实施,谢谢你的所有努力

结论 “存储库”应该明确地只负责数据持久化。因为它不包含任何域逻辑或特定于类型的logc,我认为它可以表示并实现为一个
irepositionory
接口,使用诸如
Save
Delete
Query
GetByID
等通用方法,请参考我在原帖子开头提到的前一个问题

另一方面,我认为(至少现在在我当前的项目中)为每个较低级别的域逻辑(在大多数情况下是某种查询逻辑)任务引入新类是一种过度工程化的解决方案,这对我来说是不需要的。我的意思是我不想引入像
GetUsersInRoles
GetDevicesInRegionWithType
等类。我觉得在引用它们时,我最终会得到很多类和很多样板代码

我决定
public static class DataAccessExtensions {
  public static IEnumerable<City> GetCitiesForUser(this IRepository repo, User user, Region region = null) { ... }
}
App Service
result1 = finder.query1()
result2 = finder.query2()
result3= yetanotherfinder.query();
domainresult = domainservice.calculate(result1,result2,result3);
public class GetUserStatisticsQuery 
{
    public int UserId { get; set; }
}

public class GetUserStatisticsQueryResult
{
    ...
}

public class GetUserStatisticsQueryHandler : 
    IHandleQuery<GetUserStatisticsQuery, GetUserStatisticsQueryResult> 
{
    public GetUserStatisticsQueryResult Handle(GetUserStatisticsQuery query) 
    {
        ... "SELECT * FROM x" ...
    }
}

var result = _queryExecutor.Execute<GetUserStatisticsQueryResult>(
    new GetUserStatisticsQuery(1));
public static class UserQueries {
  public static IEnumerable<User> GetInRoles(this IRepository repository, params string[] roles) 
  {
    ...
  }
}
// technical class, wraps an IRepository dummily forwarding all members to the wrapped object
public class RepositoryWrapper : IRepository 
{
  internal RepositoryWrapper(IRepository repository) {...}
}

// technical class for holding user related query extensions
public sealed class UserQueriesWrapper : RepositoryWrapper {
  internal UserQueriesWrapper(IRepository repository) : base(repository) {...}  
}

public static class UserQueries {

 public static UserQueriesWrapper Users(this IRepository repository) {
   return new UserQueriesWrapper(repository);
 }

 public static IEnumerable<User> GetInRoles(this UserQueriesWrapper repository, params string[] roles) 
 {
  ...
  }
}

...
// now I can use it with a nicer and cleaner syntax
var users = _repo.Users().GetInRoles("a", "b");
...