Lockjaw.Client
2.0.2.28
See the version list below for details.
dotnet add package Lockjaw.Client --version 2.0.2.28
NuGet\Install-Package Lockjaw.Client -Version 2.0.2.28
<PackageReference Include="Lockjaw.Client" Version="2.0.2.28" />
<PackageVersion Include="Lockjaw.Client" Version="2.0.2.28" />
<PackageReference Include="Lockjaw.Client" />
paket add Lockjaw.Client --version 2.0.2.28
#r "nuget: Lockjaw.Client, 2.0.2.28"
#:package Lockjaw.Client@2.0.2.28
#addin nuget:?package=Lockjaw.Client&version=2.0.2.28
#tool nuget:?package=Lockjaw.Client&version=2.0.2.28
Lockjaw.Client
Read the environment currently selected in the Lockjaw app from any .NET application, and react the moment it changes.
Lockjaw keeps every environment's values in one configuration — keys prefixed Development_,
Production_, or whatever else you declare — and lets you choose, per setting and while your app is
running, which one each resolves to. This package is the reading half.
using Lockjaw.Client;
var apiUrl = LockjawClient.Default.Get("ApiUrl");
var db = LockjawClient.Default.GetConnectionString("Main");
Ask for the key without its prefix; you get whichever environment is currently selected. No configuration on your side — the client and the app find each other by instance name.
Finding your instance
One Lockjaw manages several applications, each published under its own instance name. Name the one you want:
var client = new LockjawClient(new LockjawClientOptions { InstanceName = "OrderService" });
Or let the client find it. Lockjaw publishes the application names each instance serves, so a client can match its own and needs no configuration at all:
// In OrderService.exe — no instance name anywhere.
var client = new LockjawClient(new LockjawClientOptions { AutoDiscoverInstance = true });
Console.WriteLine(client.InstanceName); // "OrderService", resolved at construction
ApplicationName defaults to the entry assembly's name; set it explicitly if your executable is
named differently from the instance's target. Discovery is off by default and never overrides an
explicit choice: an explicit SnapshotPath or a non-default InstanceName wins, and if nothing
matches the client falls back to default.
To see what is available:
foreach (var instance in SnapshotStore.EnumerateInstances())
Console.WriteLine($"{instance.Instance}: {string.Join(", ", instance.Applications)}");
netstandard2.0, so it works from .NET Framework 4.6.1+ and .NET Core / .NET 5+.
Typed reads
var timeout = LockjawClient.Default.Get("TimeoutSeconds", 30); // parsed, with a fallback
var url = LockjawClient.Default.GetRequired("ApiUrl"); // throws if missing
Colon-delimited paths
Nested JSON flattens into colon paths, the same convention .NET configuration uses, so ask for a nested value by its path:
var level = LockjawClient.Default.Get("Logging:Level");
var host = LockjawClient.Default.Get("Hosts:0"); // arrays take their index
The environment prefix can sit on any segment of the path, which is what lets a single leaf vary while its siblings stay shared:
{
// The whole subtree varies:
"Development_Logging": { "Level": "Debug", "Console": { "Enabled": true } },
"Production_Logging": { "Level": "Warning", "Console": { "Enabled": false } },
// …or just one leaf, with its siblings shared:
"Telemetry": {
"Development_Endpoint": "http://localhost:4317",
"Production_Endpoint": "https://otel.contoso.com",
"SampleRate": "0.25"
}
}
Both forms give you Logging:Level, Logging:Console:Enabled and Telemetry:Endpoint — ask for the
path with the prefix removed, exactly as you would for a flat key. Connection strings nest the same
way, and a flat key that simply contains a colon (Development_Api:Url in app.config) works too.
Top-level pairs — individual keys that sit beside the AppSettings and ConnectionStrings sections
rather than inside them — are read as app settings, so Get("SomeTopLevelKey") returns them directly.
As an IConfiguration
LockjawClient is a Microsoft.Extensions.Configuration.IConfiguration, so its resolved values
are readable with the standard path syntax and bindable with IOptions:
IConfiguration config = LockjawClient.Default;
var db = config["ConnectionStrings:Main"]; // == config.GetConnectionString("Main")
var url = config["ApiUrl"]; // an AppSettings value, prefix already resolved
var flag = config["FeatureEnabled"]; // a top-level pair
App settings (including the top-level pairs and nested keys) appear at the root by their base key, and
connection strings under the conventional ConnectionStrings section. The environment prefix is
already stripped, so ConnectionStrings:Main reads whichever of Development_Main / Production_Main
is currently active — switch the environment in the Lockjaw app and the value follows. A change also
fires IConfiguration.GetReloadToken(), so IOptionsMonitor and reloadOnChange bindings refresh.
Add it to a host builder's configuration pipeline with AddLockjaw:
builder.Configuration.AddLockjaw(); // the Default client
// or a specific one:
builder.Configuration.AddLockjaw(myClient);
Because Lockjaw is layered last, its live, environment-switched values take precedence over the JSON and environment-variable sources added before it.
Reacting to changes
Changes are picked up live. The app pushes each change to the client over a SIPC semaphore mesh the moment you switch an environment or apply one to everything — the client does not poll or watch a file, so delivery is prompt and survives a locked-down machine where file-change notifications are unreliable (a redirected profile, a VDI/roaming setup, or aggressive endpoint protection). If the app is not running yet, the client keeps asking for the current snapshot and picks up the first change once it appears.
The event arrives on a background thread, so a WPF or WinForms handler must reach the UI thread
first — SubscribeOnUiThread does that for you when called from the UI thread, and returns a token
you dispose to unsubscribe:
_subscription = LockjawClient.Default.SubscribeOnUiThread((s, e) =>
{
ApiUrlText.Text = e.Value; // already on the dispatcher
});
Under the hood it captures the UI thread's SynchronizationContext — a
DispatcherSynchronizationContext on WPF — so this package needs no WPF reference of its own. For a
context you hold yourself, use SubscribeOn(context, handler). Off the UI thread, subscribe to
SettingChanged directly.
When the Lockjaw app isn't running
- Values stay available once the app has run once — the client reads the last published values.
- If nothing was ever published, the client falls back to its own
app.config,web.configorappsettings.json, read with the same prefix convention. So an app that references Lockjaw.Client still works standalone. - Published values always win, per key; the fallback only fills gaps.
SourceOf(key)tells you which it was:Published,LocalConfig, orNone.
switch (LockjawClient.Default.SourceOf("ApiUrl"))
{
case LockjawValueSource.Published: /* the user picked this in the app */ break;
case LockjawValueSource.LocalConfig: /* app not running; from my own config */ break;
case LockjawValueSource.None: /* nobody has this key */ break;
}
Your own fallback config
Ship the settings in your application's own appsettings.json and it keeps working on a machine
where Lockjaw has never run. Same prefix convention:
{
// No "Lockjaw" section — which environments exist is the Lockjaw app's business.
"AppSettings": {
"Local_ApiUrl": "http://localhost:5000/api",
"Development_ApiUrl": "https://localhost:5001/api",
"Production_ApiUrl": "https://api.contoso.com",
// Partial coverage is fine: with no Local value, Local resolves to Development.
"Development_LogLevel": "Debug",
"Production_LogLevel": "Warning",
// Unprefixed keys are served too, so ordinary settings keep working.
"SupportEmail": "support@contoso.com"
},
"ConnectionStrings": {
"Local_Main": "Server=(localdb)\\MSSQLLocalDB;Database=Contoso_Local",
"Development_Main": "Server=(localdb)\\MSSQLLocalDB;Database=Contoso_Dev",
"Production_Main": "Server=sql-prod-01;Database=Contoso"
}
}
Or in app.config / web.config:
<configuration>
<appSettings>
<add key="Local_ApiUrl" value="http://localhost:5000/api" />
<add key="Development_ApiUrl" value="https://localhost:5001/api" />
<add key="Production_ApiUrl" value="https://api.contoso.com" />
<add key="SupportEmail" value="support@contoso.com" />
</appSettings>
<connectionStrings>
<add name="Development_Main"
connectionString="Server=(localdb)\MSSQLLocalDB;Database=Contoso_Dev" />
<add name="Production_Main"
connectionString="Server=sql-prod-01;Database=Contoso" />
</connectionStrings>
</configuration>
Three things differ from the Lockjaw app's own configuration:
- No
Lockjawsection. Which environments exist is the app's business. The client needs the list only when its own keys use prefixes beyondDevelopment_/Production_. - Unprefixed keys are served, where the Lockjaw window hides them — so your ordinary settings can live in the same file, untouched.
- Prefixed keys resolve to
FallbackEnvironment, which defaults toDevelopment, so a machine without Lockjaw never quietly starts on production.
Anything beyond the defaults is set in code rather than in the file:
using var client = new LockjawClient(new LockjawClientOptions
{
// Only needed for prefixes beyond the built-in pair.
Environments = new List<EnvironmentDefinition>
{
new EnvironmentDefinition("Local", "Local_"),
new EnvironmentDefinition("Development", "Development_"),
new EnvironmentDefinition("Production", "Production_")
},
FallbackEnvironment = "Local", // which one the fallback picks
IncludeUnprefixedFallback = true, // serve plain keys too (the default)
FallBackToLocalConfig = true // false: require the Lockjaw app
});
Values the Lockjaw app publishes carry their environment name with them, so none of this affects those — it shapes the fallback only.
API
| Member | Purpose |
|---|---|
Get(key) / Get<T>(key, fallback) / GetRequired(key) |
Resolve an app setting (including top-level pairs) |
GetConnectionString(name) |
Resolve a connection string |
this[key] / GetSection / GetChildren / GetReloadToken |
IConfiguration — read by Section:Key path |
ToConfigurationSource() / AddLockjaw(builder) |
Add Lockjaw to an IConfigurationBuilder pipeline |
EnvironmentOf(key, kind) |
The environment name a key resolves to |
InstanceName |
The instance this client resolved to |
SourceOf(key, kind) |
Published, LocalConfig, or None |
All |
Every setting the client can resolve |
Environments |
The environments the app published, with names and colors |
IsAvailable / HasLocalFallback / UpdatedUtc |
Whether the app has published, whether a fallback loaded, and when |
SettingChanged |
Raised when a value changes (background thread) |
SubscribeOnUiThread(handler) / SubscribeOn(context, handler) |
Subscribe with events marshalled to the UI thread |
Refresh() |
Re-read now |
Options
| Option | Default | |
|---|---|---|
InstanceName |
default |
Must match the app's instance |
AutoDiscoverInstance |
false |
Find the instance that names this application |
ApplicationName |
the entry assembly's name | What discovery matches on |
SnapshotPath |
derived from the instance | Explicit file to read |
WatchForChanges |
true |
Receive live updates (pushed over a SIPC semaphore mesh) |
AllowCrossAccountHost |
false |
Join the app's mesh across a session/account boundary |
FallBackToLocalConfig |
true |
Read own config when the app has published nothing |
FallbackEnvironment |
Development |
Environment the fallback resolves to |
Environments |
Development_ / Production_ |
Prefixes recognised in the fallback |
IncludeUnprefixedFallback |
true |
Serve plain keys from own config |
UseLocalAppConfig / UseLocalJsonFile |
true |
Which fallback sources to read |
LocalJsonFilePath |
appsettings.json beside the app |
Fallback JSON file |
LockjawClient is thread-safe and implements IDisposable — dispose it to stop listening. Use
LockjawClient.Default for the common single-instance case.
Clients under a different account
The mesh's semaphores are session-local by default, so a same-user client on the same desktop hears
the push with no configuration. Reaching a client in another logon session or under a different
Windows account — a service account, say — puts the semaphores in the global namespace, which both
sides must opt into: set AllowCrossAccountClients on the app/host side (LockjawOptions, in
Lockjaw.Core) and AllowCrossAccountHost on the client side. The app's option is off by default
because the global namespace lets any authenticated account on the machine read the published values,
and it may require privilege to create.
Shipping
Lockjaw is a development aid. Keep production credentials out of config files that reach end users; the usual approach is to reference it from Debug builds only:
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<PackageReference Include="Lockjaw.Client" Version="2.0.1" />
</ItemGroup>
See also
- Lockjaw.Core — the shared model, if you want to read the published snapshot yourself.
- The Lockjaw app — the tray application that reads your configuration and publishes the values. Its built-in Help (F1) documents the whole system, including the real file paths on your machine.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. 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 was computed. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 is compatible. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Lockjaw.Core (>= 2.0.2.50)
- Microsoft.Extensions.Configuration (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
2.2.0
- Live updates now travel over SIPC, a semaphore-only peer mesh, instead of a named pipe — still a direct push from the Lockjaw app, with no sockets, pipes or shared files. Changes are delivered promptly even on locked-down machines where file-change notifications are unreliable.
- The client joins the app's mesh, subscribes for changes, and asks the app for the current snapshot on join, retrying until the app answers or appears. It still reads the last published snapshot at startup, so values stay available when the app is not running.
- Set AllowCrossAccountHost to receive updates from an app running in another logon session or under a different Windows account; it must match AllowCrossAccountClients on the app/host side. A same-user app on the same desktop needs no configuration.
- SettingChanged now fires when a previously published key is withdrawn — the app stops publishing it, or is disabled and publishes an empty snapshot — reporting the value it reverts to (the client's own config, or none). Previously a withdrawn value could leave consumers, and the IConfiguration provider, on the stale published value.
- The local fallback now resolves prefixed keys to the environment the client's own config declares as default (Lockjaw:DefaultEnvironment), so a key reverts to its default-environment value when nothing is published, exactly as on a production machine. FallbackEnvironment is now unset by default and, when set, still pins the fallback to a specific environment; it previously defaulted to Development.
2.1.0
- Live updates now arrive as a direct push from the Lockjaw app over a named pipe, replacing the snapshot-file watch. Changes are delivered promptly even on locked-down machines where file-change notifications are unreliable — redirected profiles, VDI/roaming setups, or aggressive endpoint protection.
- The client no longer uses FileSystemWatcher: it listens on the pipe and reconnects on its own if the app is not running yet or restarts. It still reads the last published snapshot at startup, so values stay available when the app is not running.
- Clients in another logon session are reached with no configuration; a client under a different Windows account is supported by enabling AllowCrossAccountClients on the app/host side.
2.0.2
- Environments are open-ended: declare any prefix (Local_, Test_, Staging_) instead of only Development_/Production_.
- EnvironmentOf returns the environment name; Environments exposes the published environment list.
- Falls back to the consuming application's own app.config / appsettings.json when nothing has been published; SourceOf reports where a value came from.
- SubscribeOnUiThread / SubscribeOn marshal SettingChanged onto a WPF or WinForms UI thread.
- Recovers from FileSystemWatcher buffer overflow so a dropped event cannot leave stale values.