CodelyTV / csharp-ddd-skeleton

🦈✨ C# DDD Skeleton: Bootstrap your new C# projects applying Hexagonal Architecture and Domain-Driven Design patterns
162 stars 34 forks source link

Implementación de DDD y CQRS haciendo uso de paquetes y escrito "estilo .Net" #25

Open solvingproblemswithtechnology opened 4 years ago

solvingproblemswithtechnology commented 4 years ago

Buenas!

He estado revisando la plantilla. Viendo que es un port de la arquitectura que usáis en Java y PHP, me gustaría aportar mi granito de arena pasando este repositorio. Es DDD implementando usando paquetes muy típicos de .Net (MediatR para CQRS, AutoMapper para los mapeos, etc...).

No sé si hay una palabra como pythonic pero para .Net pero ese sería el resumen. Es similar a como lo implementamos en nuestro curro, pero Open Source y os lo puedo enseñar ^^

Si queréis verlo en más de detalle no dudéis en escribirme y comentamos!

Buen trabajo chicos!

Leanwit commented 4 years ago

Hola Oscar.

Como habeis mencionado, en este skeleton de csharp y DDD se esta construyendo en base a las arquitecturas de Java y PHP pero con algunos matices de .Net como naming conventions o librerias.

Este ultimo punto es sumamente importante para que cualquier persona del mundo .Net siga trabajando como normalmente lo hace en otros proyectos y no tener que estar adecuandose a otros naming conventions, librerias etc. Por lo tanto, este repositorio que nos compartis es una maravilla.

Voy a pegarle un vistazo para ver la estructura y compararlo con lo que tenemos actualmente. No voy a dudar en contactarme contigo si nos surgen dudas especificas o conocer con mayor detalle algun concepto.

Nada mas que a modo informativo, estamos usando como guia lo recomendado por Microsoft y por Ardalis. https://github.com/dotnet-architecture/eShopOnContainers https://github.com/ardalis/CleanArchitecture Sumar uno mas a la lista nos viene muy bien.

Mil gracias por el aporte, vale oro. Un saludo

solvingproblemswithtechnology commented 4 years ago

Yo personalmente no suelo seguir el repo de eShopOnContainers, nunca me gustó desde los primeros libros de microservicios que sacaron. Tienen algunas cosas un poco feas en la implementación que he visto (y tenido que corregir) arrastrada a muchos proyectos y que además quedan muy ocultas e impactan mucho en el rendimiento. Pero en vuestro esqueleto no están, y contrastar entre varias arquitecturas es lo mejor para sacar lo mejor de todas.

No dudéis en escribirme :)

marcosvitali commented 4 years ago

Holas, buenas MediatR para CQRS es muy bueno como te recomienda smartcodinghub pero viola todo lo que es Arquitectura Hexagonal y SOLID en el sentido que no podrias terner IEventBus, etc ya que deberias usar las de las libreria y no podrias remplazarlo por otra implementacion de EventBus por ej. Yo siguiendo sus pasos continue la implementacion de EventBus, CommandBus, QueryBus con optimizaciones ya que en tu codigo haces uso de Reflexion ".GetTypeInfo() .GetDeclaredMethod(nameof(IDomainEventSuscriber.On));" y esti es prohibitivo si estamos haciendo una API de alto rendimiento que se busco con CQRS. La optimizacion mas facil seria implementar un ConcurrentDictionary<Type, MethodInfo> _methodCache = new ConcurrentDictionary<Type, MethodInfo>(); que cachee los MethodInfo. Siquiere puedo hacer un PR y asi evitamos usar Mediatr.

Leanwit commented 4 years ago

Hola Marcos. Estoy de acuerdo con vos con respecto a la integracion de MediatR y Arquitectura Hexagonal. Lo hable con Javier y Rafa en su momento y estuve buscando una alternativa para usarlo sin que viole algunos principios pero no tuve exito. Asi que opte por hacer un metodo custom pero de todos modos queria tener una review sobre este tema con ellos para validar ciertos criterios.

Con respecto al otro punto sobre rendimiento, sinceramente no estaba al tanto del issue que nombras y te agradezco por hacerlo notar. Cero drama por crear un PR con el improvement, o en caso negativo, en mi profile esta mi mail y podemos cordinar alguna meeting rapida asi tomo nota y lo implemento.

Gracias devuelta por tomarse el tiempo.

solvingproblemswithtechnology commented 4 years ago

Hmmm, Creo que solo se usa MediatR para CQRS no lo usa en el dominio para nada. Tampoco creo que se peguen, ya que MediatR hace la función de mediador (patrón que ha sido muy usado para interfaces/controllers). También las pipelines de MediatR son muy potentes como para no usarlas, ya que te permiten cosas como pasar las validaciones de FluentValidation de una forma muy limpia antes de llamar a tu Handler. Yo no uniría el concepto de MediatR con el EventBus en el sentido de si incluyes uno no incluyes el otro. De hecho, MediatR siempre lo he usado entre el Site (Web, Api) y la capa de aplicación por que queda bien ordenada. Si quieres monto un pequeño repo y lo paso para comparar. ^^

