Skip to content
Building a Real-Time Santa's Workshop Tracker with SignalR and .NET 9

Building a Real-Time Santa's Workshop Tracker with SignalR and .NET 9

Updated:ย atย 04:23 PM

This blog post is part of the 2024 C# Advent Calendar. Go check out the other 49 great posts after you read mine, of course.

๐ŸŽ„๐ŸŽ… Ho Ho Ho! Welcome to Santa's Coding Workshop! ๐Ÿงโ€โ™‚๏ธ๐Ÿ’ป

'Tis the season to be jolly... and to code! Grab your candy cane stylus and put on your coding mittens because you're about to embark on a magical journey to the North Pole's most high-tech workshop!

In this merry tutorial, you'll craft a real-time web application that brings Santa's workshop to life, powered by the enchanting duo of SignalR and .NET 9. Imagine a digital snow globe where you can peek inside and see:

All updating in real-time, faster than you can say "Jingle Bells"!Our festive application showcases modern web development practices, all wrapped up in a bow of Christmas cheer. Through a clean, simple interface that would make even Frosty the Snowman proud, users can:

And the best part? No need to refresh the page - it's all as smooth as a sleigh ride!

As you journey through the code, you'll see how to harness the power of WebSocket connections, sprinkled in some C# 13 magic dust, and leveraged SignalR's real-time capabilities to create an engaging and interactive workshop experience.

So, hang your stockings by the chimney with care, and dive into the code to see how the magic happens! Remember, in Santa's workshop, every bug is just a feature in disguise, and every successful compile is like finding a present under the tree.

Ready to code up some Christmas cheer? Jingle all the way to a winter wonderland of web development! ๐ŸŽ„โœจ

๐ŸŽ„โœจ Getting the Workshop Ready! ๐ŸŽ…๐Ÿ› ๏ธ

Kick off the festive coding adventure by setting up Santa's workshop project! First, you must create a shiny new ASP.NET Core Web Application using the magic of .NET 9. Grab your coding sleigh and run this command:

dotnet new web -n SantasWorkshopTracker -f net9.0

Dash into the project directory, where all the holiday magic will unfold. Once you're there, itโ€™s time to add the SignalR client library to bring the elves to life:

cd SantasWorkshopTracker

dotnet add package Microsoft.AspNetCore.SignalR.Client

With these steps, you're laying the foundation for a good time in Santaโ€™s Workshop! Get ready to watch the magic unfold in an interactive experience thatโ€™ll make even the North Pole proud! ๐ŸŽ๐Ÿ’ปโœจ

๐ŸŽ„๐Ÿ”” SantasWorkshopHub.cs: Santa's Communication Network ๐Ÿ””๐ŸŽ„

Create a new file, SantasWorkshopHub.cs, with the following content:

using Microsoft.AspNetCore.SignalR;

using System.Diagnostics;

namespace SantasWorkshopTracker;

public record ToyUpdate(int Count);

public record ElfActivity(string Name, string Activity);

public record SantaStatus(string Status);

public class SantasWorkshopHub : Hub

{

    private static readonly ActivitySource ActivitySource = new("SantasWorkshop.SignalR");



    private int _totalToys;

    public async Task UpdateToyProduction(ToyUpdate update)

    {

       using var activity = ActivitySource.StartActivity();

       activity?.SetTag("ToyCount", update.Count);



       var localTotal = _totalToys;

       await Task.Delay(100); // Simulating some async work

       localTotal += update.Count;

       _totalToys = localTotal;



       await Clients.All.SendAsync("ReceiveToyUpdate", new ToyUpdate(_totalToys));

    }

    public Task UpdateElfActivity(ElfActivity elfActivity)

    {

       using var activity = ActivitySource.StartActivity();

       activity?.SetTag("ElfName", elfActivity.Name);

       activity?.SetTag("Activity", elfActivity.Activity);

       return Clients.All.SendAsync("ReceiveElfUpdate", elfActivity);

    }

    public Task UpdateSantaStatus(SantaStatus status)

    {

       using var activity = ActivitySource.StartActivity();

       activity?.SetTag("Status", status.Status);

       return Clients.All.SendAsync("ReceiveSantaUpdate", status);

    }

}

