2017-10-16 89 views
1

在我的应用程序的设计,我想实现Pagination与应用于CQRS模式an implementationDecorator模式的Cross Cutting Concern
我也有一个multilayered architecture我认为分页是而不是业务逻辑的一部分(因此是一个横切关注)。这是已经做出的决定,不应在本主题中讨论。分页在CQRS横切关注点与simpleinjector

在我的设计,目的是表示层可以消耗具有特定的分页查询封闭泛型类型

IQueryHandler<GetAllItemsQuery, PaginatedQuery<Item>> 

具有以下特征:

public class GetAllItemsQuery : PaginatedQuery<Item> 

public class PaginatedQuery<TModel> : 
    IQuery<PaginatedResult<TModel>>, IQuery<IEnumerable<TModel>> 

public class PaginatedResult<TModel> 

的想法是,消费者应该为特定模型收到PaginatedResult,该模型包含分页项目和一些元数据(例如,未应用分页执行的查询项目总数),以便UI可以呈现分页。
我的设计的主要理念是查询处理程序应该应用它的业务逻辑(例如获取所有项目)。它只描述它是如何做到这一点的,它不一定要执行查询。
在我的情况下,查询处理程序上的装饰器实际上在查询上应用分页并执行它(例如,通过在Linq to Entities查询上调用.ToArray())。
我想的是,我应该queryhandler这样实现:

public class GetAllItemsQueryHandler : IQueryHandler<GetAllItemsQuery, IEnumerable<Item>> 

所以,处理程序的返回类型为IEnumerable<Item>。这样处理程序被强制为Single Responsible。 我面临的问题可能是我使用Simple Injector的方式。因为我登记我的IQueryHandler<,>

container.Register(typeof(IQueryHandler<,>), assemblies); 

这不会验证我的设计,因为一个明显的无效配置的:我注射IQueryHandler<GetAllItemsQuery, PaginatedResult<Item>>到我的消费者,但实际上并没有实现它。处理程序执行IQueryHandler<GetAllItemsQuery, IEnumerable<Item>>

因此,作为一个解决方案,我试图执行一个Interceptor并注册条件(注意C# 7.0 local functions使用):

Type PaginationInterceptorFactory(TypeFactoryContext typeContext) 
{ 
    // IQueryHandler<TQuery, TResult> where TResult is PaginatedResult<TModel> 
    var queryType = typeContext.ServiceType.GetGenericArguments()[0]; // TQuery 
    var modelType = typeContext.ServiceType.GetGenericArguments()[1].GetGenericArguments()[0]; // TModel in PaginatedResult<TModel> as TResult 
    return typeof(PaginatedQueryHandlerInterceptor<,>).MakeGenericType(queryType, modelType); 
} 
bool PaginationInterceptorPredicate(PredicateContext predicateContext) => 
    predicateContext.ServiceType.GetGenericArguments()[0].IsPaginatedQuery(); // if TQuery is of type PaginatedQuery<> 

container.RegisterConditional(typeof(IQueryHandler<,>), PaginationInterceptorFactory, Lifestyle.Singleton, PaginationInterceptorPredicate); 

,但是这给了我在验证一个例外:

System.InvalidOperationException occurred 
    Message=The configuration is invalid. Creating the instance for type [TYPE] failed. This operation is only valid on generic types. 
    Source=SimpleInjector 
    StackTrace: 
    at SimpleInjector.InstanceProducer.VerifyExpressionBuilding() 
    at SimpleInjector.Container.VerifyThatAllExpressionsCanBeBuilt(InstanceProducer[] producersToVerify) 
    at SimpleInjector.Container.VerifyThatAllExpressionsCanBeBuilt() 
    at SimpleInjector.Container.VerifyInternal(Boolean suppressLifestyleMismatchVerification) 
    at SimpleInjector.Container.Verify() 

Inner Exception 1: 
ActivationException: This operation is only valid on generic types. 

Inner Exception 2: 
InvalidOperationException: This operation is only valid on generic types. 

的例外情况在操作是什么以及它为什么无效方面不是很清楚。也许我做错了什么?

这里是拦截器的实现:

public class PaginatedQueryHandlerInterceptor<TQuery, TModel> : IQueryHandler<TQuery, PaginatedResult<TModel>> 
    where TQuery : PaginatedQuery<TModel> 
{ 
    private readonly IQueryHandler<TQuery, IEnumerable<TModel>> _queryHandler; 

    public PaginatedQueryHandlerInterceptor(IQueryHandler<TQuery, IEnumerable<TModel>> queryHandler) 
    { 
     _queryHandler = queryHandler; 
    } 

    public PaginatedResult<TModel> Handle(TQuery query) 
    { 
     return (dynamic) _queryHandler.Handle(query); 
    } 
} 

和装饰:

public class PaginationQueryHandlerDecorator<TQuery, TResult> : IQueryHandler<TQuery, TResult> 
     where TQuery : class, IQuery<TResult> 
    { 
     private readonly IQueryHandler<TQuery, TResult> _decoratee; 

     public PaginationQueryHandlerDecorator(
      IQueryHandler<TQuery, TResult> decoratee) 
     { 
      _decoratee = decoratee; 
     } 

     public TResult Handle(TQuery query) 
     { 
      query.ThrowIfNull(nameof(query)); 

      var result = _decoratee.Handle(query); 

      if (query.IsPaginationQuery(out var paginatedQuery)) 
      { 
       return Paginate(result, paginatedQuery.Pagination); 
      } 

      return result; 
     } 

     private static TResult Paginate(TResult result, Pagination pagination) 
     { 
      return Paginate(result as dynamic, pagination.Page, pagination.ItemsPerPage); 
     } 

     private static PaginatedResult<TModel> Paginate<TModel>(IEnumerable<TModel> result, int page, int itemsPerPage) 
     { 
      var items = result as TModel[] ?? result.ToArray(); 

      var paginated = items.Skip(page * itemsPerPage).Take(itemsPerPage).ToArray(); 

      return new PaginatedResult<TModel> 
      { 
       Items = paginated, 
       Count = items.Length 
      }; 
     } 
    } 

回答

1

这已经是做了一个决定,而不应这一主题进行讨论。

嘛....如果你坚持:)

