-
Notifications
You must be signed in to change notification settings - Fork 19
Quick Start
Install it via NuGet:
PM> Install-Package NSaga
The most basic configuration will look like this:
using NSaga;
class Program
{
static void Main(string[] args)
{
var builder = Wireup.UseInternalContainer();
var mediator = builder.ResolveMediator();
var repository = builder.ResolveRepository();
}
}
This will use internal dependency injection container to configure default implementations of all the dependencies. This uses in-memory persistence - once your application restarts, you will loose that data. So this is not recommended for production use.
To save your data in SQL Server follow this steps:
- Execute
install.sql
in your database. This will create required structures for NSaga to operate. Tables are created in separateNSaga
schema. The same SQL script is also provided with the NuGet package - it will be in.\packages\NSaga\lib\install.sql
. - Provide a connection string for configuration:
var builder = Wireup.UseInternalContainer()
.UseSqlServer()
.WithConnectionString(@"Data Source=.\SQLEXPRESS;integrated security=SSPI;Initial Catalog=NSaga;MultipleActiveResultSets=True");
or you can put your connection string in your app.config
or web.config
and reference the connection string by name:
<connectionStrings>
<add name="NSagaDatabase" connectionString="Data Source=.\SQLEXPRESS;integrated security=SSPI;Initial Catalog=NSaga;MultipleActiveResultSets=True" providerName="System.Data.SqlClient" />
</connectionStrings>
and configuration will be:
var builder = Wireup.UseInternalContainer()
.UseSqlServer()
.WithConnectionStringName("NSagaDatabase");
To use sagas you need to define the classes. A class is defined as saga when it implements ISaga<TData>
interface.
Where TData
is a generic parameter for payload class - this is your POCO class that defines the state of a saga.
using System;
using System.Collections.Generic;
using NSaga;
public class ShoppingBasketData
{
// properties that you want to preserve between saga messages
}
public class ShoppingBasketSaga : ISaga<ShoppingBasketData>
{
public Guid CorrelationId { get; set; }
public Dictionary<string, string> Headers { get; set; }
public ShoppingBasketData SagaData { get; set; }
}
All the properties above in ShoppingBasketSaga
are defined by ISaga<>
. This is the sceleton of your saga:
-
CorrelationId
is the overall identified for a saga. Any incoming message will be matched to a saga by this identifier. -
Headers
is a generic storage for metadata - do as you please with this. Will not touch on that here, but you can learn more here: Pipeline Hooks. -
ShoppingBasketData SagaData
is your saga state.ShoppingBasketData
class is defined by you - this data is preserved by the framework.
Sagas are driven by messages. Messages can either start a saga or continue the execution. Messages that start sagas are defined by IInitiatingSagaMessage
:
public class StartShopping : IInitiatingSagaMessage
{
public Guid CorrelationId { get; set; }
public Guid CustomerId { get; set; }
}
Here Guid CorrelationId
is the only property that is defined by IInitiatingSagaMessage
. Everything else is part of the message.
Now tell your saga about this message - add InitiatedBy<StartShopping>
as interface to the saga class. Add some intiating logic as well.
Perhaps as a starting point you would like to store customer id into saga data for persistence:
public class ShoppingBasketData
{
public Guid CustomerId { get; set; }
}
public class ShoppingBasketSaga : ISaga<ShoppingBasketData>,
InitiatedBy<StartShopping>
{
public Guid CorrelationId { get; set; }
public Dictionary<string, string> Headers { get; set; }
public ShoppingBasketData SagaData { get; set; }
public OperationResult Initiate(StartShopping message)
{
SagaData.CustomerId = message.CustomerId;
return new OperationResult(); // no errors to report
}
}
Once saga is started, it can accept more messages. After initial IInitiatingSagaMessage
it will receive messages marked as ISagaMessage
.
Saga can have multiple different messages as a starting point and multiple messages it receives afterwards.
To tell saga that it is capable of handling a message (after initialisation), put ConsumerOf<>
interface on saga:
public class ShoppingBasketData
{
public Guid CustomerId { get; set; }
public List<BasketProducts> BasketProducts { get; set; }
}
public class AddProductIntoBasket : ISagaMessage
{
public Guid CorrelationId { get; set; }
public int ProductId { get; set; }
public String ProductName { get; set; }
public String ItemCount { get; set; }
public decimal ItemPrice { get; set; }
}
public class ShoppingBasketSaga : ISaga<ShoppingBasketData>,
InitiatedBy<StartShopping>,
ConsumerOf<AddProductIntoBasket>
{
// SNIP - for shortness
public OperationResult Consume(AddProductIntoBasket message)
{
SagaData.BasketProducts.Add(new BasketProducts()
{
ProductId = message.ProductId,
ProductName = message.ProductName,
ItemCount = message.ItemCount,
ItemPrice = message.ItemPrice,
});
return new OperationResult(); // no possibility to fail here
}
}
Sagas can have dependencies injected through controller. If you are using basic configuration with internal container, you need to register the dependencies with the container.
For example let's say we want to remind our customers that they have items in their basket and have not checked out, possibly offer them a discount.
We need to inject ICustomerRepository
to retrieve customer email and IEmailService
to actually send the email. For that we need to register these services with internal container:
static void Main(string[] args)
{
var builder = Wireup.UseInternalContainer()
.UseSqlServer()
.WithConnectionStringName("NSagaDatabase");
builder.Register(typeof(IEmailService), typeof(ConsoleEmailService));
builder.Register(typeof(ICustomerRepository), typeof(SimpleCustomerRepository));
var mediator = builder.ResolveMediator();
var repository = builder.ResolveRepository();
}
After these services are registered, you can add them to your saga controller and add another method to consume the new message:
public class ShoppingBasketSaga : ISaga<ShoppingBasketData>,
InitiatedBy<StartShopping>,
ConsumerOf<AddProductIntoBasket>
{
public Guid CorrelationId { get; set; }
public Dictionary<string, string> Headers { get; set; }
public ShoppingBasketData SagaData { get; set; }
private readonly IEmailService emailService;
private readonly ICustomerRepository customerRepository;
public ShoppingBasketSaga(IEmailService emailService, ICustomerRepository customerRepository)
{
this.emailService = emailService;
this.customerRepository = customerRepository;
Headers = new Dictionary<string, string>();
SagaData = new ShoppingBasketData();
}
// SNIP - same as before
public OperationResult Consume(NotifyCustomerAboutBasket message)
{
if (!SagaData.BasketCheckedout)
{
var customer = customerRepository.Find(SagaData.CustomerId);
if (String.IsNullOrEmpty(customer.Email))
{
return new OperationResult("No email recorded for the customer - unable to send message");
}
try
{
var emailMessage =
$"We see your basket is not checked-out. We offer you a 15% discount if you go ahead with the checkout. Please visit https://www.example.com/ShoppingBasket/{CorrelationId}";
emailService.SendEmail(customer.Email, "Checkout not complete", emailMessage);
}
catch (Exception exception)
{
return new OperationResult($"Failed to send email: {exception}");
}
}
return new OperationResult(); // operation successful
}
}
Now our saga is ready to be used. Let's put it in action:
class Program
{
static void Main(string[] args)
{
var builder = Wireup.UseInternalContainer()
.UseSqlServer()
.WithConnectionStringName("NSagaDatabase");
// register dependencies for sagas
builder.Register(typeof(IEmailService), typeof(ConsoleEmailService));
builder.Register(typeof(ICustomerRepository), typeof(SimpleCustomerRepository));
var mediator = builder.ResolveMediator();
var correlationId = Guid.NewGuid();
// start the shopping.
mediator.Consume(new StartShopping()
{
CorrelationId = correlationId,
CustomerId = Guid.NewGuid(),
});
// add a product into the basket
mediator.Consume(new AddProductIntoBasket()
{
CorrelationId = correlationId,
ProductId = 1,
ProductName = "Magic Dust",
ItemCount = 42,
ItemPrice = 42.42M,
});
// retrieve the saga from the storage
var repository = builder.ResolveRepository();
var saga = repository.Find<ShoppingBasketSaga>(correlationId);
// you can access saga data this way
if (saga.SagaData.BasketCheckedout)
{
// and issue another message
mediator.Consume(new NotifyCustomerAboutBasket() { CorrelationId = correlationId });
}
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
}
Full code for this sample is available on NSaga.Samples repository
For more informaton on the usage/configuration please check other pages.