# Onova: in-place auto-update for portable .NET desktop applications

> Onova is a lightweight C# auto-update framework for desktop apps distributed as archives, resolving packages from a folder, GitHub releases, a web manifest or a NuGet feed, and applying the update with a separate executable so running files can be replaced. It suits a team shipping a portable build, and it targets Windows only.

**Tyrrrz/Onova** — Unintrusive auto-update framework

- Repository: https://github.com/Tyrrrz/Onova
- Website: https://nuget.org/packages/Onova
- Stars: 565 · Forks: 40
- Language: C#
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/tyrrrz-onova

## The problem it solves: replacing the files of a running program

Onova is a lightweight auto-update framework for desktop applications, and its stated purpose is precise: it is primarily designed for performing in-place updates for portable applications distributed via archive files, as opposed to installers or packages.

That sentence is the whole design brief. An application that ships as a zip is a folder of files that the user unzips and runs. Updating it in place means writing new files over the ones that are there, and the ones that are there belong to a process that is running, which on Windows means the executable and its assemblies are locked. The usual solutions are an installer with its own update mechanism, a store, or a launcher that does the work for you.

Onova's answer is an external executable. The feature list says it overwrites files in-place using an external executable, and the repository layout shows how that is built: an Onova/ directory for the library and an Onova.Updater/ directory for the helper. The application asks the library to prepare an update and then to launch the updater, and then the application exits, which frees the locks, and the updater writes the new files.

Two other entries in the feature list deal with the awkward cases that a naive implementation gets wrong. It works with multiple running instances of an application, which is handled by allowing only one instance of the updater to proceed, and it automatically prompts for elevated privileges if necessary, which is what makes an update possible when the application was installed into a protected directory in the first place.

The other promises in the list are about not being in the way. The library requires minimal configuration, it doesn't impose any changes to the CI/CD process, and it doesn't affect the life cycle of your application. It is fully self-contained and doesn't require additional files, and it has no external dependencies. For a desktop application, the last two matter more than they might seem, because an updater that needs extra files in the distribution directory is an updater whose own installation can go wrong.

## Resolvers: where the update comes from

Packages and their versions are resolved through an implementation of IPackageResolver, and there are five built-in implementations, and the choice between them is the main decision a user makes.

LocalPackageResolver looks for files in a specified directory using a predefined pattern, and it extracts the version from the file name, so a file named MyProject-v2.1.5.zip corresponds to version 2.1.5. This is the resolver for a product shipped to a controlled set of machines, where the update package is placed in a shared folder or a removable drive, and it is the simplest one to reason about because there is no server involved.

GithubPackageResolver looks for assets in the releases of a specified GitHub repository using a predefined pattern, with the version taken from the release name, so a release named v1.0 corresponds to version 1.0. That is the natural choice for an open source project distributing binaries on GitHub, and it means the release process is the update process, with no extra service to run.

WebPackageResolver requests a version manifest from a URL, and the server is expected to respond with a plain-text list of package versions and their URLs, separated by space, one line per package:

```
1.0 https://my.server.com/1.0.zip
2.0 https://my.server.com/2.0.zip
```

That format is worth noting because it is trivially implementable on any web server, with no database and no application behind it, and it gives the publisher full control over which builds are offered and where they are hosted. It is the resolver for a product with its own infrastructure.

NugetPackageResolver resolves packages from a specified NuGet feed, and AggregatePackageResolver aggregates over several other resolvers so that packages can be resolved and downloaded from more than one source.

The two resolvers that work with version numbers have a documented limitation that catches people out: since .NET assemblies do not support semantic versions, pre-releases are ignored. So a resolver will not offer you a beta or a release candidate, which is a limitation for a product that ships pre-releases to a subset of users.

## Extractors, version choice and the update sequence

Downloaded packages are extracted through an implementation of IPackageExtractor, and there are two built-in ones. ZipPackageExtractor extracts files from zip archives, and NugetPackageExtractor extracts files from NuGet packages, from a specified root directory. Both are extendable, and the whole resolver and extractor model is extensible with custom implementations, which is the point of defining the two interfaces.

