Wykonywanie wielu różnych commandHandlerów po sobie

0

Cześć, tworzę aplikację, która jako jedyne zadanie ma przechowywać informacje o Company, bez jakiejkolwiek logiki biznesowej. Problemem jest wiele integracji, które wymagają różnego mapowania tej encji.

Przykładowy problem nad którym się pochylam:

  1. Przychodzi request(update) z integracji A, który mapuję na model CompanyA
  2. Sprawdzam czy są jakieś różnice z zapisanym stanem, jeśli nie to stop.
  3. Zapisuję CompanyA w bazie danych.
  4. Normalizuję CompanyA na Company używając do tego modeli z innych integracji, np. CompanyB, CompanyC.
  5. Sprawdzam czy są różnice, jeśli nie to stop.
  6. Zapisuję Company w bazie danych.
  7. Mapuję Company na CompanyView i wysyłam do zewnętrznej integracji.

Zależnie od jakiej integracji jest request, ten potok może się odrobinę różnić. W jaki sposób najlepiej takie pipeline'y zdefiniować? Próbowałem skorzystać z wzorca ChainOfResposibility, ale każdy handler ma inny request i trudno mi napisać sensowną implementację. Teraz spróbowałem wykorzystać MediatR i wyszło coś takiego (pomijam na razie walidację):

public class CompanyAUpdateCommandHandler : IRequestHandler<CompanyAUpdateCommand>
{
    private readonly IMediator _mediator;
    public CompanyAUpdateCommandHandler(IMediator mediator)
    {
        _mediator = mediator;
    }
    public async Task Handle(CompanyAUpdateCommand request, CancellationToken cancellationToken)
    {
        var companyA = await _mediator.Send(request); //normalizacja request na CompanyA
        await _mediator.Send(companyA); //sprawdza różnice ze stanem zapisanym i zapisuje, później będzie obsługa przerwania przy braku różnic
        var company = await _mediator.Send(companyA); //normalizacja CompanyA na Company
        await _mediator.Send(company); //zapisuje Company
        var companyView = await _mediator.Send(company); //mapowanie
        var result = await _mediator.Send(companyView); //wysłanie do innej integracji
    }
}

I ogólnie wydaje się, że dodawanie kolejnych commandHandlerów do tego pipeline będzie proste, ale po prostu to rozwiązanie też mi się nie podoba. Szczególnie handler odwołujący się do kolejnych handlerów. Z drugiej strony mam możliwość dopisania czegokolwiek pomiędzy Execute kolejnych commandHandlerów, w tym nawet skorzystać z obecnego companyService. Wiem że podobne problemy pewnie już były wiele razy rozwiązane, ale nie mogę znaleźć nic ciekawego. Dlatego jeśli ktoś ma jakieś wskazówki, w tym o czym mógłbym poczytać, to będę wdzięczny.

0

Ja np robiłem taki ChainOfResponsibility z dependency injection.

services.Chain<IChainOfResponsibility<JakiśTypParametru>>()
.Add<FirstChainHandlerr>()
.Add<SecondChainHandlerr>()
.Configure();
public interface IChainOfResponsibility<TContext>
{
    IChainOfResponsibility<TContext> Next { get; }
    Task Handle(TContext context);
}

public static class ChainConfigurator
{
    public static IChainConfigurator<T> Chain<T>(this IServiceCollection services) where T : class {
        return new ChainConfiguratorImpl<T>(services);
    }
    public interface IChainConfigurator<T> {
        IChainConfigurator<T> Add<TImplementation>() where TImplementation : T;
        void Configure();
    }

    private class ChainConfiguratorImpl<T> : IChainConfigurator<T> where T : class {
        private readonly IServiceCollection _services;
        private List<Type> _types;
        private Type _interfaceType;

        public ChainConfiguratorImpl(IServiceCollection services) {
            _services = services;
            _types = new List<Type>();
            _interfaceType = typeof(T);
        }

        public IChainConfigurator<T> Add<TImplementation>() where TImplementation : T {
            var type = typeof(TImplementation);

            _types.Add(type);

            return this;
        }

        public void Configure() {
            if (_types.Count == 0)
                throw new InvalidOperationException($"No implementation defined for {_interfaceType.Name}");

            foreach (var type in _types) {
                ConfigureType(type);
            }
        }

        private void ConfigureType(Type currentType) {
            // gets the next type, as that will be injected in the current type
            var nextType = _types.SkipWhile(x => x != currentType).SkipWhile(x => x == currentType).FirstOrDefault();

            // Makes a parameter expression, that is the IServiceProvider x 
            var parameter = Expression.Parameter(typeof(IServiceProvider), "x");

            // get constructor with highest number of parameters. Ideally, there should be only 1 constructor, but better be safe.
            var ctor = currentType.GetConstructors().OrderByDescending(x => x.GetParameters().Count()).First();

            // for each parameter in the constructor
            var ctorParameters = ctor.GetParameters().Select(p =>
            {
                // check if it implements the interface. That's how we find which parameter to inject the next handler.
                if (_interfaceType.IsAssignableFrom(p.ParameterType)) {
                    if (nextType is null) {
                        // if there's no next type, current type is the last in the chain, so it just receives null
                        return Expression.Constant(null, _interfaceType);
                    } else {
                        // if there is, then we call IServiceProvider.GetRequiredService to resolve next type for us
                        return Expression.Call(typeof(ServiceProviderServiceExtensions), "GetRequiredService", new Type[] { nextType }, parameter);
                    }
                }

                // this is a parameter we don't care about, so we just ask GetRequiredService to resolve it for us 
                return (Expression)Expression.Call(typeof(ServiceProviderServiceExtensions), "GetRequiredService", new Type[] { p.ParameterType }, parameter);
            });

            // cool, we have all of our constructors parameters set, so we build a "new" expression to invoke it.
            var body = Expression.New(ctor, ctorParameters.ToArray());

            // if current type is the first in our list, then we register it by the interface, otherwise by the concrete type
            var first = _types[0] == currentType;
            var resolveType = first ? _interfaceType : currentType;
            var expressionType = Expression.GetFuncType(typeof(IServiceProvider), resolveType);

            // finally, we can build our expression
            var expression = Expression.Lambda(expressionType, body, parameter);

            // compile it
            var compiledExpression = (Func<IServiceProvider, object>)expression.Compile();

            // and register it in the services collection as transient
            _services.AddTransient(resolveType, compiledExpression);
        }
    }
}
0

Możesz sobie zrobić workflow na wzór sagi. Czyli request z jakiegoś UI albo API uruchamia Sagę i cały proces jest orkiestrowany w sadze. Komunikacja i kierunek przepływu sterowany jest wtedy zdarzeniami.

1 użytkowników online, w tym zalogowanych: 0, gości: 1