2

I'm using a query object pattern (similar to this) to manage disparate queries while avoiding bloaded facades/repositories.

A query object takes a number of constructor parameters, representing query arguments. The query is then passed to an IQueryHandler by the caller, into which is injected the IDataContext. The IDataContext is then passed into an Execute method of the IQuery.

The thing I don't like is this:

public interface IQuery<TResult>
{
    public TResult Execute(IDataContext context);
}

Because the IDataContext is passed into the method and thus explicitly declared in the interface, there is no option to have a query that fetches things via a different mechanism - IQuery is coupled to whatever defines the IDataContext interface. Suppose, for example, I want to switch from an SQL DB store to a document DB.

I've tried a couple of alternatives, but neither quite gets me where I want to be.

The first is to inject the data context into the query's constructor and use a factory to generate the query objects. This successfully decouples the interfaces, but now working with the queries is more cumbersome. Instead of using the constructor/object initializer to set up the query, callers have to do something like this:

var query = _queryFactory.Create<PersonQuery, Person>(); // Second type argument can't be inferred from first due to limitations in generic type inference.
query.Name = "Bob";
query.Age = 32;

var result = query.Execute();

The second is to abstract the execution of the query - and thus the dependency on IDataContext - to a handler class, then have another class that resolves a handler for each query, which again abstracts the dependency away from any interfaces:

public class ISomeQueryHandler : IQueryHandler<SomeQuery, SomeQueryResult>
{
    private readonly IDataContext _context = ...;

    public SomeQueryResult Execute(SomeQuery query) { ... }
}

I don't like this option because it means every new query involves writing two classes, which, again, is cumbersome and increases the potential for a query to exist with no way of handling it (it also involves an empty marker interface for queries since the Execute method is moved to the handler, which always feels off). It also involves some rather funky convention-based dependency resolution to get the right handler for each query.

Creating an abstraction over IDataContext is not really feasible due to its complexity.

Is there a happy medium I haven't seen?

4
  • Hmm, thinking about it, I guess I could accept that my query interface is data-context-specific and create additional I...Query interfaces for different types of data context, then use these compositionally on queries that can be executed against different types of context. Feels a bit weird though.
    – Ant P
    Commented Jan 16, 2015 at 13:49
  • I understand your IDataContext interface to be specialised for SQL-based data stores, and you want to be able to have some queries that use a different database type that can't easily be mapped to the same interface, is this correct?
    – Jules
    Commented Jan 16, 2015 at 13:54
  • Essentially, yes, where other data stores' interfaces are semantically different enough that writing a common adapter interface is not really viable (which means it's probably not possible to keep the execution logic where it currently is while still solving this problem). Unless, as mentioned above, I accept the coupling and use composition of different IQuery interfaces for different types of data context. Or unless someone has a better idea :)
    – Ant P
    Commented Jan 16, 2015 at 13:59
  • 2
    I'm pretty much with the second half of @Jules answer on this one... In my own experience trying to create layers of abstraction and indirection for a potential scenario of changing backing data stores will create more chance of error than keeping it far simpler and doing a proper migration if/when the need happens. In C# it's far easier to rip out the old, add in the new, and fix all the wholes than maintain more layers of IFoo.
    – Tracker1
    Commented Aug 15, 2015 at 6:15

2 Answers 2

3

Suppose, for example, I want to switch from an SQL DB store to a document DB.

No, you are not. Possibility of that happening is extremely slim and even if you account for this, you are going to change your model, because the relational modelling is probably leaking into it.

But if I had to do this, I would separate Execution from Initialization of the query.

First I would create interfaces :

public interface IQuery<TResult>
{
    TResult Execute();
}

public interface ISqlQuery
{
   void SetContext(IDataContext context);
}

Then, query can implement both if it needs SQL context.

Then, in handler, duck-type check if query implements specific interface and if yes, execute it. This interface is query saying "I need this service, someone provide it". And handlers tries to provide it.

public class Handler
{
    public TResult Execute<TResult>(IQuery<TResult> query)
    {
        if (query is ISqlQuery)
        {
            (query as ISqlQuery).SetContext(_datacontext);
        }

        return query.Execute();
    }
}

This has two advantages. First, different queries might go to different data stores. Second, queries can have multiple data sources simply by implementing multiple interfaces. So query can access DB, external service and maybe something else all the same time.

0

One solution I've used to this kind of problem is to add a layer of indirection to the context. In this case, I would rename IDataContext to ISQLDataContext, and create a new IDataContext with an "SQLContext" property to retrieve the existing context. Your query objects can then be trivially changed to use the new interface, and you can add new context types at the top level as you need them.

This solution isn't ideal - it doesn't conform to the open/closed principle with regards to adding new data stores, but (a) that is unlikely to be a common type of change and (b) as the implementation of the data context object will be very simple, most of the logic behind the OCP (i.e. that changing existing code is more likely to break something existing than adding entirely new code) simply doesn't apply.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.