SelfUpdater 0.4.0

dotnet add package SelfUpdater --version 0.4.0
                    
NuGet\Install-Package SelfUpdater -Version 0.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SelfUpdater" Version="0.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SelfUpdater" Version="0.4.0" />
                    
Directory.Packages.props
<PackageReference Include="SelfUpdater" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SelfUpdater --version 0.4.0
                    
#r "nuget: SelfUpdater, 0.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SelfUpdater@0.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SelfUpdater&version=0.4.0
                    
Install as a Cake Addin
#tool nuget:?package=SelfUpdater&version=0.4.0
                    
Install as a Cake Tool

SelfUpdater

A small, pluggable self-update engine for single-file / Native AOT .NET apps.

It does what dnvm does for itself, generalized: check the running version against a source, download the build for your platform, verify (SHA-256, when the source publishes a hash) and optionally validate (smoke-test) it, then replace the running executable in place via a two-process handoff so an app can update itself — including on Windows, where a running image can't overwrite itself. Single-file binaries and multi-file bundles (e.g. a macOS .app) are both supported — see Directory (multi-file) updates.

Install

dotnet add package SelfUpdater

Targets net10.0, trim/AOT-compatible, serializes with Serde.NET.

Two ready-to-use updaters

Pick the one that matches where your releases live, hand it an UpdaterOptions, and call UpdateAsync:

Updater Use it for
DirectoryUpdater A local folder or network share — LAN/offline/air-gapped rollouts, and tests. Reads the directory's files; the ones matching {appName}-{version}-{rid} (or your parser) become releases. Optional {binary}.sha256 sidecars provide integrity.
GitHubUpdater GitHub Releases. Reads each published asset; the ones matching {appName}-{version}-{rid} (or your parser) become releases — the rest are ignored. Public repos need nothing; private repos take an authToken delegate and download through the authenticated asset API.

Both share the same engine via a common Updater base: a concrete updater only supplies how to list a source's raw artifacts and how to open one's bytes — it never parses versions, compares them, decides what counts as "new", or picks which asset fits the running platform. All of that policy lives in UpdaterOptions, which knows your current version and target platform (its runtime identifier).

Usage

The engine is policy-free: you tell it your current version (it never fetches it for you) via UpdaterOptions, and it decides nothing about what is "new" unless you ask it to. Naming and platform selection live in UpdaterOptions. Build the options, create an updater, and call UpdateAsync:

using Semver;
using SelfUpdater;

var updater = new DirectoryUpdater("/path/to/releases", new UpdaterOptions
{
    AppName = "myapp",
    // You own the current version; the engine never fetches it for you. Versions
    // are Semver.SemVersion (the Semver NuGet package).
    CurrentVersion = SemVersion.Parse(MyApp.Version, SemVersionStyles.Any),
    // Rid defaults to RuntimeInformation.RuntimeIdentifier — the running platform.
    // It selects assets named `{AppName}-{version}-{Rid}` and is what lets the
    // engine split that name unambiguously (both the version and the rid may
    // contain dashes). Set Parser for a naming scheme other than the default.
    // TargetPath defaults to the running executable.
    // ValidateArgs is opt-in: leave unset to skip executing the download as a
    // smoke test, or set e.g. ["--version"] if your binary exits 0 for those.
});

// Newest-wins against CurrentVersion, for the selected platform.
var result = await updater.UpdateAsync();
if (result.Outcome == UpdateOutcome.Staged)
    return 0; // a newer build was handed off; this process should now exit

For GitHub Releases, swap in GitHubUpdater — the options are identical:

var updater = new GitHubUpdater("you", "myapp", options);

Set ReleaseFilter to ignore prereleases. To peek without applying, call FetchAsync() — it does the network round-trip and returns the exact Release you'd move to (or null when you're already current or there's no build for your platform):

var updater = new GitHubUpdater("you", "myapp", new UpdaterOptions
{
    AppName = "myapp",
    CurrentVersion = current,
    ReleaseFilter = r => !r.IsPrerelease,
});

var release = await updater.FetchAsync();
if (release is not null)
{
    Console.WriteLine($"New version {release.Version} available.");
    await updater.ApplyAsync(release); // download, verify, stage, hand off
}

The surface is three methods: FetchAsync() resolves the release to move to, ApplyAsync(release) downloads and stages it, and UpdateAsync() is shorthand for fetch-then-apply (so it never lists the source twice).

The handoff command

UpdateAsync downloads + validates the new binary, then launches it with a hidden command so the new process performs the swap once the old one exits. Wire that command up once:

// e.g. with System.CommandLine — names come from Updater constants
// Updater.HandoffVerb ("apply-update"), Updater.DestOption ("--dest"),
// Updater.PidOption ("--pid"), Updater.RelaunchOption ("--relaunch"),
// Updater.SourceDirOption ("--source-dir", directory updates only)
if (args is [Updater.HandoffVerb, ..])
{
    // sourceDir is null for single-file updates; pass it through for directory ones.
    return Updater.ApplySwap(destPath, oldPid, relaunchArgs: null, sourceDir: sourceDir);
}

Directory (multi-file) updates

Some apps are not a single file — a macOS .app bundle, or a binary that ships sidecar native assets next to it. Set TargetDirectory and the engine treats the release asset as a .zip or .tar.gz/.tgz containing one top-level directory, and replaces the whole tree in place instead of one file:

var updater = new GitHubUpdater("you", "myapp", new UpdaterOptions
{
    AppName = "myapp",
    CurrentVersion = current,
    // The directory to replace wholesale. The running executable must live inside
    // it (e.g. MyApp.app/Contents/MacOS/myapp); its location relative to the root is
    // reused to launch the staged build and to relaunch after the swap.
    TargetDirectory = bundleRoot,
});

The release asset is named the same way — {appName}-{version}-{rid}.<ext> — the default convention strips a known archive extension (.zip, .tgz, .tar.gz) before matching, and extraction dispatches on that extension (.tar.gz/.tgz are extracted as gzipped tar, everything else as zip). Executable bits inside the archive are preserved on extraction, so the swapped-in tree stays runnable. The handoff is identical; just forward the --source-dir value (above) to ApplySwap.

Private GitHub repos

The library is auth-mechanism agnostic — authToken is a Func<CancellationToken, Task<string?>> awaited per request, so a consumer can fetch/cache/refresh short-lived or rotating tokens however it likes:

var gh = new GitHubUpdater(
    owner: "you", repo: "private-app", options,
    authToken: ct => TokenCache.GetCurrentInstallationTokenAsync(ct)); // your concern

Use a fine-grained PAT (Contents: read-only), a GitHub App installation token vended from a small endpoint, or an OAuth device-flow token — the library doesn't care which.

Contributing

See CONTRIBUTING.md for build/test steps and the CSharpier formatting requirement.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.4.0 236 6/15/2026
0.3.0 110 6/12/2026
0.2.0 94 6/11/2026
0.1.1 93 6/9/2026