Несколько потребителей одного и того же сообщения через Unity не работают в MassTransit

в последнее время у меня много проблем из-за того, что кажется ошибкой в MassTransit.UnityIntegration пакет, в первую очередь из-за того, что регистрационные имена не рассматриваются.

например, если я регистрирую свои классы следующим образом:

var container = new UnityContainer()
    .RegisterType<Consumes<Command1>.All, Handler1>("Handler1")
    .RegisterType<Consumes<Command1>.All, Handler3>("Handler3");

через несколько строк я использую LoadFrom метод расширения, чтобы получить зарегистрированных потребителей в контейнере, как это:

IServiceBus massTransitBus = ServiceBusFactory.New(_sbc =>
    {
        _sbc.UseBinarySerializer();
        _sbc.UseControlBus();
        _sbc.ReceiveFrom("msmq://localhost/MyQueue");
        _sbc.UseMsmq(_x =>
            {
                _x.UseSubscriptionService("msmq://localhost/mt_subscriptions");
                _x.VerifyMsmqConfiguration();
            });
        _sbc.Subscribe(_s => _s.LoadFrom(container));
    });

происходит то, что мои обработчики никогда не называются когда связанные сообщения попадают в автобус.

задумавшись на некоторое время, я решил взглянуть на реализацию и стало понятно, почему это происходит:

это основной код внутри LoadFrom способ:

public static void LoadFrom(this SubscriptionBusServiceConfigurator configurator, IUnityContainer container)
{
    IList<Type> concreteTypes = FindTypes<IConsumer>(container, x => !x.Implements<ISaga>());
    if (concreteTypes.Count > 0)
    {
        var consumerConfigurator = new UnityConsumerFactoryConfigurator(configurator, container);

        foreach (Type concreteType in concreteTypes)
            consumerConfigurator.ConfigureConsumer(concreteType);
    }

    ...

}

обратите внимание, что он находит только типы и не передает никакой информации о именах вперед. Это FindTypes<T> реализация:

static IList<Type> FindTypes<T>(IUnityContainer container, Func<Type, bool> filter)
{
    return container.Registrations
                        .Where(r => r.MappedToType.Implements<T>())
                        .Select(r => r.MappedToType)
                        .Where(filter)
                        .ToList();
}

после нескольких косвенных ответов все сводится к этой единственной строке, внутри UnityConsumerFactory<T> класс, который фактически создает экземпляр потребителя:

var consumer = childContainer.Resolve<T>();

это абсолютно не будет работать с Unity, когда есть несколько регистраций, потому что единственный способ зарегистрировать (а затем разрешить) несколько реализаций в Unity-дать им имя на RegisterType вызов и позже указание этого имени на Resolve звонок.

возможно, я упускаю что-то совсем базовое, во всем этом и ошибка с моей стороны? Источник компоненты единства MassTransit можно найти здесь. Я не смотрел код для других контейнеров, потому что я не знаком с ними, но я предполагаю, что это было обработано каким-то образом? Я думаю, что наличие более одного потребителя для одного и того же типа сообщения внутри одного контейнера на самом деле довольно распространено.

в данном конкретном случае было бы лучше передать не только Type от регистрации в контейнере, но также имя используемое для регистрация.

обновление

кажется, я должен регистрировать типы напрямую, чтобы они были правильно разрешены внутри фабрики, например:

var container = new UnityContainer()
    .RegisterType<Handler1>()
    .RegisterType<Handler3>();

при таком подходе я также могу опустить регистрационное имя, так как теперь их ключи сборки внутри контейнера отличающийся.

Ну, это сработало бы отлично, если бы это был наш реальный сценарий, но это не так. Позвольте мне объяснить, что именно мы делаем:

прежде чем мы начали использовать MassTransit, у нас уже был интерфейс, используемый для шаблона команды, называемый ICommandHandler<TCommand>, где TCommand является базовой моделью для команд в системе. Когда мы начали рассматривать использование служебной шины, было ясно с самого начала, что позже можно будет переключиться на другую служебную шину реализация без особых хлопот. Имея это в виду, я приступил к созданию абстракции над нашим командным интерфейсом, чтобы вести себя как один из потребителей, которых ожидает MT. Вот что я придумал:--44-->

public class CommandHandlerToConsumerAdapter<T> : Consumes<T>.All
    where T : class, ICommand
{
    private readonly ICommandHandler<T> _commandHandler;

    public CommandHandlerToConsumerAdapter(ICommandHandler<T> commandHandler)
    {
        _commandHandler = commandHandler;
    }

    public void Consume(T _message)
    {
        _commandHandler.Handle(_message);
    }
}

это очень простой класс-адаптер. Он получает ICommandHandler<T> реализация и заставляет его вести себя как Consumes<T>.All экземпляра. Жаль, что MT необходимые модели сообщений должны быть классами, поскольку у нас не было этого ограничения на наши команды, но это было небольшое неудобство, и мы продолжили добавлять where T : class ограничение наших интерфейсов.