marcosvitali commented 4 years ago

MediatR: Supports request/response, commands, queries, notifications and events, synchronous and async with intelligent dispatchin

Justamente las notifications y events son un eventbus en memoria, que asi como los command and queries si usas la librerias estas atado y no podes tener tu IEventBus, ICommandBus, IQueryBus ni ICommand etc ya que estarias usando las de la libreria. Si mañana queres usar un command bus ne una cola que haces con IMediator ??

Volviendo al tema de la optimziacion sacrificando un poco las interfaces logre hacer sin reflexion y vuela. Ya hay uno metodos locos com lambdas y cache que quizas no justifiquen.

    public interface IDomainEventSuscriber
    {
        Task On(DomainEvent @event);
    }

    public abstract class DomainEventSuscriber<TDomain> : IDomainEventSuscriber where TDomain : DomainEvent
    {
        public abstract Task On(TDomain DomainEvent);

        async Task IDomainEventSuscriber.On(DomainEvent @event)
        {
            var msg = @event as TDomain;
            if (msg != null)
                await On(msg);
        }
    }
 public class InMemoryEventBus : IEventBus
    {
        private readonly IServiceProvider _serviceProvider;

        public InMemoryEventBus(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public async Task Publish(List<DomainEvent> events)
        {
            using (IServiceScope scope = _serviceProvider.CreateScope())
            {
                foreach (var @event in events)
                {
                    Type eventType = @event.GetType();
                    Type suscriberType = typeof(DomainEventSuscriber<>).MakeGenericType(eventType);
                    IEnumerable<object> suscribers = scope.ServiceProvider.GetServices(suscriberType);

                    foreach (object suscriber in suscribers)
                    {
                        await (suscriber as IDomainEventSuscriber).On(@event);
                    }

                }
            }
        }
    }

Con respecto al CommandBus y QueryBus lo implemente basandome en un articulo excelente de Dejan Stojanovic

https://dejanstojanovic.net/aspnet/2019/may/using-dispatcher-class-to-resolve-commands-and-queries-in-aspnet-core/

https://dejanstojanovic.net/aspnet/2019/may/automatic-cqrs-handler-registration-in-aspnet-core-with-reflection/

https://github.com/dejanstojanovic/WebShop

Saludos y discutimos el codigo cuando quieran.

marcosvitali commented 4 years ago

@smartcodinghub No me mal interpretes MediatR es una libreria excelente de echo si miras el codigo es recontra simple y super potente, es cierto lo de los pipelines pero tampoco es algo que me quite el sueño. Pero el acomplamiento de la libreria es enorme y la verdad implementar un CommandBus y QueryBus no es nada del otro mundo complica por las interfaces y covariance-and-contravariance que no se puede castear como JAVA. Pero fijate en los articulos uqe te pase incluso es mas rapido que MediaTr por que fiajte cuando llama en el controller usa el generic el codigo no queda tan limpio pero vuela.

marcosvitali commented 4 years ago

Perdon pero nunca pense que iba a utilizar esto en mi vida ajjajaj recien salido del horno c# 8 https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8#default-interface-methods asi que con este truco quedo con Interfaces y sin reflection :)

    public interface IDomainEventSuscriberBase
    {
        Task On(DomainEvent @event);
    }

    public interface IDomainEventSuscriber<TDomain> : IDomainEventSuscriberBase where TDomain : DomainEvent
    {
        Task On(TDomain DomainEvent);

        async Task IDomainEventSuscriberBase.On(DomainEvent @event)
        {
            var msg = @event as TDomain;
            if (msg != null)
                await On(msg);
        }
    }
   public class InMemoryEventBus : IEventBus
    {
        private readonly IServiceProvider _serviceProvider;

        public InMemoryEventBus(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public async Task Publish(List<DomainEvent> events)
        {
            using (IServiceScope scope = _serviceProvider.CreateScope())
            {
                foreach (var @event in events)
                {
                    Type eventType = @event.GetType();
                    Type suscriberType = typeof(IDomainEventSuscriber<>).MakeGenericType(eventType);
                    IEnumerable<object> suscribers = scope.ServiceProvider.GetServices(suscriberType);

                    foreach (object suscriber in suscribers)
                    {
                        await (suscriber as IDomainEventSuscriberBase).On(@event);
                    }

                }
            }
        }
    }
solvingproblemswithtechnology commented 4 years ago

Como dices, implementa todo el tema de los buses, pero nada te quita que uses lo INotification para comandos y los IRequest para las queries y ya. Y usar otro sistema para tus eventos de dominio. No creo que haya nada que te fuerce a usar lo mismo.

Sobre lo de usar una cola, precisamente uso MediatR por eso, tu registras la interfaz IMediator y nadie te impide implementarla. Es bastante sencillo de hacerlo (un método) y usar TaskCompletionSource para integrarte con una cola para los IRequest y hacer un fire and forget o esperar un ACK para los INotification.

El hecho de acoplarte más o menos con una librería no es un todo o nada. En las capas de la web api y aplicación no hay ningún problema de usarlo. Y usar otro EventBus para el dominio. De hecho, usar la misma solución para CQRS y para el dominio me parece bastante más peligroso que MediatR para lo uno y una implementación custom para lo otro.

Deberías darle una oportunidad a los pipelines para temas de logging, control de errores, validaciones y permisos, es muy potente y flexible y fácil de llevar a todos tus proyectos mediante Nugets.

Al final, usando la implementación que pasas estás reinventando un poco la rueda por que es exactamente lo mismo que hace MediatR con la ventaja de que no tienes que implementarlo ni mantener un paquete ni nada ^^

Sobre los métodos por defecto en Interfaces, están creados para modificar Interfaces después de estar ya implementadas y usadas. Si es una interfaz nueva o, como es el caso, una plantilla lo mejor es usar una clase abstracta o una interfaz + una clase abstracta.

No te interpreto mal, sé por donde vas. Pero una discusión sana sobre pros y contras siempre viene bien :)

Mi opinión general es que una plantilla/arquitectura de referencia debería tirar hacia el pragmatismo a pesar de que algo no sea 100% purista (siempre que no comprometa la evolución del sistema) e intentar reutilizar el ecosistema de .Net. En un video de PHP, Javier comentaba que el ecosistema de paquetes era mucho más maduro que en Java y JS y en .Net es muy similar, maduro y bien diseñado (generalmente).

solvingproblemswithtechnology commented 4 years ago

Con tu segundo comentario no sé a que ejemplo en concreto te refieres. MediatR también hace uso de genéricos y se registran todos los handlers que tengas en el Assembly. Y luego usarlo en un controller pidiendo como parámetro el Request es bastante simple y parecido. Pero sin reinventar la rueda (que no creo que sea malo, pero sí un poco innecesario aquí), sigo defendiendo que es más útil aprovechar MediatR como mediator que como eventbus. :)

