Getting an instance of a request class by the signature of its interface

    Not so long ago an article ( link to topic) was published on Habré by my colleague AlexanderByndyu, описывающая уход от использования Repository в сторону применения связки QueryFactory + классы запросов Query. При этом в комментариях разгорелся весьма интересный диспут, касающийся целесообразности приведенного в статье решения. Было достаточно много интересных отзывов, среди которых особенно выделялись высказывания о том, что, дескать, QueryFactory не нужен и является лишней обузой, мешающей безболезненному добавлению, изменению и удалению классов запросов. В данной статье я хочу показать подход, который позволяет избавиться от применения QueryFactory, через активное использование IoC контейнера. Данную организацию работы со структурой классов запросов мы использовали в одном из наших недавних проектов, где в качестве IoC использовался Castle.Windsor.


    Описание подхода


    To begin, I will describe the main idea inherent in the described approach. Its essence is close to how the compiler determines which version of the overloaded method is used in a particular case, namely, identification by signature. And if in the case when it comes to the method its signature is the order, number and types of arguments passed, then if it is necessary to get a specific implementation of the interface from the IoC container, then the signature is determined by the set of generic parameters in the interface. There may be some difference in concepts with those who are used to understanding the set of methods by the interface signature, but within the framework of this article I propose to accept the above interpretation of the concept.
    I hope it’s understandable in general, and if not, then when viewing the above example everything will fall into place.

    Implementation


    Suppose we have a common IQuery <,> interface for all queries:
    public interface IQuery
      where TCriterion : ICriterion
    {
      TResult Execute(TCriterion criterion);
    }

    * This source code was highlighted with Source Code Highlighter.

    Accordingly, its signature is determined by the specific implementation of ICriterion, i.e. an object containing the data necessary to build the query (mainly for Where filter predicates), as well as the type of the returned result. Thus, if there is a single implementation of the IQuery <,> interface with certain types of generic parameters TCriterion and TResult, knowing these types, you can get an implementation of the interface.
    Below is the code in the WindsorInstaller class that registers all implementations of the IQuery <,> interface in an IoC container.
    public class WindsorInstaller : IWindsorInstaller
    {
       public void Install(IWindsorContainer container, IConfigurationStore store)
       {
         var queries = AllTypes.FromAssemblyNamed("Domain.NHibernate")
           .BasedOn(typeof (IQuery<,>))
           .WithService.FirstInterface()
           .Configure(x => x.LifeStyle.Transient);

           container.Register(queries);
       }
    }

    * This source code was highlighted with Source Code Highlighter.

    In this example, we get all implementations of the IQuery <,> interface from the Domain.NHibernate assembly containing them and register the received types in the container as implementations of their first interface (which, again, is IQuery <,>).
    For further use of a specific request in the controller or, say, in the form handler, it is necessary to write a small but very important auxiliary class. But to begin with, I would like to give an example of its use, so that you can understand the capabilities it provides and the appearance (for the developer) of the mechanism for obtaining the implementation of the request by the interface signature. Call this example Listing 1.
    var account = Query.For().With(new LoginCriterion(login));

    * This source code was highlighted with Source Code Highlighter.

    In this example, we receive a request by the signature of its interface, from which we want it to return the Account entity, and search by the login of this account itself. Login is passed through LoginCriterion. Accordingly, to make the above code more transparent, I quote the request code that will be used in the above example, as well as the code for the LoginCriterion class.
    public class FindAccountByLoginQuery : LinqQueryBase, IQuery
    {
        public FindAccountByLoginQuery(ILinqProvider linqProvider)
            : base(linqProvider)
        {
        }

        public Account Execute(LoginCriterion criterion)
        {
            return Query()
                .SingleOrDefault(x => x.Login.ToLower() == criterion.Login.ToLower());
        }
    }

    public class LoginCriterion : ICriterion
    {
       public LoginCriterion(string login)
       {
         Login = login;
       }

       public string Login { get; set; }
    }

    * This source code was highlighted with Source Code Highlighter.

    Now, with regard to the auxiliary code, it is represented by two interfaces:
    public interface IQueryBuilder
    {
       IQueryFor For();
    }

    public interface IQueryFor
    {
       T With(TCriterion criterion) where TCriterion : ICriterion;
      
       T ById(int id);
      
       IEnumerable All();
    }

    * This source code was highlighted with Source Code Highlighter.

    ... and their implementations:
    public class QueryBuilder : IQueryBuilder
    {
       private readonly IDependencyResolver dependencyResolver;

       public QueryBuilder(IDependencyResolver dependencyResolver)
       {
         this.dependencyResolver = dependencyResolver;
       }

       public IQueryFor For()
       {
         return new QueryFor(dependencyResolver);
       }

       #region Nested type: QueryFor

       private class QueryFor : IQueryFor
       {
         private readonly IDependencyResolver dependencyResolver;

         public QueryFor(IDependencyResolver dependencyResolver)
         {
            this.dependencyResolver = dependencyResolver;
         }

         public TResult With(TCriterion criterion) where TCriterion : ICriterion
         {
            return dependencyResolver.GetService>().Execute(criterion);
         }
        
         public TResult ById(int id)
         {
            return dependencyResolver.GetService>().Execute(new IdCriterion(id));
         }

         public IEnumerable All()
         {
            return dependencyResolver.GetService>().Execute(new EmptyCriterion());
         }    
       }

       #endregion
    }

    * This source code was highlighted with Source Code Highlighter.

    In fact, to implement our approach, we could restrict ourselves to a single interface, but then we would have to explicitly specify a generic parameter such as the implementation of ICriterion, which would burden the IQueryBuilder interface. With the indicated implementation, only the type of value returned from the request is explicitly indicated. The query instance is directly obtained from the IoC container and executed by the QueryFor <> class. In this case, the interface to the IoC container provided by the internal ASP.NET MVC3 tools, IDependencyResolver, is used. In our case, as a result, all requests to the container will be delegated to Castle.Windsor.
    One of the main features of using QueryBuilder is to register it in the IoC container as an implementation of the IQueryBuilder interface. Due to the injection of dependencies into the constructor, as well as the implementation substitution mechanism introduced in ASP.NET MVC3 for all public properties of objects whose types are registered in the container, if the object instance itself is also obtained from the container, the following becomes possible (abstract example, UnitOfWork opening code omitted).
    public class AccountController : Controller
    {
       public IQueryBuilder Query { get; set; }

       public ActionResult Index(string login)
       {  
         var account = Query.For().With(new LoginCriterion(login));

         // делаем тут что-нибудь полезное
       }
    }

    * This source code was highlighted with Source Code Highlighter.


    Conclusion


    This approach allows not only to get rid of QueryFactory, which makes adding, changing and deleting query classes painless, but also allows you to completely abstract from the concept of a query in the context of where it is used. I would even call such a mechanism for receiving requests “real”, because he operates with two key concepts that are closely related to the query in the context of the CQS approach: these are the input selection criteria and the return result. It would seem that various implementations of ICriterion can "clutter up" the code, but in reality this is not so, because they can be reused for various requests.
    A similar approach to obtaining an implementation based on an interface signature can be used not only for queries, but also for implementations of other generic interfaces, especially if there are a lot of these implementations and / or their composition is subject to frequent changes. For example, teams (from the CQS approach) can serve as similar objects.

    I hope for a good discussion in the comments and look forward to constructive proposals on this approach from the habrasociety. If you have comments on spelling and punctuation in this article, then please write them through private messages.

    Also popular now: