- Designing the Query Engine
- Encapsulating common behavior (template pattern)
- Implementing Queries
- Creating the Query Resolver
In my last post I blogged about writing a Query Repository in a test-friendly way but even when the code was testable I still believe it can be improved in several ways.
I’m going to build a small, simple and test-friendly Query Engine. This will be the first post in this series.
If you are familiar with CQRS you will find the code of this post familiar, since I’m following the same patterns used in the Commands branch in a CQRS architecture
The traditional query repository
Before start, let’s see how a typical query repository looks like (assuming a fictitious Movies query repository):
public interface IMoviesQueryRepository
{
Movie FindByID(Guid movieID);
IQueryable<Movie> FindAll();
IQueryable<Movie> FindByTitle(string title);
IQueryable<Movie> FindByDirectors(string[] director);
IQueryable<Movie> FindByActors(string[] actors);
IQueryable<Movie> FindByReleaseDate(DateTime date);
}
There are several problems with an interface like this, I will talk about them in a minute.
The problem
Now let’s say that you start with a design like this and that you implement all these methods and use DI to inject the query repository wherever you want to use it. Now imagine that after awhile, you find out that you need to query your movies by Category so you just go and add the following method to your interface:
IQueryable<Movie> FindByCategory(string category);
You have to go and edit all your implementations of your IMoviesQueryRepository
(including all testing mocks) in order to be able to compile again your solution, then you add implementation of this new method in all of them.
So far so good, after a couple of days you realize that now you need a couple of filters more, let’s say that now you need to query by a list of categories and by the average cost of making each movie, so again you add the following methods to your interface:
IQueryable<Movie> FindByCategories(string[] categories);
IQueryable<Movie> FindByAverageCost(decimal averageCost);
And you repeat all the implementation process again.
This process will continue during the whole development lifecycle. And there are several filters that might be needed during this time for example, querying by rating, by amount of tickets sold, etc. etc.
So a design like this is not flexible, is not testable and is hard to maintain
Let’s see several problems with this design:
It violates the open-closed pricniple (open for extension/closed to modification).
The problem when you don’t follow this principle is the problem described before, you will have to go and edit all your implementations in order to add new functionality. You could potentially introduce new bugs to the application that would affect more than just one query (perhaps all of them?)
A design like this can lead to violate the YAGNI principle (You Ain’t Gonna Need It)
Why? Simply because in order to avoid the problem described before, you could try to think about all the possible filters that you might need in the application. This will lead you to write tons of query filters that you won’t ever need
This design is not test friendly I just wrote the result of these queries as
IQueryable
objects on purpose to ilustrate the point that this design is not test friendly, and actually you won’t be able to test in isolation your queries if you expose in your public APIIQueryable
objects. The reason is that your query logic will actually be spread out in several places in your application.
Let’s see some approaches that won’t work
Anti Unit Testing approach
The simplest approach is to forget about unit testing (oO) and consume the IQueryable
object directly in all the places where you will need it (usually the user interface)
I don’t have to tell you that I would never recommend something like this. But surprisingly in Stackoverflow I've seen a lot of code like this, and a lot of examples over the internet about MVC using this approach.
Naive approach
Well let’s say that you realize that unit test is so important in any application that you decide to go back to the original design and try to figure out how to fix it.
So a really simplistic approach would be to call for a small meeting with all your developers and tell them that even when you expose an IQueryable
object in your query repository they should never add new queries on it.... in order to “guarantee” that the query is encapsulated and therefore testable in isolation.....
….Good luck with that!
So now hopefully you will want to see the solution already... and here it is
This is my solution to the problem
Encapsulating the query
The first thing to do to refactor the original design is to encapsulate the query in order to guarantee that the query will actually be unit-tested in isolation. I’m going to follow the same approach I used in my last post, I will create a new DTO object to wrap my query results.
This object would look something like:
public sealed class QueryResults
{
public static QueryResults<TTarget> Of<TTarget>(IList<TTarget> results, int virtualRowsCount)
{
Condition.Requires(results).IsNotNull();
Condition.Requires(virtualRowsCount).IsGreaterOrEqual(0);
return new QueryResults<TTarget>(results, virtualRowsCount);
}
}
public class QueryResults<TTarget>
{
public QueryResults(IList<TTarget> results, int virtualRowsCount)
{
Condition.Requires(results).IsNotNull();
Condition.Requires(virtualRowsCount).IsGreaterOrEqual(0);
this.VirtualRowsCount = virtualRowsCount;
this.Results = results;
}
public int VirtualRowsCount { get; protected set; }
public IList<TTarget> Results { get; protected set; }
}
Since the query will be encapsulated you also need an object to pass paging and sorting parameters:
public sealed class PagingAndSortingInfo
{
public PagingAndSortingInfo(
int page = 1,
int pageSize = 10,
string orderByField = "",
OrderDirection orderDirection = OrderDirection.Ascending)
{
Condition.Requires(page).IsGreaterOrEqual(1);
Condition.Requires(pageSize).IsGreaterOrEqual(1); ;
this.Page = page;
this.PageSize = pageSize;
this.OrderByField = orderByField;
this.OrderDirection = orderDirection;
}
public int Page { get; private set; }
public int PageSize { get; private set; }
public string OrderByField { get; private set; }
public OrderDirection OrderDirection { get; private set; }
}
public enum OrderDirection
{
Ascending,
Descending
}
So your query repository would look like:
public interface IMoviesQueryRepository
{
Movie FindByID(Guid movieID);
QueryResults<Movie> FindAll(PagingAndSortingInfo pagingAndSortingInfo = null);
QueryResults<Movie> FindByTitle(string title, PagingAndSortingInfo pagingAndSortingInfo = null);
QueryResults<Movie> FindByDirectors(string[] director, PagingAndSortingInfo pagingAndSortingInfo = null);
QueryResults<Movie> FindByActors(string[] actors, PagingAndSortingInfo pagingAndSortingInfo = null);
QueryResults<Movie> FindByReleaseDate(DateTime date, PagingAndSortingInfo pagingAndSortingInfo = null);
QueryResults<Movie> FindByCategory(string category, PagingAndSortingInfo pagingAndSortingInfo = null);
QueryResults<Movie> FindByCategories(string[] categories, PagingAndSortingInfo pagingAndSortingInfo = null);
QueryResults<Movie> FindByAverageCost(decimal averageCost, PagingAndSortingInfo pagingAndSortingInfo = null);
}
Following this approach you will be able to unit test in isolation your queries, but you would still have the original problem.
Designing the Query Engine
OK, so now let’s design the query engine.
First of all I have a couple of interfaces that I’m using to mark my objects:
public interface IQuery
{
}
public interface IQueryID
{
}
The
IQuery
interface is the equivalent to theICommand
interface in the Commands branch in a CQRS architecture. This is used to mark value objects as query objectsThe
IQueryID
interface is used to mark a value object as the query id. This will allows us to query by id using any type of id and even with composed id’s.
I created three interfaces in order to represent the queries in an application This is the core of the Query Engine:
public interface IQueryHandler<TQuery, TQueryResult> where TQuery : IQuery
{
QueryResults<TQueryResult> HandleQuery(TQuery query, PagingAndSortingInfo pagingAndSortingInfo = null);
}
public interface IQueryHandler<TQueryResult>
{
QueryResults<TQueryResult> HandleQuery(PagingAndSortingInfo pagingAndSortingInfo = null);
}
public interface IQueryByIDHandler<TQueryID, TQueryResult> where TQueryID : IQueryID
{
TQueryResult HandleQuery(TQueryID queryID);
}
The
IQueryHandler
overload using theIQuery
interface will be used to define explicitly the queries in your applicationThe
IQueryHandler
overload without theIQuery
interface will be used to query all the records from the tableThe
IQueryByIDHandler
will be used to query an entity/table/view by ID
This is a graphical representation:
Stay in touch, in the next post I will create base classes to encapsulate common behavior
Hola juan tengo algunas Dudas con CQRS, quizas si tienes tiempo pudieras ayudarme, saludos.
ReplyDelete