Automating Santa's Workshop with NServiceBus

December 19, 2024#Software Development
Series: NServiceBus
Article
Author image.

Kyle McMaster, Senior Consultant

This blog post is part of the 2024 C# Advent Calendar. Thanks to Matt Groves for organizing this awesome collection of knowledge and festivities. I’d also like to give a special shout out to the other NimblePros’ team members who contributed to this event, Sadukie and Ardalis. 🎉

Winter Workflow Disclaimer ❄️

Santa’s Workshop is a magical place where toys are made, elves are hard at work, and reindeer prepare to fly! Kids from all over the world send Santa letters asking for their most prized gifts each year. But how does Santa keep track of all the toys being made? How does he ensure that all the presents are delivered on time? It certainly is no easy task! In this blog post, we’ll explore how Santa can automate his workshop using Event-Driven Architecture (EDA) and NServiceBus. For simplicity, we’ll place some basic constraints on the system to keep the length of this post from becoming a full novel, but the reader will be able to squint and see how this mirrors a production system in many ways. We won’t worry about the differences between command and events or correctness of our bounded contexts between microservices. We just want to have fun and build a pipeline that mirrors Santa’s holiday workflow. So let’s get started! ⛷️

Sending Letters to Santa 📨

The first step in the Christmas season is for kids to send letters to Santa. Our model of these letters will contain the present they want for Christmas. We’ll simulate this process by creating a web application called NorthPole.API where kids enter their names and the toy they wish to receive. When the kids submit the request, the application will ingest the kid’s letter and forward an NServiceBus message LetterToSanta to the queue santas-inbox. This message will contain the kid’s name and the toy they requested. Here is the code for the sample ingestion endpoint:

public record CreateLetterRequest(string ChildName, string Present);

public class Create(ILogger<Create> logger) : Endpoint<CreateLetterRequest>
{
  private readonly IMessageSession _messageSession = messageSession;

  public override void Configure()
  {
    Post("/letters");
    AllowAnonymous();
    Summary(s =>
    {
      s.ExampleRequest = new CreateLetterRequest("Child's Name", "Most Coveted Present");
    });
  }
    public override async Task HandleAsync(CreateLetterRequest req, CancellationToken cancellationToken)
    {
        var message = new LetterToSanta(req.ChildName, req.Present);
        await _messageSession.Send(message, cancellationToken);
    }
}

Now that our NorthPole.API endpoint has processed the HTTP request, we’re ready for Santa to receive the message. Time to visit Santa! 🎅

Checking the Naughty or Nice List ✅

When a LetterToSanta has been received, Santa takes a big sip of hot cocoa ☕ and checks the kid’s name against his Naughty or Nice List. If the child has been good all year round, Santa will put in a note for the elves in his workshop to fulfill the wish by building the present destined to end up under the Christmas tree. If the child has been naughty, Santa will let the elves know to put a lump of coal in the outgoing bin destined for the child’s stocking. A naive implementation of the NaughtyOrNiceList might look something like the following:

public class NaughtyOrNiceList
{
  // In reality this is probably a database or some other data store
  private readonly Dictionary<string, bool> _children = new()
        {
            { "Bob", true },
            { "David", true },
            { "Elon", false },
            // { "Kyle", 🤷‍♂️ },
            { "Michelle", true },
            { "Nathan", false },
            { "Sarah", true },
        };

  public bool IsNice(string childName)
  {
    return _children.TryGetValue(childName, out var isNice) && isNice;
  }
}

Putting the pieces together, our LetterToSantaHandler checks the NaughtyOrNiceList to determine if the child is nice or naughty. It then sends a CreatePresentCommand to the santas-workshop queue with the child’s name and the present they are destined to receive.

public class LetterToSantaHandler(NaughtyOrNiceList naughtyOrNiceList) : IHandleMessages<LetterToSanta>
{
  private readonly NaughtyOrNiceList _naughtyOrNiceList = naughtyOrNiceList;

  public async Task Handle(LetterToSanta message, IMessageHandlerContext context)
  {
    bool isNice = _naughtyOrNiceList.IsNice(message.ChildName);
    var present = isNice ? message.Present : "Coal";
    await context.Send(new CreatePresentCommand(message.ChildName, present));
  }
}

So how does Santa’s Workshop process the CreatePresentCommand? How do the elves load the presents on Santa’s sleigh? Let’s journey to Santa’s workshop to find out!

Making Presents at the Workshop 🔨

Just as quickly as a present is created, it seems like a dozen other requests are added to the backlog for elves this time of year. The holiday season is a busy time of year for elves and they’re hard at work in Santa’s workshop making presents or collecting coal. When a CreatePresentCommand is received, the elves will start making the present or collecting coal. The elves process for creating presents is as follows:

