精心设计的查询命令和/或规范

Eri*_*sch 88 c# command-pattern specification-pattern repository-pattern

我一直在寻找一个很好的解决方案来解决典型的Repository模式所带来的问题(专门的查询方法的增长列表等等.请参阅:http://ayende.com/blog/3955/repository-是新单身人士).

我非常喜欢使用Command查询的想法,特别是通过使用规范模式.但是,我的规范问题是它只涉及简单选择的标准(基本上是where子句),而不涉及查询的其他问题,例如连接,分组,子集选择或投影等.基本上,许多查询必须通过所有额外的箍来获得正确的数据集.

(注意:我在命令模式中使用术语"命令",也称为查询对象.我不是在命令/查询分离中讨论命令,其中查询和命令之间存在区别(更新,删除,插入))

因此,我正在寻找封装整个查询的替代方案,但仍然足够灵活,以至于您不仅仅需要交换spaghetti Repositories以进行大量的命令类.

我已经使用过,例如Linqspecs,虽然我发现能够为选择标准指定有意义的名称有一些价值,但这还不够.也许我正在寻找一种结合多种方法的混合解决方案.

我正在寻找其他人可能已经开发的解决方案,以解决这个问题,或解决不同的问题,但仍满足这些要求.在链接的文章中,Ayende建议直接使用nHibernate上下文,但我觉得这很大程度上使您的业务层复杂化,因为它现在还必须包含查询信息.

等待期过后,我会在此提供赏金.因此,请提供有价值的解决方案,并提供良好的解释,我将选择最佳解决方案,并向选手投票.

注意:我正在寻找基于ORM的东西.不必明确是EF或nHibernate,但这些是最常见的并且最适合.如果它可以很容易地适应其他ORM,那将是一个奖励.Linq兼容也不错.

更新:我真的很惊讶这里没有很多好的建议.似乎人们完全是CQRS,或者他们完全在资源库中.我的大多数应用程序都不够复杂,无法保证CQRS(大多数CQRS倡导者都会说你不应该使用它).

更新:这里似乎有点混乱.我不是在寻找新的数据访问技术,而是在业务和数据之间设计合理的界面.

理想情况下,我正在寻找的是Query对象,规范模式和存储库之间的某种交叉.正如我上面所说,规范模式只处理where子句方面,而不是查询的其他方面,如连接,子选择等.存储库处理整个查询,但一段时间后失控.查询对象也处理整个查询,但我不想简单地用查询对象的爆炸替换存储库.

dav*_*d.s 88

免责声明:由于还没有很好的答案,我决定在我刚读过的一篇很棒的博客文章中发帖,几乎是逐字复制的.你可以在这里找到完整的博客文章.所以这里是:


我们可以定义以下两个接口:

public interface IQuery<TResult>
{
}

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}
Run Code Online (Sandbox Code Playgroud)

IQuery<TResult>指定定义与它返回使用的数据的特定的查询消息TResult的通用类型.使用先前定义的接口,我们可以定义如下的查询消息:

public class FindUsersBySearchTextQuery : IQuery<User[]>
{
    public string SearchText { get; set; }
    public bool IncludeInactiveUsers { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

该类定义了一个带有两个参数的查询操作,这将导致一个User对象数组.处理此消息的类可以定义如下:

public class FindUsersBySearchTextQueryHandler
    : IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
    private readonly NorthwindUnitOfWork db;

    public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db)
    {
        this.db = db;
    }

    public User[] Handle(FindUsersBySearchTextQuery query)
    {
        return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray();
    }
}
Run Code Online (Sandbox Code Playgroud)

我们现在可以让消费者依赖于通用IQueryHandler接口:

public class UserController : Controller
{
    IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler;

    public UserController(
        IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler)
    {
        this.findUsersBySearchTextHandler = findUsersBySearchTextHandler;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        User[] users = this.findUsersBySearchTextHandler.Handle(query);    
        return View(users);
    }
}
Run Code Online (Sandbox Code Playgroud)

这个模型立刻为我们提供了很大的灵活性,因为我们现在可以决定注入什么UserController.我们可以注入一个完全不同的实现,或者包含实际实现的实现,而不必对UserController(和该接口的所有其他使用者)进行更改.