The feature list also notes that updating to any available version is allowed, not only the latest. That matters for a product where a new release turns out to be broken, and it is why the API separates resolving, preparing and applying rather than combining them into a single call.

The basic usage example shows the whole thing in one operation, looking in a local directory and treating the packages as zips:

```csharp
// Configure to look for packages in specified directory and treat them as zips
using (var manager = new UpdateManager(
    new LocalPackageResolver("c:\\test\\packages", "*.zip"),
    new ZipPackageExtractor()))
{
    // Check for new version and, if available, perform full update and restart
    await manager.CheckPerformUpdateAsync();
}
```

The manual form is longer and shows why the library is designed the way it is:

```csharp
// Check for updates
var result = await manager.CheckForUpdatesAsync();
if (result.CanUpdate)
{
    // Prepare an update by downloading and extracting the package
    // (supports progress reporting and cancellation)
    await manager.PrepareUpdateAsync(result.LastVersion);

    // Launch an executable that will apply the update
    // (can be instructed to restart the application afterwards)
    manager.LaunchUpdater(result.LastVersion);

    // Terminate the running application so that the updater can overwrite files
    Environment.Exit(0);
}
```

The three steps are separate, and the separation is the design. Checking is a network operation you might want to run silently in the background. Preparing is a download and an extraction, and the library supports progress reporting and cancellation for it, so a user on a slow connection sees a progress bar and can abandon the update. Launching the updater and then exiting is the moment where the running application releases its file handles, which is why the comment on the last line spells out the reason for calling Environment.Exit.

Whether the updater restarts the application afterwards is a decision you pass in, and the library can be instructed to do it.

## The repository, the packaging and the state of the project

The repository is laid out as a solution with separate projects, and the split tells you how the pieces fit. There is Onova/ for the library, Onova.Updater/ for the helper executable that performs the replacement, Onova.Tests/ for the tests, and Onova.Tests.Dummy/ which is a dummy application used as a test subject, since testing an updater requires something to update.

The build configuration is modern. There is a Directory.Packages.props, which is central package management, and a Directory.Build.props for shared build settings, plus a NuGet.config and a global.json that pins the SDK version, so every contributor and every build server uses the same toolchain and the same dependency versions. The solution file is Onova.slnx, the newer XML solution format, and there is a .devcontainer/ directory so a contributor can work in a prepared container rather than installing the SDK locally. The test coverage is reported through Codecov, and the build badge points at a GitHub Actions workflow on the prime branch, which is also the default branch.

The package is distributed through NuGet, and the install is a single command:

```bash
dotnet add package Onova
```

The documentation also points at Binternal, the same author's tool for internalizing a dependency, so an application that does not want an external dependency can vendor the source instead of taking a package reference. That option matters for a desktop application where shipping a NuGet package reference means shipping the package alongside the executable.

The versions are 2.6.13 from 2025-03-28, 2.6.12 from 2024-05-23 and 2.6.11 from 2024-04-08, and the last push to the repository was on 2026-10-09, so the source is being worked on while the packaged releases move at a slower pace. The project status badge in the documentation reads maintenance, which is a deliberate statement about the project's stage rather than a claim that it is unmaintained.

One thing a commercial team should read before taking the dependency is the terms of use section, which the project attaches to the source and which states political conditions on using it. It is unusual in a library README, and whether it affects your own use is a question for your own organisation, but it is written down and it is not hidden.

## Where Onova is the wrong tool

The first limit is the platform and it is stated plainly in the feature list: the library targets .NET Framework 4.6.1 and .NET Standard 2.0, Windows only, and supports desktop apps built with .NET Core 3.0 or newer. There is no macOS or Linux build, and the reason is structural rather than a missing port. The technique is a separate executable that overwrites files in a running application's directory, and on Unix you can replace a running executable's file while the process keeps running, whereas on Windows the file is locked. The approach is built around the platform that makes it hard.

