NServiceBus for dummies who want to be smarties 5… fifth post, this time with an example on how sagas can be used to implement a workflow in ASP.NET MVC.
One of the typical workflows in web applications is when someone signs up for an account, but the web site wants to check if the email address is valid. Let’s look at an example that goes like this:
- User enters email address as a signup request
- Web application sends email with a “secret” confirmation link
- User visits the secret link, thereby activating the account
To keep things simple, I have made a simple page containing two forms: one for the email address, and one that simulates visiting a secret link by letting us enter a “ticket”. The view looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
<p> Enter your email address to begin registration </p> <form method="post" action="!{Url.Action("BeginRegistration")}"> Email: <input name="email" type="text" /> <input type="submit" value="Begin registration" /> </form> <p> - or enter your ticket to confirm your email </p> <form method="post" action="!{Url.Action("ConfirmRegistration")}"> Ticket: <input name="ticket" type="text" /> <input type="submit" value="Confirm registration" /> </form> |
And then I have made this simple controller to handle the posts from the registration page:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public class RegistrationController : TxBaseController { readonly IBus bus; public RegistrationController(IBus bus) { this.bus = bus; } public ViewResult Index() { return View(); } public RedirectToRouteResult BeginRegistration(string email) { bus.Send(new RequestRegistration {Email = email}); return RedirectToAction("Index"); } public RedirectToRouteResult ConfirmRegistration(int ticket) { bus.Send(new ConfirmRegistration {Ticket = ticket}); return RedirectToAction("Index"); } } |
Now, to model this workflow we need some kind of persistence on our backend, which is where sagas come into the picture. Sagas is the built-in mechanism in NServiceBus that helps in building stateful services. Stateful services are, as the word implies, services that preserve some kind of state between receiving messages.
The NServiceBus saga is a nifty way to declare what that state should contain (by letting a class implement
ISagaEntity) and – given a message that the saga can handle – how to retrieve that saga (by overriding
ConfigureHowToFindSaga and setting up which properties to compare).
Lets start out by specifying that NServiceBus should take care of persisting the saga entity (which will by done through Fluent NHibernate/NHibernate/SQLite under the hood)… that can easily be achieved by changing our backend’s endpoint configuration to this:
|
Configure.With() .CastleWindsorBuilder(container) .Sagas() .NHibernateSagaPersisterWithSQLiteAndAutomaticSchemaGeneration() .XmlSerializer(); |
This will make NServiceBus persist ongoing sagas in an SQLite db file called “NServiceBus.Sagas.sqlite” inside the backend’s execution directory. If you omit the
NHibernateSagaPersisterWithSQLiteAndAutomaticSchemaGeneration() thing, NServiceBus will store ongoing sagas by using its
InMemorySagaPersister, which – surprise! – stores sagas in memory.
I had some problems with the in-memory persister however, as I could not make it correlate messages with my saga unless I correlated with interned strings only, by implementing getters on my messages like so:
|
public string Email { get { return string.Intern(email); } set { email = value; } } |
I imagine this has to do with a deserializer somewhere, somehow generating strings that are not interned, although I have not verified this – I am only guessing! No biggie though, as long as the SQLite saga persister is so easy to use.
In my example the saga is initiated by the
RequestRegistration message which contains only an email. When the saga receives that message, the email is stored, and a secret ticket is generated, emailed to the user, and stored in the saga data. The saga is completed when it receives a
ConfirmRegistration message with the right ticket.
Let’s specify what will constitute the saga data:
|
public class UserRegistrationSagaData : ISagaEntity { public virtual Guid Id { get; set; } public virtual string Originator { get; set; } public virtual string OriginalMessageId { get; set; } public virtual string Email { get; set; } public virtual int Ticket { get; set; } } |
The first three properties come from the
ISagaEntity interface – and then come my two properties for storing the email and the ticket.
Please do remember to mark all the properties of your saga data as
virtual! Otherwise, Fluent NHibernate will throw some stupid exception, 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 being able to create proxies if there were un-interceptable properties on the data class – the error message is just weird…
To create a saga, you let a class inherit from
Saga<TSagaEntity> where
TSagaEntity would be
UserRegistrationSagaData in my example… and then we implement
ISagaStartedBy<TMessage> and
IMessageHandler<TMessage> where the two
TMessage type parameters should be filled out with
RequestRegistration and
ConfirmRegistration respectively. Like so:
|
public class UserRegistrationSaga : Saga<UserRegistrationSagaData>, ISagaStartedBy<RequestRegistration>, IMessageHandler<ConfirmRegistration> { //... } |
– and then – given the two kind of messages my saga can handle – how to correlate the messages with my saga:
|
public override void ConfigureHowToFindSaga() { ConfigureMapping<RequestRegistration>(saga => saga.Email, message => message.Email); ConfigureMapping<ConfirmRegistration>(saga => saga.Ticket, message => message.Ticket); } |
Even though my saga is initiated by
RequestRegistration, I set up a mapping that ensures that a new saga will not be created if someone requests registration twice with the same email.
Lastly, the actual logic carried out by the saga – the two message handlers (note that
MailSender and
NewUserService are application services that are automagically injected becuase they’re public properties of the saga):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public void Handle(RequestRegistration message) { // generate new ticket if it has not been generated if (Data.Ticket == 0) { Data.Ticket = NewUserService.CreateTicket(); } Data.Email = message.Email; MailSender.Send(message.Email, "Your registration request", "Please go to /registration/confirm and enter the following ticket: " + Data.Ticket); Console.WriteLine("New registration request for email {0} - ticket is {1}", Data.Email, Data.Ticket); } public void Handle(ConfirmRegistration message) { Console.WriteLine("Confirming email {0}", Data.Email); NewUserService.CreateNewUserAccount(Data.Email); MailSender.Send(Data.Email, "Your registration request", "Your email has been confirmed, and your user account has been created"); // tell NServiceBus that this saga can be cleaned up afterwards MarkAsComplete(); } |
Note how I mark the saga as complete by calling
MarkAsComplete(), thus allowing the saga data to be deleted. This could also be a nifty place to save the time of when the last registration email was sent to a particular address in order to disallow sending another registration email for the next hour or so, to avoid people spamming each other by using our web site.
Now, if I fire up my backend and request registration with the email [email protected], it looks like this:
Then, if I request registration with [email protected] followed by [email protected], I can verify that a new saga is only created for [email protected]:
And then, when I confirm a registration request, it looks like this:
Isn’t that great? I cannot imagine a framework with an API more elegant and terse than this.
Conclusion
That was the fifth post in my “NServiceBus for dummies who want to be smarties” series. Sixth and last post will be about our backend publishing messages whenever interesting stuff happens. This will provide a message based interface for interested parties to subscribe to.