# Use AR51.Net in Your App — Hosting, UI Threads and Data Flow

The samples are console apps. Real apps — dashboards, back ends, desktop tools — need a few more patterns: one long-lived client, knowing what arrives by itself and what to ask for, data handed off from the receiving thread, and UI updates on the right thread.

## What's pushed, what to poll

Some data arrives by itself; some you ask for. A dashboard needs both.

<div className="apiTable">

| Arrives by itself (events or live lists) | Ask for it (call when you need it, e.g. every 2–5 s for a status view) |
|---|---|
| Skeletons and objects (`SkeletonReceived`, `ObjectReceived`) | Server program and tracking status (`GetStatusAsync`) |
| Connection state (`ConnectionStateChanged`) | Cameras and their connection state (`GetCamerasAsync`) |
| Server machines appearing and disappearing (`ServersChanged`) | Camera image settings (`GetSettingsAsync`) |
| Devices connecting and disconnecting (`DevicesChanged`; battery levels refresh about every 10 s) | The tracked area (`GetCaptureBoundsAsync`) |
| Recording state (`RecordingStateChanged`, checked about once a second) | |
| Calibration and auto-tune progress (their `…Started`, `…ProgressChanged` and `…Ended` events) | |

</div>

:::tip[A missing camera isn't necessarily gone]
`GetCamerasAsync` leaves out cameras whose camera server didn't answer in time. In a status view, keep the last known entry and show it as **unreachable** rather than removing it.
:::

## One client for the whole app

Create **one** `IAR51Client` and keep it for the life of your application. In ASP.NET Core or a .NET worker service, register it as a singleton and connect from a background service, so a missing AR 51 system never blocks your app's startup:

```csharp title="AR51Connection.cs"
using AR51.Net;
using Microsoft.Extensions.Hosting;

// Connects in the background and disconnects when the app stops.
public sealed class AR51Connection(IAR51Client ar51) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            await ar51.StartAsync(stoppingToken);   // returns once connected; keeps trying until then
        }
        catch (OperationCanceledException)
        {
            // the app is shutting down
        }
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        await ar51.StopAsync(cancellationToken);
        await base.StopAsync(cancellationToken);
    }
}
```

Register it with the app's logging, and read the server address from configuration (`"AR51": { "Host": "192.168.1.20" }` in `appsettings.json`; leave it out to use discovery):

```csharp title="Program.cs"
using AR51.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = Host.CreateApplicationBuilder(args);   // or WebApplication.CreateBuilder(args) in ASP.NET Core
builder.Services.AddSingleton(services => AR51Client.Create(
    new AR51ClientOptions { Host = builder.Configuration["AR51:Host"] },
    services.GetRequiredService<ILoggerFactory>()));
builder.Services.AddHostedService<AR51Connection>();
await builder.Build().RunAsync();
```

The host disposes the client when it shuts down. Inject `IAR51Client` wherever you need it. (If you build a service provider yourself, dispose it with `DisposeAsync` — the client is async-disposable only.)

:::caution[Restarting your service cancels its calibration]
Stopping the client cancels a calibration **this client started**. Redeploying or restarting a back end in the middle of a calibration it started cancels that calibration — see [Calibration](/docs/csharp-api/calibration/#run-a-calibration).
:::

## Hand data off the receiving thread

`SkeletonReceived` and `ObjectReceived` run on the stream's background thread, and slow handlers back up the stream. Copy what you need and return. Two patterns cover most apps.

### Keep the latest skeleton per person

For dashboards, views and web pushes that sample at their own rate, store the latest skeleton per person and read a snapshot whenever you need it:

```csharp
using System.Collections.Concurrent;

var latest = new ConcurrentDictionary<string, Skeleton>();
ar51.Skeletons.SkeletonReceived += (_, e) => latest[e.Skeleton.Id] = e.Skeleton;   // constant time; locks only briefly

// Elsewhere — a timer, a UI frame, a web request — read it. Enumerating the dictionary takes no locks
// (unlike .Values, which copies under every lock and makes the receiving thread wait).
foreach ((string id, Skeleton skeleton) in latest)
    Console.WriteLine($"{id}: hips at {skeleton.GetPosition(Joint.MidHip)} m");

// Drop people you haven't heard from for a while (the timeout is your choice).
// Removing the exact entry you checked never deletes a newer skeleton that just arrived.
DateTimeOffset cutoff = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(1);
foreach (KeyValuePair<string, Skeleton> entry in latest)
    if (entry.Value.ReceivedTime < cutoff)
        latest.TryRemove(entry);
```

`Skeleton` is immutable, so a snapshot can be read from any thread.

### Process skeletons in order without blocking

For analytics that work through skeletons in order, queue them into a bounded channel and process on your own task. If processing falls behind, the **oldest** skeletons are dropped instead of stalling the stream. A buffer of 4096 holds about 1.7 seconds at 20 people and 120 frames per second:

```csharp
using System.Threading.Channels;

long dropped = 0;
var queue = Channel.CreateBounded<Skeleton>(
    new BoundedChannelOptions(4096) { FullMode = BoundedChannelFullMode.DropOldest, SingleReader = true },
    itemDropped: _ => Interlocked.Increment(ref dropped));   // log this: drops mean your consumer is too slow
ar51.Skeletons.SkeletonReceived += (_, e) => queue.Writer.TryWrite(e.Skeleton);

await foreach (Skeleton skeleton in queue.Reader.ReadAllAsync(cancellationToken))
{
    // heavy work here — on your task, not the receiving thread
}
```

## Update a UI

Events are raised on background threads. To touch UI controls, marshal to the UI thread: capture the UI's `SynchronizationContext` **on the UI thread** (or use your framework's dispatcher — `Dispatcher` in WPF, `DispatcherQueue` in WinUI, `InvokeAsync` in Blazor):

```csharp
// Run this on the UI thread, for example in your window's constructor.
SynchronizationContext ui = SynchronizationContext.Current
    ?? throw new InvalidOperationException("Capture the SynchronizationContext on the UI thread.");

ar51.ConnectionStateChanged += (_, e) =>
    ui.Post(_ => Console.WriteLine($"Connection: {e.State}"), null);   // e.g. statusText.Text = e.State.ToString()
```

For live skeletons, don't post every event to the UI. Render from the [latest-skeleton snapshot](#keep-the-latest-skeleton-per-person) on your UI's own frame tick.

## Relay to a web front end

To show live data in a browser, keep the latest-skeleton snapshot on the server and push it at a fixed rate — for example 30 times a second over SignalR or a WebSocket — rather than forwarding every event. Bandwidth stays predictable however many people are tracked, and a slow browser never slows the stream.

:::caution[Project to your own types before serializing]
`System.Text.Json` skips public fields by default, and `System.Numerics` vectors keep `X`, `Y`, `Z` in fields — so serializing a `Skeleton` directly gives empty joints, with no error. Copy what the browser needs into your own types first.
:::

```csharp title="SkeletonDto.cs"
// What the browser receives: positions flattened to x, y, z per joint, in meters.
public sealed record SkeletonDto(string Id, string Name, float[] Positions, float[] Confidences);
```

```csharp
using System.Collections.Concurrent;
using System.Text.Json;

var latest = new ConcurrentDictionary<string, Skeleton>();
ar51.Skeletons.SkeletonReceived += (_, e) => latest[e.Skeleton.Id] = e.Skeleton;

using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1.0 / 30));   // 30 updates a second
while (await timer.WaitForNextTickAsync(cancellationToken))
{
    DateTimeOffset cutoff = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(1);   // leave out people who have left
    SkeletonDto[] frame = latest
        .Where(entry => entry.Value.ReceivedTime >= cutoff)
        .Select(entry => entry.Value)
        .Select(s => new SkeletonDto(
            s.Id,
            s.EntityDisplayName,
            s.Positions.SelectMany(p => new[] { p.X, p.Y, p.Z }).ToArray(),
            s.Confidences.ToArray()))
        .ToArray();

    string json = JsonSerializer.Serialize(frame);
    // send json over your WebSocket — or, with SignalR, send frame itself: await hub.Clients.All.SendAsync("skeletons", frame)
}
```

## A complete example

The [web dashboard sample](/docs/csharp-api/web-dashboard/) puts these patterns together in an ASP.NET Core app: hosting, live skeleton frames pushed to the browser, server status, camera previews, calibration and recording.

## See also

- [Data conventions, threading and errors](/docs/csharp-api/conventions/)
- [Connecting](/docs/csharp-api/connecting/) — reconnection, logging and shutdown
- [`IAR51Client`](/docs/sdk-api/csharp-api/classes/iar51client/) · [`ISkeletonClient`](/docs/sdk-api/csharp-api/classes/iskeletonclient/)