The second is the distribution model. The library is for portable applications distributed as archives, and it says so explicitly as opposed to installers or packages. If you ship through an MSI, an MSIX, the Microsoft Store, a Linux package manager or an auto-updater that already exists, you have a mechanism and this one duplicates it.

The third is the pre-release limitation. Because .NET assemblies do not support semantic versions, the GitHub and NuGet resolvers ignore pre-releases entirely. A product that ships a beta to a subset of users, or a release candidate to testers, cannot use those resolvers to deliver it, and has to fall back on the web manifest, where the publisher controls the list by hand.

The fourth is the trust model. An update mechanism that downloads an executable and runs it is the highest-value target in a desktop application, and Onova's resolvers do not describe any signature verification of the downloaded package. The GithubPackageResolver in particular resolves assets from a GitHub release by pattern, so whoever can publish a release can ship a new binary to every user of your application. A team with a policy on code signing will have to add that verification itself, or use a signing step in the publishing pipeline and treat the library as the transport rather than the trust boundary.

And the fifth is the fragmentation of the model. Five resolvers and two extractors, each with its own configuration and its own naming convention, is a fair amount of surface for a small utility, and the version parsing is convention-based rather than formal, so a file named MyProject-v2.1.5.zip and a release named v1.0 are two different sets of rules that a user has to get right by hand.

## Conclusion

Onova is worth adopting if you ship a portable .NET desktop application as a zip archive, because it does the one thing that is awkward to do yourself, replacing the files of a running program, and it does it with a separate updater executable so the running process can exit first, and because the resolver model lets the same updater read a local folder, a GitHub release, a plain text manifest on your own server or a NuGet feed. It is the wrong tool if you distribute through an installer or a package manager, because the whole model is in-place replacement of a portable folder. Before you adopt it, check three things: that you are on Windows, since the library targets .NET Framework 4.6.1 and .NET Standard 2.0 and the project states Windows only; that your application is .NET Core 3.0 or newer for the self-contained updater to work as described; and that you read the terms of use the project attaches to the source, because it states conditions of use that a commercial product team should read before taking a dependency, and it also notes that the development is funded by the community and that a project status badge is set to maintenance.

## FAQ

### How do I install Onova and check for an update?

Run dotnet add package Onova, then create an UpdateManager with a resolver and an extractor, for example new LocalPackageResolver("c:\\test\\packages", "*.zip") and new ZipPackageExtractor(), and call CheckPerformUpdateAsync() to check and install in one operation. If you want to control the steps yourself, use CheckForUpdatesAsync, PrepareUpdateAsync, LaunchUpdater and then exit the process so the updater can overwrite the files.

### Where can Onova look for update packages?

Through an IPackageResolver implementation. The built-in options are LocalPackageResolver for a directory on disk, GithubPackageResolver for GitHub release assets, WebPackageResolver for a plain-text version manifest on a URL, NugetPackageResolver for a NuGet feed, and AggregatePackageResolver to combine several sources. Versions come from the file or release names by convention, and pre-releases are ignored by the GitHub and NuGet resolvers.

### Does Onova work on macOS and Linux?

No. The project states that it targets .NET Framework 4.6.1 and .NET Standard 2.0, Windows only, and supports desktop apps built with .NET Core 3.0 or newer. The technique is to overwrite the files of a running application through a separate updater executable, which is a constraint specific to how Windows locks running executables.

## Sources

- [License: MIT](https://github.com/Tyrrrz/Onova/blob/prime/LICENSE)
- [Project website](https://nuget.org/packages/Onova)
- [README](https://github.com/Tyrrrz/Onova/blob/prime/README.md)
- [Releases](https://github.com/Tyrrrz/Onova/releases)
- [Tyrrrz/Onova on GitHub](https://github.com/Tyrrrz/Onova)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tyrrrz-onova
