Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p><strong>Disclaimer:</strong> Since there aren't any great answers yet, I decided to post a part from a great blog post I read a while ago, copied almost verbatim. You can find the full blog post <a href="https://cuttingedge.it/blogs/steven/pivot/entry.php?id=92" rel="noreferrer">here</a>. So here it is:</p> <hr> <p>We can define the following two interfaces:</p> <pre><code>public interface IQuery&lt;TResult&gt; { } public interface IQueryHandler&lt;TQuery, TResult&gt; where TQuery : IQuery&lt;TResult&gt; { TResult Handle(TQuery query); } </code></pre> <p>The <code>IQuery&lt;TResult&gt;</code> specifies a message that defines a specific query with the data it returns using the <code>TResult</code> generic type. With the previously defined interface we can define a query message like this:</p> <pre><code>public class FindUsersBySearchTextQuery : IQuery&lt;User[]&gt; { public string SearchText { get; set; } public bool IncludeInactiveUsers { get; set; } } </code></pre> <p>This class defines a query operation with two parameters, which will result in an array of <code>User</code> objects. The class that handles this message can be defined as follows:</p> <pre><code>public class FindUsersBySearchTextQueryHandler : IQueryHandler&lt;FindUsersBySearchTextQuery, User[]&gt; { private readonly NorthwindUnitOfWork db; public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db) { this.db = db; } public User[] Handle(FindUsersBySearchTextQuery query) { return db.Users.Where(x =&gt; x.Name.Contains(query.SearchText)).ToArray(); } } </code></pre> <p>We can now let consumers depend upon the generic <code>IQueryHandler</code> interface:</p> <pre><code>public class UserController : Controller { IQueryHandler&lt;FindUsersBySearchTextQuery, User[]&gt; findUsersBySearchTextHandler; public UserController( IQueryHandler&lt;FindUsersBySearchTextQuery, User[]&gt; findUsersBySearchTextHandler) { this.findUsersBySearchTextHandler = findUsersBySearchTextHandler; } public View SearchUsers(string searchString) { var query = new FindUsersBySearchTextQuery { SearchText = searchString, IncludeInactiveUsers = false }; User[] users = this.findUsersBySearchTextHandler.Handle(query); return View(users); } } </code></pre> <p>Immediately this model gives us a lot of flexibility, because we can now decide what to inject into the <code>UserController</code>. We can inject a completely different implementation, or one that wraps the real implementation, without having to make changes to the <code>UserController</code> (and all other consumers of that interface).</p> <p>The <code>IQuery&lt;TResult&gt;</code> interface gives us compile-time support when specifying or injecting <code>IQueryHandlers</code> in our code. When we change the <code>FindUsersBySearchTextQuery</code> to return <code>UserInfo[]</code> instead (by implementing <code>IQuery&lt;UserInfo[]&gt;</code>), the <code>UserController</code> will fail to compile, since the generic type constraint on <code>IQueryHandler&lt;TQuery, TResult&gt;</code> won't be able to map <code>FindUsersBySearchTextQuery</code> to <code>User[]</code>.</p> <p>Injecting the <code>IQueryHandler</code> interface into a consumer however, has some less obvious problems that still need to be addressed. The number of dependencies of our consumers might get too big and can lead to constructor over-injection - when a constructor takes too many arguments. The number of queries a class executes can change frequently, which would require constant changes into the number of constructor arguments.</p> <p>We can fix the problem of having to inject too many <code>IQueryHandlers</code> with an extra layer of abstraction. We create a mediator that sits between the consumers and the query handlers:</p> <pre><code>public interface IQueryProcessor { TResult Process&lt;TResult&gt;(IQuery&lt;TResult&gt; query); } </code></pre> <p>The <code>IQueryProcessor</code> is a non-generic interface with one generic method. As you can see in the interface definition, the <code>IQueryProcessor</code> depends on the <code>IQuery&lt;TResult&gt;</code> interface. This allows us to have compile time support in our consumers that depend on the <code>IQueryProcessor</code>. Let's rewrite the <code>UserController</code> to use the new <code>IQueryProcessor</code>:</p> <pre><code>public class UserController : Controller { private IQueryProcessor queryProcessor; public UserController(IQueryProcessor queryProcessor) { this.queryProcessor = queryProcessor; } public View SearchUsers(string searchString) { var query = new FindUsersBySearchTextQuery { SearchText = searchString, IncludeInactiveUsers = false }; // Note how we omit the generic type argument, // but still have type safety. User[] users = this.queryProcessor.Process(query); return this.View(users); } } </code></pre> <p>The <code>UserController</code> now depends on a <code>IQueryProcessor</code> that can handle all of our queries. The <code>UserController</code>'s <code>SearchUsers</code> method calls the <code>IQueryProcessor.Process</code> method passing in an initialized query object. Since the <code>FindUsersBySearchTextQuery</code> implements the <code>IQuery&lt;User[]&gt;</code> interface, we can pass it to the generic <code>Execute&lt;TResult&gt;(IQuery&lt;TResult&gt; query)</code> method. Thanks to C# type inference, the compiler is able to determine the generic type and this saves us having to explicitly state the type. The return type of the <code>Process</code> method is also known.</p> <p>It is now the responsibility of the implementation of the <code>IQueryProcessor</code> to find the right <code>IQueryHandler</code>. This requires some dynamic typing, and optionally the use of a Dependency Injection framework, and can all be done with just a few lines of code:</p> <pre><code>sealed class QueryProcessor : IQueryProcessor { private readonly Container container; public QueryProcessor(Container container) { this.container = container; } [DebuggerStepThrough] public TResult Process&lt;TResult&gt;(IQuery&lt;TResult&gt; query) { var handlerType = typeof(IQueryHandler&lt;,&gt;) .MakeGenericType(query.GetType(), typeof(TResult)); dynamic handler = container.GetInstance(handlerType); return handler.Handle((dynamic)query); } } </code></pre> <p>The <code>QueryProcessor</code> class constructs a specific <code>IQueryHandler&lt;TQuery, TResult&gt;</code> type based on the type of the supplied query instance. This type is used to ask the supplied container class to get an instance of that type. Unfortunately we need to call the <code>Handle</code> method using reflection (by using the C# 4.0 dymamic keyword in this case), because at this point it is impossible to cast the handler instance, since the generic <code>TQuery</code> argument is not available at compile time. However, unless the <code>Handle</code> method is renamed or gets other arguments, this call will never fail and if you want to, it is very easy to write a unit test for this class. Using reflection will give a slight drop, but is nothing to really worry about.</p> <hr> <p>To answer one of your concerns:</p> <blockquote> <p>So I'm looking for alternatives that encapsulate the entire query, but still flexible enough that you're not just swapping spaghetti Repositories for an explosion of command classes.</p> </blockquote> <p>A consequence of using this design is that there will be a lot of small classes in the system, but having a lot of small/focused classes (with clear names) is a good thing. This approach is clearly much better then having many overloads with different parameters for the same method in a repository, as you can group those in one query class. So you still get a lot less query classes than methods in a repository.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload