Sunday, November 11, 2012

CQRS Query Engine - Writing a Testable, maintainable and extensible CQRS Query Engine - Part 2

  1. Designing the Query Engine
  2. Encapsulating common behavior (template pattern)
  3. Implementing Queries
  4. Creating the Query Resolver

In the previous post I created the design of the Query Engine, in this post I will create the initial implementation


I created several abstract classes to encapsulate common behavior:


public abstract  class QueryPageAndSortingBase<TQueryResult>
{
    protected abstract IQueryable<TQueryResult> ApplyDefaultOrder(IQueryable<TQueryResult> items);

    protected virtual IQueryable<TQueryResult> ApplyPagingAndSorting(IQueryable<TQueryResult> items, PagingAndSortingInfo pagingAndSortingInfo)
    {
        Condition.Requires(pagingAndSortingInfo).IsNotNull();
        Condition.Requires(items).IsNotNull();

        var customQuery = items;
        var page = pagingAndSortingInfo.Page;
        var pageIndex = page - 1;
        var pagesize = pagingAndSortingInfo.PageSize;
        var orderDirection = pagingAndSortingInfo.OrderDirection;
        var orderField = pagingAndSortingInfo.OrderByField;

        if (!string.IsNullOrWhiteSpace(orderField))
        {
            switch (orderDirection)
            {
                case OrderDirection.Ascending:
                    customQuery = customQuery.OrderBy(orderField);
                    break;
                case OrderDirection.Descending:
                    customQuery = customQuery.OrderBy(string.Format("{0} descending", orderField));
                    break;
                default:
                    throw new ArgumentOutOfRangeException("pagingAndSortingInfo", "Sorting can only be Ascending or Descending.");
            }
        }
        else
        {
            customQuery = this.ApplyDefaultOrder(customQuery);
        }

        customQuery = customQuery.Skip(pageIndex * pagesize).Take(pagesize);
        Condition.Ensures(customQuery).IsNotNull();

        return customQuery;
    }
}

The QueryPageAndSortingBase<TQueryResult> class is used to encapsulate paging and sorting behavior


public abstract class QueryHandlerBase<TQueryResult> : QueryPageAndSortingBase<TQueryResult>
{
    protected abstract IQueryable<TQueryResult> InitialItems { get; }

    protected virtual IQueryable<TQueryResult> HandleCustomQuery(IQueryable<TQueryResult> items, PagingAndSortingInfo pagingAndSortingInfo = null)
    {
        Condition.Requires(items).IsNotNull();

        var customQuery = items;

        if (pagingAndSortingInfo != null)
        {
            customQuery = this.ApplyPagingAndSorting(items: customQuery, pagingAndSortingInfo: pagingAndSortingInfo);
        }

        Condition.Ensures(customQuery).IsNotNull();

        return customQuery;
    }
}

The QueryHandlerBase<TQueryResult> controls the flow to call the QueryPageAndSortingBase<TQueryResult>.ApplyPagingAndSorting method. This method is in charge to apply the paging and sorting.


public abstract class QueryHandler<TQuery, TQueryResult> : QueryHandlerBase<TQueryResult>,
    IQueryHandler<TQuery, TQueryResult>
    where TQuery : IQuery
{
    public virtual QueryResults<TQueryResult> HandleQuery(TQuery query, PagingAndSortingInfo pagingAndSortingInfo = null)
    {
        Condition.Requires(query).Evaluate(query != null);
        Condition.Requires(this.InitialItems).IsNotNull();

        var queryProcessed = this.ApplyQuery(query, this.InitialItems);
        Condition.Ensures(queryProcessed).IsNotNull();

        var res = this.HandleCustomQuery(queryProcessed, pagingAndSortingInfo);
        Condition.Ensures(res).IsNotNull();

        return QueryResults.Of(res.ToList(), queryProcessed.Count());
    }

    protected abstract IQueryable<TQueryResult> ApplyQuery(TQuery query, IQueryable<TQueryResult> items);
}

The QueryHandler<TQuery, TQueryResult> controls the flow to apply a custom and explicit query, and apply paging and sorting when required.


public abstract class QueryHandler<TQueryResult> : QueryHandlerBase<TQueryResult>,
    IQueryHandler<TQueryResult>
{
    public virtual QueryResults<TQueryResult> HandleQuery(PagingAndSortingInfo pagingAndSortingInfo = null)
    {
        Condition.Requires(this.InitialItems).IsNotNull();

        var initialItems = this.InitialItems;
        var res = this.HandleCustomQuery(initialItems, pagingAndSortingInfo);
        Condition.Ensures(res).IsNotNull();

        return QueryResults.Of(res.ToList(), initialItems.Count());
    }
}

The QueryHandler<TQueryResult> class is used to control the flow to query all the elements applying paging and sorting when required


Unit testing


