SharpAstro.AppShell 1.1.71

This package has a SemVer 2.0.0 package version: 1.1.71+acd368945e65e5b11a4438b34f693af6b52575a2.
dotnet add package SharpAstro.AppShell --version 1.1.71
                    
NuGet\Install-Package SharpAstro.AppShell -Version 1.1.71
                    
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="SharpAstro.AppShell" Version="1.1.71" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SharpAstro.AppShell" Version="1.1.71" />
                    
Directory.Packages.props
<PackageReference Include="SharpAstro.AppShell" />
                    
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 SharpAstro.AppShell --version 1.1.71
                    
#r "nuget: SharpAstro.AppShell, 1.1.71"
                    
#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 SharpAstro.AppShell@1.1.71
                    
#: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=SharpAstro.AppShell&version=1.1.71
                    
Install as a Cake Addin
#tool nuget:?package=SharpAstro.AppShell&version=1.1.71
                    
Install as a Cake Tool

SharpAstro.AppShell

Desktop app-shell plumbing for single-window native applications. Pure managed, AOT- and trim-friendly, one dependency (Microsoft.Extensions.Logging.Abstractions).

dotnet add package SharpAstro.AppShell

Why this exists

Register a file type and every double-click in Explorer becomes a fresh process — its own GPU device, its own font atlas, its own copy of every cache it touches. What the user wanted was for the file to appear in the window already on screen.

InstanceGate turns those later launches into a message to the running instance, and ForegroundActivation is the half that makes its window actually come to the front.

The identity is yours to choose

A gate is claimed on a channel, and a channel is a scope plus an arbitrary identity string. That one parameter is the whole policy:

// One instance for the whole application: every file joins the running window.
var channel = InstanceGate.ChannelFor("my-viewer");

// One instance per open folder: a file in a folder already on screen activates that window,
// a file anywhere else gets a new one.
var channel = InstanceGate.ChannelFor("my-viewer", InstanceGate.NormalizePathIdentity(folder));

Startup then reads:

var gate = InstanceGate.TryClaim(channel, logger);
if (gate is null)
{
    // Somebody already owns this identity. Hand them the file and leave.
    if (InstanceGate.TryHandOff(channel, filePath, TimeSpan.FromSeconds(5), logger))
    {
        return 0;
    }
    // Hand-off failed: fall through and open it here. An extra window beats nothing happening.
}

// ... build the window, then once per frame:
while (gate?.TryDequeue(out var request) == true)
{
    Open(request.Payload);
    RaiseWindow();          // your toolkit's raise; the grant has already been made for you
}

NormalizePathIdentity is what makes the per-folder mode work: it folds a trailing separator, a relative path and (on Windows and macOS only) a difference in case into one identity, so C:\Data and c:\data\ do not open two windows onto the same folder.

Three things it gets right that are easy to get wrong

The pipe is the lock. A named pipe with a single server instance can only be created once, so claiming it is the primacy test — there is no separate mutex, one lifetime to get right, and no abandoned-mutex case.

The accept loop is not on the thread pool. The pipe is deliberately not Asynchronous, and the accept runs on a dedicated thread. An awaited accept resumes on a pool worker, and an app of this kind saturates its own pool with decode work — so the accept would queue behind that and a client would time out while the app was merely busy. A busy app refusing hand-offs is exactly the stray window this is meant to prevent, and an idle measurement never shows it.

Activation has to be granted by the process that is leaving. Windows will not let a background process pull itself to the front; the right must come from a process that currently holds it. The launching process does hold it — the shell just started it — so it spends the right on the target before sending the payload, via AllowSetForegroundWindow. Skip this and the running window flashes its taskbar button and stays behind, which reads as the hand-off silently failing. It is a no-op off Windows, where focus policy belongs to the compositor.

Failure is never fatal

Every path out of a failed hand-off returns false rather than throwing, and TryClaim returns null rather than throwing when it cannot gate at all. The caller's fallback is always "do the work in this process", because an extra window is a poor outcome and a double-click that does nothing is an unacceptable one.

Licence

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 (1)

Showing the top 1 NuGet packages that depend on SharpAstro.AppShell:

Package Downloads
SdlVulkan.Renderer

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.71 31 8/21/2026
1.0.61 21 8/20/2026
1.0.41 53 8/20/2026
1.0.31 24 8/20/2026
1.0.21 17 8/20/2026
1.0.11 28 8/20/2026