How to give a C# (Windows) desktop app OAuth + entitlement-gated NetSparkle updates straight from
your RedGuides resource, using the RedGuides.Client package. EQBZ
Multiboxer is the worked example throughout. (Rolling your own client instead? The raw server contract
is in netsparkle-server-reference.md.)
This guide assumes you have a resource on RedGuides and know its resource id. If you're shy and want to test privately, post under registered testing and ask a mod to move it to a real category when you're production-ready.
One package, RedGuides.Client (net8.0-windows):
dotnet add package RedGuides.Client
(or grab it from nuget.org.)
It ships two layers — both sit on RedGuidesAuthService (OAuth PKCE + refresh-on-401 handler, DPAPI
token store, X-RG-* entitlement parsing), which you build once either way (Wiring step 1 below). Pick
the update layer on top:
| Layer | What it adds | Use when |
|---|---|---|
RedGuidesSparkleFactory |
A configured SparkleUpdater: authenticated appcast/installer downloaders + the Ed25519 download verifier. |
You drive your own loop/UI. |
RedGuidesUpdateService |
The above, plus a background check loop, interactive "Check for Updates", NetSparkle's WPF update window, and an entitlement-aware renewal prompt. Pulls in NetSparkleUpdater.UI.WPF. |
The common path. |
Only three values are required per app; everything else defaults to shared values, so most apps never touch the rest. Download integrity is automatic (see Download verification).
RedGuidesUpdateOptions |
Required | Default | Notes |
|---|---|---|---|
ResourceId |
yes | - | Which resource's appcast/installer to pull (e.g. 3322). |
ProductName |
yes | - | Dialog titles ("{ProductName} Updates") + renewal copy. |
InstallerFileName |
yes | - | What NetSparkle saves the download as — URLs carry no filename. Needs a launchable extension (.exe/.msi). |
AppIconPackUri |
no | null |
Pack URI for the NetSparkle dialog icon. |
CheckInterval |
no | 24h | Background re-check cadence. Also throttles launch checks, but only when CheckOnEveryLaunch is false. |
CheckOnEveryLaunch |
no | true |
Check on every launch (the default). Set false to honor the CheckInterval launch throttle instead. |
RedGuidesAuthOptions (host, client id, scopes, redirect) is separate and defaults to the shared
public values. Override ClientId only if you obtained your own from Redbot.
Two services: RedGuidesAuthService (the OAuth session) and RedGuidesUpdateService (the update UX).
using RedGuides.Client.Auth;
// Shared token store: one sign-in across all RedGuides desktop tools. DPAPI-encrypted,
// %LocalAppData%\RedGuides, package-default entropy namespace.
var tokenDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"RedGuides");
var auth = new RedGuidesAuthService(
RedGuidesAuthOptions.CreateDefault(), // shared public client id + scopes; override if you have your own
new OAuthTokenStore(tokenDir)); // default entropy + filename = the shared storeNo browser opens here. sign-in is triggered on demand by the update flow.
RedGuidesUpdateService creates WPF windows, so construct it after your app's UI exists and on
the UI thread. From Multiboxer's App.OnStartup:
using RedGuides.Client.Updates;
var updates = new RedGuidesUpdateService(auth, new RedGuidesUpdateOptions
{
ResourceId = 3322,
ProductName = "EQBZ Multiboxer",
InstallerFileName = "EQBZ-Multiboxer-Setup.exe",
AppIconPackUri = "pack://application:,,,/Assets/logo.png",
});
// Throttled check-on-launch + background loop; no-ops when not signed in.
_ = updates.StartBackgroundChecksAsync();// e.g. a tray menu item. Signs in interactively (browser) first if needed, then checks.
updateMenuItem.Click += (_, _) => _ = updates.CheckForUpdatesInteractiveAsync();That's the whole integration:
| Call | When | Behavior |
|---|---|---|
StartBackgroundChecksAsync |
Launch + on the interval | Throttled, silent; never opens a browser. |
CheckForUpdatesInteractiveAsync |
User-initiated | Drives auth and license messages — e.g. an expired license becomes a renewal prompt instead of a misleading "you're up to date". |
The appcast carries a per-release Ed25519 signature over the installer bytes, and the library
checks it against RedGuides' public key (baked in) using NetSparkle's built-in Ed25519Checker
(SecurityMode.OnlyVerifySoftwareDownloads).
RedGuides signs server-side: when you publish a build, the server signs the uploaded installer bytes,
so you never generate or hold a signing key. (Contract:
netsparkle-server-reference.md.)
NetSparkle compares the running app's informational version against the feed's version. Both
sides must be dotted, SemVer-like values for that comparison to work.
| Side | Pitfall | Fix |
|---|---|---|
| Client | .NET 8+ appends the git commit hash (3.9.2+abc123), breaking the comparison. |
Set <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> in your .csproj. |
| Server | The feed's version is your resource's version_string served verbatim (not normalized). |
Keep version_string dotted (e.g. 4.4.3); v0.42, 2782, or a date won't order correctly. |
Skip RedGuidesUpdateService and call the factory directly. RedGuidesSparkleFactory.Create builds the
configured SparkleUpdater; you pass your own IUIFactory and drive the loop/UI yourself:
using RedGuides.Client.Updates;
var sparkle = RedGuidesSparkleFactory.Create(auth, options, uiFactory: myUiFactory);For a headless entitlement check (no NetSparkle UI), pass onAppcastResponse to
RedGuidesSparkleFactory.Create and read the X-RG-* headers with EntitlementInfo.FromResponse.
The library logs through Microsoft.Extensions.Logging. Pass an ILoggerFactory to any service to
route handled errors into your logging; omit it for a silent no-op (NullLoggerFactory.Instance).