๐ŸŒŸ Unwrapping the Festive Features ๐ŸŒŸ

Record-Breaking Christmas Records:

Our ToyUpdate, ElfActivity, and SantaStatus are wrapped up neatly as records. They're like perfectly packed presents, immutable and ready to spread joy!

Santa's Magic Telescope (ActivitySource):

private static readonly ActivitySource ActivitySource = new("SantasWorkshop.SignalR");

It isn't just any telescope; it's Santa's special way of watching all the workshop activities! ๐Ÿ”ญ

Tagging Toys and Elves:

Each method uses ActivitySource to create activities and set tags. It's like Santa putting name tags on all the presents and elves!

activity?.SetTag("ToyCount", update.Count);

activity?.SetTag("ElfName", elfActivity.Name);

Async Christmas Magic:

Our UpdateToyProduction method works asynchronously, just like elves working through the night to prepare all the toys!

SignalR's Christmas Broadcast:

Clients.All.SendAsync is like Santa's magical announcement system, keeping everyone in the North Pole updated!

๐ŸŽŠ Why This Code Sleighs ๐ŸŽŠ

This SantasWorkshopHub isn't just a piece of code; it's the heart of the virtual North Pole, pumping Christmas cheer and toy updates to all!๐ŸŽ„๐ŸŽ…๐Ÿป๐Ÿงโ€โ™€๏ธ

๐Ÿงโ€โ™‚๏ธ Elf Code Workshop: The Magical WorkshopSimulator ๐Ÿงโ€โ™€๏ธ

Create a new file, WorkshopSimulator.cs:

using Microsoft.AspNetCore.SignalR;

namespace SantasWorkshopTracker;

public partial class WorkshopSimulator

{

    private int _toyCount;

    public partial int ToyCount { get; }

}

public partial class WorkshopSimulator(IHubContext<SantasWorkshopHub> hubContext) : BackgroundService

{

    private readonly Random _random = new();

    private const int ToyGoal = 100;

    private readonly Lock _wrappingStationLock = new();

    private int _availableWrappingStations = 3;



    public partial int ToyCount => _toyCount;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)

    {

        string[] elfNames = ["Buddy", "Jingle", "Sparkle", "Twinkle", "Holly"];

        string[] elfActivities = ["making toys", "feeding reindeer", "baking cookies", "checking the list"];

        while (!stoppingToken.IsCancellationRequested && _toyCount < ToyGoal)

        {

            var tasks = new List<Task>();

            for (var i = 0; i < 5; i++) // Process 5 elves concurrently

            {

                var elfName = elfNames[_random.Next(elfNames.Length)];

                var activity = elfActivities[_random.Next(elfActivities.Length)];

                tasks.Add(ProcessElfActivity(elfName, activity, stoppingToken));

            }

            await Task.WhenAll(tasks);

            await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);

        }

        if (_toyCount >= ToyGoal)

        {

            await hubContext.Clients.All.SendAsync("ReceiveSantaStatus", new SantaStatus("Ho Ho Ho! All toys are ready! Santa has departed! Merry Christmas!"), stoppingToken);

        }

    }

    private async Task ProcessElfActivity(string elfName, string activity, CancellationToken stoppingToken)

    {

        if (activity == "making toys")

        {

            var toysProduced = _random.Next(1, 15);

            Interlocked.Add(ref _toyCount, toysProduced);

            await hubContext.Clients.All.SendAsync("ReceiveToyUpdate", new ToyUpdate(_toyCount), stoppingToken);

            await WrapToy(elfName, stoppingToken);

        }

        else

        {

            await hubContext.Clients.All.SendAsync("ReceiveElfUpdate", new ElfActivity(elfName, activity), stoppingToken);

        }

    }

    private async Task WrapToy(string elfName, CancellationToken stoppingToken)

    {

        var wrapped = false;

        while (!wrapped && !stoppingToken.IsCancellationRequested)

        {

            using (var lockScope = _wrappingStationLock.EnterScope())

            {

                if (_availableWrappingStations > 0)

                {

                    _availableWrappingStations--;

                    wrapped = true;

                }

            }

            if (wrapped)

            {

                await hubContext.Clients.All.SendAsync("ReceiveElfUpdate", new ElfActivity(elfName, "wrapping a gift"), stoppingToken);

                await Task.Delay(3000, stoppingToken); // Simulating wrapping time

                using var lockScope = _wrappingStationLock.EnterScope();

                _availableWrappingStations++;

            }

            else

            {

                await hubContext.Clients.All.SendAsync("ReceiveElfUpdate", new ElfActivity(elfName, "waiting for a wrapping station"), stoppingToken);

                await Task.Delay(1000, stoppingToken); // Wait before trying again

            }

        }

    }

}