marcosvitali commented 4 years ago

@smartcodinghub creo se te mezclaron un poco las cosas el código que pase es para evitar el uso de Invoke en c# que es carisimo, y que c# no permite hacer una implementacion simple IDomainEventSuscriber . Mediator afronta el mismo problema si ves su código y mete mucho codigo para evitar Refection que es prohibitiva en un sistema de alto rendimiento. Aclaración hablo de reflection para invocar a los handlers y no para registrarlos en un extension method. Vas a econtrar mil post sobre el tema. Con respecto a lo que decis justamente lo que plantean los chicos de codely es tener en el Domino compartido las interfaces ICommandBus, IQueryBus, IEventBus para dps implementarlas como quieras, eso es lo que Mediator te quita, no es un tema de reinventar la rueda, si mañana la librería cambia todo tu código de aplicación tb y lo que vos estas planteando es que la capa de aplicación no sea independiente de la infraestructura, vos mismo lo decís en tus palabras y es lo que yo opino de mi lado que no quiero.

marcosvitali commented 4 years ago

Y por ultimo dejo la solución con lambdas y cache que seria dejar el codigo como esta y usar esto. Saludos


  public static class GenericHelper
    {
        public delegate object LateBoundMethod(object target, object[] arguments);
        public delegate object LateBoundSingleParameterMethod(object target, object argument);
        private static readonly ConcurrentDictionary<Type, LateBoundMethod> _lateBoundMethodcache = new ConcurrentDictionary<Type, LateBoundMethod>();
        private static readonly ConcurrentDictionary<Type, LateBoundSingleParameterMethod> _LateBoundSingleParameterMethodcache = new ConcurrentDictionary<Type, LateBoundSingleParameterMethod>();
        private static Expression[] CreateParameterExpressions(MethodInfo method, Expression argumentsParameter)
        {
            return method.GetParameters().Select((parameter, index) =>
                Expression.Convert(
                    Expression.ArrayIndex(argumentsParameter, Expression.Constant(index)),
                    parameter.ParameterType)).ToArray();
        }

        private static Expression[] CreateSingleParameterExpression(MethodInfo method, Expression argumentsParameter)
        {
            return new Expression[] { Expression.Convert(argumentsParameter, method.GetParameters().First().ParameterType) };
        }

        public static LateBoundMethod BuildLateBoundMethod(Type declaringType, string methodName)
        {
            return _lateBoundMethodcache.GetOrAdd(declaringType, (key) =>
            {
                var method = key
                .GetTypeInfo()
                .GetDeclaredMethod(methodName);
                ParameterExpression instanceParameter = Expression.Parameter(typeof(object), "target");
                ParameterExpression argumentsParameter = Expression.Parameter(typeof(object[]), "arguments");

                MethodCallExpression call = method.IsStatic ? Expression.Call(method,
                    CreateParameterExpressions(method, argumentsParameter)) : Expression.Call(Expression.Convert(instanceParameter, method.DeclaringType), method,
                    CreateParameterExpressions(method, argumentsParameter));

                Expression<LateBoundMethod> lambda = Expression.Lambda<LateBoundMethod>(
                    Expression.Convert(call, typeof(object)),
                    instanceParameter,
                    argumentsParameter);

                return lambda.Compile();
            });
        }

        public static LateBoundSingleParameterMethod BuildLateBoundSingleParameterMethod(Type declaringType, string methodName)
        {
            return _LateBoundSingleParameterMethodcache.GetOrAdd(declaringType, (key) =>
            {
                var method = key
                .GetTypeInfo()
                .GetDeclaredMethod(methodName);
                ParameterExpression instanceParameter = Expression.Parameter(typeof(object), "target");
                ParameterExpression argumentsParameter = Expression.Parameter(typeof(object), "argument");

                MethodCallExpression call = method.IsStatic ? Expression.Call(method,
                    CreateSingleParameterExpression(method, argumentsParameter)) : Expression.Call(Expression.Convert(instanceParameter, method.DeclaringType), method,
                    CreateSingleParameterExpression(method, argumentsParameter));

                Expression<LateBoundSingleParameterMethod> lambda = Expression.Lambda<LateBoundSingleParameterMethod>(
                    Expression.Convert(call, typeof(object)),
                    instanceParameter,
                    argumentsParameter);

                return lambda.Compile();
            });
        }

    }
    public class InMemoryEventBus : IEventBus
    {
        private readonly IServiceProvider _serviceProvider;

        public InMemoryEventBus(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public async Task Publish(List<DomainEvent> events)
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                foreach (var @event in events)
                {
                    Type eventType = @event.GetType();
                    Type suscriberType = typeof(IDomainEventSuscriber<>).MakeGenericType(eventType);
                    IEnumerable<object> suscribers = scope.ServiceProvider.GetServices(suscriberType);

                    foreach (object suscriber in suscribers)
                    {
                        await Notify(suscriberType, suscriber, @event);
                    }

                }
            }
        }

        private async Task Notify(Type suscriberType, object suscriber, DomainEvent @event)
        {
            var handler = GenericHelper.BuildLateBoundSingleParameterMethod(suscriberType, nameof(IDomainEventSuscriber<DomainEvent>.On));
            await (Task)handler(suscriber, @event);
        }
    }
