通过Unity的同一消息的多个消费者无法在MassTransit中工作

时间:2013-12-20 19:23:18

标签: .net unity-container esb masstransit command-pattern

我最近遇到了很多问题,因为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 Unity组件的来源here。我没有查看其他容器的代码,因为我不熟悉它们,但我认为这已经以某种方式处理了?我认为在同一个容器中为同一个消息类型设置多个消费者实际上很常见。

在这种特殊情况下,不仅要传递容器中注册的Type,还要传递用于注册的名称。

更新

现在特拉维斯花时间解释它,问题就更清楚了。我应该早点注意到它。

我似乎应该直接注册这些类型,以便在工厂内正确解析它们,如下所示:

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

使用这种方法,我也可以省略注册名称,因为现在它们在容器内的构建密钥是不同的。

嗯,如果这是我们真实的情况,这将是完美的,但它不是。让我解释一下我们到底在做什么:

在我们开始使用MassTransit之前,我们已经有一个用于命令模式的接口,称为ICommandHandler<TCommand>,其中TCommand是系统中命令的基本模型。当我们开始考虑使用服务总线时,从一开始就很清楚,应该可以稍后切换到另一个服务总线实现而不会有太多麻烦。考虑到这一点,我开始在我们的命令界面上创建一个抽象,表现得像MT期望的消费者之一。这就是我想出的:

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 required message models to be classes,因为我们没有对命令进行约束,但这是一个小小的不便,我们继续向我们的接口添加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:

跟随Travis&#39;建议,我尝试了这个简单的代码也行不通(我不明白为什么,因为它似乎完全有效)。它是一个明确的消费者工厂注册,没有任何自动容器集成:

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

正确的解析调用给了我之前注册的CommandHandlerToConsumerAdapter<ApplicationInstallationCommand>实例,该实例实现了Consumes<ApplicationInstallationCommand>.All,而ApplicationInstallationCommand又应该是支持的基本接口之一。在此之后立即发布_sbc.Consume(() => (CommandHandlerToConsumerAdapter<ApplicationInstallationCommand>) container.resolve<Consumes<ApplicationInstallationCommand>.All>("Recorder")) 不会做任何事情,就好像处理程序无效或类似。

但这有效:

Consumes<X>.All

很明显,API中的一些内容正在以非泛型的方式处理编译类型,而不是基于通用接口。

我的意思是......这是可行的,但是注册码在没有明显原因的情况下变得错综复杂(由于我认为是非标准的实施细节&#39;在MT&#39; s部分)。也许我只是抓住稻草在这里?也许这一切归结为“为什么MT不接受它自己的,已经是通用的界面?”#39;为什么在编译时需要具体类型才能看到它是一个消息处理程序,即使我传递给它的实例也被编译为Consumer,也是在编译时?

更新3:

在与Travis讨论之后,我决定完全放弃UnityIntegration程序集,并在订阅时使用独立的public static class CommandHandlerEx { public static CommandHandlerToConsumerAdapter<T> ToConsumer<T>(this ICommandHandler<T> _handler) where T : class, ICommand { return new CommandHandlerToConsumerAdapter<T>(_handler); } } 调用。

我在MassTransit特定程序集中创建了一个小扩展类,以方便用户:

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());
}

最后注册了这样的处理程序:

Resolve