但至少可以阻止那些来自查询返回IEnumerable<T>,但返回IQueryable<T>代替。使用IEnumerable<T>将导致全部数据从数据库中返回,即使您翻页。

这就是说,我不知道什么是你的代码错误,但我想提出一个稍微不同的方法:

public class PagedQueryHandler<TQuery, TItem> 
    : IQueryHandler<PagedQuery<TQuery, TItem>, Paged<TItem>> 
    where TQuery : IQuery<IQueryable<TItem>> 
{ 
    private readonly IQueryHandler<TQuery, IQueryable<TItem>> handler; 

    public PagedQueryHandler(IQueryHandler<TQuery, IQueryable<TItem>> handler) 
    { 
     this.handler = handler; 
    } 

    public Paged<TItem> Handle(PagedQuery<TQuery, TItem> query) 
    { 
     var paging = query.PageInfo ?? new PageInfo(); 
     IQueryable<TItem> items = this.handler.Handle(query.Query); 
     return new Paged<TItem> 
     { 
      Items = items.Skip(paging.PageIndex * paging.PageSize) 
       .Take(paging.PageSize).ToArray(), 
      Paging = paging, 
     }; 
    } 
} 

这个通用IQueryHandler可以实现分页查询映射到一个非分页查询。这里Paged<T>PageInfoPagedQuery<TQuery, TItem>定义如下:

public class Paged<T> 
{ 
    public PageInfo Paging { get; set; } 
    public T[] Items { get; set; } 
} 

public class PageInfo 
{ 
    public int PageIndex { get; set; } 
    public int PageSize { get; set; } = 20; 
} 

public class PagedQuery<TQuery, TItem> : IQuery<Paged<TItem>> 
    where TQuery : IQuery<IQueryable<TItem>> 
{ 
    public TQuery Query { get; set; } 
    public PageInfo PageInfo { get; set; } 
} 

PageInfoPaged<T>从这个Github上回购来源:https://github.com/dotnetjunkie/solidservices/tree/master/src/Contract/

PagedQueryHandler<TQuery, TItem>可以注册如下:

container.Register(typeof(IQueryHandler<,>), typeof(PagedQueryHandler<,>)); 

有了这个类和它的注册,你可以简单地注入一个可分页的查询处理程序到消费者,例如:

public class ItemsController 
{ 
    IQueryHandler<PagedQuery<GetAllItemsQuery, Item>, Paged<Item>> handler; 

    public ItemsController(
     IQueryHandler<PagedQuery<GetAllItemsQuery, Item>, Paged<Item>> handler) 
    { 
     this.handler = handler; 
    } 

    public ActionResult Index(PagedQuery<GetAllItemsQuery, Item> query) 
    { 
     return View(this.handler.Handle(query)); 
    } 
} 
+0

解决方案只有一个小问题。这就是说,当'PagedQueryHandler'调用'.ToArray()'时,我的DbContext被放置。因为我用'LifetimeScopeQueryHandlerProxy'装饰我的查询处理器。 – QuantumHive

+0

这意味着你不应该修饰返回'IQueryable '实例的查询处理程序。或者确保LifetimeScopeQueryHandlerProxy检测它是否嵌套并阻止它启动并处理一个DbContext。但是,这对我的设计并不是一个问题,因为您的原始文章包含相同的问题。 – Steven

+0

我已经将我的初始设计重构为*稍有不同的方法*,现在完美地工作。我在原始设计中使用'IEnumerable'的原因是因为我的查询会从'PaginatedQuery'继承,迫使处理程序返回一个'IEnumerable',但让业务决定它是否是'IQueryable'。基础数据模型是超级高性能的)。在你的设计中,查询保持自然,泛型类型约束迫使处理程序返回'IQueryable'。缺点是处理程序被迫只返回一个查询。 – QuantumHive

0

实际的问题确实是我做错了,但与简单注射器无关。

当使用RegisterConditional,在谓词正在调用该扩展方法:

bool PaginationInterceptorPredicate(PredicateContext predicateContext) => 
    predicateContext.ServiceType.GetGenericArguments()[0].IsPaginatedQuery(); 

IsPaginatedQuery实施是错误的,导致异常:

public static bool IsPaginatedQuery(this Type queryType) => 
    queryType.GetInterfaces().Any(i => i.GetGenericTypeDefinition() == typeof(PaginatedQuery<>)); 

由于查询还实现了非通用接口IPagination,因此GetGenericTypeDefinition()方法引起异常。