Enhanced query objects with S#arp Architecture

March 21, 2011

I’ve been using S#arp Architecture (and the Who Can Help Me structure) on a few projects for a while now and on the whole I’m very happy with it. However, Ayende’s recent post Architecting in the pit of doom: The evils of the repository abstraction layer hit a nerve, and got me thinking that maybe I’ve been applying the services (or tasks) / repository abstraction a bit too liberally. The following day I saw this tweet mentioning @fabiomaulo’s Enhanced Query Object which looks just the ticket to solving this issue. So, I’ve updated my SharpArchitecture-MultiTenant project on GitHub to use enhanced query objects.

In the SharpArchitecture.MultiTenant.Data project I’ve created an NHibernate folder (and namespace) and added a base class for queries that provides access to the ISession (similar to the existing code in the Repository base class):

using NHibernate;
using SharpArch.Data.NHibernate;

namespace SharpArchitecture.MultiTenant.Data.NHibernate
{
  public class NHibernateQuery
  {
    protected virtual ISession Session
    {
      get
      {
        var factoryKey = SessionFactoryKeyHelper.GetKey(this);
        return NHibernateSession.CurrentFor(factoryKey);
      }
    }
  }
}

As this project is enabled for multi-tenants I’ve also created a marker interface to indicate if the query is tenant specific:

namespace SharpArchitecture.MultiTenant.Framework.Contracts
{
  public interface IMultiTenantQuery { }
}

In MultiTenantSessionFactoryKeyProvider, I’ve updated the GetKeyFrom method to test for implementation of the IMultiTenantQuery interface:

public string GetKeyFrom(object anObject)
{
  var type = anObject.GetType();
  var isMultiTenant = type.IsImplementationOf<IMultiTenantQuery>() ||
                      type.IsImplementationOf<IMultiTenantRepository>() ||
                      IsRepositoryForMultiTenantEntity(type);
  return isMultiTenant
    ? GetKey()
    : NHibernateSession.DefaultFactoryKey;
}

Now everything is in place to create some queries, so for the list of Customers I’ve created an interface for the query as below:

public interface ICustomerListQuery : IMultiTenantQuery
{
  IPagination<CustomerViewModel> GetPagedList(int pageIndex, int pageSize);
}

and an implementation:

public class CustomerListQuery : NHibernateQuery, ICustomerListQuery
{
  public IPagination<CustomerViewModel> GetPagedList(int pageIndex, int pageSize)
  {
    var query = Session.QueryOver<Customer>()
      .OrderBy(customer => customer.Code).Asc;

    var countQuery = query.ToRowCountQuery();
    var totalCount = countQuery.FutureValue<int>();

    var firstResult = (pageIndex - 1) * pageSize;

    CustomerViewModel viewModel = null;
    var viewModels = query.SelectList(list => list
                            .Select(mission => mission.Id).WithAlias(() => viewModel.Id)
                            .Select(mission => mission.Code).WithAlias(() => viewModel.Code)
                            .Select(mission => mission.Name).WithAlias(() => viewModel.Name))
      .TransformUsing(Transformers.AliasToBean(typeof(CustomerViewModel)))
      .Skip(firstResult)
      .Take(pageSize)
      .Future<CustomerViewModel>();

    return new CustomPagination<CustomerViewModel>(viewModels, pageIndex, pageSize, totalCount.Value);
  }
}

As you can see, this code is using NHibernate projections and transforms to get a list of the required view models, bypassing the need for a data transfer object (DTO) and mappers. This is a trivial example, but in reality the query would be more complex and likely to flatten the object structure.

Now the controller itself can use the query interface to get the paged list of view models:

public ActionResult Index(int? page)
{
  var customers = _customerListQuery.GetPagedList(page ?? 1, DefaultPageSize);
  var viewModel = new CustomerListViewModel { Customers = customers };
  return View(viewModel);
}

I quite like this solution; the unnecessary layers of abstraction are removed and the queries are nicely encapsulated, if anything more complex is required (e.g. multiple data sources) then it is always possible to fall back to the services / repositories style as before.