在昨天使用了整整一天来尝试解决问题后,我强烈建议您远离容器扩展程序集,如果您希望从容器中预期的行为和/或您想要自定义类等(如我确实将我们的消息类与MT特定代码分离出来有两个主要原因:

  1. 扩展中的逻辑遍历容器中的注册以查找使用者类。在我看来,这是一个糟糕的设计。如果有什么东西需要容器中的实现,那么它应该只在它的接口上调用ResolveAllMappedToType(或者在非Unity术语中调用它们的等价物),而无需关心注册的内容和内容他们的具体类型是。对于假定容器可以返回未明确注册的类型的代码,这会产生严重后果。幸运的是,这些类并非如此,但我们确实有一个容器扩展,可以根据构建密钥自动创建装饰器类型,并且不需要在容器上显式注册。

  2. 使用者注册使用ContainerRegistration实例上的Resolve属性来调用容器上的From。这在任何情况下都是完全错误的,而不仅仅是在MassTransit的背景下。 Unity中的类型要么注册为映射(如上面的摘录,带有ToRegisteredType组件),要么直接作为单个具体类型注册。在两种情况下,逻辑应使用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); } } } 类型从容器中解析。它现在的工作方式是,如果你碰巧用他们的接口注册处理程序,MT将完全绕过你的注册逻辑并调用具体类型的解析,而works in Unity out of the box可能导致不可预测的行为,因为你认为它应该像你注册的单身一样,但它最终会成为一个瞬态对象(默认),例如。

  3. 现在回想一下,我可以看到它原本相信的要复杂得多。在这个过程中也有相当多的学习,所以这很好。

    更新4:

    昨天我决定在进行最终检查之前对整个适配器方法进行一些重构。我使用MassTransit的界面模式来创建我的适配器,因为我认为这是一个非常好的和干净的语法。

    结果如下:

    ToShortTypeName

    不幸的是,这会破坏MassTransit的代码,因为引用的Magnum库中的实用程序方法在名为2.<>c__DisplayClass1.<Selector>b__0(IConsumeContext的扩展方法上有未处理的异常。

    以下是例外:

    ArgumentOutOfRangeException in MassTransit receive

      

    在System.String.Substring(Int32 startIndex,Int32 length)
         在Magnum.Extensions.ExtensionsToType.ToShortTypeName(类型类型)
         在MassTransit.Pipeline.Sinks.ConsumerMessageSink {{1}} 1上下文中的d:\ BuildAgent-02 \ work \ aa063b4295dfc097 \ src \ MassTransit \ Pipeline \ Sinks \ ConsumerMessageSink.cs:第51行      在MassTransit.Pipeline.Sinks.InboundConvertMessageSink`1。&lt;&gt; c__DisplayClass2。&lt;&gt; c__DisplayClass4.b__1(IConsumeContext x)在d:\ BuildAgent-02 \ work \ aa063b4295dfc097 \ src \ MassTransit \ Pipeline \ Sinks \ InboundConvertMessageSink中。 cs:第45行      在D:\ BuildAgent-02 \ work \ aa063b4295dfc097 \ src \ MassTransit \ Context \ ServiceBusReceiveContext.cs中的MassTransit.Context.ServiceBusReceiveContext.DeliverMessageToConsumers(IReceiveContext context):第162行

1 个答案:

答案 0 :(得分:4)

虽然我不知道所有容器的Unity集成,但您必须将您的使用者注册为容器中的具体类型,而不是Consumes<>接口。我认为它只是RegisterType<Handler1, Handler1>()但我对此并不完全确定。

如果您不喜欢容器的LoadFrom扩展名,则无论如何都不需要使用它。您可以随时自行解决消费者,并在配置中通过_sbc.Consume(() => container.resolve<YourConsumerType>())进行注册。 LoadFrom扩展名只是对以普通方式使用容器的用户的说服力。

以下代码可以正常运行,它使用的方式与我期望的方式相同,而不需要知道您的域名,使用它的方式。如果你想了解消息如何更好地绑定,我建议使用RabbitMQ,因为你可以通过停止交换绑定轻松地看到事情的结果。在这一点上,这远远超出了一个SO问题,如果你还有更进一步的话,我会把它带到邮件列表中。

using System;
using MassTransit;
using Microsoft.Practices.Unity;

namespace MT_Unity
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var container = new UnityContainer()
                .RegisterType<ICommandHandler<MyCommand>, MyCommandHandler>()
                .RegisterType<CommandHandlerToConsumerAdapter<MyCommand>>())

            using (IServiceBus consumerBus = ServiceBusFactory.New(sbc =>
                    {
                        sbc.ReceiveFrom("rabbitmq://localhost/consumer");
                        sbc.UseRabbitMq();


                        sbc.Subscribe(s => s.Consumer(() => container.Resolve<CommandHandlerToConsumerAdapter<MyCommand>>()));
                    }))
            using (IServiceBus publisherBus = ServiceBusFactory.New(sbc =>
                    {
                        sbc.ReceiveFrom("rabbitmq://localhost/publisher");
                        sbc.UseRabbitMq();
                    }))
            {
                publisherBus.Publish(new MyCommand());

                Console.ReadKey();
            }
        }
    }

    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);
        }
    }

    public interface ICommand { }
    public class MyCommand : ICommand { }

    public interface ICommandHandler<T> where T : class, ICommand
    {
        void Handle(T message);
    }

    public class MyCommandHandler : ICommandHandler<MyCommand>
    {
        public MyCommandHandler()
        {

        }
        public void Handle(MyCommand message)
        {
            Console.WriteLine("Handled MyCommand");
        }
    }

}