📜 ⬆️ ⬇️

Getting an instance of a query class by its interface signature

Not so long ago, an article ( link to the topic) of my colleague AlexanderByndyu was published on Habré, describing avoiding using Repository in the direction of using the QueryFactory bundle + Query query classes. At the same time, a very interesting dispute broke out in the comments regarding the expediency of the decision given in the article. There were a lot of interesting reviews, among which were the statements that, say, QueryFactory is not needed and is an extra burden that prevents painless adding, changing and deleting query classes. In this article I want to show an approach that allows you to get rid of the use of QueryFactory, through the active use of the IoC container. We used this organization of work with the class structure of queries in one of our recent projects, where Castle.Windsor was used as IoC.


Description of the approach


To begin with, I will describe the main idea embedded 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 signature identification. 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 obtain a specific implementation of the interface from the IoC container, then the signature is determined by a set of generic parameters in the interface. It is possible that there is some discrepancy in terms with those who are used to understanding the set of its methods under the interface signature, but in this article I propose to adopt the above interpretation of the concept.
I hope it is generally understandable, and if not, then when viewing the above example, everything will fall into place.

Implementation


Suppose we have a common for all requests interface IQuery <,>:
public interface IQuery< in TCriterion, out TResult>
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 filtering 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 TCriterion and TResult generic parameters, knowing these types you can get an interface implementation.
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, all implementations of the IQuery <,> interface from the Domain.NHibernate assembly contain them and the obtained types are registered in the container as implementations of their first interface (which again is IQuery <,>).
For further use of a specific query in the controller or, let's say, in the form handler, it is necessary to write a small but very important auxiliary class. But first, I would like to give an example of its use, so that you can understand the opportunities it provides and the appearance (for the developer) of the mechanism for obtaining the implementation of a request by interface signature. Let's call this example listing 1.
var account = Query.For<Account>().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 to be done by the login of this account itself. Login passed through LoginCriterion. It is appropriate that the above code be more transparent I cite the request code that will be used in the above example, as well as the code of the LoginCriterion class.
public class FindAccountByLoginQuery : LinqQueryBase<Account>, IQuery<LoginCriterion, Account>
{
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<TResult> For<TResult>();
}

public interface IQueryFor< out T>
{
T With<TCriterion>(TCriterion criterion) where TCriterion : ICriterion;

T ById( int id);

IEnumerable <T> 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<TResult> For<TResult>()
{
return new QueryFor<TResult>(dependencyResolver);
}

#region Nested type: QueryFor

private class QueryFor<TResult> : IQueryFor<TResult>
{
private readonly IDependencyResolver dependencyResolver;

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

public TResult With<TCriterion>(TCriterion criterion) where TCriterion : ICriterion
{
return dependencyResolver.GetService<IQuery<TCriterion, TResult>>().Execute(criterion);
}

public TResult ById( int id)
{
return dependencyResolver.GetService<IFindByIdQuery<TResult>>().Execute( new IdCriterion(id));
}

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

#endregion
}


* This source code was highlighted with Source Code Highlighter .

In fact, for the implementation of our approach, we could restrict ourselves to a single interface, but then we would have to explicitly specify a generic parameter such as an ICriterion implementation, which would make the IQueryBuilder interface heavy. At the indicated implementation, only the type of the value returned from the request is explicitly indicated. The immediate receipt of the request instance from the IoC container and its execution are performed by the QueryFor <> class. It uses the interface to the IoC container provided by the internal ASP.NET MVC3, IDependencyResolver. 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, the opening code UnitOfWork omitted).
public class AccountController : Controller
{
public IQueryBuilder Query { get ; set ; }

public ActionResult Index( string login)
{
var account = Query.For<Account>().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 its place of use. I would even call such a mechanism for receiving requests "real", since it operates with two key concepts closely related to the query in the context of the CQS approach: these are the input criteria of the sample and the return result. It would seem that different implementations of ICriterion can “clutter up” the code, but in fact this is not the case, since they can be reused for various requests.
Such an approach to obtaining implementations by interface signature can be used not only for requests, but also for implementations of other generic interfaces, especially if there are many of these implementations and / or their composition is subject to frequent changes. For example, commands can serve as similar objects (from the CQS approach).

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

Source: https://habr.com/ru/post/125720/


All Articles