DevTunnels.Client.DependencyInjection
1.1.3-dev6
dotnet add package DevTunnels.Client.DependencyInjection --version 1.1.3-dev6
NuGet\Install-Package DevTunnels.Client.DependencyInjection -Version 1.1.3-dev6
<PackageReference Include="DevTunnels.Client.DependencyInjection" Version="1.1.3-dev6" />
<PackageVersion Include="DevTunnels.Client.DependencyInjection" Version="1.1.3-dev6" />
<PackageReference Include="DevTunnels.Client.DependencyInjection" />
paket add DevTunnels.Client.DependencyInjection --version 1.1.3-dev6
#r "nuget: DevTunnels.Client.DependencyInjection, 1.1.3-dev6"
#:package DevTunnels.Client.DependencyInjection@1.1.3-dev6
#addin nuget:?package=DevTunnels.Client.DependencyInjection&version=1.1.3-dev6&prerelease
#tool nuget:?package=DevTunnels.Client.DependencyInjection&version=1.1.3-dev6&prerelease
DevTunnels.Client
A .NET client for Microsoft Dev Tunnels. It drives the devtunnel CLI through a
typed, async, cancellable API: sign in, create a tunnel with a stable address, host it, and follow the host
through disconnects, reconnects and takeovers.
Packages
| Package | Contents |
|---|---|
DevTunnels.Client |
The client, host sessions and the host monitor |
DevTunnels.Client.DependencyInjection |
AddDevTunnelsClient for IServiceCollection |
DevTunnels.Client.Installer |
Installs the devtunnel CLI (winget, Homebrew, or the official script) |
Targets .NET 9, 10 and 11. Native AOT compatible.
dotnet add package DevTunnels.Client
Quick start
A tunnel with an ID you choose keeps its address for as long as it exists. Create it once, host it on every run, and the public URL never changes.
using DevTunnels.Client;
using DevTunnels.Client.Hosting;
using DevTunnels.Client.Ports;
using DevTunnels.Client.Tunnels;
DevTunnelsClient client = new();
DevTunnelCliProbeResult probe = await client.ProbeCliAsync();
if (!probe.IsInstalled || !probe.MeetsMinimumVersion)
{
throw new InvalidOperationException(probe.FailureReason);
}
await client.EnsureLoggedInAsync();
// Create-or-update is idempotent: an existing tunnel keeps its address.
DevTunnelStatus tunnel = await client.CreateOrUpdateTunnelAsync(
"my-app",
new DevTunnelOptions { AllowAnonymous = true, ExpirationHours = 720 });
// Use the ID the service returns from here on: it carries the region.
await client.CreateOrReplacePortAsync(
tunnel.TunnelId,
5000,
new DevTunnelPortOptions { Protocol = "http", AllowAnonymous = true });
await using IDevTunnelHostSession session = await client.StartHostSessionAsync(
new DevTunnelHostStartOptions { TunnelId = tunnel.TunnelId });
await session.WaitForReadyAsync();
Console.WriteLine(session.PublicUrl);
await using DevTunnelHostMonitor monitor = new(client, session);
monitor.StatusChanged += (_, e) =>
Console.WriteLine($"{e.Current.Connectivity}, taken over: {e.Current.IsTakenOver}");
await session.WaitForExitAsync();
How tunnels behave
Addresses. A port's public URL is https://<tunnel-id>-<port>.<region>.devtunnels.ms/. The tunnel ID is
yours to choose; the service picks the region and reports the full ID (my-app.usw2). Pass that reported ID
to port and host calls. A tunnel belongs to the signed-in account, so two machines hosting the same ID
compete for it: derive the ID from the machine when each machine needs its own address.
Expiration. The service deletes a tunnel after a period without activity: 30 days unless
ExpirationHours says otherwise (1 to 720 hours, which is 30 days). The window slides, and every host
connection renews it. ExpirationHours = null leaves the service's value, and an existing tunnel's, alone.
Access. Tunnels and ports are private to their owner by default. AllowAnonymous on the tunnel or on a
port lets anyone with the URL connect. A browser opening the URL for the first time sees Microsoft's
anti-phishing page once and continues from there; non-GET requests, requests that do not accept
text/html, and requests with an X-Tunnel-Skip-AntiPhishing-Page header skip it, so webhooks and API
clients are unaffected.
Protocols. A port is http, https or auto (the default). The relay terminates TLS, so a local
plain-HTTP server uses http. The host rewrites the Host header of forwarded requests to localhost.
Sign-in. Microsoft Entra ID, Microsoft accounts and GitHub accounts. The CLI caches the login in the
system keychain for several days; GetLoginStatusAsync tells you when it has lapsed.
Restarts. CreateOrUpdateTunnelAsync and CreateOrReplacePortAsync leave a tunnel, port and access
that already match alone, so restarting an app does not drop anonymous access for a moment or change
anything a client depends on.
Following a host
IDevTunnelHostSession is the devtunnel host process. DevTunnelHostMonitor follows it for as long as it
runs and reports a DevTunnelHostStatus:
Connectivity |
Meaning |
|---|---|
Starting |
The host has not connected yet |
Connected |
This host holds the relay and serves |
Disconnected |
This host lost the relay. The CLI reconnects by itself, so this can recover |
Stopped |
The host process ended. Start a new session |
IsTakenOver is true when this host is disconnected while the service still counts a host on the tunnel:
another machine or process on the account holds it. Ports maps each hosted port to its public URL, and
HostConnections is the service's count across the whole account.
The monitor reads the host's output first, because that is immediate and local, and asks the service every
ReconcileInterval (30 seconds by default) and whenever the output is not understood.
WaitForConnectedAsync completes on the first Connected; ReconcileAsync asks the service now.
Disposing the monitor leaves the session running. A host process also outlives a killed parent and keeps a
new host off the tunnel while it holds the relay, so record IDevTunnelHostSession.ProcessId and end a
leftover host before starting the next one.
Requirements
The devtunnel CLI, version 1.0.1435 or newer. ProbeCliAsync reports whether it is installed and new
enough. The client looks for it in this order:
DevTunnelsClientOptions.CliPathOverride- The
DEVTUNNEL_CLI_PATHenvironment variable - Known install locations, which need no
PATHentry:- Windows:
%LOCALAPPDATA%\Microsoft\DevTunnels\,%LOCALAPPDATA%\Microsoft\WinGet\Links\,~/.devtunnels/bin/,~/bin/ - macOS and Linux:
~/bin/,~/.local/bin/,/usr/local/bin/,/opt/homebrew/bin/
- Windows:
- The system
PATH
Installing the CLI
using DevTunnels.Client.Installer;
DevTunnelCliInstaller installer = new();
DevTunnelCliInstallResult result = await installer.InstallAsync();
Console.WriteLine(result.Success
? $"Installed with {result.InstallerUsed} at {result.InstalledPath}"
: $"Install failed: {result.FailureReason}");
| Platform | Installer | Command |
|---|---|---|
| Windows | winget |
winget install Microsoft.devtunnel (user scope, no elevation) |
| macOS | brew |
brew install --cask devtunnel |
| macOS without Homebrew, Linux | curl or wget |
curl -sL https://aka.ms/DevTunnelCliInstall \| bash |
Every location an installer writes to is in the client's search list, so ProbeCliAsync finds a fresh
install right away without a restart or a PATH change. UninstallAsync removes it again.
Dependency injection
using DevTunnels.Client.DependencyInjection;
builder.Services.AddDevTunnelsClient(options => options.CommandTimeout = TimeSpan.FromSeconds(15));
IDevTunnelsClient is registered as a singleton.
API
| Area | Members |
|---|---|
| CLI | ProbeCliAsync, GetVersionAsync |
| Sign-in | GetLoginStatusAsync, EnsureLoggedInAsync, LoginAsync, LogoutAsync |
| Tunnels | CreateOrUpdateTunnelAsync, GetTunnelAsync, ListTunnelsAsync, DeleteTunnelAsync |
| Ports | CreateOrReplacePortAsync, UpdatePortAsync, GetPortListAsync, DeletePortAsync |
| Access | GetAccessAsync, CreateAccessAsync, ResetAccessAsync, GetAccessTokenAsync |
| Hosting | StartHostSessionAsync, IDevTunnelHostSession, DevTunnelHostMonitor |
| Anything else | ExecuteRawAsync(["user", "show", "--json", "--nologo"]) runs any CLI command |
DevTunnelsClientOptions sets the CLI path, the command timeout (10 seconds), retries (3 attempts, 2
seconds apart), the minimum CLI version and the preferred sign-in provider. CLI failures throw
DevTunnelsCliException.
Building
dotnet build DevTunnels.Client.slnx -c Release
dotnet test DevTunnels.Client.slnx -c Release --no-build
dotnet csharpier check .
examples/DevTunnels.Client.Example is an interactive sample.
License
MIT. See LICENSE.txt.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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. net11.0 is compatible. |
-
net10.0
- DevTunnels.Client (>= 1.1.3-dev6)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.15)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.15)
- Microsoft.Extensions.Options (>= 10.0.15)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.15)
-
net11.0
- DevTunnels.Client (>= 1.1.3-dev6)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 11.0.0)
-
net9.0
- DevTunnels.Client (>= 1.1.3-dev6)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.15)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.15)
- Microsoft.Extensions.Options (>= 10.0.15)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.15)
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 |
|---|---|---|
| 1.1.3-dev6 | 46 | 11/11/2026 |
| 1.1.3-dev5 | 43 | 11/10/2026 |
| 1.1.3-dev4 | 85 | 11/3/2026 |
| 1.1.3-dev3 | 52 | 11/3/2026 |
| 1.1.3-dev2 | 56 | 11/3/2026 |
| 1.1.3-dev1 | 300 | 9/23/2026 |
| 1.1.2-dev1 | 137 | 8/13/2026 |
| 1.1.1-dev1 | 256 | 6/23/2026 |
| 1.1.0 | 636 | 6/21/2026 |
| 1.1.0-dev1 | 260 | 4/18/2026 |
| 1.0.4 | 399 | 4/18/2026 |
| 1.0.3 | 132 | 4/17/2026 |
| 1.0.2 | 134 | 4/17/2026 |
| 1.0.1 | 130 | 4/15/2026 |
| 1.0.0 | 135 | 4/15/2026 |
| 1.0.0-rc1 | 397 | 3/27/2026 |
| 0.1.1 | 129 | 4/15/2026 |
| 0.1.0 | 186 | 3/13/2026 |