Modern technology makes it possible to build complex data-driven applications with widely available development tools and frameworks. A real-time racing data dashboard is a good example. The dashboard itself may appear relatively simple, but the complexity increases significantly when processing and distributing continuously changing data.
A racing data application may need to handle race entries, post positions, odds, jockey changes, results, news, and other information. This means the application needs an initial snapshot, incremental live updates, reliable reconnection handling, and a way to keep multiple application servers working with consistent shared state.
In this article, we will explore how to build a real-time racing data dashboard using ASP.NET Core and SignalR.
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 several sources for obtaining 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 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.
The first step is to create an abstraction around the external data provider:
public interface IRacingFeed
{
IAsyncEnumerable<RaceUpdate> ReadUpdatesAsync(
CancellationToken cancellationToken);
}
public sealed record RaceUpdate(
string RaceId,
int PostPosition,
string HorseName,
decimal? Odds,
bool IsScratched,
DateTimeOffset ReceivedAt);
A 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
A newly connected user needs the complete race data. Existing users, however, do not need the entire dataset every time a single value changes.
The application should therefore distinguish between an initial snapshot and incremental updates.
For example, if one runner's odds change, there is no need to broadcast the complete race state again. Instead, the application can send a delta containing only the changed information.
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);
The version number provides an ordering mechanism. The browser can use it to reject an older message that arrives after a newer snapshot or update.
Use a Strongly Typed SignalR Hub
SignalR hubs can send messages to all clients, individual connections, or named groups. A strongly typed Hub<T> provides compile-time checking for server-to-client methods instead of relying on string-based method names.
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}");
}
Deepak TewatiaPosted Sep 3, 2026, 5:06 AM
Nice
Praveen KumarPosted Sep 3, 2026, 5:06 AM
👏
Deepak TewatiaPosted Sep 3, 2026, 5:06 AM
😮