public class CreatePresentCommandHandler(IElfAssigner elfAssigner, IRepository<Present> repository) 
  : IHandleMessages<CreatePresentCommand>
{
  private readonly IElfAssigner _elfAssigner = elfAssigner;
  private IRepository<Present> _repository = repository;

  public async Task Handle(CreatePresentCommand message, IMessageHandlerContext context)
  {
    var elf = await _elfAssigner.GetNextAvailable();
    var present = elf.CreatePresent(message.ChildName, message.Present);
    await _repository.AddAsync(present, context.CancellationToken);
    await context.Send(new AddPresentToSleighCommand(present.Id));
  }
}

The ElfAssigner is responsible for assigning an elf to create the present. It searches the available elves for the elf with the least amount of workload.

public interface IElfAssigner
{
    Task<Elf> GetNextAvailable();
}

public class ElfAssigner(IRepository<Elf> repository) : IElfAssigner
{
  private readonly IRepository<Elf> _repository = repository;

  public async Task<Elf> GetNextAvailable()
  {
    var elf = await _repository.SingleOrDefaultAsync(new ElfByLeastWorkloadSpec());
    return elf;
  }
}

public class ElfByLeastWorkloadSpec : Specification<Elf>, ISingleResultSpecification<Elf>
{
  public ElfByLeastWorkloadSpec()
  {
    Query.OrderBy(elf => elf.CurrentWorkload);
  }
}

Once the elf is assigned, the present is created and added to the repository in the CreatePresentCommandHandler.

public class Elf : EntityBase<Guid>,
{
    // properties and methods omitted for brevity

    public Present CreatePresent(string childName, string present)
    {
        CurrentWorkload++;
        return new Present(childName, present, PresentStatus.Created, default);
    }
}

Finally, the elves will send an AddPresentToSleighCommand to the santas-sleigh queue for the present to be loaded onto Santa’s sleigh.

Packing Santa’s Sleigh 🛷

On Christmas Eve, Santa and all his little helpers load the sleigh with all the gifts that have been made. In our system, every child will be receiving a present, so the santas-sleigh endpoint will receive a message per child and need to determine their Chimney destination too. This process starts by looking up the present information, assigning the respective Chimney identifier as the destination, and adding it to the list of presents on Santa’s Sleigh.

public class AddPresentToSleighCommandHandler(
    ILogger<AddPresentToSleighCommand> logger,
    IRepository<Chimney> chimneyRepository,
    IRepository<Sleigh> sleighRepository,
    IRepository<Present> presentRepository) : IHandleMessages<AddPresentToSleighCommand>
{
  private readonly ILogger<AddPresentToSleighCommand> _logger = logger;
  private readonly IRepository<Chimney> _chimneyRepository = chimneyRepository;
  private readonly IRepository<Sleigh> _sleighRepository = sleighRepository;
  private readonly IRepository<Present> _presentRepository = presentRepository;

  public async Task Handle(AddPresentToSleighCommand message, IMessageHandlerContext context)
  {
    var present = await _presentRepository.GetByIdAsync(message.PresentId, context.CancellationToken);
    var chimney = await _chimneyRepository.SingleOrDefaultAsync(new ChimneyByChildNameSpec(present.ChildName), context.CancellationToken);
    var sleigh = await _sleighRepository.SingleOrDefaultAsync(new SantasSleighSpecification(), context.CancellationToken);
    present.SetDestination(chimney.Id);
    sleigh.AddPresent(present);
    await context.Publish(new SleighContainsNewPresentEvent(sleigh.Id));
  }
}

Every present has a destination down a chimney and under the tree or in a stocking so setting the destination is as simple as finding the child’s chimney and setting the present’s destination to the chimney’s identifier.

public class Present : EntityBase<Guid>
{
  public string ChildName { get; init; }
  public string Type { get; init; }
  public PresentStatus Status { get; private set; }
  public Guid? ChimneyId { get; private set; }

  public void SetDestination(Guid chimneyId)
  {
    ChimneyId = chimneyId;
  }

  public void PlaceOnSleigh()
  {
    Status = PresentStatus.OnSleigh;
  }
}

The Sleigh entity keeps a simple list of presents that have been loaded onto the sleigh so adding another is like adding an item to a list. We also mark the present as being on the sleigh so we can track its status.

public class Sleigh : EntityBase<Guid>
{
  private List<Present> _presents { get; init; }
  public IReadOnlyCollection<Present> Presents => _presents.AsReadOnly();  

  // other properties and methods omitted for brevity

  public void AddPresent(Present present)
  {
    _presents.Add(present);
    present.PlaceOnSleigh();
  }
}

