Register Services Dynamically with connections.json¶
Instead of registering services in code with ServiceLocator.Register(...), you can declare them in a file and let the API auto-load them at startup. This is ideal for environments where you want to add, remove, or switch repositories without recompiling.
See the template reference: Project Template – Connections Web API
What You’ll Get¶
- Zero controller code and zero manual registrations per connection
- A single
connections.jsonthat lists named connections (yourconnectionIds) - Support for placeholders like
[AppData]and[env:...] - Works side-by-side with imperative
ServiceLocator.Register(...)if you want to mix both
Prerequisites¶
Install the packages for Connections plus any domain(s) you plan to expose:
dotnet add package DHI.Services.Connections.WebApi
# Add your domain(s)
dotnet add package DHI.Services.TimeSeries.WebApi
# Optional provider packages (examples)
dotnet add package DHI.Services.USGS
dotnet add package DHI.Services.PostgreSQL
Step 1 – Enable the Connections Repository¶
In Program.cs, enable the Connections Repository. This is what reads connections.json and performs lazy creation of services on demand:
// Resolve configuration & logging as you already do earlier in Program.cs
var lazyCreation = builder.Configuration.GetValue("AppConfiguration:LazyCreation", true);
// Point the repository at your connections file (placed in App_Data)
Services.Configure(new ConnectionRepository("[AppData]connections.json".Resolve()), lazyCreation);
Tip: The BaseWebApi / Connections templates already set
AppDomain.CurrentDomain.SetData("DataDirectory", ...), so[AppData]maps to your project’sApp_Datafolder.
You can optionally mirror this in appsettings.json:
{
"AppConfiguration": {
"LazyCreation": "true",
"HstsMaxAgeInDays": 1
},
"Tokens": {
"Issuer": "dhigroup.com",
"Audience": "dhigroup.com",
"PublicRSAKey": "[env:PublicRSAKey]"
},
"Swagger": {
"SpecificationName": "MyApplicationOpenAPISpecification",
"DocumentName": "MyApplicationWebAPI",
"DocumentTitle": "My Application Web API",
"DocumentDescription": "[AppData]SwaggerInfo.md"
}
}
Step 2 – Create App_Data/connections.json¶
Create a dictionary keyed by your connectionId. Each entry is a Connection class that knows how to construct the concrete service (and which repository to use).
Example: Time Series (CSV repository)¶
{
"$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",
"connections-csv": {
"$type": "DHI.Services.TimeSeries.WebApi.TimeSeriesServiceConnection, DHI.Services.TimeSeries.WebApi",
"ConnectionString": "[AppData]",
"RepositoryType": "DHI.Services.TimeSeries.CSV.TimeSeriesRepository, DHI.Services.TimeSeries",
"Name": "CSV time series service connection",
"Id": "connections-csv"
}
}
Example: Discrete Time Series (CSV)¶
{
"$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",
"discrete-csv": {
"$type": "DHI.Services.TimeSeries.WebApi.DiscreteTimeSeriesServiceConnection, DHI.Services.TimeSeries.WebApi",
"ConnectionString": "[AppData]",
"RepositoryType": "DHI.Services.TimeSeries.CSV.DiscreteTimeSeriesRepository, DHI.Services.TimeSeries",
"Name": "CSV discrete time series",
"Id": "discrete-csv"
}
}
Example: Time Series via USGS provider¶
{
"$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",
"timeseries-usgs": {
"$type": "DHI.Services.TimeSeries.WebApi.TimeSeriesServiceConnection, DHI.Services.TimeSeries.WebApi",
"ConnectionString": "http://waterservices.usgs.gov/nwis/dv/?startDT=2015-01-01&endDt=2015-01-31",
"RepositoryType": "DHI.Services.Provider.USGS.TimeSeriesRepository, DHI.Services.Provider.USGS",
"Name": "USGS time series service connection",
"Id": "timeseries-usgs"
}
}
How it works
- The connection class (e.g.,
TimeSeriesServiceConnection) knows how toCreate()the domain service.RepositoryTypeis the fully qualified type of the repository (CSV, USGS, PostgreSQL, etc.).ConnectionStringis passed into the repository (supports[AppData]and[env:VAR_NAME]).
Step 3 - SignalR prerequisites (Time Series)¶
Time Series Web API endpoints expect an IFilterRepository in DI. Even if you don’t plan to use SignalR filters right now, you must register the dependency; the file itself is optional.
Add this to Program.cs:
// Required for Time Series endpoints to resolve IFilterRepository
builder.Services.AddSingleton<IFilterRepository>(
new FilterRepository("[AppData]signalr-filters.json".Resolve())
);
// SignalR (optional but recommended for diagnostics & notifications)
builder.Services.AddSignalR(hubOptions =>
{
hubOptions.EnableDetailedErrors = true;
});
Or, you can uncomment this part inside Program.cs:
// SignalR
#warning Enable if using SignalR capabilities
//builder.Services.AddSignalR();
And then add IFilterRepository below:
// Configure the necessary services for constructor injection into controllers of DHI Domain Services Web APIs
#warning In production code, replace the JSON-file based repositories with a provider like PostgreSQL
builder.Services.AddScoped(_ => new DHI.Services.Connections.WebApi.ConnectionTypeService(AppContext.BaseDirectory));
// Other Services
builder.Services.AddSingleton<IFilterRepository>(
new FilterRepository("[AppData]signalr-filters.json".Resolve())
);
Note: You do not need to ship
signalr-filters.json. The registration satisfies DI so the endpoints won’t complain; if the file is missing, the repository simply has nothing to load.
Step 4 – Run & Verify¶
- Start your Web API.
- Open Swagger. You’ll see the domain endpoints (e.g., Time Series).
- Where a route accepts
{connectionId}, use the key from yourconnections.json(e.g.,connections-csv,discrete-csv,timeseries-usgs). - First calls to a given
connectionIdwill instantiate the service ifLazyCreationis enabled.
If your API uses JWT auth, you’ll still need a valid token (or temporarily add the local dev bypass you used earlier for Jobs).
Using connections.json in the Base Web API (Jobs example context)¶
Previously, for Jobs you registered services imperatively with:
ServiceLocator.Register(
new JobService<Workflow, string>(
new JobRepository("[AppData]jobs.json".Resolve()),
new TaskService<Workflow, string>(
new WorkflowRepository("[AppData]workflows.json".Resolve(), SerializerOptionsDefault.Options.Converters)
),
null
),
"wf-jobs"
);
With Connections, the same idea becomes declarative. You define a connection entry whose connection class creates the Jobs service using your repository(ies). The concrete connection class and repository type names depend on the packages you include for Jobs. Follow the same pattern as Time Series:
- Pick the Jobs connection class from the Jobs Web API package (e.g., the
...Jobs.WebApiassembly). - Set
RepositoryTypeto your Jobs repository (JSON, PostgreSQL, etc.). - Provide
ConnectionString(file path, DSN, etc., with optional placeholders).
Once added to
connections.json, calls likePOST /api/jobs/{connectionId}andGET /api/jobs/{connectionId}will route to the service created by that connection, just like your earlier"wf-jobs"mapping.
Mixing Styles (Optional)¶
You can mix both approaches:
// Declarative via connections.json
Services.Configure(new ConnectionRepository("[AppData]connections.json".Resolve()), lazyCreation: true);
// Imperative for a one-off test or prototype
ServiceLocator.Register(
new DHI.Services.TimeSeries.TimeSeriesService<string,double>(
new DHI.Services.TimeSeries.CSV.TimeSeriesRepository("[AppData]ts".Resolve())),
"ad-hoc-csv");
Connections declared in JSON and registrations done in code coexist. The route {connectionId} resolves to whichever registration owns that name.
Placeholders & Secrets¶
- Use
[AppData]to keep dev/test data with the project. - Use
[env:VAR_NAME]to avoid committing secrets:
"ConnectionString": "[env:POSTGRES_CONN]"
In your deployment, set
POSTGRES_CONN(e.g.,"Server=...;Port=5432;Database=...;User Id=...;Password=...").
Troubleshooting¶
- 401 Unauthorized – Same as before: JWT required unless you’ve added your dev bypass.
- “Could not load type …” – Check the
RepositoryTypeassembly-qualified name (spelling and assembly match the installed NuGet package). - “Connection not found” – The
{connectionId}in the route must match the key inconnections.json. - File paths – Use
[AppData]and ensure the file(s) exist (e.g., CSV folders, JSON stores). - VPN/Internal feeds – If a repository or connection class lives in an internal package, ensure the DHI feed is configured and VPN is connected.
Migration Cheat-Sheet (From ServiceLocator to connections.json)¶
| You did this before… | Do this now… |
|---|---|
Instantiate *Service(...) with repository in code |
Pick the corresponding Connection class and set RepositoryType + ConnectionString in JSON |
Register with ServiceLocator.Register(service, "name") |
Create an entry under "name" (the connectionId) in connections.json |
| Recompile to change repo/params | Edit connections.json (or env vars) and restart the app |
Next: Providers & Repositories