Skip to content

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.json that lists named connections (your connectionIds)
  • 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’s App_Data folder.

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 to Create() the domain service.
  • RepositoryType is the fully qualified type of the repository (CSV, USGS, PostgreSQL, etc.).
  • ConnectionString is 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

  1. Start your Web API.
  2. Open Swagger. You’ll see the domain endpoints (e.g., Time Series).
  3. Where a route accepts {connectionId}, use the key from your connections.json (e.g., connections-csv, discrete-csv, timeseries-usgs).
  4. First calls to a given connectionId will instantiate the service if LazyCreation is 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.WebApi assembly).
  • Set RepositoryType to your Jobs repository (JSON, PostgreSQL, etc.).
  • Provide ConnectionString (file path, DSN, etc., with optional placeholders).

Once added to connections.json, calls like POST /api/jobs/{connectionId} and GET /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 RepositoryType assembly-qualified name (spelling and assembly match the installed NuGet package).
  • “Connection not found” – The {connectionId} in the route must match the key in connections.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