Skip to content

Getting Started with Pragmatic.Discovery

This guide walks through setting up topology discovery in a Pragmatic.Design host.

  • Pragmatic.Discovery NuGet package referenced
  • Pragmatic.Composition with at least one [Module] class using [Include<T>]
  • The Pragmatic source generator configured (generates PragmaticMetadata assembly attributes)
  1. The Composition source generator emits [assembly: PragmaticMetadata(HostTopology, ...)] with a JSON payload describing which modules, databases, and boundaries the host includes.
  2. At startup, DiscoveryHostedService reads this metadata from the entry assembly.
  3. It registers the topology with the IDiscoveryBackend (InMemory by default).
  4. It optionally validates the topology against other registered hosts.

In your Program.cs or IStartupStep:

using Pragmatic.Discovery.Extensions;
services.AddDiscovery();

This registers:

  • IDiscoveryService (singleton)
  • IDiscoveryBackend (InMemoryDiscoveryBackend, singleton)
  • DiscoveryHostedService (auto-registers topology on startup)

When the host starts, you should see log messages:

info: Pragmatic.Discovery.Services.DiscoveryService
Registering host topology: Showcase.Host with 3 module(s)

If you see a warning instead:

warn: No HostTopology metadata found in entry assembly.

This means the Composition source generator is not producing metadata. Ensure your host project has [Module] classes with [Include<T>] attributes.

For development, the default settings are fine (log warnings, don’t block startup). For production:

services.AddDiscovery(opts =>
{
opts.ThrowOnValidationFailure = true; // Block startup on conflicts
});

Or via appsettings.json:

{
"Pragmatic": {
"Discovery": {
"ThrowOnValidationFailure": true,
"ValidateOnStartup": true
}
}
}

Inject IDiscoveryService to query the topology:

public class TopologyHealthCheck(IDiscoveryService discovery) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken ct = default)
{
var hosts = await discovery.GetAllHostsAsync(ct);
if (hosts.Count == 0)
return HealthCheckResult.Degraded("No hosts registered");
var data = new Dictionary<string, object>();
foreach (var host in hosts)
{
data[host.HostName] = new
{
Modules = host.Modules.Select(m => m.ModuleName).ToList(),
RegisteredAt = host.RegisteredAt
};
}
return HealthCheckResult.Healthy($"{hosts.Count} host(s) registered", data);
}
}
var billingHosts = await discovery.FindHostsForModuleAsync("BillingModule", ct);
if (billingHosts.Count == 0)
logger.LogWarning("No host owns BillingModule");
else
logger.LogInformation("BillingModule is hosted by: {Hosts}",
string.Join(", ", billingHosts.Select(h => h.HostName)));

You can also read the topology metadata directly without the hosted service:

// From the running host
var topology = HostTopologyInfo.FromEntryAssembly();
if (topology is not null)
{
Console.WriteLine($"Host: {topology.HostName}");
foreach (var module in topology.Modules)
Console.WriteLine($" Module: {module.ModuleName} -> DB: {module.DatabaseName}");
foreach (var boundary in topology.Boundaries)
Console.WriteLine($" Boundary: {boundary.BoundaryName} reads [{string.Join(", ", boundary.EntityTypes)}]");
}

In a multi-host deployment, each host registers independently. Replace the InMemory backend with a distributed one:

services.AddDiscovery(opts =>
{
opts.ThrowOnValidationFailure = true;
});
// Replace InMemory with Redis
services.UseDiscoveryBackend<RedisDiscoveryBackend>();

When Host B registers, it validates against Host A’s already-registered topology:

  • DISC001 (Info): Same module in multiple hosts on different databases — allowed, just logged
  • DISC002 (Warning): Same module + database with different providers — likely misconfiguration
  • DISC003 (Info): ReadAccess declarations flagged for awareness

If you want to register manually (e.g., in tests):

services.AddDiscovery(opts =>
{
opts.AutoRegisterOnStartup = false;
opts.ValidateOnStartup = false;
});

Then register when ready:

var topology = new HostTopologyInfo
{
HostName = "TestHost",
Modules =
[
new ModuleDeploymentInfo { ModuleName = "CatalogModule", DatabaseName = "TestDb", Provider = "InMemory" }
]
};
await discoveryService.RegisterAsync(topology);

Pragmatic.Composition.Host uses IDiscoveryService to locate remote modules when [RemoteBoundary<T>] is declared. The discovery service tells the composition layer which host URL to call for a given module.

Showcase.Host --> registers: [Booking, Catalog]
Showcase.Billing.Host --> registers: [Billing]
Showcase.Host wants to call Billing:
1. FindHostsForModuleAsync("BillingModule") --> Showcase.Billing.Host
2. HTTP call to configured base URL