Upgrading NServiceBus 5 to 6 and Domain Events

149 views
Skip to first unread message

Dennis Smith

unread,
Sep 18, 2017, 9:37:59 AM9/18/17
to Particular Software
Product name: NServiceBus
Version: 5, 6
Description: I'm trying to upgrade my application from NServiceBus 5 to NServiceBus 6.  The application makes heavy use Udi's domain event pattern (http://udidahan.com/2009/06/14/domain-events-salvation/) in order to publish/send NServiceBus events in response to events that occur in the domain.  We have many domain methods that can be called from either a web application or an NServiceBus handler interchangeably which then turn around and publish NServiceBus events in response to the action.  With the changes that remove IBus in NServiceBus 6 (https://docs.particular.net/nservicebus/upgrades/5to6/moving-away-from-ibus), it is not clear to me how to modify this code so that it would continue to work correctly.

As a example:

public class Order
{
   public void Cancel()
   {
      // TODO: whatever domain logic is required to implement the business logic
     DomainEvents.Raise(new OrderCancelledEvent(this));
   }
}

public class OrderCancelledEventHandler : Handles<OrderCancelledEvent>
{
   private IBus Bus {get;}

   public OrderCancelledEventHandler (IBus bus)
   {
      Bus = bus;
   }

   public void Handle(OrderCancelledEvent args)
   {
      Bus.Publish<OrderCancelled>(m => 
      {
         // TODO: populate necessary properties
      });
   }
}

Then the Order.Cancel method ends up being called in two different contexts -- one from a web application and one from an NServiceBus handler.

In this architecture, the project that contains the domain has no dependencies on any infrastructure code like NServiceBus, and we would like to keep  it that way, which makes the MyContextAccessingDependency example from the NServiceBus 6 upgrade guide impractical.  The MyReturningDependency example is also problematic, as in many cases calling a single domain method may generate zero to multiple domain events.

I realize that if nothing else I need to make my DomainEvents implementation async, but it's not clear to me how to deal with the IMessageHandlerContext vs. IMessageSession distinction and how to get an IMessageHandlerContext into my domain event handlers without adding a forcing my domain to take a dependency on NServiceBus.  This architecture has served us well in the past, since previous NServiceBus upgrades did not require any updates to the domain itself; all of the changes were isolated to the NServiceBus/domain event handlers.  If it wasn't for all of the stern warnings against using IMessageSession in the context of a message handler, that would have appeared to be the easiest way to approach this problem.

Some else had a question about this on StackOverflow (see https://stackoverflow.com/questions/42443515/is-there-some-way-to-keep-domain-events-context-unaware-with-nservicebus-6-and-t) but there were not any good answers posted there.

Is there any additional guidance or documentation about how to handle this case?  Is there another approach that I have missed that could be used here to allow me to upgrade my large application to NServiceBus 6 without a substantial re-write of the domain?

Thanks for your help.

Dennis

Daniel Marbach

unread,
Sep 22, 2017, 3:39:17 AM9/22/17
to Particular Software
Hi Dennis

Sorry that we left you alone without a response for so long. I will look into this today and respond soon with some guidance.

Regards
Daniel

Daniel Marbach

unread,
Sep 22, 2017, 6:09:25 AM9/22/17
to Particular Software
Hi Dennis,

I'm going to assume that the DomainEvents static class has access to a statically configured container as mentioned by Udi's post. 

With NServiceBus v6 we moved away from ambient context data such as thread locals and per thread scope because in the async world these concepts can be dangerous and error-prone. Instead, we encourage our users to float side-effect producing dependencies into things that need it over method injection or even better return the side effects and make it a top-level concern to decide whether the side effects should be written to the bus, database etc. 

We understand this opinionated approach that we take is not always feasible to follow for projects with a history. One way to overcome the problem you are facing could be to float the dependency but rely on the container. Here are a few snippets that outline the refactoring:

Before

public class MyDependencyUsedInVariousContexts
    {
        IBus bus;

        public MyDependencyUsedInVariousContexts(IBus bus)
        {
            this.bus = bus;
        }

        // might be called from webapi or from within the handler
        public void Do()
        {
            foreach (var changedCustomer in LoadChangedCustomers())
            {
                bus.Publish(new CustomerChanged { Name = changedCustomer.Name });
            }
        }

        static IEnumerable<Customer> LoadChangedCustomers()
        {
            return Enumerable.Empty<Customer>();
        }
    }

    [Route("api/[controller]")]
    public class WebController : Controller
    {
        MyDependencyUsedInVariousContexts dependency;

        public WebController(MyDependencyUsedInVariousContexts dependency)
        {
            this.dependency = dependency;
        }

        [HttpPost]
        public IActionResult Create()
        {
            dependency.Do();

            return null;
        }
    }

    public class HandlerWithDependencyUsedInVariousContexts :
        IHandleMessagesFromPreviousVersions<MyMessage>
    {
        MyDependencyUsedInVariousContexts dependency;

        public HandlerWithDependencyUsedInVariousContexts(MyDependencyUsedInVariousContexts dependency)
        {
            this.dependency = dependency;
        }

        public void Handle(MyMessage message)
        {
            dependency.Do();
        }
    }

After

public class ContextDecorator
    {
        IMessageSession messageSession;
        IMessageHandlerContext messageHandlerContext;

        public ContextDecorator(IEndpointInstance session)
        {
            messageSession = session;
        }

        public ContextDecorator(IMessageHandlerContext context)
        {
            messageHandlerContext = context;
        }

        public Task Publish(object message)
        {
            if (messageSession != null)
            {
                return messageSession.Publish(message);
            }

            if (messageHandlerContext != null)
            {
                return messageHandlerContext.Publish(message);
            }

            throw new InvalidOperationException("Decorator was not properly resolved.");
        }
    }

    public class MyDependencyUsedInVariousContextsNew
    {
        ContextDecorator bus;

        public MyDependencyUsedInVariousContextsNew(ContextDecorator bus)
        {
            this.bus = bus;
        }

        // might be called from webapi or from within the handler
        public async Task Do()
        {
            foreach (var changedCustomer in LoadChangedCustomers())
            {
                await bus.Publish(new CustomerChanged { Name = changedCustomer.Name })
                    .ConfigureAwait(false);
            }
        }

        static IEnumerable<Customer> LoadChangedCustomers()
        {
            return Enumerable.Empty<Customer>();
        }
    }

    [Route("api/[controller]")]
    public class WebControllerNew : Controller
    {
        MyDependencyUsedInVariousContextsNew dependency;

        // binding resolves ctor with IEndpointInstance
        public WebControllerNew(MyDependencyUsedInVariousContextsNew dependency)
        {
            this.dependency = dependency;
        }

        [HttpPost]
        public async Task<IActionResult> Create()
        {
            await dependency.Do();

            return null;
        }
    }

    public class HandlerWithDependencyUsedInVariousContextsNew :
        IHandleMessages<MyMessage>
    {
        ScopeOrBetterConcreteFactory scope;

        public HandlerWithDependencyUsedInVariousContextsNew(ScopeOrBetterConcreteFactory scope)
        {
            this.scope = scope;
        }

        public async Task Handle(MyMessage message, IMessageHandlerContext context)
        {
            var dependency = scope.Resolve<MyDependencyUsedInVariousContextsNew>(new NamedParameter("context", context));
            await dependency.Do();
        }
    }

Would that help?

Another option is to switch back to ambient context and use AsyncLocal. Although we don't recommend that.

Regards

Daniel



Reply all
Reply to author
Forward
0 new messages