JoshMakeStuff.Aspire.OpenLdap
0.8.0-preview.1
dotnet add package JoshMakeStuff.Aspire.OpenLdap --version 0.8.0-preview.1
NuGet\Install-Package JoshMakeStuff.Aspire.OpenLdap -Version 0.8.0-preview.1
<PackageReference Include="JoshMakeStuff.Aspire.OpenLdap" Version="0.8.0-preview.1" />
<PackageVersion Include="JoshMakeStuff.Aspire.OpenLdap" Version="0.8.0-preview.1" />
<PackageReference Include="JoshMakeStuff.Aspire.OpenLdap" />
paket add JoshMakeStuff.Aspire.OpenLdap --version 0.8.0-preview.1
#r "nuget: JoshMakeStuff.Aspire.OpenLdap, 0.8.0-preview.1"
#:package JoshMakeStuff.Aspire.OpenLdap@0.8.0-preview.1
#addin nuget:?package=JoshMakeStuff.Aspire.OpenLdap&version=0.8.0-preview.1&prerelease
#tool nuget:?package=JoshMakeStuff.Aspire.OpenLdap&version=0.8.0-preview.1&prerelease
JoshMakeStuff.Aspire.OpenLdap
A .NET Aspire client integration for OpenLDAP. Registers an LdapConnection (from System.DirectoryServices.Protocols) in DI, wired to the connection string published by the JoshMakeStuff.Aspire.Hosting.OpenLdap resource.
The package ID carries the
JoshMakeStuff.prefix becauseAspire.*is reserved on nuget.org. The API namespace is stillMicrosoft.Extensions.Hosting, soAddOpenLdapClient(...)resolves without any extrausing.
Install
dotnet add package JoshMakeStuff.Aspire.OpenLdap
Install this package in the service project that talks to LDAP (not the AppHost).
Usage
var builder = WebApplication.CreateBuilder(args);
builder.AddOpenLdapClient("ldap");
var app = builder.Build();
app.MapGet("/whoami", (LdapConnection conn) =>
{
var who = (ExtendedResponse)conn.SendRequest(new ExtendedRequest("1.3.6.1.4.1.4203.1.11.3"));
return Results.Text(Encoding.UTF8.GetString(who.ResponseValue ?? []));
});
app.Run();
The connectionName ("ldap" above) must match the resource name passed to AddOpenLdap(...) in your AppHost.
What gets registered
OpenLdapClientFactory(singleton) — parses the connection string, applies settings, and createsLdapConnectioninstances (CreateConnection()) or instrumentedOpenLdapClientinstances (CreateClient()). A service that outlives a single LDAP operation takes the factory and creates a client per operation, rather than capturing the transientOpenLdapClient— neither it nor the connection it wraps is thread-safe.LdapConnection(transient) — resolved from the factory.OpenLdapClient(transient) — an instrumented wrapper overLdapConnection; use it to get OpenTelemetry traces/metrics (see Telemetry).- A health check named
openldap_{connectionName}that performs a root-DSE search (disable withsettings.DisableHealthChecks = true).
Multiple directories (keyed)
To connect to more than one OpenLDAP resource, register each with AddKeyedOpenLdapClient — the connection name doubles as the DI service key:
builder.AddKeyedOpenLdapClient("corp");
builder.AddKeyedOpenLdapClient("partners");
app.MapGet("/corp", ([FromKeyedServices("corp")] LdapConnection conn) => /* ... */);
This registers OpenLdapClientFactory and LdapConnection as keyed services under the name, plus a health check named openldap_{name}.
Configuration
Bind from Aspire:OpenLdap in configuration, or pass a callback:
builder.AddOpenLdapClient("ldap", settings =>
{
settings.DisableHealthChecks = false;
// settings.ConnectionString — overrides the resolved connection string
});
The connection string is normally provided automatically by Aspire via ConnectionStrings:{connectionName}.
Reading and writing connection strings
OpenLdapConnectionStringBuilder handles both directions, so the format and its quoting rules
live in one place instead of being re-implemented by every consumer:
// Read
var settings = OpenLdapConnectionStringBuilder.Parse(connectionString);
var baseDn = settings.BaseDn;
// Write — for a host that synthesizes its own connection string from env vars or config
var connectionString = new OpenLdapConnectionStringBuilder
{
Endpoint = new Uri($"ldap://{host}:{port}"),
BaseDn = baseDn,
BindDn = bindDn,
BindPassword = password,
CaCertFile = caPath, // optional; omitted from the output when null or empty
}.Build();
Build() quotes any value containing ; or ", carrying leading/trailing whitespace, or empty
— so arbitrary passwords and DNs survive the round trip — and rejects the same endpoints Parse
rejects, meaning it cannot emit a string the parser would refuse.
It is a named method rather than a ToString() override on purpose: the object holds
BindPassword, and an override would turn any log line or string interpolation mentioning the
instance into a credential leak.
LDAPS trust
When the connection string carries CaCertFile=... (the hosting integration appends it under WithRequiredTls()), connections trust that CA. The mechanism is platform-specific:
- Windows — a managed callback validates the chain against the CA and that the certificate names the endpoint host.
settings.DisableTlsHostnameValidationrelaxes the hostname part only. - Linux — the CA is trusted natively by
libldapvia an OpenSSL hash-named certificate directory (staged automatically under the temp folder).libldapalways validates the peer, soDisableTlsHostnameValidationis rejected there. DNS-name endpoints are dialed by IP literal (libldap checks a name dial against the peer's reverse-DNS name, which loopback resolves to the machine hostname), so the server certificate must carry the endpoint's IP SAN — e.g.127.0.0.1for local development; the hosting integration's generated certificates do. - macOS — Apple's
LDAP.frameworkcannot trust a custom CA from managed code; connection creation fails with guidance. Setsettings.TrustConnectionStringCaCertificate = falseand add the CA to the system trust store out of band instead.
Telemetry
Operations issued through OpenLdapClient (resolve it from DI and call Send / SendAsync instead of using the raw LdapConnection) emit OpenTelemetry traces and metrics under the source/meter name Aspire.OpenLdap. AddOpenLdapClient registers the source and meter with the app's OpenTelemetry pipeline automatically, so they flow to whatever exporter you've configured (e.g. via Aspire's AddServiceDefaults).
builder.AddOpenLdapClient("ldap");
app.MapGet("/users", (OpenLdapClient ldap) =>
{
var resp = (SearchResponse)ldap.Send(
new SearchRequest("ou=users,dc=example,dc=org", "(objectClass=person)", SearchScope.Subtree, null));
return Results.Ok(resp.Entries.Count);
});
- Traces — one span per operation named
LDAP <op>(e.g.LDAP search), kindClient, with attributesdb.system.name=openldap,db.operation.name,server.address,server.port,db.response.status_code/db.ldap.result_code, and for searchesdb.ldap.scope,db.ldap.entries_returned,db.ldap.controls(control OIDs),db.ldap.paged. - Metrics — a histogram
db.client.operation.duration(seconds) tagged by operation, server, and result/error. - Privacy — search filters, DNs, entry attributes, control values, and paging-cookie bytes are never recorded.
- Disabling — set
settings.DisableTracingand/orsettings.DisableMetrics(orAspire:OpenLdap:DisableTracingin configuration). - Note — only
OpenLdapClientis instrumented; the rawLdapConnectionand theOpenLdapClient.Connectionescape hatch are not.
For a runnable end-to-end demo (AppHost + Web API + dashboard), see the examples/ folder.
AI coding agents
This package ships an agent-facing API reference covering both the client and hosting integrations. After restore it is at ~/.nuget/packages/joshmakestuff.aspire.openldap/<version>/AGENTS.md (also skills/SKILL.md). Add a pointer to it from your repo's AGENTS.md, or copy it to .agents/skills/aspire-openldap/SKILL.md, so coding agents find it.
Requirements on Linux
LdapConnection comes from System.DirectoryServices.Protocols, which on Linux P/Invokes the native OpenLDAP client library. (On Windows it uses the built-in wldap32.dll and needs nothing extra.) The runtime hardcodes a load of libldap-2.5.so.0 — still true on .NET 10 (dotnet/runtime#123676) — but modern distros (Ubuntu 24.04+, Fedora, Alpine 3.20+) ship the upstream soname libldap.so.2 instead.
AddOpenLdapClient / AddKeyedOpenLdapClient handle this automatically: they register a resolver that probes the sonames distros actually ship (libldap-2.5.so.0, libldap.so.2, libldap-2.6.so.0, libldap-2.4.so.2). You only need the OpenLDAP client library installed — no symlinks:
sudo apt-get install -y libldap2 # Ubuntu 24.04+ (22.04: libldap-2.5-0)
sudo dnf install -y openldap # Fedora
sudo apk add libldap # Alpine
The automatic resolution only applies when you register through the Add* methods. If you use System.DirectoryServices.Protocols directly elsewhere in your app before calling them, symlink the shipped soname to the 2.5 name as a fallback (confirm what you have with ldconfig -p | grep -E 'libldap|liblber'):
# Path is /usr/lib/x86_64-linux-gnu on Debian/Ubuntu, /usr/lib64 on Fedora.
sudo ln -sf .../libldap.so.2 .../libldap-2.5.so.0
sudo ldconfig
Without either, you'll see Unable to load shared library 'libldap-2.5.so.0' at the first LDAP call.
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.11)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.11)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.11)
- OpenTelemetry.Extensions.Hosting (>= 1.18.0)
- System.DirectoryServices.Protocols (>= 10.0.11)
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 |
|---|---|---|
| 0.8.0-preview.1 | 73 | 9/8/2026 |
| 0.7.0-preview.1 | 95 | 8/8/2026 |
| 0.6.0-preview.1 | 73 | 8/7/2026 |
| 0.5.0-preview.1 | 118 | 7/18/2026 |
| 0.4.0-preview.1 | 70 | 7/17/2026 |
| 0.3.0-preview.1 | 81 | 7/14/2026 |
| 0.2.0-preview.1 | 72 | 6/28/2026 |
| 0.1.0-preview.1 | 70 | 6/27/2026 |