затем, поскольку наши интерфейсы обработчика уже были зарегистрированы в контейнере, было бы вопросом регистрации интерфейса MT с этой реализацией адаптера и позволить контейнеру вводить реальные реализации над ним. Например, более реалистичный пример (взятый прямо из нашей базы кода):

.RegisterType<ICommandHandler<ApplicationInstallationCommand>, CommandRecorder>("Recorder")
.RegisterType<ICommandHandler<ApplicationInstallationCommand>, InstallOperation>("Executor")
.RegisterType<Consumes<ApplicationInstallationResult>.All, CommandHandlerToConsumerAdapter<ApplicationInstallationResult>>()
.RegisterType<Consumes<ApplicationInstallationCommand>.All, CommandHandlerToConsumerAdapter<ApplicationInstallationCommand>>
  ("Recorder", new InjectionConstructor(new ResolvedParameter<ICommandHandler<ApplicationInstallationCommand>>("Recorder")))
.RegisterType<Consumes<ApplicationInstallationCommand>.All, CommandHandlerToConsumerAdapter<ApplicationInstallationCommand>>
  ("Executor", new InjectionConstructor(new ResolvedParameter<ICommandHandler<ApplicationInstallationCommand>>("Executor")))

именованные регистрации есть немного запутанный, но необходимый, поскольку теперь у нас есть два потребителя для одного и того же сообщения. Хотя не так чисто, как мы надеялись, мы могли бы жить с этим, так как это способствует огромной развязке нашего кода от конкретной логики MassTransit: класс адаптера находится в отдельной сборке, на которую ссылается только конечный слой в системе, для целей регистрации контейнера. Это кажется очень хорошей идеей, но теперь подтверждается неподдерживаемой логикой поиска за интеграцией контейнера занятия.

обратите внимание, что я не могу зарегистрировать конкретные классы здесь, так как в середине есть общий класс адаптера.

обновление 2:

следуя совету Трэвиса, я попробовал этот простой код, который также не работает (я не понимаю, почему, поскольку он кажется совершенно действительным). Это явная Регистрация потребительской фабрики без автоматической интеграции контейнеров:

_sbc.Consume(() => container.resolve<Consumes<ApplicationInstallationCommand>.All>("Recorder"))

этот вызов разрешения правильно дает меня то ранее зарегистрировали CommandHandlerToConsumerAdapter<ApplicationInstallationCommand> экземпляр, который реализует Consumes<ApplicationInstallationCommand>.All, который в свою очередь должен быть одного из поддерживаемых базовых интерфейсов. Публикация ApplicationInstallationCommand сразу после этого ничего не делает, как если бы обработчик был недопустимым или что-то подобное.

это работает:

_sbc.Consume(() => (CommandHandlerToConsumerAdapter<ApplicationInstallationCommand>) container.resolve<Consumes<ApplicationInstallationCommand>.All>("Recorder"))

ясно, что что-то глубоко в API обрабатывает тип компиляции не общим способом, а вместо того, чтобы основываться на общем интерфейсе.

I означать... это возможно, но регистрационный код становится запутанным без видимых причин (из-за того, что я бы счел "нестандартными деталями реализации" со стороны MT). Возможно, я просто хватаюсь за соломинку? Возможно, все это сводится к " почему MT не принимает свой собственный, уже общий интерфейс?"Почему ему нужен конкретный тип во время компиляции, чтобы увидеть, что это обработчик сообщений, даже если экземпляр, который я передаю ему, набран как Consumes<X>.All, также при компиляции время?

обновление 3:

после обсуждения с Трэвисом ниже, я решил полностью отказаться от сборки UnityIntegration и перейти к автономному Consumer звонки по подписке.

я создал небольшой класс расширения в нашей конкретной сборке MassTransit, чтобы облегчить вещи:

public static class CommandHandlerEx
{
    public static CommandHandlerToConsumerAdapter<T> ToConsumer<T>(this ICommandHandler<T> _handler)
        where T : class, ICommand
    {
        return new CommandHandlerToConsumerAdapter<T>(_handler);
    }
}

и, наконец, зарегистрировал обработчиков так:

var container = new UnityContainer()
    .RegisterType<ICommandHandler<ApplicationInstallationCommand>, CommandRecorder>("Recorder")
    .RegisterType<ICommandHandler<ApplicationInstallationCommand>, InstallOperation>("Executor");

IServiceBus massTransitBus = ServiceBusFactory.New(_sbc =>
    {
        _sbc.UseBinarySerializer();
        _sbc.UseControlBus();
        _sbc.ReceiveFrom("msmq://localhost/MyQueue");
        _sbc.UseMsmq(_x =>
            {
                _x.UseSubscriptionService("msmq://localhost/mt_subscriptions");
                _x.VerifyMsmqConfiguration();
            });
        _sbc.Subscribe(RegisterConsumers);
    });

