Skip to main content

.NET Integration

Consul Guardian runs as a sidecar next to your .NET application. Your app reads configuration from Consul KV as it normally does. Guardian independently watches those same keys, commits every change to Git, and can restore any key to a previous value when things go wrong.

No code changes are required in your .NET application.

How .NET apps typically read from Consul

Most .NET applications use one of these packages to read configuration from Consul KV:

PackageDescription
Winton.Extensions.Configuration.ConsulIntegrates Consul KV into IConfiguration
Consul (G-Research)Low-level Consul client for .NET
SteeltoeCloud-native .NET framework with Consul config support

Example: Winton.Extensions.Configuration.Consul

A typical Program.cs that loads configuration from Consul KV:

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddConsul(
"config/myapp",
options =>
{
options.ConsulConfigurationOptions = co =>
{
co.Address = new Uri("http://consul:8500");
};
options.ReloadOnChange = true;
options.Optional = false;
}
);

var app = builder.Build();

app.MapGet("/", (IConfiguration config) =>
{
var connectionString = config["ConnectionString"];
var featureFlag = config.GetValue<bool>("EnableNewFeature");
return Results.Ok(new { connectionString, featureFlag });
});

app.Run();

Your Consul KV structure might look like this:

config/myapp/ConnectionString  →  "Server=db.prod;Database=orders;..."
config/myapp/EnableNewFeature → "true"
config/myapp/MaxRetries → "3"

This setup works fine -- until someone pushes a bad value to ConnectionString at 2am and you have no idea what the previous value was.

Adding Guardian

Guardian watches the same config/ prefix your app reads from. It runs alongside your app, not inside it.

Docker Compose

version: "3.8"

services:
consul:
image: hashicorp/consul:1.17
ports:
- "8500:8500"
command: agent -dev -client=0.0.0.0

myapp:
build: .
ports:
- "5000:5000"
environment:
- CONSUL_HTTP_ADDR=http://consul:8500
depends_on:
- consul

guardian:
image: ghcr.io/consul-guardian/consul-guardian:latest
environment:
- CONSUL_GUARDIAN_CONSUL_ADDRESS=http://consul:8500
- CONSUL_GUARDIAN_WATCH_PREFIXES=config/,env/
- CONSUL_GUARDIAN_GIT_REPO_PATH=/data/repo
volumes:
- guardian-data:/data
depends_on:
- consul

volumes:
guardian-data:

That's it. Your myapp service is unchanged. Guardian runs as a separate container watching the same Consul instance.

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 2
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myregistry/myapp:latest
ports:
- containerPort: 5000
env:
- name: CONSUL_HTTP_ADDR
value: "http://consul.consul.svc.cluster.local:8500"

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: consul-guardian
spec:
replicas: 1
selector:
matchLabels:
app: consul-guardian
template:
metadata:
labels:
app: consul-guardian
spec:
containers:
- name: guardian
image: ghcr.io/consul-guardian/consul-guardian:latest
env:
- name: CONSUL_GUARDIAN_CONSUL_ADDRESS
value: "http://consul.consul.svc.cluster.local:8500"
- name: CONSUL_GUARDIAN_WATCH_PREFIXES
value: "config/,env/,feature-flags/"
- name: CONSUL_GUARDIAN_GIT_REPO_PATH
value: "/data/repo"
volumeMounts:
- name: guardian-data
mountPath: /data
volumes:
- name: guardian-data
persistentVolumeClaim:
claimName: guardian-pvc

Guardian runs as its own Deployment with a single replica. It does not need to be a sidecar container inside your app pod -- it watches Consul independently.

Restore scenario

Here's a real situation: someone updates the ConnectionString to a wrong value, and your .NET app starts throwing SqlException errors.

1. The bad change happens

consul kv put config/myapp/ConnectionString "Server=wrong-host;Database=orders;..."

Your app picks up the new value (thanks to ReloadOnChange = true) and starts failing.

2. Find the previous value with Guardian

# See the change history for this key
consul-guardian log --key config/myapp/ConnectionString

# Output:
# 2024-03-15T14:32:01Z abc1234 config/myapp/ConnectionString modified
# 2024-03-15T09:00:00Z def5678 config/myapp/ConnectionString modified
# 2024-01-10T11:20:00Z aaa1111 config/myapp/ConnectionString created

3. Restore the key

# Restore to the commit before the bad change
consul-guardian restore --key config/myapp/ConnectionString --commit def5678

Guardian writes the old value back to Consul using CAS (Check-And-Set). Your .NET app reloads the config automatically and starts working again.

4. Verify

consul kv get config/myapp/ConnectionString
# Server=db.prod;Database=orders;... ← correct value restored

The entire incident takes under a minute to resolve. No redeployment, no guessing what the old value was.

Zero code changes

To be clear: Guardian does not require any changes to your .NET application. It does not inject itself into your IConfiguration pipeline, does not proxy Consul requests, and does not modify your app's behavior in any way.

Your app talks to Consul. Guardian also talks to Consul. They are independent.