Archives

  • Categories

  • Getting Started With NServiceBus: Part 3 Sagas

    Here are the links to the other posts in the series.

    Part 1 Unicast Messaging.
    Part 2 Publishing and Subscribing.
    Part 3 Sagas
    Part 4 Integrating With StructureMap

    Sagas are long running business processes where state needs to be shared between message handlers.

    Udi gives a much better explanation of them here and there is some more information about them on the NServiceBus Wiki here.

    Imagine the following scenario for a leave approval system.

    • An employee applies for leave.
    • The request for leave must be approved by the HR manager
    • The request for leave must be approved by the employees line Manager.
    • If both the HR manager and the line manager approve the request it can then be authorized.
    • Both the HR manager and the line manager will make their decisions independently of each other.

    The problem with this scenario is that we don’t know how long it is going to take for either the HR manager or line manager to make their decision and we don’t know in which order we are going to receive their decisions.

    NServiceBus solves these types of problems using Sagas. Lets walk through the steps we need to go through to code up this example.

    Persisting Our Saga Data

    Because a Saga is a long running transaction we will most probably want to persist some data. In our leave approval Saga example if we receive a decision from the HR Manager as to whether the request for leave has been approved or rejected for a particular employee then we will want to persist that information for later when a decision from the line manager arrives.

    Out of the box NServiceBus will allow you save the Saga data to a SQLite database using NHibernate. To define the data we wish to persist we are going to create a class called LeaveApprovalData. Because we are using NHibernate it important to make all the properties virtual if not you’ll probably get some exception like “Database was not configured through Database method.”. We also need to implement the ISagaEntity interface. You can see the the three important properties on this class are ApprovedByHR, ApprovedByLineManager and EmployeeName.

        public class LeaveApprovalData : ISagaEntity
        {
            #region Properties
    
            public virtual bool? ApprovedByHR
            {
                get; set;
            }
    
            public virtual bool? ApprovedByLineManager
            {
                get; set;
            }
    
            public virtual string EmployeeName
            {
                get; set;
            }
    
            public virtual bool HasBeenApproved
            {
                get
                {
                    var hasBeenReviewedByAll = ApprovedByHR.HasValue
                                                           && ApprovedByLineManager.HasValue;
    
                    return (hasBeenReviewedByAll
                                                         && (ApprovedByHR.Value
                                                         && ApprovedByLineManager.Value));
                }
            }
    
            public virtual bool HasBeenReviewedByAllParties
            {
                get { return ApprovedByHR.HasValue && ApprovedByLineManager.HasValue; }
            }
    
            public virtual Guid Id
            {
                get; set;
            }
    
            public virtual string OriginalMessageId
            {
                get; set;
            }
    
            public virtual string Originator
            {
                get; set;
            }
    
            #endregion Properties
        }

    Writing our Saga

    The code sample below illustrates how we could write a Saga in NServiceBus to process whether the leave request should be accepted or rejected.

        public class LeaveApprovalSaga :
                  Saga<LeaveApprovalData>,
                  ISagaStartedBy<HRLeaveApproval>,
                  ISagaStartedBy<LineManagerLeaveApproval>
    
        {
            public IBus Bus { get; set; }
    
            public override void ConfigureHowToFindSaga()
            {
                ConfigureMapping<HRLeaveApproval>(
                                     saga => saga.EmployeeName,
                                     message => message.EmployeeName);
    
                ConfigureMapping<LineManagerLeaveApproval>(
                                     saga => saga.EmployeeName,
                                     message => message.EmployeeName);
            }
    
            public void Handle(HRLeaveApproval message)
            {
                Data.EmployeeName = message.EmployeeName;
                Data.ApprovedByHR = message.Approved;
    
                Process(Data.EmployeeName);
            }
    
            private void Process(string employeeName)
            {
                if (Data.HasBeenReviewedByAllParties == false) return;
    
                if (Data.HasBeenApproved)
                {
                    LeaveApproved(employeeName);
                }
                else
                {
                    LeaveRejected(employeeName);
                }
            }
    
            public void Handle(LineManagerLeaveApproval message)
            {
                Data.EmployeeName = message.EmployeeName;
                Data.ApprovedByLineManager = message.Approved;
    
                Process(Data.EmployeeName);
            }
        }

    Lets start by looking at what the code is doing.

    The first thing to note is that we have to inherit from the generic base class Saga which is closed with our

    LeaveApprovalSaga data class.  This tells NServiceBus that we will persist an instance of the LeaveApprovalSaga for each saga.

    Starting Our Saga

    Our Saga can be started in either two ways by either a LineManagerLeaveApproval or a HRLeaveApproval message. We define this in code by implementing the following interfaces:

    ISagaStartedBy<HRLeaveApproval>, ISagaStartedBy<LineManagerLeaveApproval>

    We can receive either a LineManagerLeaveApproval or a HRLeaveApproval message in any order for a particular request which can start a new Saga.

    However we don’t always want to start a new Saga. If for example we received an HRLeaveApproval message for the employee called Tom and then an LineManagerLeaveApproval for the same employee we would not want to create a new Saga for the second message but retrieve the original one.

    So how do we tell NServiceBus to retrieve an existing Saga for a given message type? By overriding the method ConfigureHowToFindSaga and calling the method ConfigureMapping. In the example below we are stating that when we receive a HRLeaveAproval message we will look for any previously started Saga data based on our message’s  EmployeeName property. We do the same for any incoming LineManagerLeaveAproval messages to.  If either message type comes in for an employee it will start off a new saga, from then on the saga will be retrieved from the database.

            public override void ConfigureHowToFindSaga()
            {
                ConfigureMapping<HRLeaveApproval>(
                                        saga => saga.EmployeeName,
                                        message => message.EmployeeName);
    
                ConfigureMapping<LineManagerLeaveApproval>(
                                        saga => saga.EmployeeName,
                                        message => message.EmployeeName);
            }

    Handling our Messages

    Handling our messages in our Saga is very similar to implementing a normal message handler in NServiceBus when we inherit from the ISagaStartedBy<T>  interface we need to implement a Handle(T) method on our Saga.

    The main difference between a normal MessageHandler is that we want to pull out the relevant data from our incoming message and persist it in our SagaData. We can do this by accessing it through the Data property on the Saga. In the example above for the handler code for  LineManagerApproval message we firstly update our Saga with the name of the employee and then update whether or not the line manager has approved the request for leave. This is automatically persisted to our SQL Lite database.

    Configuring Your Host To Run The Saga


    1) Reference NServiceBus.dll & NServiceBus.Core.dll. I’m using StructureMap as my IOC container so I have also referenced the NServiceBus.ObjectBuilder.StructureMap.dll as well.
    2)
    In your Bootstrapper you need to configure NServiceBus.

    Configure
                    .With()
                    .StructureMapBuilder(container)
                    .XmlSerializer()
                    .MsmqTransport()
                    .IsTransactional(true)
                    .PurgeOnStartup(false)
                    .UnicastBus()
                    .LoadMessageHandlers()
                    .ImpersonateSender(false)
                    .Sagas()
                    .NHibernateSagaPersisterWithSQLiteAndAutomaticSchemaGeneration()
                    .CreateBus()
                    .Start();
    

    3) In the Application Config add the following

    
    

    4) Now define the Host’s Input Queue and the Error Queue – where undelivered messages are put if there is an exception delivering the message in the config file

    
    

    That’s it you’ve implemented your first Saga in NServiceBus!

    kick it on DotNetKicks.com

    7 Responses to “Getting Started With NServiceBus: Part 3 Sagas”


    1. [...] Part 1 Unicast Messaging. Part 2 Publishing and Subscribing. Part 3 Sagas [...]

    2. Udi Dahan says:

      This is a really great example, you should put it up under the links section of the saga page of the wiki.

    3. Stuart C says:

      Thanks Udi I’ll do that.

    4. [...] Part 1 Unicast Messaging. Part 2 Publishing and Subscribing. Part 3 Sagas [...]

    5. [...] This post was mentioned on Twitter by UdiDahan, Mark Harris. Mark Harris said: RT @UdiDahan: Nice blog post on using NServiceBus sagas to model HR leave of absence process: http://blogs.planbsoftware.co.nz/?p=247 [...]

    6. [...] saying that “Database was not configured through Database method” (wtf?) (thanks to this post for sorting that one out!). The exception is fair though, as NHibernate would complain about not [...]

    7. Hello There. I discovered your blog the use of msn. This is a very well written article. I’ll be sure to bookmark it and come back to read more of your useful info. Thanks for the post. I’ll certainly comeback.

    Leave a Reply