C# SDK

Real-time LiveFeed

Grote.Daas.LiveFeed

A managed gRPC stream that fans real-time trailer and telemetry updates out to your handlers, with authentication, heartbeat filtering, and reconnect handled for you.

Install

Install the LiveFeed package for the real-time gRPC push stream. If you only need request/response queries, the REST client (documented on its own page) is a separate package you can take on its own, without gRPC and protobuf dependencies.

Shell
dotnet add package Grote.Daas.LiveFeed
Pin the package version you install so a later release cannot change behavior under you. The package listing is the source of truth for the current version and the frameworks it targets.

Configure & authenticate

Authentication is two-step: you hold a long-lived API key, and the SDK exchanges it for a short-lived JWT against the Grote auth server, then calls the data API with that token. The token is cached and refreshed automatically, so you never handle the JWT yourself.

Set Audience to match the key you were issued. Use Customer for a fleet-owner key (the default) or Partner for an integration-partner key. The wrong audience fails authentication at the auth server.

C#Dependency injection (recommended)
using Grote.Daas.Client;

// Resolve the key up front so a missing value fails with a clear message instead of
// silently overwriting the environment-variable fallback with null.
var apiKey = builder.Configuration["DAAS_API_KEY"];
if (string.IsNullOrWhiteSpace(apiKey))
    throw new InvalidOperationException("DAAS_API_KEY is not configured.");

builder.Services.AddDaas(options =>
{
    options.ApiKey   = apiKey;
    options.Audience = DaasAudience.Customer;   // or DaasAudience.Partner
});

Then inject IDaasClient anywhere. Registration goes through IHttpClientFactory and keeps the auth handler as a singleton, so the cached JWT is shared across requests.

C#
public sealed class FleetReporter(IDaasClient daas)
{
    public Task<Fleet> GetFleetAsync(int id, CancellationToken ct)
        => daas.Fleets.GetAsync(id, ct);
}

For a console app, worker, or script, construct the client directly. It owns its HttpClient, so dispose it when you are done.

C#Standalone, without DI
using Grote.Daas.Client;

using var client = new DaasClient(new DaasClientOptions
{
    ApiKey = Environment.GetEnvironmentVariable("DAAS_API_KEY")!
});
ApiKey reads the DAAS_API_KEY environment variable when the options are created, so leaving it alone picks the key up from the environment. Assigning it always wins, including when the value you assign is empty: binding it from configuration that has no such key overwrites the fallback with null and registration then fails even though the environment variable is set. Assign it only from a source you know is populated, or leave it unset and let the environment supply it.
Keep API keys in user secrets, environment variables, or a secret store, never in source control.

Real-time LiveFeed

The Grote.Daas.LiveFeed package subscribes to the gRPC live feed and fans updates out to your handlers. Authentication, heartbeat filtering, and reconnect are handled for you, so there is no connection code or stream loop in your application. It uses the same API key and audience model as the REST client.

C#Register the feed and its handlers
using Grote.Daas.LiveFeed;

builder.Services
    .AddDaasLiveFeed(options =>
    {
        options.ApiKey   = apiKey;   // resolved as shown under Configure & authenticate
        options.Audience = DaasAudience.Customer;
    })
    .AddHandler<TrailerFaultAlerter>()
    .AddHandler<TelemetryArchiver>()
    .OnUpdate((update, services, ct) =>
    {
        Console.WriteLine($"{update.Trailer.UnitId} @ {update.Ts}");
        return Task.CompletedTask;
    });

A handler is a plain class, constructor-injected like any other service. Each update is dispatched to every registered handler inside a fresh DI scope, so handlers can depend on scoped services such as an EF Core DbContext. If one handler throws, the failure is logged, the other handlers still run, and the stream keeps going.

C#A handler
public sealed class TrailerFaultAlerter : ILiveFeedHandler
{
    private readonly ILogger<TrailerFaultAlerter> _log;

    public TrailerFaultAlerter(ILogger<TrailerFaultAlerter> log) => _log = log;

    public Task HandleAsync(LiveUpdate update, CancellationToken ct)
    {
        if (update.Trailer.Status?.IsFault == true)
            _log.LogWarning("Trailer {Unit} faulted", update.Trailer.UnitId);

        return Task.CompletedTask;
    }
}

