Funcionalidad de extremo a extremo a través de envoltorios

Durante el desarrollo, a menudo nos encontramos con una situación en la que, al ejecutar cualquier lógica empresarial, es necesario escribir registros, auditorías y enviar alertas. En general, implemente algunas funciones de un extremo a otro.



Cuando la escala de producción es pequeña, no puede ser especialmente celoso y hacer todo esto correctamente en los métodos. Gradualmente, el constructor de servicios comienza a crecer con los servicios entrantes para la implementación de BL y la funcionalidad de un extremo a otro. Y esto es ILogger, IAuditService, INotifiesSerice.

No sé ustedes, pero no me gustan muchas inyecciones y métodos grandes que realizan muchas acciones a la vez. Traté de encontrar un término medio. Si estos problemas no le han escapado, bienvenido a cat.



Puede terminar cualquier implementación de AOP en el código. En la pila .NET, tales implementaciones se inyectan en su aplicación en los lugares correctos, se ven como magia de nivel 80 y, a menudo, tienen problemas de escritura y depuración.







Revelación. De hecho, pude resolver un poco más de problemas de los que describí anteriormente. Por ejemplo, puedo dar desarrollo BL a un desarrollador y colgar la funcionalidad de un extremo a otro e incluso la validación de los datos entrantes, a otro al mismo tiempo .



Y los decoradores y un complemento de DI me ayudaron con esto. Alguien dirá además que esto es un proxy, estaré encantado de discutir esto en los comentarios.



Entonces, ¿qué quiero como desarrollador?



  • Al implementar BL, no se distraiga con el funcional izquierdo.
  • Poder probar solo BL en pruebas unitarias. Y no me gusta hacer 100500 simulacros para deshabilitar todas las funciones auxiliares. 2-3 - está bien, pero no quiero.
  • Comprenda lo que está sucediendo sin tener 7 palmos en la frente. :)
  • ¡Sea capaz de gestionar la vida útil del servicio y cada uno de sus envoltorios POR SEPARADO!


¿Qué quiero como diseñador y líder de equipo?



  • Poder descomponer las tareas de la forma más óptima y con la menor coherencia, para que al mismo tiempo sea posible involucrar al mayor número posible de desarrolladores en diferentes tareas y al mismo tiempo para que dediquen el menor tiempo posible a la investigación (si el desarrollador necesita desarrollar un BL, y en paralelo pensar qué y cómo asegurar , dedicará más tiempo a la investigación. Y así, con cada pieza de BL. Es mucho más fácil obtener los registros de auditoría y agruparlos durante todo el proyecto).
  • Mantenga el orden en el que se ejecuta el código por separado de su desarrollo.


Esta interfaz me ayudará con esto.



    /// <summary>
    ///       .
    /// </summary>
    /// <typeparam name="T">  . </typeparam>
    public interface IDecorator<T>
    {
        /// <summary>
        ///        .
        /// </summary>
        Func<T> NextDelegate { get; set; }
    }


Puedes usar algo como esto
interface IService
{
    Response Method(Request request);
}

class Service : IService
{
    public Response Method(Request request)
    {
        // BL
    }
}

class Wrapper : IDecorator<IService>, IService
{
    public Func<IService> NextDelegate { get; set; }

    public Response Method(Request request)
    {
        // code before
        var result = NextDelegate().Method(request);
        // code after
        return result;
    }
}




Así, nuestra acción será más profunda.



wrapper1
    wrapper2
        service
    end wrapper2
end wrapper1


Pero espera. Esto ya está en OOP y se llama herencia. : D



class Service {}
class Wrapper1: Service {}
class Wrapper2: Wrapper1 {}


Como imaginaba, aparecería una funcionalidad adicional de extremo a extremo, que tendría que implementarse en toda la aplicación en el medio o intercambiar las existentes, por lo que se me erizó el pelo de la espalda.



Pero mi pereza no es una buena razón. La buena razón es que habrá grandes problemas al probar unitariamente la funcionalidad en las clases Wrapper1 y Wrapper2, mientras que en mi ejemplo, NextDelegate puede simplemente descartarse. Además, el servicio y cada envoltorio tienen su propio conjunto de herramientas que se inyectan en el constructor, mientras que con la herencia, el último envoltorio debe tener herramientas innecesarias para pasarlas a sus padres.



Entonces, el enfoque es aceptado, queda por averiguar dónde, cómo y cuándo asignar NextDelegate.



Decidí que la solución más lógica sería hacer esto donde registro servicios. (Startup.sc, predeterminado).