Once the present has been added to the sleigh, the SleighContainsNewPresentEvent is published to the santas-sleigh queue to notify the elves that the sleigh has been loaded with a new present. As soon as the first present is added to the sleigh, an NServiceBus Saga begins that times out on Christmas Eve alerting the sleigh that it is time for takeoff. This year is particularly special because Rudolf has been given the honor of leading the sleigh. 🦌

public class SleighTakeoffSaga : Saga<SleighTakeoffSagaData>,
    IAmStartedByMessages<SleighContainsNewPresentEvent>,
    IHandleTimeouts<ChristmasEveTimeout>
{
  protected override void ConfigureHowToFindSaga(SagaPropertyMapper<SleighTakeoffSagaData> mapper)
  {
    mapper.ConfigureMapping<SleighContainsNewPresentEvent>(message => message.SleighId)
      .ToSaga(sagaData => sagaData.SleighId);
  }

  public async Task Handle(SleighContainsNewPresentEvent message, IMessageHandlerContext context)
  {
    int year = DateTime.UtcNow.Year;
    await RequestTimeout(context, DateTimeOffset.Parse($"12/24/{year}"), new ChristmasEveTimeout());
  }

  public async Task Timeout(ChristmasEveTimeout state, IMessageHandlerContext context)
  {
    await context.Send(new LetsGoRudolfCommand(Data.SleighId));
    MarkAsComplete();
  }
}

public class SleighTakeoffSagaData : ContainSagaData
{
  public Guid SleighId { get; init; }
}

public record ChristmasEveTimeout();

Preparing Santa’s Sleigh for Takeoff 🦌🛷

Christmas Eve is here! It’s particularly snowy this year so Rudolf has been given the honor of leading Santa’s sleigh on this special night. The LetsGoRudolfCommandHandler notifies the sleigh that it is time for takeoff. Once the sleigh has taken off, an event is published notifying Chimneys all around the word to prepare for receiving presents. 🌍

public class LetsGoRudolfCommandHandler(IRepository<Sleigh> repository) : IHandleMessages<LetsGoRudolfCommand>
{
  private readonly IRepository<Sleigh> _repository = repository;

  public async Task Handle(LetsGoRudolfCommand message, IMessageHandlerContext context)
  {
    var sleigh = await _repository.SingleOrDefaultAsync(new SantasSleighSpecification(), context.CancellationToken);
    sleigh.Takeoff();
    await context.Publish(new SleighHasTakenOffEvent());
  }
}

Delivering the Toys Down the Chimney 🏚️🎁

Once Santa’s sleigh is in that cold frigid air, Santa and his reindeer fly around the world delivering gifts, landing on rooftops, and sliding down the chimneys to deliver the toys or coal to kids all around the world. In the SleighHasTakenOffEventHandler, this process first looks up Santa’s sleigh with all the presents. It then loops through each present, delivering it to the child’s chimney where the present 🎁 will end up under the Christmas tree 🎄 or coal will end up in a stocking 🧦.

public class SleighHasTakenOffEventHandler(IRepository<Sleigh> sleighRepository, IRepository<Chimney> chimneyRepository)
   : IHandleMessages<SleighHasTakenOffEvent>
{
  private IRepository<Sleigh> _sleighRepository = sleighRepository;
  private IRepository<Chimney> _chimneyRepository = chimneyRepository;

  public async Task Handle(SleighHasTakenOffEvent message, IMessageHandlerContext context)
  {
    var sleigh = await _sleighRepository.SingleOrDefaultAsync(new SantasSleighSpecification(), context.CancellationToken);

    foreach (var present in sleigh.Presents)
    {
      var chimney = await _chimneyRepository.SingleOrDefaultAsync(new ChimneyByChildNameSpec(present.ChildName), context.CancellationToken);
      sleigh.DeliverPresent(present, chimney);
    }
  }
}

Now that each present has been delivered (and Santa has had his fill of cookies 🍪) Santa and his reindeer fly back to the North Pole to rest and prepare for next year. By this point, Santa is looking forward to sitting down by the fireplace and enjoying a cup of hot cocoa with Mrs. Claus. 🎅🤶💝

Recap 🌙

The holiday season is a time filled with joy and cheer. After a busy season of reading letters, checking the Naughty or Nice List, making presents, loading the sleigh, and delivering presents, Santa, and his reindeer are ready to rest. The elves have worked hard to make sure every child receives a present or coal. On Christmas Eve night, each child is fast asleep dreaming of the presents they will receive in the morning, thanks to Santa’s powerfully automated workshop of wonder. 🎁

If you would like to try running this code yourself, you can find the full source code on GitHub at the NorthPoleMessagingAdventures repository. I hope you enjoyed this whimsical journey through Santa’s Workshop and learned a little bit about how to automate Santa’s Workshop with NServiceBus. Happy holidays to everyone and to all a good night! 🎄🌟😁

Resources 📚


Copyright © 2025 NimblePros - All Rights Reserved