All handlers for a single update run inside one shared DI scope, so you can hang a per-update context off that scope: register it as scoped, populate it once, and every handler for that update reads the same instance. This keeps each handler from repeating the same lookups (for example fetching the trailer's devices) when they all work the same trailer.

Ordering is explicit. A pre-handler runs before the regular handlers and a post-handler runs after them. Every pre-handler completes before any regular handler starts, and every regular handler completes before any post-handler starts, so the phases are barriers even when DispatchMode is Parallel. Within a phase, handlers honor the configured DispatchMode. Pre- and post-handlers implement the same ILiveFeedHandler interface as a regular handler; only how you register them differs.

C#A context and a pre-handler that fills it
// A per-update context, populated once and shared by every handler for that update.
public sealed class TrailerContext
{
    public IReadOnlyList<Device> Devices { get; set; } = [];
}

// Runs before the regular handlers: do the shared lookups once.
public sealed class TrailerContextLoader : ILiveFeedHandler
{
    private readonly IDaasClient _client;
    private readonly TrailerContext _ctx;

    public TrailerContextLoader(IDaasClient client, TrailerContext ctx)
        => (_client, _ctx) = (client, ctx);

    public async Task HandleAsync(LiveUpdate update, CancellationToken ct)
        => _ctx.Devices = await _client.Devices.ListAsync(update.Trailer.Id, ct: ct);
}
C#Register a context and ordered handlers
builder.Services
    .AddDaasLiveFeed(o => o.ApiKey = apiKey)
    .AddScoped<TrailerContext>()           // shared per-update, resolved in the same scope
    .AddPreHandler<TrailerContextLoader>() // runs first, fills the context
    .AddHandler<TrailerFaultAlerter>()     // regular handlers read ctx.Devices
    .AddHandler<TelemetryArchiver>()
    .OnBeforeUpdate((update, sp, ct) => Task.CompletedTask)  // inline pre, mirrors OnUpdate
    .OnAfterUpdate((update, sp, ct) => Task.CompletedTask);  // inline post

As with regular handlers, an exception thrown by a pre- or post-handler is logged and swallowed so the other handlers and the stream keep going. A failing pre-handler does not stop the regular handlers, so treat the context as possibly unpopulated if your loader can fail.

Reconnect itself is not optional, but the backoff is tunable, and you can observe the connection lifecycle to flip a health flag or reconcile through the REST client.

C#Lifecycle and backoff
builder.Services
    .AddDaasLiveFeed(o =>
    {
        o.ApiKey                 = apiKey;
        o.InitialReconnectDelay  = TimeSpan.FromSeconds(2);
        o.ReconnectBackoffFactor = 2.0;
        o.MaxReconnectDelay      = TimeSpan.FromSeconds(60);
    })
    .OnConnected((ctx, sp, ct) => Task.CompletedTask)
    .OnDisconnected((ctx, sp, ct) => Task.CompletedTask)   // ctx: Error, Attempt, WillRetry, NextDelay
    .OnReconnecting((ctx, sp, ct) => Task.CompletedTask);  // ctx: Attempt, Delay, LastError
DaasLiveFeedOptionsDefault
ApiKeyDAAS_API_KEY environment variable
BaseUrlhttps://4see.groteintegrations.com/
AuthUrlhttps://auth.groteintegrations.com/
AudienceCustomer (or Partner)
DispatchModeSequential (or Parallel for concurrent handlers)
InitialReconnectDelay2 seconds
ReconnectBackoffFactor2.0
MaxReconnectDelay60 seconds
AuthTimeout30 seconds
KeepAlivePingDelay / KeepAlivePingTimeout25s / 20s

If you would rather own the loop, use the low-level client directly. Heartbeats are still filtered, but there is no automatic reconnect at this level.

C#Low-level escape hatch
using var client = new DaasLiveFeedClient(new DaasLiveFeedOptions { ApiKey = apiKey });

await foreach (LiveUpdate update in client.SubscribeAsync(ct))
{
    // Your logic. Heartbeats are already filtered out.
}
Both packages declare a DaasAudience enum in their own namespace. If you register the REST client and the live feed in the same file, qualify them (Grote.Daas.Client.DaasAudience and Grote.Daas.LiveFeed.DaasAudience) or alias one.

Best practices

Changelog

Release history for the LiveFeed package. Pin the version you install (see Install) so a later release cannot change behavior under you.

Grote.Daas.LiveFeed
1.0.3
2026-09-14
Added
  • Ordered handler phases: AddPreHandler<T>() runs before, and AddPostHandler<T>() after, the regular handlers, with inline OnBeforeUpdate(...) and OnAfterUpdate(...) equivalents that mirror OnUpdate(...).
  • AddScoped<T>() builder sugar for registering a per-update context resolved from the same scope as the handlers, so a pre-handler can populate it once and every handler for that update reads the same instance.
Changed
  • Dispatch runs in three phases against the shared per-update scope (pre-handlers, then the regular handlers, then post-handlers). Each phase is a barrier that completes before the next begins, and each still honors DispatchMode.
Notes
  • Fully backwards compatible: existing AddHandler/OnUpdate registrations are unchanged and empty pre/post phases are no-ops.
1.0.2
Notes
  • Baseline release. See the package listing for details predating this changelog.

Need help?

Questions about the SDK, onboarding, or requesting an API key?