Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command and Query Responsibility Segregation pattern #3

Open
dotnetjunkie opened this issue Aug 21, 2015 · 1 comment
Open

Command and Query Responsibility Segregation pattern #3

dotnetjunkie opened this issue Aug 21, 2015 · 1 comment
Labels

Comments

@dotnetjunkie
Copy link
Owner

dotnetjunkie commented Aug 21, 2015

Migrated from: https://solidservices.codeplex.com/discussions/574373

LauriSaar wrote:

Hi,

There is one great blog post about Command-query separation which is located here.

I've played with this architecture for some time and one question pops up: how would you handle sorting through IQuery<TResult>?

Let’s say that I have MVC webapp that needs server-side sorting and I want that sorting and paging happens in DB.

Behind these interfaces it is matter of type parameters if single entity’s property must be sortable at DB level. But as soon as I want to sort by more than one property it might get somewhat unintuitive. And I haven’t figured out simple way to pass ordering of multi sorting (not sort direction).

At first I passed lambda of Expression<Func<TEntity,TKey>> together with sort direction, used separate class for that. Overall it got quite verbose because all of those type parameters, but it worked fine, Linq2Entity’s IQueryable<>.OrderBy() was happy.

But as soon as I moved to multi sorting I found out that I cannot use IEnumerable< Expression<Func<TEntity,TKey>>> because TKey can vary with different properties. Tried with Expression<Func<TEntity,object>> and dynamic -- Linq2Entity’s IQueryable<>.OrderBy() was unhappy.

Next thing that worked was “public SortDirection? SortAmount {get;set}”-like properties on query implementation. SortDirection is enum with 2 constants. Handler has “if” statements to check for which properties to call .OrderBy().

But this solution’s drawback is that I haven’t found intuitive way to pass ordering of .OrderBy() calls.

@dotnetjunkie
Copy link
Owner Author

I would suggest not passing along Expression objects with the query. Strive the messages you sent in your system (commands, queries, events) to be serializable. This means that they can only be composed of primitives, or other serializable DTOs. Not only will this allow you to keep your messages simple, and keep you from passing along entities and other internal stuff, being able to serialize those messages allows you to plug-in cross-cutting concerns that depend on this serializability, such as audit trailing, logging, diagnosis and caching.

Just as keeping the query messages simple, the same holds for the return types. For instance, do not return IQueryable<T> instances. You can't serialize an IQueryable and you can't send it over the wire or cache it. But even if this isn't (currently) an issue, letting the business layer return IQueryable<T> will cause the business layer to lose control over the returned query. Obvious disadvantage is that you can't measure the performance of the execution of an query handler anymore, because it will return almost instantly, since the execution of the real database query is deferred. Another less obvious downside is that you lose control over the executed query, and this makes the system much more fragile. It basically means that the presentation layer now becomes aware about the actual used O/RM, since not every LINQ transformation can be executed using your O/RM. This will force you to write integration tests for your presentation layer as well. Another downside is that of performance. It might seem strange that returning IQueryable<T> from the business layer can cause performance problems, but I've actually seen this.

Applying sorting and paging after you do the transformation from entity to DTO can cause the executed SQL query to become much more complex. In an application I was working on, this even caused the SQL queries to becomes that much more complex, that it became practically impossible for our DBA to tune the database. This caused quite severe performance problems. Moving paging and sorting inside the query handler on the other hand, gives you more freedom to optimize and tune your LINQ query and therefore simplify the constructed SQL query.

With that out of the way, you can use the following data structures to apply paging:

public sealed class PagingInformation
{
    public readonly int PageIndex;
    public readonly int PageSize;

    public PagingInformation(int pageIndex, int pageSize)
    {
        if (pageIndex < 0) throw new ArgumentOutOfRangeException("pageIndex");
        if (pageSize <= 0) throw new ArgumentOutOfRangeException("pageSize");

        this.PageIndex = pageIndex;
        this.PageSize = pageSize;
    }
}

public sealed class PagedResult<T> {
    public readonly PagingInformation Paging;
    public readonly int PageCount;
    public readonly int ItemCount;
    public readonly ReadOnlyCollection<T> Page;

    public PagedResult(
        PagingInformation paging, int pageCount, int itemCount, ReadOnlyCollection<T> page) {
        this.Paging = paging;
        this.PageCount = pageCount;
        this.ItemCount = itemCount;
        this.Page = page;
    }