Así es como se ve en la versión básica.
            services.AddScoped<Service>();
            services.AddTransient<Wrapper1>();
            services.AddSingleton<Wrapper2>();
            services.AddSingleton<IService>(sp =>
            {
                var wrapper2 = sp.GetService<Wrapper2>();
                wrapper2.NextDelegate = () =>
                {
                    var wrapper1 = sp.GetService<Wrapper1>();
                    wrapper1.NextDelegate = () =>
                    {
                        return sp.GetService<Service>();
                    };

                    return wrapper1;
                };

                return wrapper2;
            });




En general, se cumplieron todos los requisitos, pero apareció otro problema: el anidamiento.



Este problema puede resolverse mediante fuerza bruta o recursividad. Pero bajo el capó. Exteriormente, todo debe verse simple y comprensible.



Esto es lo que logré lograr
            services.AddDecoratedScoped<IService, Service>(builder =>
            {
                builder.AddSingletonDecorator<Wrapper1>();
                builder.AddTransientDecorator<Wrapper2>();
                builder.AddScopedDecorator<Wrapper3>();
            });




Y estos métodos de extensión me ayudaron con esto.



Y estos métodos de extensión y el creador de escenarios me ayudaron con esto.
    /// <summary>
    ///        .
    /// </summary>
    public static class DecorationExtensions
    {
        /// <summary>
        ///        .
        /// </summary>
        /// <typeparam name="TDefinition">  . </typeparam>
        /// <typeparam name="TImplementation">  . </typeparam>
        /// <param name="lifeTime"></param>
        /// <param name="serviceCollection">  . </param>
        /// <param name="decorationBuilder">  . </param>
        /// <returns>     . </returns>
        public static IServiceCollection AddDecorated<TDefinition, TImplementation>(
            this IServiceCollection serviceCollection, ServiceLifetime lifeTime,
            Action<DecorationBuilder<TDefinition>> decorationBuilder)
            where TImplementation : TDefinition
        {
            var builder = new DecorationBuilder<TDefinition>();
            decorationBuilder(builder);

            var types = builder.ServiceDescriptors.Select(k => k.ImplementationType).ToArray();

            var serviceDescriptor = new ServiceDescriptor(typeof(TImplementation), typeof(TImplementation), lifeTime);

            serviceCollection.Add(serviceDescriptor);

            foreach (var descriptor in builder.ServiceDescriptors)
            {
                serviceCollection.Add(descriptor);
            }

            var resultDescriptor = new ServiceDescriptor(typeof(TDefinition),
                ConstructServiceFactory<TDefinition>(typeof(TImplementation), types), ServiceLifetime.Transient);
            serviceCollection.Add(resultDescriptor);

            return serviceCollection;
        }

        /// <summary>
        ///            Scoped.
        /// </summary>
        /// <typeparam name="TDefinition">  . </typeparam>
        /// <typeparam name="TImplementation">  . </typeparam>
        /// <param name="serviceCollection">  . </param>
        /// <param name="decorationBuilder">  . </param>
        /// <returns>     . </returns>
        public static IServiceCollection AddDecoratedScoped<TDefinition, TImplementation>(
            this IServiceCollection serviceCollection,
            Action<DecorationBuilder<TDefinition>> decorationBuilder)
            where TImplementation : TDefinition
        {
            return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Scoped,
                decorationBuilder);
        }

        /// <summary>
        ///            Singleton.
        /// </summary>
        /// <typeparam name="TDefinition">  . </typeparam>
        /// <typeparam name="TImplementation">  . </typeparam>
        /// <param name="serviceCollection">  . </param>
        /// <param name="decorationBuilder">  . </param>
        /// <returns>     . </returns>
        public static IServiceCollection AddDecoratedSingleton<TDefinition, TImplementation>(
            this IServiceCollection serviceCollection,
            Action<DecorationBuilder<TDefinition>> decorationBuilder)
            where TImplementation : TDefinition
        {
            return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Singleton,
                decorationBuilder);
        }

        /// <summary>
        ///            Transient.
        /// </summary>
        /// <typeparam name="TDefinition">  . </typeparam>
        /// <typeparam name="TImplementation">  . </typeparam>
        /// <param name="serviceCollection">  . </param>
        /// <param name="decorationBuilder">  . </param>
        /// <returns>     . </returns>
        public static IServiceCollection AddDecoratedTransient<TDefinition, TImplementation>(
            this IServiceCollection serviceCollection,
            Action<DecorationBuilder<TDefinition>> decorationBuilder)
            where TImplementation : TDefinition
        {
            return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Transient,
                decorationBuilder);
        }

        /// <summary>
        ///     
        /// </summary>
        /// <typeparam name="TService"></typeparam>
        /// <param name="implType"></param>
        /// <param name="next"></param>
        /// <returns></returns>
        private static Func<IServiceProvider, TService> ConstructDecorationActivation<TService>(Type implType,
            Func<IServiceProvider, TService> next)
        {
            return x =>
            {
                var service = (TService) x.GetService(implType);

                if (service is IDecorator<TService> decorator)
                    decorator.NextDelegate = () => next(x);
                else
                    throw new InvalidOperationException(" ");

                return service;
            };
        }

        /// <summary>
        ///         .
        /// </summary>
        /// <typeparam name="TDefinition">   . </typeparam>
        /// <param name="serviceType">   . </param>
        /// <param name="decoratorTypes">     . </param>
        /// <returns>     DI. </returns>
        private static Func<IServiceProvider, object> ConstructServiceFactory<TDefinition>(Type serviceType,
            Type[] decoratorTypes)
        {
            return sp =>
            {
                Func<IServiceProvider, TDefinition> currentFunc = x =>
                    (TDefinition) x.GetService(serviceType);
                foreach (var decorator in decoratorTypes)
                {
                    currentFunc = ConstructDecorationActivation(decorator, currentFunc);
                }

                return currentFunc(sp);
            };
        }
    }




    /// <summary>
    ///       .
    /// </summary>
    /// <typeparam name="TService">  . </typeparam>
    public class DecorationBuilder<TService>
    {
        private readonly List<ServiceDescriptor> _serviceDescriptors = new List<ServiceDescriptor>();

        /// <summary>
        ///       .
        /// </summary>
        public IReadOnlyCollection<ServiceDescriptor> ServiceDescriptors => _serviceDescriptors;

        /// <summary>
        ///      .
        /// </summary>
        /// <typeparam name="TDecorator">  . </typeparam>
        /// <param name="lifeTime">   . </param>
        public void AddDecorator<TDecorator>(ServiceLifetime lifeTime) where TDecorator : TService, IDecorator<TService>
        {
            var container = new ServiceDescriptor(typeof(TDecorator), typeof(TDecorator), lifeTime);
            _serviceDescriptors.Add(container);
        }

        /// <summary>
        ///          Scoped.
        /// </summary>
        /// <typeparam name="TDecorator">  . </typeparam>
        public void AddScopedDecorator<TDecorator>() where TDecorator : TService, IDecorator<TService>
        {
            AddDecorator<TDecorator>(ServiceLifetime.Scoped);
        }

        /// <summary>
        ///          Singleton.
        /// </summary>
        /// <typeparam name="TDecorator">  . </typeparam>
        public void AddSingletonDecorator<TDecorator>() where TDecorator : TService, IDecorator<TService>
        {
            AddDecorator<TDecorator>(ServiceLifetime.Singleton);
        }

        /// <summary>
        ///          Transient.
        /// </summary>
        /// <typeparam name="TDecorator">  . </typeparam>
        public void AddTransientDecorator<TDecorator>() where TDecorator : TService, IDecorator<TService>
        {
            AddDecorator<TDecorator>(ServiceLifetime.Transient);
        }
    }