在我们的代码中IQuery<TResult>指定或注入时,该接口为我们提供了编译时支持IQueryHandlers.当我们改变FindUsersBySearchTextQueryto返回UserInfo[](通过实现IQuery<UserInfo[]>)时,UserController将无法编译,因为泛型类型约束IQueryHandler<TQuery, TResult>将无法映射FindUsersBySearchTextQueryUser[].

IQueryHandler然而,将接口注入到消费者中有一些不太明显的问题仍然需要解决.我们的消费者的依赖关系数量可能会变得太大,并且可能导致构造函数过度注入 - 当构造函数需要太多参数时.类执行的查询数可能会频繁更改,这需要不断更改构造函数参数的数量.

我们可以解决必须注入太多IQueryHandlers额外的抽象层的问题.我们创建一个位于使用者和查询处理程序之间的中介:

public interface IQueryProcessor
{
    TResult Process<TResult>(IQuery<TResult> query);
}
Run Code Online (Sandbox Code Playgroud)

IQueryProcessor是一个非通用接口,具有一个通用方法.正如您在界面定义中看到的,IQueryProcessor取决于IQuery<TResult>接口.这使我们可以在依赖于我们的消费者中获得编译时支持IQueryProcessor.让我们改写UserController使用新的IQueryProcessor:

public class UserController : Controller
{
    private IQueryProcessor queryProcessor;

    public UserController(IQueryProcessor queryProcessor)
    {
        this.queryProcessor = queryProcessor;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        // Note how we omit the generic type argument,
        // but still have type safety.
        User[] users = this.queryProcessor.Process(query);

        return this.View(users);
    }
}
Run Code Online (Sandbox Code Playgroud)

UserController现在依赖于IQueryProcessor能够处理所有的查询.所述UserControllerSearchUsers方法调用IQueryProcessor.Process传递初始化的查询对象的方法.由于FindUsersBySearchTextQuery实现了IQuery<User[]>接口,我们可以将它传递给泛型Execute<TResult>(IQuery<TResult> query)方法.由于C#类型推断,编译器能够确定泛型类型,这节省了我们必须明确说明类型.该Process方法的返回类型也是已知的.

现在有责任实施IQueryProcessor找到合适的权利IQueryHandler.这需要一些动态类型,并且可选地使用依赖注入框架,并且只需几行代码即可完成:

sealed class QueryProcessor : IQueryProcessor
{
    private readonly Container container;

    public QueryProcessor(Container container)
    {
        this.container = container;
    }

    [DebuggerStepThrough]
    public TResult Process<TResult>(IQuery<TResult> query)
    {
        var handlerType = typeof(IQueryHandler<,>)
            .MakeGenericType(query.GetType(), typeof(TResult));

        dynamic handler = container.GetInstance(handlerType);

        return handler.Handle((dynamic)query);
    }
}
Run Code Online (Sandbox Code Playgroud)

QueryProcessor类构造一个特定的IQueryHandler<TQuery, TResult>基础上,提供的查询实例的类型类型.此类型用于请求提供的容器类获取该类型的实例.不幸的是,我们需要Handle使用反射调用该方法(在这种情况下使用C#4.0 dymamic关键字),因为此时无法转换处理程序实例,因为泛型TQuery参数在编译时不可用.但是,除非Handle重命名该方法或获取其他参数,否则此调用将永远不会失败,如果您愿意,可以很容易地为此类编写单元测试.使用反射会略微下降,但没有什么可担心的.


回答你的一个问题:

因此,我正在寻找封装整个查询的替代方案,但仍然足够灵活,以至于您不仅仅需要交换spaghetti Repositories以进行大量的命令类.

使用这种设计的结果是系统中会有很多小类,但是有很多小/焦点类(名字清晰)是一件好事.这种方法显然比使用存储库中相同方法的不同参数的许多重载要好得多,因为您可以将它们组合在一个查询类中.因此,您仍然可以获得比存储库中的方法少得多的查询类.

  • @Cemre我最终将我的查询封装在Extension方法中,返回`IQueryable`并确保不枚举集合,然后从`QueryHandler`我刚刚调用/链接查询.这使我可以灵活地对我的查询进行单元测试并将其链接起来.我在`QueryHandler`之上有一个应用程序服务,我的控制器负责直接与服务而不是处理程序对话 (4认同)
  • 看起来你获得了奖项.我喜欢这些概念,我只是希望有人能提出真正不同的东西.恭喜. (2认同)