    public static PagedResult<T> ApplyPaging(IQueryable<T> query, PagingInformation paging) {
        int count = query.Count();
        var page = query.Skip(paging.PageSize * paging.PageIndex).Take(paging.PageSize).ToList();
        return new PagedResult<T>(
            paging: paging,
            pageCount: (count + (paging.PageSize - 1)) / paging.PageSize,
            itemCount: count,
            page: page.AsReadOnly());
    }
}

Using these two data structures, you can construct your query messages that look as follows:

public class SearchOrdersQuery : IQuery<PagedResult<OrderDetails>> {
    public string SearchText { get; set; }
    public PagingInformation Paging { get; set; }
}

Here the SearchOrdersQuery allows us to search for orders in the system that can be matched by the given SearchText property. Furthermore, the query is supplied with the PagingInformation that allows describes with page the consumer would like to retrieve and the size of the pages. The query result is an PagedResult<OrderDetails>. This structure holds information about the total number of items in the query, the number of total pages and all the items for the requested page.

The corresponding handler, might look like this:

public class SearchOrdersQueryHandler : IQueryHandler<SearchOrdersQuery, PagedResult<OrderDetails>> {
    private readonly NorthwindDbContext context;

    public SearchOrdersQueryHandler(NorthwindDbContext context) {
        this.context = context;
    }

    public PagedResult<OrderDetails> Handle(SearchOrdersQuery query) {
        var results =
            from order in this.context.Orders
            where order.Description.Contains(query.SearchText)
            select new OrderDetails
            {
                OrderId = order.Id,
                Customer = order.Customer.Name,
                OrderDate = order.OrderDate
            };

        return PagedResult<OrderDetails>.ApplyPaging(results, query.Paging);
    }
}

For sorting on the other hand, I think there are a couple of different ways to achieve this and it depends a bit on what is most convenient for the client. Probably the most easy -yet flexible- way to achieve this is using the System.Linq.Dynamic NuGet package and passing in the ordering information as string:

var page = this.queryProcessor.Execute(new SearchOrdersQuery
{
    Ordering = "OrderDate DESC, Customer ASC, OrderId DESC",
    SearchText = model.SearchText,
    Paging = model.Paging,
});

Here we simply pass in the property names of the OrderDetails DTO in the Ordering string property. This is obviously not nice from a compile-time perspective, but using the C# 6.0 nameof operator, we will be able to do the following:

new SearchOrdersQuery
{
    Ordering = string.Format("{0} DESC, {1} ASC, {2} DESC",
        nameof(OrderDetails.OrderDate),
        nameof(OrderDetails.Customer),
        nameof(OrderDetails.OrderId))
}

C# 6.0 however, is currently just a future dream. It will probably take quite some time before it will reach RTM. Besides this, the nameof operator will always strip everything before the last dot. So nameof(OrderDetails.Address.Country.IsoCode) will simply return the "IsoCode" string literal. Because of those reasons, we might want to write our own 'improved' nameof method, as discussed here. The query would look as follows:

new SearchOrdersQuery
{
    Ordering = string.Format("{0} DESC, {1} ASC, {2} DESC",
        Utilities.NameOf<OrderDetails>(o => o.OrderDate),
        Utilities.NameOf<OrderDetails>(o => o.Customer),
        Utilities.NameOf<OrderDetails>(o => o.OrderId))
}

Using the System.Linq.Dynamic NuGet package we can simply apply the ordering in our query handler as follows:

public PagedResult<OrderDetails> Handle(SearchOrdersQuery query) {
    var results =
        from order in this.context.Orders
        where order.Description.Contains(query.SearchText)
        select new OrderDetails
        {
            OrderId = order.Id,
            Customer = order.Customer.Name,
            OrderDate = order.OrderDate
        };

    if (!string.IsNullOrEmpty(query.Ordering)) {
        // using System.Linq.Dynamic
        results = results.OrderBy(query.Ordering);
    }

    return PagedResult<OrderDetails>.ApplyPaging(results, query.Paging);
}

Obviously, since the supplied Ordering string maps directly on the property names of the OrderDetails DTO, it will take a bit more work to apply the ordering on the original IQueryable<Order> collection (in case we need to optimize this), but we can still apply this optimization without having to do any changes to the client.

Repository owner locked and limited conversation to collaborators Jan 29, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

1 participant