Tuesday, 11 August 2026

Using SignalR and ASP.NET Core to Create a Real-Time Horse Racing Data Dashboard

Leave a Comment

The ability to construct things yourself is one of the benefits of contemporary technology. even when discussing intricate constructions, such as a data dashboard for horse racing. The dashboard's actual layout may appear straightforward, but processing information becomes challenging. Additionally, a lot of scraped data is used in horse racing.



Race entries, post placements, odds, jockey changes, results, news, and everything else are all under discussion. In essence, we are developing an application that requires an initial snapshot, minor real-time updates, recognition processing, and a mechanism to maintain consensus across several servers.

So, in today’s article, we will dive deeper into how to build a horse racing data dashboard with ASP.NET and SignalR. Let’s start outlining the important stuff that is crucial for this build.

Start With a Licensed Data Source

Before you start writing even a single line of code, you have to figure out your data source. A horse racing data dashboard heavily relies on source accuracy. So, you have to choose a licensed, accurate, and reliable data source for the information your application needs.

There are plenty of places to obtain racing data. Equibase is a well-known source for North American Thoroughbred racing information and provides entries, results, and racing statistics.

It also provides an API that can be used as a direct data source. However, access requires an authenticated account. Live odds and other commercial data feeds should therefore be obtained through an authorized provider rather than collected from a public webpage.

It is also useful to limit the initial scope of the dashboard. Instead of trying to include every horse race, start with one country, racing circuit, or a small number of races. Focusing on a single racetrack can make the initial implementation easier to manage.

Starting with a limited geographic or racing scope also makes it easier to organize the available data and test the application. The dashboard can initially focus on information such as entries, race schedules, results, and other race-related details. The goal should be to provide accurate and useful information while keeping the initial system manageable. Once the basic implementation works reliably, additional tracks and data sources can be added.

So, you should first find your provider, then build a code around it to hide it. Something like this:

public interface IRacingFeed
{
    IAsyncEnumerable<RaceUpdate> ReadUpdatesAsync(
        CancellationToken cancellationToken);
}

public sealed record RaceUpdate(
    string RaceId,
    int PostPosition,
    string HorseName,
    decimal? Odds,
    bool IsScratched,
    DateTimeOffset ReceivedAt);

Your development implementation can read recorded JSON messages from disk. Production can use a licensed HTTP, WebSocket, or streaming feed without changing the rest of the application.

Model Snapshots and Deltas Separately

Now let’s talk about snapshots. A newly connected user needs the entire race data. On the other hand, you cannot bombard an existing user with information they already know.

In other words, you need a system to detect what information the user saw and give updates to only what’s changed. Do not broadcast the complete field every time one horse moves from 4-1 to 7-2.

Here is how to do that:

public sealed record RunnerQuote(
    int PostPosition,
    string HorseName,
    decimal? Odds,
    bool IsScratched);

public sealed record RaceSnapshot(
    string RaceId,
    long Version,
    DateTimeOffset UpdatedAt,
    IReadOnlyList<RunnerQuote> Runners);

public sealed record RaceDelta(
    string RaceId,
    long Version,
    RunnerQuote Runner);

As you can see, the code runs on versions. It lets the browser reject an old message that arrives after the newer snapshot.

Use a Strongly Typed SignalR Hub

SignalR hubs can send messages to all clients, individual connections, or named groups. A strongly typed Hub<T> adds compile-time checking for server-to-client methods instead of relying on method names written as strings.

public interface IRaceClient
{
    Task ReceiveSnapshot(RaceSnapshot snapshot);
    Task ReceiveDelta(RaceDelta delta);
}

public sealed class RaceHub(IRaceStateStore state)
    : Hub<IRaceClient>
{
    public async Task Subscribe(string raceId)
    {
        var group = $"race:{raceId}";

        await Groups.AddToGroupAsync(
            Context.ConnectionId,
            group);

        var snapshot = await state.GetAsync(
            raceId,
            Context.ConnectionAborted);

        if (snapshot is not null)
        {
            await Clients.Caller.ReceiveSnapshot(snapshot);
        }
    }

    public Task Unsubscribe(string raceId) =>
        Groups.RemoveFromGroupAsync(
            Context.ConnectionId,
            $"race:{raceId}");
}

A SignalR group is ideal here because users watching one race do not need every update from other tracks and races.

Register the Hub in Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR();
builder.Services.AddSingleton<IRaceStateStore, RaceStateStore>();
builder.Services.AddSingleton<IRacingFeed, ReplayRacingFeed>();
builder.Services.AddHostedService<RaceFeedWorker>();

var app = builder.Build();

app.UseDefaultFiles();
app.UseStaticFiles();

