Saga with Application layer / DDD

Hello,

I would like to use NSB Saga in DDD project. I’m using MessageSessionAdapter instance injected to BookingService to be able to publish events. That’s ok, but I have troubles with scheduling timeouts since RequestTimeout method is bound to Saga instance and requires context. Why it’s not included in IMessageSession interface and how I can call it from an application layer? It looks like NSB doesn’t support DDD philosophy at all. I consider NSB handlers as Asp.Net Core Controllers, so what I’m trying to do is to move the logic from handlers/saga handlers/api controllers to the application layer.

An example:

// NServiceBus

public class BookingSaga : Saga<BookingSagaData>, IAmStartedByMessages<StartBooking>, IHandleMessages<ConfirmBooking>, IHandleTimeouts<BookingTimeout>
{
    private readonly IBookingService bookingService;

    public BookingSaga(IBookingService bookingService)
    {
        bookingService = bookingService;
    }

    public async Task Handle(StartBooking command, IMessageHandlerContext context)
    {
        await bookingService.StartBooking(command.Id, Data);
    }

    public async Task Handle(ConfirmBooking command, IMessageHandlerContext context)
    {
        await bookingService.ConfirmBooking(this.Data.BookingId);
    }
    
    public async Task Timeout(BookingTimeout state, IMessageHandlerContext context)
    {
        await bookingService.CancelBooking(Data.BookingId, Data.TimeoutIndex);
        MarkAsComplete();
    }
}


// Infrastructure

public class MessageSessionAdapter : IMessageSessionAdapter
{
    private readonly IMessageSession session;

    public MessagePublisher(IMessageSession session)
    {
        session = session;
    }
    
    public async Task PublishBookingStartedEvent(Guid bookingId)
    {
        await session.Publish(new BookingStarted { BookingId = bookingId });
    }

    public async Task PublishBookingCancelledEvent(Guid bookingId)
    {
        await session.Publish(new BookingCancelled { BookingId = bookingId });
    }

    public async Task SetTimeout(int seconds)
    {
        // not possible: RequestTimeout requires the specific Saga instance and context
        Data.TimeoutIndex++;
        await session.RequestTimeout(context, TimeSpan.FromSeconds(seconds), new BookingTimeout());
    }
}

// Application

public class BookingService : IBookingService
{
    private readonly IMessageSession session;
    private readonly IBookingEntityFactory factory;
    private readonly IBookingRepository repository;

    public BookingService(IMessageSession session, IBookingEntityFactory factory, IBookingRepository repository)
    {
        session = session;
        factory = factory;
        repository = repository;
    }

    public async Task StartBooking(Guid bookingId, BookingSagaData data)
    {
        data.BookingId = bookingId;
        var entity = factory.CreateBookingEntity(data);
        entity.BookingLogic();
        await repository.Add(entity);
        if (entity.IsCancellable && entity.NotConfirmed) {
          await session.SetTimeout(60);
        }
        await session.PublishBookingStartedEvent(bookingId);
    }

    public async Task ConfirmBooking(Guid bookingId)
    {
        var entity = repository.Get(bookingId);
        entity.Confirm();
        await repository.SaveChanges();
    }

    public async Task CancelBooking(Guid bookingId, int timeoutIndex)
    {
        var entity = repository.GetBookingEntity(bookingId);
        entity.Cancel();
        await repository.SaveChanges();
        await session.PublishBookingCancelledEvent(bookingId);
    }
}

Hi @pinggi

Although IMessageHandlerContext might look similar to ASP.NET’s context, it actually is meant to sit in a different layer. The message handlers (classes that implement IHandleMessages) are meant to be your application services layer. There is usually not gain in abstracting the message handling context and creating another class for the application service.

Another thing that I noticed is that you are using Sagas but you don’t actually make use of Saga data much. NServiceBus Sagas can be used in two ways, either as process managers or as way of implementing your aggregates. In your booking example, you could have a BookingSaga that is itself a booking aggregate and contains all that information. Your BookingLogic() would be part of the saga data class, not a separate class.

Alternatively, if you don’t like implementing aggregates through sagas, I would recommend you to not use sagas here but just make your BookingService handle all the messages. You won’t have access to the Timeout API but you can instead send a delayed message to yourself.

Is there an advantage in using IMessageHandlerContext over registering IMessageSession impl (Endpoint instance) in a DI container?
With IMessageSession in the container I can have the application layer independent of NServiceBus.

NServiceBus Sagas can be used in two ways, either as process managers or as way of implementing your aggregates.

Thanks for mentioning it. It opens new ways of thinking about it.
In this case it seems I use the saga as a process manager.

If it was an implementation of an aggregate, the saga should really be a part of the application layer.
Also it seems to be a good idea to have the app services as actual handlers like you suggest:

// Application

public class BookingService : IHandleMessages<ConfirmBooking>
{
...

However, this would mean the application layer had to be dependent on a specific technology like NServiceBus. I consider NServiceBus more like a communication library used in a client app (a separate project creating an endpoint).

I can imagine a situation where there are more aggregates per process (a saga with one aggregate doesn’t fit here) or more clients (Asp. Net Core REST client, a console app client) using the application layer. They won’t call a logic hidden in a IHandleMessages.Handler method. At least they can’t pass IMessageHandlerContext there. It seems in this case the saga handler should call the application logic the same as the rest api client would do.

A picture how I imagine it (1) and what I understood you suggest (2 or 3). Consider the rest client, nsb client, application, domain and infrastructure to be separate projects.

1.
rest client (controllers) ->
nsb client (endpoint,sagas,handlers) ->
                                                  application -> domain
                                                              -> infrastructure
                                                                          -> db
                                                                          -> http
                                                                          -> nsb
2.
rest client (controllers) ->
nsb client (endpoint) ->
                                 application (sagas,handlers) -> domain
                                                              -> infrastructure
                                                                          -> db
                                                                          -> http
                                                                          -> nsb
3.
rest client (controllers) ->
                         nsb client (endpoint,sagas,handlers) -> domain
                                                              -> infrastructure
                                                                          -> db
                                                                          -> http
                                                                          -> nsb

you can instead send a delayed message to yourself.

Is there any difference between the delayed message and the timeout?
Can I use the delayed message as it would be a timeout or the timeout is safer or better for something.