๐ŸŽ Unwrapping the Festive Features ๐ŸŽ

  1. Magical Lock Spell: The _wrappingStationLock is like Santa's magic key, ensuring orderly access to gift-wrapping stations!

  2. Partial Properties - A Christmas Miracle: Youโ€™ve split the ToyCount property like sharing a Christmas cookie. Half the declaration, twice the joy!

  3. Collection Expressions - A Sack Full of Goodies: The elf names and activities are now as neatly packed as Santa's gift bag, thanks to the new collection expression syntax!

  4. Concurrent Elf Magic: Youโ€™re processing five elves at once, spreading holiday cheer faster than Rudolph on Christmas Eve!

  5. Festive Randomness: Just like you never know what's in a Christmas cracker, the _random adds a sprinkle of surprise to elf activities!

This code isn't just functional; it's a winter wonderland of C# 13 features! It's as exciting as finding the biggest present under the tree on Christmas morning! ๐ŸŽ„๐ŸŽ

๐ŸŽ…๐ŸŽ„๐Ÿ—๏ธ Program.cs: The North Pole's Command Center

Update your Program.cs file to add SignalR services and map the hub:

using SantasWorkshopTracker;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR();

builder.Services.AddHostedService<WorkshopSimulator>();

var app = builder.Build();

app.MapHub<SantasWorkshopHub>("/santasworkshop");

app.UseStaticFiles();

app.MapFallbackToFile("index.html");

app.Run();

This jolly Program.cs sets up the North Pole operations center, connecting Santa's workshop to the digital world faster than you can say "Ho Ho Ho!" ๐ŸŽ…๐Ÿป๐ŸŒŸ

๐ŸŽ„ Crafting Santa's Digital Dashboard ๐Ÿ–ฅ๏ธ

