Нашел решение, хотя у меня было две проблемы.
Первая проблема возникла из-за способа, которым объект был зарегистрирован / настроен с помощью ядра Ninject в методе IContainer.Configure
моего класса NinjectObjectBuilder
. Изучив существующие реализации nServiceBus ObjectBuilder
с использованием других контейнеров IoC, я отметил, что общий подход к регистрации заключался в регистрации самого конкретного типа, а также всех интерфейсов реализуемого типа. В Ninject это означает «привязать конкретный тип к себе», а затем привязать каждый интерфейс, который этот тип также реализует к типу. Это было довольно просто, за исключением того, что после профилирования с помощью dotTrace я обнаружил, что в случае синглтон-активаций не похоже, что я действительно получаю ссылки на Синглтон. Фактически, то, что случилось бы, - то, что я получил бы новый объект в зависимости от типа запрошенной услуги. Например, конкретный тип UnicastBus
реализует IBus
, а также IStartableBus
и зарегистрирован с одноэлементной областью действия. nServiceBus ожидает получения одного и того же объекта, независимо от того, запрашивается ли IBus
или IStartableBus
, если они являются синглетонами и оба «связаны» с одной и той же реализацией. Ninject интерпретирует синглтон, по-видимому, в отношении сервиса или интерфейса - другими словами, вы получаете один и тот же экземпляр UnicastBus
каждый раз, когда запрашиваете IBus
; тем не менее, вы получите новый, другой UnicastBus
для запроса на IStartableBus
. Я решил это, реализовав метод IContainer.Configure
следующим образом:
void IContainer.Configure(Type concreteComponent,
ComponentCallModelEnum callModel) {
if (HasComponent(concreteComponent))
return;
var services = concreteComponent.GetAllServices()
.Where(t => t != concreteComponent);
var instanceScope = GetInstanceScopeFrom(callModel);
// Bind the concrete type to itself ...
kernel
.Bind(concreteComponent)
.ToSelf()
.InScope(instanceScope);
// Bind "aliases" to the binding for the concrete component
foreach (var service in services)
kernel
.Bind(service)
.ToMethod(ctx => ctx.Kernel.Get(concreteComponent))
.InScope(instanceScope);
}
Это решило проблему разрешения одиночных экземпляров способом, соответствующим ожиданиям nServiceBus. Однако у меня все еще была проблема с получением «призрачных» сообщений в моих обработчиках. После прочесывания файлов журнала log4net, профилирования и, наконец, чтения проблемы, как обсуждалось здесь и здесь . Проблема, в частности, связана с тем, что обработчики событий Mutliple подключаются во время внедрения свойства. В частности, проблема вызвана тем, что UnicastBus имеет его свойство Transport
, установленное несколько раз. Вот фрагмент кода из UnicastBus.cs в кодовой базе nServiceBus:
public virtual ITransport Transport
{
set
{
transport = value;
transport.StartedMessageProcessing += TransportStartedMessageProcessing;
transport.TransportMessageReceived += TransportMessageReceived;
transport.FinishedMessageProcessing += TransportFinishedMessageProcessing;
transport.FailedMessageProcessing += TransportFailedMessageProcessing;
}
}
Подумав об этом, я удивился, почему это свойство устанавливалось несколько раз. UnicastBus
зарегистрирован в одноэлементной области nServiceBus, и я только что исправил эту проблему, как обсуждалось выше. Оказывается, Ninject, при активации объекта - будь то новый или из его внутреннего кэша - все равно будет пытаться внедрить свойства объекта. Он будет вызывать эвристические классы внедрения, зарегистрированные в его внутреннем контейнере компонента ядра, и на основе их ответа (то есть результата вызова их реализации bool ShouldInject(MemberInfo member)
) вставлять свойства перед каждой активацией. Таким образом, решение состояло в том, чтобы запретить Ninject выполнять инъекцию свойств в ранее активированных экземплярах, которые были одиночными. Мое решение состояло в том, чтобы создать новую стратегию внедрения свойств, которая отслеживала ранее активированные экземпляры, которые не были переходными в области действия, и пропускала стратегию внедрения свойств по умолчанию для запросов активации для таких экземпляров. Моя стратегия выглядит так:
/// <summary>
/// Only injects properties on an instance if that instance has not
/// been previously activated. This forces property injection to occur
/// only once for instances within a scope -- e.g. singleton or within
/// the same request, etc. Instances are removed on deactivation.
/// </summary>
public class NewActivationPropertyInjectStrategy : PropertyInjectionStrategy {
private readonly HashSet<object> activatedInstances = new HashSet<object>();
public NewActivationPropertyInjectStrategy(IInjectorFactory injectorFactory)
: base(injectorFactory) { }
/// <summary>
/// Injects values into the properties as described by
/// <see cref="T:Ninject.Planning.Directives.PropertyInjectionDirective"/>s
/// contained in the plan.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="reference">A reference to the instance being
/// activated.</param>
public override void Activate(IContext context,
InstanceReference reference) {
if (activatedInstances.Contains(reference.Instance))
return; // "Skip" standard activation as it was already done!
// Keep track of non-transient activations...
// Note: Maybe this should be
// ScopeCallback == StandardScopeCallbacks.Singleton
if (context.Binding.ScopeCallback != StandardScopeCallbacks.Transient)
activatedInstances.Add(reference.Instance);
base.Activate(context, reference);
}
/// <summary>
/// Contributes to the deactivation of the instance in the specified context.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="reference">A reference to the instance being
/// deactivated.</param>
public override void Deactivate(IContext context,
InstanceReference reference) {
activatedInstances.Remove(reference.Instance);
base.Deactivate(context, reference);
}
}
Моя реализация сейчас работает. Единственная другая проблема, с которой я столкнулся, заключалась в «замене» существующей стратегии активации для внедрения свойств. Я думал о создании собственного ядра (и это может быть лучшим способом); однако тогда вы не сможете поддерживать «предварительно сконфигурированное» ядро для nServiceBus. На данный момент у меня есть метод расширения, который добавляет новые компоненты к любому ядру Ninject.
public static void ConfigureForObjectBuilder(this IKernel kernel) {
// Add auto inject heuristic
kernel.Components.Add<IInjectionHeuristic, AutoInjectBoundPropertyTypeHeuristic>();
// Replace property injection activation strategy...
/* NOTE: I don't like this! Thinking about replacing the pipeline component
* in Ninject so that it's smarter and selects our new activation
* property inject strategy for components in the NServiceBus DLLs and
* uses the "regular strategy" for everything else. Also, thinking of
* creating a custom kernel.
*/
kernel.Components.RemoveAll<IActivationStrategy>();
kernel.Components.Add<IActivationStrategy,
NewActivationPropertyInjectStrategy>();
// The rest of the "regular" Ninject strategies ...
kernel.Components.Add<IActivationStrategy, MethodInjectionStrategy>();
kernel.Components.Add<IActivationStrategy, InitializableStrategy>();
kernel.Components.Add<IActivationStrategy, StartableStrategy>();
kernel.Components.Add<IActivationStrategy, BindingActionStrategy>();
kernel.Components.Add<IActivationStrategy, DisposableStrategy>();
}
Это прямой хак на этом этапе, поскольку в контейнере компонента ядра нет механизма для «замены» существующего компонента. И, поскольку я хотел переопределить существующее поведение стратегии внедрения свойств, у меня не могло быть одновременно более одного из этих конкретных типов стратегий в ядре. Другая проблема с этой текущей реализацией состоит в том, что любые другие пользовательские IActivationStrategy
компоненты, которые могли быть настроены, будут потеряны. Я хотел написать код, который бы получал все IActivationStrategy
компоненты в списке, удалял их из ядра, заменял стратегию внедрения свойств в созданном мною списке, а затем добавлял их все обратно в ядро, эффективно заменяя их. Однако контейнер компонента ядра поддерживает только общий метод Add
, и мне не хотелось писать какой-то забавный код для создания динамического вызова.
** РЕДАКТИРОВАТЬ **
После того, как я опубликовал вчера, я решил лучше справиться со стратегией. Вот что я сделал, связав все в методе расширения для настройки ядра Ninject:
public static void ConfigureForObjectBuilder(this IKernel kernel) {
// Add auto inject heuristic
kernel.Components.Add<IInjectionHeuristic, AutoInjectBoundPropertyTypeHeuristic>();
// Get a list of all existing activation strategy types
// with exception of PropertyInjectionStrategy
var strategies = kernel.Components.GetAll<IActivationStrategy>()
.Where(s => s.GetType() != typeof (PropertyInjectionStrategy))
.Select(s => s.GetType())
.ToList();
// Add the new property injection strategy to list
strategies.Add(typeof (NewActivationPropertyInjectStrategy));
// Remove all activation strategies from the kernel
kernel.Components.RemoveAll<IActivationStrategy>();
// Add the list of strategies
var addMethod = kernel.Components.GetType().GetMethod("Add");
strategies
.ForEach(
t => addMethod
.MakeGenericMethod(typeof (IActivationStrategy), t)
.Invoke(kernel.Components, null)
);
}