Skip to content

Quick Start

trailmax edited this page Nov 17, 2016 · 3 revisions

NSaga Quick Start

Install it via NuGet:

PM> Install-Package NSaga

Basic Configuration

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.

Sql Server Storage

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 separate NSaga 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");

Define Saga

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.

Messages

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
    }
}

Injecting Dependencies

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
    }
}

Using Saga

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.