A bit of special container magic

    In a previous article, I gave an example of a factory for receiving IQuery implementations, but did not explain the mechanism of its operation.
    _queryFactory.GetQuery()
        .Where(Product.ActiveRule)
        .OrderBy(x => x.Id)
        .Paged(0, 10) // получаем 10 продуктов для первой страницы
    // Мы решили подключить полнотекстовый поиск и добавили ElasticSearch, не вопрос:
    _queryFactory.GetQuery()
        .Where(new FullTextSpecification(«зонтик»))
        .All()
    // Или EF тормозит и мы решили переделать на хранимую процедуру и Dapper
    _queryFactory.GetQuery()
        .Where(new DictionarySpecification (someDirctionary))
        .All()
    

    In this article I want to share the technique of registering the necessary components of the assembly by agreement. Now I have at hand a code base with a different CQRS implementation, so the examples will differ. This is not fundamental: the basic idea remains unchanged.

    Let's say you have an interface where ListParams is a specification coming from the frontend
    public interface IListOperation
    {
         ListResult List(ListParams listParam);
    }
    

    The task of
    Delivering application developers from the need to write controllers, projections and services.


    Solution
    Create a base class for the List operation:
        public class ListOperationBase : IListOperation
            where TEntity: IEntity
            where TDto: IHaveId
        {
            protected readonly IDbContext DbContext ;
            public ListOperationBase(IDbContext dbContext )
            {
                if (dbContext == null) throw new ArgumentNullException(nameof(dbContext));
                DbContext = dataStore;
            }
            public virtual ListResult List(ListParam listParam)
            {
                var data = AddProjectionBusinessLogic(AddEntityBusinessLogic(DataStore
                    .GetAll())
                    .ProjectTo())
                    .Filter(listParam);
                return new ListResult()
                {
                    Data = data
                        .Paging(listParam)
                        .ToList(),
                    TotalCount = data.Count()
                };
            }
            protected virtual IQueryable AddEntityBusinessLogic(IQueryable queryable) => queryable;
            protected virtual IQueryable AddProjectionBusinessLogic(IQueryable queryable) => queryable;
        }
    

    The ProjectTo method is an AutoMapper feature that allows you to build projections according to agreements. Eliminates the need to lift the memory all the Entity , while allowing to write dull design Select species
    Query.Select(x => {
        Name = x.Name,
        ParentUrl = x.Parent.Url,
        Foo = x.Foo
    })
    

    The virtual methods AddEntityBusinessLogic and AddProjectionBusinessLogic allow you to add filtering conditions before and after creating the projection.

    Now for quick prototyping we can use ListOperationBaseand for real implementations, you need to create real operations with the correct logic. To do this, at the start of the application, you need to register everything that is in the assembly by agreement. In my case, the modular architecture is used and this is the module loading code. For monolithic applications, you will also need to make a list of assemblies from which you want to load types.
    var types = GetType().Assembly.GetTypes();
    var operations = types
    	.Where(t.IsClass
    		&& !t.IsAbstract
    		&& t.ImplementsOpenGenericInterface(typeof(IListOperation<>)));
    foreach (var operation in operations)
    {
    	var definitions =
    		operation.GetInterfaces().Where(i => i.ImplementsOpenGenericInterface(typeof (IListOperation<>)));
    	foreach (var definition in definitions)
    	{
    		Container.Register(definition, operation);
    	}
    	// ...
    }
    

    You will need only one controller for all Crud operations. The implementation of the ControllerSelector for Generic WebApi controllers can be found at: github.com/hightechtoday/costeffectivecode/blob/master/src/CostEffectiveCode.WebApi2/WebApi/Infrastructure/RuntimeScaffoldingHttpControllerSelector

    public ListResult List(ListParam loadParams) =>
      (_container.ResolveAll>().SingleOrDefault() ?? new ListOperationBase(DataStore))
      .List(loadParams);
    

    Passing the container to the controller is of course an idea so-so ( ServiceLocator ), and in fact it is much better to wrap the call in the factory method (as is done in the QueryFactory example ). Another weak point is what to do if 2 IListOperation implementations with the same types are registered . There is no definite answer to this question: it all depends on the specifics of your application and system requirements.

    As a result, we got a system for rapid prototyping, which saves the programmer from writing controllers and registering services in the container. All you need to do is add an entity, DTO, and describe mapping. If you use AutoMapper , you should definitely add the Mapper.AssertConfigurationIsValid () construct ;It will help you learn about errors if you have to change Entity or Dto. By the way, by analogy with the registration of operations, it is possible to automate the creation of mappings under agreements for cases where all mappings are obvious. However, in real life, you need to append several lines to mapping quite often, so I prefer to do this manually, since it's only a couple of lines.

    Step by step
    1. Add SomeEntity: IEntity
    2. Add SomeEntityListDto
    3. We register mapping SomeEntity -> SomeEntityListDto
    4. Automatically get the / SomeEntity / List method
    5. Adding business logic to SomeEntityListOperation
    6. The / SomeEntity / List method begins to use the new implementation with the “correct” business logic

    Mapping can be omitted if Entity can be passed to the presentation layer / serialized painlessly “as is”.

    Also popular now: