A .NET 8 (Windows) library and service that keeps deployed applications automatically up to date
from GitHub Releases. Drop the library into a WinForms, console, or ASP.NET Core (IIS) app, ship
a small updater.json, and the app silently (or with a prompt) updates itself to the latest release
on startup — including the privileged file replacement that normally requires an administrator.
See
specification.mdfor the authoritative requirements. This README describes what is actually built in this repository.
- Purpose
- Key features
- Solution layout
- How it works
- Getting started
- Configuration reference
- Update modes: Silent vs. Prompt
- Building & packaging
- Sample use cases
- Requirements
- Limitations & out of scope
Auto-updating a deployed Windows application is deceptively hard:
- The running
.exe/.dllfiles are locked, so an installer cannot overwrite them while the app runs. - Application files often live in protected directories (e.g.
C:\Program Files), so updating them needs administrative privileges the app itself usually doesn't have. - Web apps hosted in IIS hold file locks until the worker process unloads.
- A half-applied update (power loss, full disk) can leave the app in a corrupted state that fails to start — and a naïve retry can turn that into an infinite boot loop.
This solution handles all of the above: it detects new GitHub releases, downloads the installer,
and performs the install from a SYSTEM-level Windows Service using a disposable runner
process extracted to %TEMP%, with per-release retry tracking to avoid boot loops.
- Drop-in client library (
AutoUpdater.Core) referenced by any .NET 8 Windows app. - GitHub Releases as the distribution channel, with authenticated token support for private repos and higher rate limits.
- Privileged installs via a per-app Windows Service running as
SYSTEM. - Lock-safe runner: a single-file runner is embedded in the service and extracted to
%TEMP%, so it can replace the locked app binaries and then self-delete. - Target-aware shutdown/restart: force-close + relaunch for desktop/console;
app_offline.htmfor IIS web apps. - Silent or prompt update modes (a turnkey WinForms confirm dialog ships in
AutoUpdater.WinForms). - Resilient retries: up to 3 install attempts per release tag, then the tag is abandoned to prevent boot loops.
- Release-mode lock: auto-update is disabled in Debug builds so it never interferes with development.
- Logging to a rolling file and the Windows Event Log.
AutoUpdater.sln
├─ src/
│ ├─ AutoUpdater.Core # NuGet-packable client library referenced by host apps
│ ├─ AutoUpdater.Service # Per-app Windows Service (runs as SYSTEM); hosts the named-pipe server
│ ├─ AutoUpdater.Runner # Temp "runner" exe (embedded in the service, extracted to %TEMP%)
│ └─ AutoUpdater.WinForms # Optional: turnkey WinForms confirm dialog for Prompt mode
├─ samples/
│ ├─ SampleApp/ # Runnable WinForms host wired up end-to-end
│ ├─ updater.json # Desktop/Console app, Silent mode (default)
│ ├─ updater.webapp.json # IIS-hosted WebApp
│ └─ updater.winforms-prompt.json # WinForms app, Prompt mode
├─ installer/
│ ├─ SampleApp.iss # Documented InnoSetup script (install + update payload)
│ ├─ publish.ps1 # Publishes app + service (with embedded runner) into one folder
│ └─ README.md # Full distribution walkthrough
└─ specification.md # Requirements / design decisions
| Component | Spec | Responsibility |
|---|---|---|
AutoUpdater.Core |
§1, §3, §6 | Config + state models, GitHub release client, named-pipe IPC contract, logging, prompt abstraction, and the UpdaterClient entry point the host calls at startup. |
AutoUpdater.Service |
§2.1–§2.3 | Per-app SYSTEM service. Listens on a per-app named pipe, validates requests, extracts and launches the runner. Also exposes install/uninstall/start/stop verbs for the installer. |
AutoUpdater.Runner |
§2.1, §4, §5 | Extracted to %TEMP%. Shuts down the target (force-kill or app_offline.htm), runs the InnoSetup installer silently, restarts the app in the user session, then self-deletes. |
AutoUpdater.WinForms |
§3.5 | Optional. WinFormsUpdatePrompt — a single Yes/No MessageBox used in Prompt mode. |
- Host app +
AutoUpdater.Core— runs the update check at startup (unprivileged). AutoUpdater.Service(SYSTEM) — the only component with rights to write protected dirs.AutoUpdater.Runner(%TEMP%) — the disposable process that actually shuts down, installs, and restarts, so no permanently installed binary is locked during the swap.
These talk over a per-app named pipe (AutoUpdater.<applicationId>).
sequenceDiagram
participant App as Host App (Core)
participant GH as GitHub Releases
participant Svc as Service (SYSTEM)
participant Run as Runner (%TEMP%)
participant Inno as InnoSetup installer
App->>App: Startup, abort if Debug build (§2.4)
App->>GH: GET /releases/latest (token auth) (§3)
GH-->>App: Latest tag + single .exe asset
App->>App: New tag? not already completed? (§3.4)
opt Prompt mode (§3.5)
App->>App: Ask user "close & update now?"
App-->>App: Declined → defer to next startup
end
App->>GH: Download installer asset (§4.1)
App->>App: Register install attempt (1..3) (§5.3)
App->>Svc: UpdateRequest over named pipe (§2.2)
Svc->>Run: Extract embedded runner to %TEMP% & launch (§2.1)
Run->>App: Shutdown (force-kill, or drop app_offline.htm) (§4.3)
Run->>Inno: Run /VERYSILENT /DIR=<installDir> (§4.2)
Inno-->>Run: Exit code
alt Success
Run->>Run: Mark tag current/completed (§5.1)
Run->>App: Restart in user session / remove app_offline.htm
else Failure
Run->>App: Recover, next startup retries (≤3) (§5.2/§5.3)
end
Run->>Run: Self-delete temp files (§2.1)
- When the check runs — strictly on application startup (§3.1), asynchronously so it never blocks the app. Once an update is confirmed, it hands off immediately on that same launch (§3.2).
- Which release — the repo's "Latest" release; drafts and pre-releases are ignored. Each
release is expected to contain exactly one
.exeasset (the InnoSetup installer) (§3.4). - Shutdown strategy (§4.3):
- Desktop / Console — the runner force-closes the process (after consent in Prompt mode).
- WebApp (IIS) — the runner drops
app_offline.htminto the web root to release file locks.
- Restart strategy (§5.1):
- Desktop / Console — relaunched into the active interactive session via
CreateProcessAsUser, preserving the original startup arguments. - WebApp —
app_offline.htmis deleted, bringing the site back online.
- Desktop / Console — relaunched into the active interactive session via
- Retries & boot-loop protection (§5.3) — each install attempt for a tag is counted in a machine-wide state file. After 3 failed attempts the tag is marked completed (ignored) so it is never retried again. A declined prompt or a failed download does not consume an attempt — only actual install attempts do.
Reference AutoUpdater.Core (and AutoUpdater.WinForms for prompt mode) and place an
updater.json next to your executable (see Configuration reference).
using AutoUpdater.Core;
// Fire-and-forget: non-blocking, and automatically disabled in Debug builds (spec §2.4).
_ = UpdaterClient.FromConfigFile().CheckForUpdatesAsync(args);For prompt mode, pass a prompt (see Update modes).
The app's installer must register the per-app service (it needs admin once). The service exposes verbs for this:
; InnoSetup [Run]
Filename: "{app}\AutoUpdater.Service.exe"; \
Parameters: "install --application-id MyApp"; Flags: runhidden waituntilterminatedEquivalent manual command:
sc.exe create AutoUpdater.MyApp binPath= "\"C:\Program Files\MyApp\AutoUpdater.Service.exe\" --application-id MyApp" obj= LocalSystem start= autoAutoUpdater.Service.exe uninstall|start|stop --application-id MyApp are also available.
| Field | Required | Description |
|---|---|---|
applicationId |
yes | Stable id; namespaces the service, named pipe, state and logs. |
repositoryUrl |
yes | GitHub repo URL, e.g. https://github.com/org/repo. |
githubToken |
for private repos / rate limits | Personal access token. Stored in plaintext in this iteration (§3.3). |
applicationType |
yes | Desktop, Console, or WebApp. Determines shutdown/restart strategy. |
updateMode |
no (default Silent) |
Silent or Prompt. Prompt is valid only for Desktop/Console. |
installDirectory |
yes | Deployed location; passed to InnoSetup as /DIR=. Must match the installer's install dir. |
webRootPath |
WebApp only | Folder where app_offline.htm is created/deleted. |
serviceName |
no | Override the derived AutoUpdater.<applicationId> service name. |
{
"applicationId": "MyApp",
"repositoryUrl": "https://github.com/your-org/your-repo",
"githubToken": "ghp_xxx",
"applicationType": "Desktop",
"updateMode": "Silent",
"installDirectory": "C:\\Program Files\\MyApp"
}Stored separately from the user config under
%ProgramData%\AutoUpdater\<applicationId>\state.json:
| Field | Description |
|---|---|
currentTag |
Currently deployed release tag. |
retryCounts |
Per-tag install attempt counts. |
completedTags |
Tags that succeeded or were abandoned after 3 attempts (never retried). |
Logs are written to %ProgramData%\AutoUpdater\<applicationId>\logs\*.log and to the Windows Event
Log (source AutoUpdater.<applicationId>).
Silent(default) — downloads and installs in the background with no user interaction.Prompt— shows a single dialog at startup asking the user to close and update now; intended for interactive desktop/WinForms apps. The prompt appears before any download or shutdown, so declining costs no bandwidth and never closes the app unexpectedly.
The prompt UI is host-supplied so the core library stays UI-agnostic. A ready-made WinForms
implementation ships in AutoUpdater.WinForms; construct it on the UI thread so the dialog is
marshaled correctly:
using AutoUpdater.Core;
using AutoUpdater.WinForms;
public partial class MainForm : Form
{
private async void MainForm_Shown(object? sender, EventArgs e)
{
var prompt = new WinFormsUpdatePrompt(); // captures the UI SynchronizationContext
await UpdaterClient.FromConfigFile()
.CheckForUpdatesAsync(Program.StartupArgs, prompt);
}
}Fail-safe: if
updateModeisPromptbut no prompt is supplied, the update is deferred rather than applied — the app is never force-closed without asking. Declining defers to the next startup and does not consume a retry attempt (§5.3).
CheckForUpdatesAsync returns an UpdateCheckResult: UpToDate, UpdateStarted, Deferred,
Disabled (Debug build), or Failed.
dotnet build AutoUpdater.sln -c ReleaseAuto-update only activates in Release builds (§2.4).
The runner is embedded inside AutoUpdater.Service.exe so it can be extracted to %TEMP% at
runtime (§2.1). Publishing the service with -p:EmbedRunner=true (plus a RuntimeIdentifier)
publishes the runner as a framework-dependent single-file exe and embeds it as a resource.
installer/publish.ps1 does this automatically. A plain dotnet build (no EmbedRunner) skips
embedding and the service falls back to a runner placed beside its binary — convenient for local
debugging.
The compiled InnoSetup .exe is both the first-time installer and the update payload uploaded
to each GitHub release. See installer/README.md for the full walkthrough:
pwsh installer/publish.ps1 -Version 1.0.0 # merge app + service (embedded runner) into publish/SampleApp
iscc installer/SampleApp.iss # produce installer/Output/SampleApp-Setup-1.0.0.exeThen create a GitHub Release whose tag matches the version and attach the setup .exe as its
single asset.
samples/SampleApp is a runnable WinForms host that calls UpdaterClient on startup and shows the
update log on screen. Configured for Prompt mode, the user is asked to close and update on launch.
Build and package it via installer/README.md to see the full
publish → release → auto-update loop.
Set "applicationType": "Console" and keep "updateMode": "Silent". On startup the app checks for
updates; if found, the runner force-closes it, installs, and relaunches it with the same command
line arguments it was started with — ideal for scheduled tasks or headless agents.
Use samples/updater.webapp.json as a template: set "applicationType": "WebApp" and point
webRootPath at the site root. On update, the runner drops app_offline.htm (IIS gracefully
unloads the app and releases file locks), installs, then removes app_offline.htm to bring the site
back online — no manual IIS reset required.
- Windows (x64). The service, runner session-launch, and Event Log are Windows-specific.
- .NET 8 runtime on target machines (binaries are framework-dependent).
- InnoSetup 6 (
iscc) to build installers. - A GitHub repository with releases; a token for private repos or higher rate limits.
- Administrative rights once, at install time, to register the SYSTEM service.
Intentionally not implemented in this iteration (per the agreed spec decisions):
- Token encryption —
githubTokenis stored in plaintext inupdater.json(§3.3). Treat the config as sensitive and avoid committing real tokens. - Installer verification — no Authenticode/checksum verification of the downloaded installer beyond a successful download (§4.1). Use a trusted/private repo.
- Rollback / backup — recovery is retry-only; there is no automatic restore of the previous version if all 3 attempts fail (§5.2).
Other constraints worth noting:
- Windows-only, single active console session assumed for desktop restart.
- Exactly one
.exeinstaller asset per release is expected, built with InnoSetup. - The check runs at startup only — long-running apps won't update until restarted.
- Updates are gated to Release builds; Debug builds never auto-update.
These are reasonable candidates for a follow-up hardening pass.