2017-10-16 81 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()方法引起異常。