private void RegisterConsumers(SubscriptionBusServiceConfigurator _s)
{
    _s.Consumer(() => container.Resolve<ICommandHandler<ApplicationInstallationCommand>>("Recorder").ToConsumer());
    _s.Consumer(() => container.Resolve<ICommandHandler<ApplicationInstallationCommand>>("Executor").ToConsumer());
}

после использования весь день вчера, чтобы попробовать работая, я настоятельно рекомендую вам держаться подальше от сборок расширения контейнера, если вы хотите, чтобы ожидаемое поведение из контейнера и / или если вы хотите настроить классы и т. д. (Как я сделал, чтобы отделить наши классы обмена сообщениями от кода MT) по 2 основным причинам:

  1. логика в расширениях пересекает регистрации в контейнере, чтобы найти классы-потребители. Это, на мой взгляд, ужасный дизайн. Если что-то хочет реализация из контейнера, он должен просто вызвать Resolve или ResolveAll на его интерфейсе (или их эквиваленте в терминах не Единства), не заботясь о том, что именно зарегистрировано и каковы их конкретные типы. Это может иметь серьезные последствия с кодом, который предполагает, что контейнер может возвращать типы, которые не были явно зарегистрированы. К счастью, это не так с этими классами, но у нас есть расширение контейнера, которое автоматически создает типы декораторов на основе ключа сборки, и их не нужно явно регистрировать в контейнере.

  2. Регистрация потребителя использует MappedToType собственности на ContainerRegistration например, чтобы позвонить Resolve на контейнере. Это просто совершенно неправильно в любой ситуации, не только в контексте в MassTransit. Типы в Unity либо регистрируются как отображение (как в приведенных выше выдержках, с From и To компонент) или сразу как одиночный конкретный тип. В обоих случаях логика должна использовать RegisteredType введите для разрешения из контейнера. Теперь это работает так: если вам случится зарегистрировать обработчики с их интерфейсами, MT полностью обойдет вашу логику регистрации и вызовет resolve на конкретном типе, который работает в Unity из коробки, возможно, вызывая непредсказуемое поведение, потому что вы думаете, что это должен быть синглтон, как вы зарегистрировали, но он в конечном итоге является переходным объектом (по умолчанию), например.

обновление 4:

вчера я решил немного переработать весь подход адаптера, прежде чем сделать окончательную проверку. Я пошел с шаблоном интерфейса MassTransit, чтобы создать мои адаптеры тоже, потому что я думаю, что это очень приятно и чисто синтаксис.

вот результат:

public sealed class CommandHandlerToConsumerAdapter<T>
    where T : class, ICommand
{
    public sealed class All : Consumes<T>.All
    {
        private readonly ICommandHandler<T> m_commandHandler;

        public All(ICommandHandler<T> _commandHandler)
        {
            m_commandHandler = _commandHandler;
        }

        public void Consume(T _message)
        {
            m_commandHandler.Handle(_message);
        }
    }
}

к сожалению, это нарушает код MassTransit из-за необработанного исключения на метод утилиты в ссылочной библиотеке Magnum, на метод расширения под названием ToShortTypeName.

вот исключение:

ArgumentOutOfRangeException in MassTransit receive

в систему.Строка.Подстрока (Int32 startIndex, длина Int32)
в Магнуме.Увеличение.ExtensionsToType.ToShortTypeName (Тип type)
в широкие.Трубопровод.Умывальники.ConsumerMessageSink2.<>c__DisplayClass1.<Selector>b__0(IConsumeContext1 контекст) в d:BuildAgent-02workaa063b4295dfc097srcMassTransitPipelineSinksConsumerMessageSink - ... cs: линия 51 в широкие.Трубопровод.Умывальники.InboundConvertMessageSink ' 1.с__DisplayClass2.с__DisplayClass4.b__1 (IConsumeContext x) в d:BuildAgent-02workaa063b4295dfc097srcMassTransitPipelineSinksInboundConvertMessageSink - ... cs: линия 45 на Для MassTransit.Контекст.ServiceBusReceiveContext.DeliverMessageToConsumers (контекст IReceiveContext) в d:BuildAgent-02workaa063b4295dfc097srcMassTransitContextServiceBusReceiveContext - ... cs: строка 162

1 ответов


хотя я не знаю интеграции Unity, со всеми контейнерами вы должны зарегистрировать своих потребителей как конкретный тип в контейнере, а не Consumes<> интерфейсы. Я предполагаю, что это просто RegisterType<Handler1, Handler1>() но я не совсем уверен в этом.

Если вам не нравится LoadFrom расширение для вашего контейнера, вам не нужно использовать его в любом случае. Вы всегда можете просто решить потребителей самостоятельно и зарегистрировать их через _sbc.Consume(() => container.resolve<YourConsumerType>()) в конфигурации. The