I’ll show you the unit tests of these base classes, since they are abstract, and most of the methods I want to test are protected I can’t use Moq =((((( buaaa, meaning that I have to write some testing mocks manually..... but anyway, let’s do it:


QueryPageAndSortingBase Tests


Manual Mock


public class QueryPageAndSortingBaseMock : QueryPageAndSortingBase<Movie>
{
    protected override IQueryable<Movie> ApplyDefaultOrder(IQueryable<Movie> items)
    {
        return items.OrderByDescending(x => x.Title);
    }
}

Tests


[TestClass]
public class QueryPageAndSortingBaseTests
{
    [TestClass]
    public class TheApplyPagingAndSortingMethod
    {
        [TestMethod]
        public void it_should_apply_the_default_order_when_the_order_field_is_null_or_empty()
        {
            var sut = new QueryPageAndSortingBaseMock();
            var items = Builder<Movie>.CreateListOfSize(10).Build().AsQueryable();
            var expectedItems = items.OrderByDescending(x => x.Title);
            var methodInfo = sut.GetType().GetMethod("ApplyPagingAndSorting", BindingFlags.Instance | BindingFlags.NonPublic);
            var pagingInfo = new PagingAndSortingInfo(orderByField: string.Empty);

            var res = (IQueryable<Movie>)methodInfo.Invoke(sut, new object[] { items, pagingInfo });

            res.Should().NotBeNull()
                .And.HaveCount(items.Count())
                .And.ContainInOrder(expectedItems);
        }

        [TestMethod]
        public void it_should_order_in_ascending_mode_when_the_order_fiels_is_not_null_and_the_order_direction_is_Ascending()
        {
            var sut = new QueryPageAndSortingBaseMock();
            var items = Builder<Movie>.CreateListOfSize(10).Build().AsQueryable();
            var expectedItems = items.OrderBy(x => x.ID);
            var methodInfo = sut.GetType().GetMethod("ApplyPagingAndSorting", BindingFlags.Instance | BindingFlags.NonPublic);
            var pagingInfo = new PagingAndSortingInfo(orderByField: "ID", orderDirection: OrderDirection.Ascending);

            var res = (IQueryable<Movie>)methodInfo.Invoke(sut, new object[] { items, pagingInfo });

            res.Should().NotBeNull()
                .And.HaveCount(items.Count())
                .And.ContainInOrder(expectedItems);
        }

        [TestMethod]
        public void it_should_order_in_descending_mode_when_the_order_fiels_is_not_null_and_the_order_direction_is_Descending()
        {
            var sut = new QueryPageAndSortingBaseMock();
            var items = Builder<Movie>.CreateListOfSize(10).Build().AsQueryable();
            var expectedItems = items.OrderByDescending(x => x.ID);
            var methodInfo = sut.GetType().GetMethod("ApplyPagingAndSorting", BindingFlags.Instance | BindingFlags.NonPublic);
            var pagingInfo = new PagingAndSortingInfo(orderByField: "ID", orderDirection: OrderDirection.Descending);

            var res = (IQueryable<Movie>)methodInfo.Invoke(sut, new object[] { items, pagingInfo });

            res.Should().NotBeNull()
                .And.HaveCount(items.Count())
                .And.ContainInOrder(expectedItems);
        }

        [TestMethod]
        public void it_should_paginate_the_results()
        {
            var sut = new QueryPageAndSortingBaseMock();
            var page = 3;
            var pageSize = 4;
            var pageIndex = page - 1;
            var pagingInfo = new PagingAndSortingInfo(orderByField: "Title", orderDirection: OrderDirection.Descending, page: page, pageSize: pageSize);
            var items = Builder<Movie>.CreateListOfSize(20).Build().AsQueryable();
            var expectedItems = items.OrderByDescending(x => x.Title).Skip(pageIndex * pageSize).Take(pageSize);
            var methodInfo = sut.GetType().GetMethod("ApplyPagingAndSorting", BindingFlags.Instance | BindingFlags.NonPublic);

            var res = (IQueryable<Movie>)methodInfo.Invoke(sut, new object[] { items, pagingInfo });

            res.Should().NotBeNull()
                .And.HaveCount(pageSize)
                .And.ContainInOrder(expectedItems);
        }
    }
}

QueryHandlerBase Tests


Manual Mocks


public class QueryHandlerBaseMock : QueryHandlerBase<Movie>
{
    protected override IQueryable<Movie> InitialItems
    {
        get { throw new NotImplementedException(); }
    }

    protected override IQueryable<Movie> ApplyDefaultOrder(IQueryable<Movie> items)
    {
        throw new NotImplementedException();
    }
}

Tests


[TestClass]
public class QueryHandlerBaseTests
{
    [TestClass]
    public class TheHandleCustomQueryMethod
    {
        [TestMethod]
        public void it_should_not_apply_paging_nor_sorting_when_the_pagingAndSortingInfo_parameter_is_null()
        {
            var sut = new QueryHandlerBaseMock();
            var items = Builder<Movie>.CreateListOfSize(20).Build().AsQueryable();
            var paging = (PagingAndSortingInfo)null;
            var methodInfo = sut.GetType().GetMethod("HandleCustomQuery", BindingFlags.NonPublic | BindingFlags.Instance);

            var res = (IQueryable<Movie>)methodInfo.Invoke(sut, new object[] { items, paging });

            res.Should().NotBeNull()
                .And.HaveCount(items.Count())
                .And.ContainInOrder(items);
        }

        [TestMethod]
        public void it_should_apply_paging_and_sorting_when_the_pagingAndSortingInfo_parameter_is_not_null()
        {
            var sut = new QueryHandlerBaseMock();
            var items = Builder<Movie>.CreateListOfSize(20).Build().AsQueryable();
            var paging = new PagingAndSortingInfo(orderByField: "ID");
            var pageIndex = paging.Page - 1;
            var expectedItems = items.OrderBy(x => x.ID).Skip(pageIndex * paging.PageSize).Take(paging.PageSize);
            var methodInfo = sut.GetType().GetMethod("HandleCustomQuery", BindingFlags.NonPublic | BindingFlags.Instance);

            var res = (IQueryable<Movie>)methodInfo.Invoke(sut, new object[] { items, paging });

            res.Should().NotBeNull()
                .And.HaveCount(expectedItems.Count())
                .And.ContainInOrder(expectedItems);
        }
    }
}

Uff that was too much code for a post, the rest of the tests will be available when I post the full code =)


In the next post, I will show you how to use these objects and how easy is to create query objects


This is the graphical representation of these classes:




No comments:

Post a Comment

If you found this post useful please leave your comments, I will be happy to hear from you.