Ahora algo de azúcar para funcionalistas



Ahora algo de azúcar para funcionalistas
    /// <summary>
    ///       .
    /// </summary>
    /// <typeparam name="T">   . </typeparam>
    public class DecoratorBase<T> : IDecorator<T>
    {
        /// <summary>
        ///           .
        /// </summary>
        public Func<T> NextDelegate { get; set; }

        /// <summary>
        ///           .
        /// </summary>
        /// <typeparam name="TResult">   . </typeparam>
        /// <param name="lambda">  . </param>
        /// <returns></returns>
        protected Task<TResult> ExecuteAsync<TResult>(Func<T, Task<TResult>> lambda)
        {
            return lambda(NextDelegate());
        }

        /// <summary>
        ///           .
        /// </summary>
        /// <param name="lambda">  . </param>
        /// <returns></returns>
        protected Task ExecuteAsync(Func<T, Task> lambda)
        {
            return lambda(NextDelegate());
        }
    }


, , ,



    public Task<Response> MethodAsync(Request request)
    {
        return ExecuteAsync(async next =>
        {
            // code before
            var result = await next.MethodAsync(request);
            // code after
            return result;
        });
    }


, :



    public Task<Response> MethodAsync(Request request)
    {
        return ExecuteAsync(next => next.MethodAsync(request));
    }




Todavía queda un poco de magia. Es decir, el propósito de la propiedad NextDelegate. No está claro de inmediato qué es y cómo usarlo, pero un programador experimentado lo encontrará, pero uno sin experiencia debe explicarlo una vez. Es como DbSets en DbContext.



No lo puse en el concentrador de git. No hay mucho código, ya está generalizado, por lo que puede extraerlo desde aquí.



En conclusión, no quiero decir nada.



All Articles