Leanwit commented 4 years ago

Lo siento por la demora en la respuesta. Gracias marcosvitali por el aporte. Por ahora implemente la solución con interfaces y sin reflection y mas adelante, analizaré la posibilidad de implementar con lambdas y cache. Prefiero ahora priorizar el avance con temas criticos para completar con el skeleton.

Mi opinión general es que una plantilla/arquitectura de referencia debería tirar hacia el pragmatismo a pesar de que algo no sea 100% purista (siempre que no comprometa la evolución del sistema) e intentar reutilizar el ecosistema de .Net. En un video de PHP, Javier comentaba que el ecosistema de paquetes era mucho más maduro que en Java y JS y en .Net es muy similar, maduro y bien diseñado (generalmente).

Con respecto a lo que opinas smartcodinghub, es un punto que lo hable varias veces con Javier y Rafa para evitar estar agregando codigo custom cuando MediaTr te soluciona en ese aspecto con un la seguridad de que es una liberia soportada y sumamente usada. El tema que bajo ese aspecto no sigue las directrices de DDD que pregona CodelyTv por lo que, por el momento, usar la libreria MediaTr queda descartada.

Por otro lado, estoy siguiendo muy de cerca los repositorios de jasontaylordev, CleanArchitecture y NorthwindTraders, y en los ultimos dias le preguntaron justamente por este tema donde destaco dos cosas (por cierto, concuerda 100% con tu opinion):

I'm not going for a DDD approach as part of this template. I want this approach to be as simple as possible, a starting point for any enterprise app. https://github.com/jasontaylordev/CleanArchitecture/issues/34

In relation to Clean Architecture, we should aim to be independent of frameworks and database. In the usage of EF Core, I am not independent of a framework but still independent of the database. The same goes for MediatR and FluentValidation. However, if I head down that road, I feel that I would be overcomplicating the solution, for very little gain https://github.com/jasontaylordev/CleanArchitecture/issues/42

Muchisimias gracias a los dos por sus aportes y exponer sus opiniones.