Create a festive index.html in the wwwroot folder:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Santa's Workshop Live Tracker</title>
    <style>
        body { font-family: Arial, sans-serif; background-color: #f0f0f0; }
        .container { max-width: 800px; margin: 0 auto; padding: 20px; background-color: #fff; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
        h1 { color: #c41e3a; font-size: 24px; text-align: center; }
        .update-section { margin-bottom: 20px; }
    </style>
</head>
<body>
<div class="container">
    <h1>Santa's Workshop Live Tracker</h1>
    <div class="update-section">
        <h2>Toy Production</h2>
        <p id="toyCount">Toys made: 0</p>
    </div>
    <div class="update-section">
        <h2>Elf Activities</h2>
        <ul id="elfActivities"></ul>
    </div>
    <div class="update-section">
        <h2>Santa's Status</h2>
        <p id="santaStatus">Preparing for Christmas Eve</p>
    </div>
    <div class="update-section">
        <h2>Workshop Updates</h2>
        <ul id="workshopUpdates"></ul>
    </div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.min.js"></script>
<script>
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/santasworkshop")
        .configureLogging(signalR.LogLevel.Information)
        .build();

    connection.on("ReceiveToyUpdate", (update) => {
        document.getElementById("toyCount").textContent = `Toys made: ${update.count}`;
    });

    connection.on("ReceiveElfUpdate", (activity) => {
        const li = document.createElement("li");
        li.textContent = `${activity.name} is ${activity.activity}`;
        document.getElementById("elfActivities").appendChild(li);
    });

    connection.on("ReceiveSantaStatus", (status) => {
        const santaStatus = document.getElementById("santaStatus");
        santaStatus.textContent = status.status;
        if (status.status.includes("departed")) {
            santaStatus.style.color = "#c41e3a";
            santaStatus.style.fontWeight = "bold";
            connection.stop();
        }
    });

    connection.on("ReceiveMessage", (message) => {
        const li = document.createElement("li");
        li.textContent = message;
        document.getElementById("workshopUpdates").appendChild(li);
    });

    connection.start()
        .then(() => {
            console.log("Connected to Santa's Workshop!");
            document.getElementById("connectionStatus").textContent = "Connected";
        })
        .catch((err) => {
            console.error(`Connection error: ${err.toString()}`);
            document.getElementById("connectionStatus").textContent = "Connection failed";
            alert("Failed to connect to Santa's Workshop. Please try refreshing the page.");
        });

    connection.onclose((error) => {
        console.error(`Connection closed: ${error ? error.message : "Unknown error"}`);
        document.getElementById("connectionStatus").textContent = "Disconnected";
        alert("Connection to Santa's Workshop was lost. Please refresh the page to reconnect.");
    });

</script>
</body>
</html>

This magical interface brings Santa's workshop to life, updating faster than Rudolph's nose blinks! ๐ŸŽ…โœจ

๐ŸŽ… Launching Santa's Workshop! ๐ŸŽ„

Fire up the magic with:

dotnet run

Then, dash over to http://localhost:5000 in your browser to watch Santa's Workshop Live Tracker sparkle into action! โœจ๐ŸŽ

You should see something that looks an awful lot like this!

๐ŸŽ Wrapping Up the Workshop

๐ŸŽ„๐ŸŽ… Ho ho ho! Time to unwrap the festive .NET 9 features for each class.

๐ŸŽ…๐ŸŽ„๐Ÿ—๏ธ Program.cs: The North Pole's Command Center

๐Ÿงโ€โ™‚๏ธ Elf Code Workshop: The Magical WorkshopSimulator ๐Ÿงโ€โ™€๏ธ

๐Ÿ”” SantasWorkshopHub.cs: Santa's Communication Network

๐ŸŽ General .NET 9 Goodies:

These magical .NET 9 features will make this the most productive Christmas season yet for developers worldwide! ๐ŸŒŸ๐ŸŽ„

๐Ÿ”ง Troubleshooting

Here are some common issues developers might encounter and how to resolve them:

  1. SignalR Connection IssuesProblem: SignalR fails to establish a connection.
    Solution: Ensure your firewall isn't blocking WebSocket connections. Check the console for any CORS-related errors and update your CORS policy if necessary.

     // In Program.cs
     builder.Services.AddCors(options =>
     {
         options.AddPolicy("CorsPolicy", builder => builder
             .WithOrigins("http://localhost:5000")
             .AllowAnyMethod()
             .AllowAnyHeader()
             .AllowCredentials());
     });
    
  2. Performance Bottlenecks in Toy ProductionProblem: Toy production simulation runs slowly.
    Solution: Utilize parallel processing for elf activities and consider using System.Threading.Channels for efficient producer-consumer scenarios.

     // In WorkshopSimulator.cs
     private Channel<ElfActivity> _elfActivityChannel = Channel.CreateUnbounded<ElfActivity>();
    
     // Producer
     await _elfActivityChannel.Writer.WriteAsync(new ElfActivity(elfName, activity));
    
     // Consumer
     while (await _elfActivityChannel.Reader.WaitToReadAsync())
     {
         if (_elfActivityChannel.Reader.TryRead(out var elfActivity))
         {
             await ProcessElfActivity(elfActivity);
         }
     }
    
  3. Memory Leaks in Long-Running SimulationsProblem: Application memory usage grows over time.
    Solution: Implement proper disposal of resources and use weak references for long-lived objects.

     // In WorkshopSimulator.cs
     private WeakReference<List<ElfActivity>> _recentActivities = new WeakReference<List<ElfActivity>>(new List<ElfActivity>());
    
     // Usage
     if (_recentActivities.TryGetTarget(out var activities))
     {
         activities.Add(new ElfActivity(elfName, activity));
     }
    

Important Tip*: Always monitor your application's performance using tools like dotnet-counters or Application Insights to catch and address issues early.*

Warning*: Be cautious when implementing parallel processing in SignalR hubs. Ensure thread-safety and consider using* SemaphoreSlim for controlled concurrency.