app.MapHub<RaceHub>("/hubs/races");

app.Run();
Let a Background Service Process the Feed

The hub should manage subscriptions.

It should not maintain a permanent connection to the racing provider or poll an external API every time a browser opens the page.

ASP.NET Core allows services outside a hub to publish messages through an injected IHubContext. Microsoft specifically supports using it from controllers, middleware, and dependency-injected background services.

public sealed class RaceFeedWorker(
    IRacingFeed feed,
    IRaceStateStore state,
    IHubContext<RaceHub, IRaceClient> hub,
    ILogger<RaceFeedWorker> logger)
    : BackgroundService
{
    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        await foreach (var update in
            feed.ReadUpdatesAsync(stoppingToken))
        {
            try
            {
                var delta = await state.ApplyAsync(
                    update,
                    stoppingToken);

                if (delta is null)
                {
                    continue;
                }

                await hub.Clients
                    .Group($"race:{delta.RaceId}")
                    .ReceiveDelta(delta);
            }
            catch (Exception ex)
            {
                logger.LogError(
                    ex,
                    "Failed to process update for {RaceId}",
                    update.RaceId);
            }
        }
    }
}

ApplyAsync should compare the incoming value with the stored runner. When nothing changed, it returns null.

That small check prevents the dashboard from broadcasting identical odds repeatedly because the provider sends periodic refresh messages.

Real-time does not mean sending everything all the time.

It means sending the right thing quickly.

Connect the Browser and Enable Reconnection

SignalR’s JavaScript client does not automatically reconnect unless withAutomaticReconnect() is enabled.

let currentVersion = 0;

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/races")
    .withAutomaticReconnect()
    .build();

connection.on("ReceiveSnapshot", snapshot => {
    if (snapshot.version < currentVersion) return;

    currentVersion = snapshot.version;
    renderRace(snapshot);
});

connection.on("ReceiveDelta", delta => {
    if (delta.version <= currentVersion) return;

    currentVersion = delta.version;
    updateRunner(delta.runner);
});

connection.onreconnected(async () => {
    await connection.invoke("Subscribe", raceId);
});

await connection.start();
await connection.invoke("Subscribe", raceId);

After reconnecting, subscribe again and request a fresh snapshot. Do not assume the browser received every update while the connection was unavailable.

A dashboard that reconnects but preserves stale prices is technically online.

Keep Race State Outside the Hub

SignalR hub interfaces are transient. This means that you should store the current race state in a separate service rather than fields on the hub. If you’re using a single-server setup, a thread-safe in-memory store is just enough.

In other words, multiple application interfaces need access to the same latest snapshot.

ASP.NET Core exposes distributed caching through IDistributedCache, with Redis available through Microsoft.Extensions.Caching.StackExchangeRedis. A distributed cache remains consistent across multiple application servers and survives ordinary app restarts and deployments.

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration =
        builder.Configuration.GetConnectionString("Redis");

    options.InstanceName = "RaceDashboard:";
});

Persist the current RaceSnapshot under a key such as:

race:DMR:2026-08-22:8

Historical odds movements should go to a database or event store, not remain only in cache.

Final Thoughts

Don’t try to overcomplicate things from the start. A good racing dashboard has to start simple. If you start including large data streams, multiple sources, and try to cover hundreds of races, it is a recipe for disaster.

So, a good plan is to find a licensed provider for data and structure updates around a single race or racetrack, build a system that works, and try to duplicate that as you expand.

Remember, SignalR sends those changes only to clients subscribed to the relevant race, and Redis keeps multiple application interfaces consistent, which is crucial for expanding your platform.

So, the build is not as simple as copy/pasting code. It needs some personalization and fine-tuning, but since resources are available everywhere, it becomes much easier.

ASP.NET Core 10.0 Hosting Recommendation

One of the most important things when choosing a good ASP.NET Core 10.0 hosting is the feature and reliability. HostForLIFE is the leading provider of Windows hosting and affordable ASP.NET Core, their servers are optimized for PHP web applications. The performance and the uptime of the hosting service are excellent and the features of the web hosting plan are even greater than what many hosting providers ask you to pay for. 

At HostForLIFE.eu, customers can also experience fast ASP.NET Core hosting. The company invested a lot of money to ensure the best and fastest performance of the datacenters, servers, network and other facilities. Its datacenters are equipped with the top equipments like cooling system, fire detection, high speed Internet connection, and so on. That is why HostForLIFE.eu guarantees 99.9% uptime for ASP.NET Core. And the engineers do regular maintenance and monitoring works to assure its Orchard hosting are security and always up.

 

 

0 comments:

Post a Comment