Updates, Versioning & Telemetry
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Shipping version 1.0 of a desktop application is the start of its life, not the end. After release the same few questions come back every week. Which build is this user running? How does version 1.1 reach them without a reinstall? What happens when 1.1 turns out to be broken? How do you find out it is broken before the support inbox fills up? On Windows each question has a different answer for each distribution channel, and the answers interact: a version number that is perfectly good for WinGet can be rejected by the Microsoft Store, and a crash report channel that exists for Store installs says nothing about a ClickOnce install. This page walks through the four jobs in order — numbering, updating, rolling back and observing — for Bookshelf Desktop, the running scenario of this section.
The page was written against these versions, current in October 2026:
-
.NET 10 LTS, latest patch 10.0.13, with C# 14. .NET 11 is at release candidate 2 (general availability is expected 10 to 13 November 2026); nothing on this page depends on a .NET 11 change. .NET 8 and .NET 9 both end support on 10 November 2026, so a "which runtime is the user on" telemetry field has a real use.
-
Visual Studio 2026 18.11.1 and the Windows App SDK 2.5.1 (stable, released 16 September 2026), which adds the
windows.diagnosticServiceModulepackage extension discussed under crash reporting. -
WinGet: winget-cli 1.29.380 and winget-create 1.12.13.0.
-
Microsoft Store Policies 7.20, published 15 September 2026.
-
CommunityToolkit.Mvvm 8.4.2 for view models. Versioning and telemetry code in this page sits in services behind interfaces, so the view models stay free of it.
How the packages themselves are built, signed and listed belongs to the publishing pages of this section: .NET Publishing Options, MSIX Packaging & Signing and Distribution Channels. This page assumes you know which channel you ship through and covers what happens to a build after it leaves your machine. CI/CD with GitHub Actions shows how to stamp the version numbers below from a pipeline.
The Update Lifecycle at a Glance
Every channel answers the same four questions, but with different machinery. The diagram shows where each channel gets its notion of "newer", who performs the update, and what a rollback means.
The common thread is that every automatic mechanism compares a version number, and moves only to a higher one unless you explicitly say otherwise. Getting numbering right is therefore the foundation of everything else.
Version Numbers in a .NET Desktop App
A single Bookshelf Desktop build carries several version numbers, each read by a different consumer. They can all differ, which is a feature when you understand it and a source of confusion when you do not.
| Number | Where it lives | Read by | Format |
|---|---|---|---|
Assembly version |
|
The CLR, as part of the assembly identity |
Four parts, |
File version |
|
Windows Explorer’s Details tab, installers, some update tools |
Four parts |
Informational version |
|
People: shown as "Product version" in file properties; not used at run time |
Any string, typically SemVer plus a commit hash |
Package version |
|
Windows packaging, the Store, App Installer |
Four parts, |
ClickOnce publish version |
|
ClickOnce update checks |
Four parts |
NuGet package version |
|
NuGet restore |
SemVer, with an optional pre-release suffix |
The assembly version is part of an assembly’s identity: two assemblies that differ only by version number are, to the
runtime, different assemblies. The informational version is documented as text for people, with no run-time role. That
division suggests a practical rule for an application (as opposed to a library): keep AssemblyVersion stable or
tied to the major version, and put the real release identity in the file, informational and package versions.
Stamping the Version in the Project File
The SDK-style project file is the single place to set all of them. A Directory.Build.props file in the solution
root lets every project in the Bookshelf Desktop solution (Bookshelf.Core, Bookshelf.Desktop, the test
projects) share the same numbers.
<!-- Directory.Build.props -->
<Project>
<PropertyGroup>
<!-- The SemVer release the humans talk about -->
<VersionPrefix>1.4.0</VersionPrefix>
<!-- Empty for a final release; "rc.1", "beta.2" etc. for a pre-release -->
<VersionSuffix></VersionSuffix>
<!-- Identity for the runtime: only changes with a breaking release -->
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<!-- What Explorer shows on the Details tab; the build number comes from CI -->
<FileVersion>1.4.0.$(BUILD_NUMBER)</FileVersion>
<!-- Appends the commit hash (SourceRevisionId) when Source Link is present -->
<IncludeSourceRevisionInInformationalVersion>true</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>
Two details are worth knowing. IncludeSourceRevisionInInformationalVersion defaults to true, and from .NET 8 the
SDK fills SourceRevisionId with the commit hash when Source Link is present, so the informational version of a
default build already ends in the commit hash. A bare $(BUILD_NUMBER) that is empty on a developer machine
produces a malformed file version, so a real build gives it a default (for example 0) in the same file.
Showing the Version to the User and to Telemetry
Users report problems using the version they see in the About box, so that is the number that matters most for support. Read it from the informational version, which carries the commit hash, behind a small service so the view model stays testable.
using System.Reflection;
namespace Bookshelf.Desktop.Services;
public interface IAppInfo
{
string Version { get; } // "1.4.0+3f2a9c1"
string ShortVersion { get; } // "1.4.0"
}
public sealed class AppInfo : IAppInfo
{
public AppInfo()
{
var assembly = typeof(AppInfo).Assembly;
Version = assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion
?? assembly.GetName().Version?.ToString()
?? "0.0.0";
// Drop the "+commit" build metadata for display
var plus = Version.IndexOf('+');
ShortVersion = plus >= 0 ? Version[..plus] : Version;
}
public string Version { get; }
public string ShortVersion { get; }
}
Source: Assembly versioning
A packaged app has a second source of truth, the package version. In a packaged WinUI 3 or WPF app with package
identity, Windows.ApplicationModel.Package.Current.Id.Version returns it; an unpackaged app has no package, so the
assembly attributes remain the portable choice. Log both when they exist, because a mismatch between the manifest and
the assemblies is one of the commonest packaging mistakes.
Semantic Versioning for Applications
Semantic Versioning (SemVer 2.0.0) writes a version as MAJOR.MINOR.PATCH. Increase MAJOR for incompatible API
changes, MINOR for backward-compatible features and PATCH for backward-compatible fixes; a hyphen introduces a
pre-release identifier (1.0.0-alpha.1, ordered below 1.0.0) and a plus sign introduces build metadata
(for example 1.0.0+exp.sha.5114f85), which is ignored when comparing precedence. A book on building CLI applications with .NET
recommends semantic versioning together with a plan for rolling back in its distribution chapter, and the advice
transfers to a desktop app with one adjustment: an application has no public API to break, so decide what "breaking"
means to your users.
| Part | Library meaning | Suggested meaning for Bookshelf Desktop |
|---|---|---|
MAJOR |
Incompatible API change |
Incompatible change to the data on disk, a dropped runtime or OS, a changed licence |
MINOR |
New backward-compatible feature |
A new feature or screen, a new import format |
PATCH |
Backward-compatible fix |
A bug fix, a dependency security update |
Pre-release |
|
A build for a Store flight, a WinGet preview channel or an internal ring |
Build metadata |
|
The commit hash and CI run number, shown only in the About box |
The wrinkle on Windows is that the formats do not match. SemVer has three numeric parts and optional text; the assembly, file, package and ClickOnce versions have exactly four numeric parts and no text. The conversion therefore has to be defined once, written down and automated.
| SemVer | Four-part version | Rule |
|---|---|---|
|
|
Append a zero revision. For the Store this is mandatory (see below). |
|
|
The patch maps to the third part. |
|
|
Four-part versions cannot carry text, and the fourth part must stay |
Two rules keep this honest. Every four-part part is limited to a 16-bit integer (0 to 65535) in a Store package, so a
CI run number or a date such as 20261023 in the build part would overflow; use a short counter. And a pre-release
must never be compared as a higher version than the release it precedes, so a package for 1.5.0-rc.1 should use a
lower four-part number than the one for 1.5.0.
MSIX Package Versions
The package version lives in the Identity element of Package.appxmanifest. It is the number App Installer and the
Store compare, so it is the number that decides whether an update happens at all.
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10">
<Identity
Name="Example.BookshelfDesktop"
Publisher="CN=Example Publisher"
Version="1.4.0.0"
ProcessorArchitecture="x64" />
<!-- ... Properties, Dependencies, Applications ... -->
</Package>
Source: Identity element
The schema defines Version as a required version string in quad notation, major.minor.build.revision, for example
1.2.3.4. The Microsoft Store adds rules on top of the schema, from its package requirements page:
| Store rule | Consequence for Bookshelf Desktop |
|---|---|
For Windows 10 and 11 packages the last (fourth) part is reserved for Store use and must be |
Build |
The other parts are integers from 0 to 65535, and the first part cannot be |
The very first release is |
The Store always offers the highest-versioned applicable package, and submissions may arrive in any order |
A forgotten bump is not an error; the old package simply never wins. Higher number means newer. |
Several packages may share a version if their architecture differs |
An x64 and an Arm64 package can both be |
The package Publisher must match the subject of the signing certificate, and Name is case-sensitive. Those are
packaging concerns and appear in MSIX Packaging & Signing; for versioning the one thing to remember is that the
same identity (Name plus Publisher) with a higher Version is an update, while a different identity is a
different application.
Updates per Channel
Microsoft Store
For an MSIX submitted to the Store, the Store re-signs the package with a Microsoft certificate after certification
and hosts it on its content network, and the operating system performs the update; you write no update code. Your
tasks are to submit a package whose Version is higher than the live one and to wait for certification. The Store
offers every customer the highest-versioned package that applies to their device. Store package flights let you send
a build to a chosen group before everyone, which is how a pre-release channel is done there. EXE and MSI submissions
are different: you host the installer, sign it yourself, and the installer is not allowed to change after submission,
so updates are a new submission each time.
App Installer: OnLaunch and AutomaticBackgroundTask
An .appinstaller file is an XML file that names the MSIX and says how to update it. Users install by opening the
.appinstaller, which associates the app with it; later you update the file to point at the new package, and Windows
picks the change up. The documented update settings are small:
| Setting | Minimum Windows 10 version | Behaviour |
|---|---|---|
|
1709 |
Checks for an update when the user launches the app. Can show UI. |
|
1803 |
0 to 255 hours, default 24: do not check again if the last check was more recent. |
|
1903 |
Whether the user sees UI for the update. |
|
1903 |
|
|
1803 |
Checks every 8 hours regardless of launches. Cannot show UI. |
|
1903 (the schema reference page says 1809) |
Allows moving to a lower version as well as a higher one. Without it the app can only move up. |
The following file mirrors the documented structure: launch-time checks with a prompt, a background check and no forced downgrade.
<?xml version="1.0" encoding="utf-8"?>
<AppInstaller
xmlns="http://schemas.microsoft.com/appx/appinstaller/2021"
Version="1.4.0.0"
Uri="https://downloads.example.com/bookshelf/Bookshelf.appinstaller">
<MainPackage
Name="Example.BookshelfDesktop"
Publisher="CN=Example Publisher"
Version="1.4.0.0"
ProcessorArchitecture="x64"
Uri="https://downloads.example.com/bookshelf/Bookshelf_1.4.0.0_x64.msix" />
<UpdateSettings>
<OnLaunch HoursBetweenUpdateChecks="12" ShowPrompt="true" UpdateBlocksActivation="false" />
<AutomaticBackgroundTask />
</UpdateSettings>
</AppInstaller>
A few cautions. The Version on the root element is the version of the .appinstaller file, and the MainPackage
Name, Publisher and Version must match the package identity exactly. Updates can be served over https, http or
smb. And the ms-appinstaller: URI protocol has been disabled by default since December 2023, so a "Install"
button that uses it no longer works on most machines: link to the .appinstaller file itself, or publish to the
Store. The file uses the 2021 schema namespace on purpose: Visual Studio still generates the 2017/2 schema, which
does not support ShowPrompt, UpdateBlocksActivation or HoursBetweenUpdateChecks and silently ignores them, as
Distribution Channels explains.
WinGet
WinGet updates are pull-based: nothing happens until the user, a script or a management tool runs winget upgrade.
Your part is to publish a new manifest version to the winget-pkgs repository, which the wingetcreate tool
automates.
# What can be upgraded? (no arguments lists available upgrades)
winget upgrade
# Upgrade one package by id
winget upgrade --id Example.BookshelfDesktop
# Upgrade everything that has an update
winget upgrade --all
# Include packages whose installed version cannot be determined
winget upgrade --all --include-unknown
# Include packages with a non-blocking pin
winget upgrade --all --include-pinned
Source: upgrade command
The documentation notes that packages without a version are always "latest", so they are skipped unless
--include-unknown is given, and that pinned packages are skipped by --all unless --include-pinned is given (and
then only those with non-blocking pins). winget update is an alias of winget upgrade.
On the publisher side, wingetcreate update takes the id of an existing manifest and the URL of the new installer,
generates the new manifest version and, with --submit, opens the pull request. The same flow is scripted in CI/CD with GitHub Actions.
# After publishing the new installer to a stable URL
wingetcreate update Example.BookshelfDesktop `
--version 1.4.0 `
--urls https://downloads.example.com/bookshelf/Bookshelf-1.4.0-x64.exe `
--submit
Source: winget-create
The update command takes the package id followed by --urls (-u), --version (-v) and --submit (-s);
wingetcreate update --help lists the rest. The pull request goes through automated validation and, for
many packages, a moderator review, so a WinGet release can lag behind your own site by hours or days. The manifest
must also list the correct installer hash, and the installer URL must never be rewritten afterwards: a changed file
at an old URL fails the hash check.
ClickOnce
ClickOnce applications are self-updating by design. The deployment manifest holds the version clients should run, and a client compares it with what is installed. For .NET (Core 3.1, 5 and later) the options are narrower than for .NET Framework:
| Feature | .NET Framework | .NET 5 and later (WPF and WinForms) |
|---|---|---|
Check before start |
Default strategy |
The only supported strategy |
Check after start in the background, |
Supported |
Unsupported |
|
Supported |
Not available; since .NET 7 deployment properties are exposed through environment variables |
Mandatory update with a minimum required version |
|
Same attribute in the deployment manifest |
Manifest tool |
|
|
With the default before-start check, a client that finds a newer version either installs it (when it is required) or asks the user whether to upgrade or start the existing version. To make an update mandatory, set the minimum required version; the documentation says that only the before-start strategy guarantees an older version can no longer run.
<!-- Deployment manifest fragment: clients below 1.3.0.0 must update before running -->
<deployment install="true" minimumRequiredVersion="1.3.0.0">
ClickOnce is not available for WinUI 3 applications, and updates need network connectivity: with no connection the app runs without checking, whatever strategy you chose.
Installers You Host, and Third-Party Updaters
MSI and EXE installers (built with tools such as WiX, Inno Setup or NSIS) carry no update mechanism of their own that Windows drives for you. Options are a manual download, a managed deployment, WinGet, or an updater inside the application. Velopack is a third-party installer and update framework for .NET desktop apps; it is not a Microsoft product and is only named here so you can evaluate it at https://velopack.io/. Whatever you choose, an in-app updater must follow the rules in the security page: fetch over https, verify a signature on what you download, and never run unsigned code. See Security & App Isolation, which discusses update-channel security.
Rolling Back
A rollback is a decision made at release time, not a button you press during an incident, so plan it per channel.
-
Store. Create a new submission, remove the problematic package and upload the earlier one. People who already got the bad package keep it, because your older package has a lower version; this only stops new acquisitions. To fix the people who already have it, submit a package with a version higher than the bad one as soon as you can.
-
App Installer. Updates normally only move up. Add
ForceUpdateFromAnyVersionset totrueto allow a lower version, point the.appinstallerat the previous package, and keep theMainPackageidentity identical. -
WinGet. Publish a new, higher manifest version rather than editing an old one, so the fix reaches everyone who runs
winget upgrade. -
ClickOnce. ClickOnce documents that updates can be rolled back to an earlier version by an end user or an administrator, and publishing a version with a raised minimum required version forces everyone forward.
-
Everything. Keep the previous release’s installers, symbols and a note of the schema version of any data the app wrote. A rollback that reads a database written by a newer version is a data bug, not a packaging problem.
Because the answer depends on data compatibility, make migrations forward-only and tolerant: Bookshelf Desktop’s
local database stores a schema_version, and an older build that meets a higher number opens the file read-only and
says why instead of corrupting it.
Crash Reporting and Telemetry After App Center
Visual Studio App Center was the default crash and analytics service for many desktop apps. It was scheduled for retirement on 31 March 2025, after which sign-in and API calls stopped; the Analytics and Diagnostics features were given extensions, the latest of which keeps them running until the end of March 2027. Microsoft’s guidance is to move to other services and points Store app developers to Partner Center for analytics and diagnostics. There is no single drop-in replacement from Microsoft for a desktop app, so assemble one from the layers below.
| Layer | What it gives you | Applies to |
|---|---|---|
Windows Error Reporting (WER) |
Crash and hang reports collected by the operating system when users opt in |
Every app on Windows |
Partner Center health report |
Crash rate, hang rate, devices affected, stack traces and trends per app version |
Store submissions |
|
Lets a self-contained .NET MSIX app declare diagnostic modules so WER can load them and collect actionable crash dumps |
Self-contained .NET MSIX apps |
Local dumps |
Full or mini dumps written locally on crash, no upload |
Any app, on machines you control |
Your own handlers and logging |
Exceptions you catch, with context you choose |
Your code |
OpenTelemetry |
Logs, metrics and traces exported to a backend you choose |
Your code, with your backend |
Windows Error Reporting and the Partner Center Health Report
WER lets users send Microsoft information about application faults and unresponsive applications; users choose
whether to report, and administrators can override with Group Policy. Developers can register with the Windows
Desktop Application Program to receive information about problems customers hit. For apps in the Store, the same
data reaches you through the Health report in Partner Center, which shows crash counts and unresponsiveness rates,
stack traces, affected OS and device combinations, and trends, and lets you compare app versions. The crash rate is
calculated from devices that opted in to share optional diagnostic data, so it is a sample, not a count of every
user. To get symbolicated stacks, submit the .msixupload file that Visual Studio creates: it includes an .appxsym
file (a compressed PDB of public symbols), and if you omit it no crash analytics are available for the app.
Source: Windows Error Reporting; Health report
The Windows App SDK 2.5.1 release notes list "Windows Error Reporting support": the windows.diagnosticServiceModule
package-manifest extension, which a self-contained .NET MSIX app can use to declare diagnostic modules so that WER
can load them and collect actionable crash dumps. The release notes do not show the manifest XML, so this page does
not either; read the current release notes before relying on it.
Local Dumps and Unhandled Exceptions
For machines you control (a pilot group, a test lab) WER can write dumps locally after a crash. The feature is not
enabled by default and needs administrator rights. The settings live under
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps: DumpFolder (default
%LOCALAPPDATA%\CrashDumps), DumpCount (default 10) and DumpType (0 custom, 1 mini, 2 full; default 1). A subkey
named after the executable, for example Bookshelf.Desktop.exe, overrides the global settings for that program only.
Full dumps contain process memory, which can include book data, file paths and tokens, so keep them in a folder with
a restricted ACL and delete them when the case closes. See Collecting user-mode dumps.
Inside the app, a WPF application has three places where an exception can surface after every try block has failed:
Application.DispatcherUnhandledException for the UI thread, AppDomain.CurrentDomain.UnhandledException for any
thread when the process is about to end, and TaskScheduler.UnobservedTaskException for tasks nobody awaited. Hook
all three in App.xaml.cs, route them to one ILogger (see Dependency Injection & the Generic Host) together with
the version from IAppInfo, and decide deliberately whether to continue. Setting Handled to true swallows an
exception and leaves the application in an unknown state; for most exceptions it is safer to log, offer to restart and
let the process end so that WER and the Health report record the crash. A WinUI 3 app uses
Application.UnhandledException and WinForms uses Application.ThreadException.
Telemetry with OpenTelemetry
OpenTelemetry is a cross-platform, open standard for emitting logs, metrics and traces. In .NET it builds on the
platform APIs you already have — ILogger<T> for logs, System.Diagnostics.Metrics.Meter for metrics and
System.Diagnostics.ActivitySource for traces — and exports to a backend over the vendor-neutral OTLP protocol or
a specific exporter. The backend is your choice: Azure Monitor and Application Insights, Prometheus and Grafana, or
a commercial vendor. For a desktop app the model differs from a server in two ways: the telemetry leaves a machine
you do not own, so it must be opt-in, and the network is unreliable, so exporters must buffer and drop rather than
block the UI thread.
using System.Diagnostics.Metrics;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
// In App.xaml.cs, after the user's consent has been read from settings
if (settings.TelemetryEnabled)
{
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService("Bookshelf.Desktop", serviceVersion: appInfo.ShortVersion))
.WithMetrics(m => m
.AddMeter(BookshelfMetrics.MeterName)
.AddOtlpExporter()); // endpoint from OTEL_EXPORTER_OTLP_ENDPOINT
}
public static class BookshelfMetrics
{
public const string MeterName = "Bookshelf.Desktop";
private static readonly Meter Meter = new(MeterName);
// Counts, never titles or file names
public static readonly Counter<long> BooksImported = Meter.CreateCounter<long>("books.imported");
}
The packages involved are OpenTelemetry, OpenTelemetry.Exporter.OpenTelemetryProtocol and, for the hosting
extension method used above, OpenTelemetry.Extensions.Hosting. The first two are listed on the Microsoft page; the
hosting package and its fluent configuration API are documented by the OpenTelemetry .NET project, and the API can
change between package versions.
Choose the metrics before the tool. A short list of counts answers most product questions: sessions started, features used, import failures by category, startup time, and the runtime and OS version (to know when you can drop .NET 8 or Windows 10). None of them needs a book title or a path.
Privacy Obligations
Crash reports and telemetry describe a person’s behaviour, so they carry legal and platform obligations. The page cannot give legal advice; it lists the engineering consequences.
-
Opt-in by default. Ask on first run, store the answer in the settings service, and make it changeable in the settings window. Register the OpenTelemetry providers only when the answer is yes.
-
Collect the minimum. Version, OS, runtime and anonymous counts. No book titles, library contents, file paths, user names, machine names or tokens. Scrub exception messages: an
IOExceptionmessage contains a path. -
Document it. State in a privacy policy what you collect, why, where it is stored, for how long and how to opt out. The Microsoft Store Policies apply to apps in the Store; read the current version (7.20 at the time of writing) for the privacy requirements that affect you.
-
Respect the platform setting. WER and the Partner Center crash rate are driven by the user’s Windows diagnostic data choices; your own telemetry must not behave as if those choices did not exist.
-
Protect what you keep. Dumps and logs can hold personal data. Restrict access, set a retention period, and delete on request.
-
Test it. A unit test that serialises every telemetry event for a representative session and asserts that no book title, path or token appears is cheap and catches the regression that a new developer will eventually introduce.
Source: Microsoft Store Policies
Pitfalls
-
Building a Store package with a non-zero fourth part (
1.4.0.17), or putting a date in a version part and overflowing 65535. -
Rewriting a file at an installer URL that a WinGet manifest hashes, so every install fails the hash check.
-
Linking to
ms-appinstaller:when the protocol is disabled by default on user machines. -
Setting
UpdateBlocksActivationtotruewithoutShowPromptset totrue. -
Expecting ClickOnce background update checks or the
ApplicationDeploymentAPI on .NET 5 and later. -
Assuming a Store rollback removes the bad build from machines that already have it.
-
Shipping without symbols, leaving the Health report with unreadable stacks.
-
Enabling telemetry before consent, or sending titles and paths in events.
Summary
Give every build one source of truth for its version, convert SemVer to the four-part forms once and automate it, and remember that the Store wants the fourth part to be zero. Let each channel update the way it was designed to — the Store and App Installer in the background, WinGet on request, ClickOnce before start — and decide your rollback path before you ship, because each channel rolls back differently. For observation, combine Windows Error Reporting and the Partner Center health report with your own exception logging and, only with consent, minimal OpenTelemetry metrics, and treat every dump and event as personal data until proven otherwise.
References
-
Velopack (third party)
-
Tidjani Belmansour, Building CLI Applications with C# and .NET, Packt, 2025, chapter 11, "Packaging and Distribution": https://www.packtpub.com/en-us/product/building-cli-applications-with-c-and-net-9781835882740