lundi 16 octobre 2017

Pagination as cross cutting concern in cqrs with simpleinjector

In my application design I'm trying to implement Pagination as a Cross Cutting Concern with the Decorator pattern applied to an implementation of the CQRS pattern.
I also have a multilayered architecture and I have the opinion that pagination is not part of business logic (and thus a cross cutting concern). This is a decision already made and should not be discussed in this topic.

In my design, the intention is that the presentation layer can consume a paginated query with a specific closed generic type

IQueryHandler<GetAllItemsQuery, PaginatedQuery<Item>>

with the following signatures:

public class GetAllItemsQuery : PaginatedQuery<Item>

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

public class PaginatedResult<TModel>

The idea is that the consumer should receive a PaginatedResult for a specific model, that contains the paginated items and some metadata (e.g. the total number of items of the query performed without pagination applied), so that the UI can render it's pagination.
The main philosophy of my design is that the queryhandler should just apply it's business logic (e.g. getting all items). It only describes how it would do this, it doesn't necessarily has to execute the query.
In my case a decorator on the queryhandler actually applies pagination on the query and executes it (e.g. by calling .ToArray() on a Linq to Entities query).
What I want is that my queryhandler should be implemented like this:

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

So that the return type of the handler is IEnumerable<Item>. This way the handler is forced to be Single Responsible. The problem I'm facing is probably the way I'm using Simple Injector. Because I'm registering my IQueryHandler<,> like

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

which wouldn't verify my design, because of an obvious invalid configuration: I'm injecting IQueryHandler<GetAllItemsQuery, PaginatedResult<Item>> into my consumer, but don't actually implement it. Instead the handler implements IQueryHandler<GetAllItemsQuery, IEnumerable<Item>>.

So as a solution I tried to implement an Interceptor and register that conditionally (note the usage of 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);

but this gives me an exception on verify:

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.

The exception is not really clear on what the operation is and why it's invalid. Perhaps I'm doing something wrong?

Here is the implementation of the Interceptor:

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);
    }
}

and the decorator:

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
            };
        }
    }

Aucun commentaire:

Enregistrer un commentaire