DHI.Services.Models.WebApi — Internal Developer Guide¶
This document covers what the API gives you, how to wire it up, and the endpoints & payloads.
1) What this package gives you¶
- Two controllers under
api/models/*:ModelsController-> CRUD for model data readers.ScenariosController-> CRUD for scenarios, run & query simulations, fetch output time series.
- Connection-ID based resolution (via
ServiceLocator) so the same controllers can front multiple backends (JSON/DB/etc.). - System.Text.Json defaults (via
SerializerOptionsDefault) that include all converters needed to serialize:IModelDataReader,Scenario,Simulation- time-series types (
ITimeSeriesData<T>, vectors, flags) - domain primitives (
DateRange,Enumeration, permissions, etc.)
2) Package layout & prerequisites¶
Add these packages to your host:
DHI.Services.Models.WebApi(this API)DHI.Services.Models(core services/entities)DHI.Services.WebApiCore(exception mapping, helpers)
Your model readers and worker implementations must be referenced by the host so their assemblies are loaded at runtime.
3) Minimal wiring (Startup/Program)¶
using DHI.Services;
using DHI.Services.Models;
using DHI.Services.Models.WebApi;
using DHI.Services.WebApiCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.OpenApi.Models;
// 1) JSON & MVC (IMPORTANT: reuse SerializerOptionsDefault)
builder.Services
.AddControllers()
.AddJsonOptions(o =>
{
// Keep JSON consistent across the platform
o.JsonSerializerOptions.DefaultIgnoreCondition = SerializerOptionsDefault.Options.DefaultIgnoreCondition;
o.JsonSerializerOptions.PropertyNamingPolicy = SerializerOptionsDefault.Options.PropertyNamingPolicy;
o.JsonSerializerOptions.AddConverters(SerializerOptionsDefault.Options.Converters);
});
builder.Services.AddApiVersioning();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer();
builder.Services.AddAuthorization();
// 2) Pipeline
var app = builder.Build();
app.UseExceptionHandling(); // from WebApiCore
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// 3) DataDirectory for [AppData] resolution (used by JSON repos)
AppDomain.CurrentDomain.SetData("DataDirectory",
Path.Combine(app.Environment.ContentRootPath, "App_Data"));
// 4) Register services behind connection IDs
var modelSvc = new ModelDataReaderService(
new ModelDataReaderRepository("[AppData]models.json".Resolve(),
SerializerOptionsDefault.Options.Converters));
ServiceLocator.Register(modelSvc, "models-json");
var worker = new MyScenarioWorker(); // your IScenarioWorker
var scenarioSvc = new ScenarioService(
new ScenarioRepository("[AppData]scenarios.json".Resolve(),
SerializerOptionsDefault.Options.Converters),
modelSvc, worker);
ServiceLocator.Register(scenarioSvc, "scenarios-json");
app.Run();
If you keep connections in config, you can also use the connection objects:
ModelDataReaderServiceConnectionandScenarioServiceConnectionto instantiate services from type names + connection strings.
4) Route model & connection IDs¶
All endpoints include a {connectionId} path segment. At runtime the controllers resolve the correct service:
var modelSvc = Services.Get<ModelDataReaderService>(connectionId);
var scenSvc = Services.Get<ScenarioService>(connectionId);
Register each service under the ID you plan to use (e.g., "models-json", "scenarios-json").
ID encoding: when an id lives in the URL, the controllers accept | as a slash-safe separator (via FullNameString.FromUrl).
If you need to represent a literal |, escape it as ||.
5) Endpoints¶
5.1 Model data readers¶
Base: api/models/readers/{connectionId} (v1)
| Method & Route | Description | Auth | Returns |
|---|---|---|---|
GET /{id} |
Get one reader | ✓ | ModelDataReaderDtoResponse |
GET / |
Get all readers | ✓ | IEnumerable<ModelDataReaderDtoResponse> |
GET /count |
Count readers | ✓ | int |
GET /ids |
List reader IDs | ✓ | IEnumerable<string> |
POST / |
Create reader | ✓ | 201 Created + body |
PUT / |
Update reader | ✓ | 200 OK + body |
DELETE /{id} |
Delete reader | ✓ | 204 No Content |
DTO shapes
The DTOs wrap IModelDataReader. Keep payloads minimal:
- Request (
ModelDataReaderDtoRequest) – to create/update a reader:
{
"Id": "hydro-reader-01",
"Name": "Hydrodynamic v2024",
"TypeName": "Acme.Models.Hydro.HydroReader",
"ModelType": "3D Hydro"
}
TypeNamemust be the full name of your concrete reader type (IModelInputReader+IModelOutputReader). The assembly containing the type must be loaded by the host (reference it).
- Response (
ModelDataReaderDtoResponse) – typical shape:
{
"Id": "hydro-reader-01",
"Name": "Hydrodynamic v2024",
"TypeName": "Acme.Models.Hydro.HydroReader",
"ModelType": "3D Hydro"
}
(Depending on converter settings, responses may also include friendly summaries, but the core fields above are guaranteed.)
Examples
# Create
curl -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"Id":"hydro-reader-01","Name":"Hydro v2024","TypeName":"Acme.Models.Hydro.HydroReader","ModelType":"3D Hydro"}' \
https://host/api/models/readers/models-json
# List
curl -H "Authorization: Bearer <token>" \
https://host/api/models/readers/models-json
5.2 Scenarios & simulations¶
Base: api/models/scenarios/{connectionId} (v1)
| Method & Route | Description | Auth | Returns |
|---|---|---|---|
POST / |
Create a scenario | ✓ | 201 Created + Scenario |
PUT / |
Update a scenario | ✓ | 200 OK + Scenario |
DELETE /{id} |
Delete a scenario | ✓ | 204 No Content |
GET /{id} |
Get a scenario | ✓ | Scenario |
GET / |
List scenarios | ✓ | IEnumerable<Scenario> |
GET /{id}/simulations |
List simulations for a scenario | ✓ | IEnumerable<Simulation> |
GET /{id}/simulations/{simulationId}/data/{timeSeriesId} |
Output time series for a simulation/time-series key | ✓ | ITimeSeriesData<double> |
POST /execute/{id} |
Execute a scenario -> returns new Simulation Id | ✓ | Guid |
POST /{id}/derived?derivedName=...&simulationId=...&...extraParams |
Create derived scenario from an existing simulation | ✓ | 201 Created + Scenario |
Scenario DTO (request/response)
Matches the Scenario entity:
{
"Id": "scenario-001",
"Name": "Spring Flood 2024",
"ModelDataReaderId": "hydro-reader-01",
"ParameterValues": {
"simulationStart": "2024-03-01T00:00:00Z",
"simulationEnd": "2024-03-10T00:00:00Z",
"meshLevel": 3
},
"InputTimeSeriesValues": {
"boundary-north": "telemetry/bnd/north",
"precip": "telemetry/grids/precip-01h"
},
"Metadata": {
"owner": "alice",
"tag": "flood-ops"
}
}
Validation (server-side)
On Add/Update, the API validates via ScenarioService:
ModelDataReaderIdmust exist.ParameterValueskeys must be in the reader’sGetParameterList()and type-compatible (numeric types auto-convert).InputTimeSeriesValueskeys must exist inGetInputTimeSeriesList().- If a
DiscreteTimeSeriesServiceis injected at construction time, it is consulted to check that bound TS identifiers exist.
Derived scenarios
POST /{id}/derived requires:
derivedName(query string)simulationId(query string,Guid)- Any additional query string keys are forwarded to the repo factory as
Parameters.
Your scenario repo must implement IDerivedScenarioFactory for this endpoint to work; otherwise the service throws an explicit error.
Examples
# Create scenario
curl -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"Id":"scenario-001",
"Name":"Spring Flood 2024",
"ModelDataReaderId":"hydro-reader-01",
"ParameterValues":{"simulationStart":"2024-03-01T00:00:00Z","simulationEnd":"2024-03-10T00:00:00Z","meshLevel":3},
"InputTimeSeriesValues":{"boundary-north":"telemetry/bnd/north","precip":"telemetry/grids/precip-01h"}
}' \
https://host/api/models/scenarios/scenarios-json
# Execute
curl -H "Authorization: Bearer <token>" \
-X POST \
https://host/api/models/scenarios/scenarios-json/execute/scenario-001
# Get simulation data
curl -H "Authorization: Bearer <token>" \
https://host/api/models/scenarios/scenarios-json/scenario-001/simulations/<SIM-ID>/data/discharge-L1
# Create derived (with extra params passed through)
curl -H "Authorization: Bearer <token>" \
-X POST \
"https://host/api/models/scenarios/scenarios-json/scenario-001/derived?derivedName=ALT%20Mesh&simulationId=<SIM-ID>©Inputs=true"
6) Serializer defaults (don’t reinvent this)¶
SerializerOptionsDefault.Options is a ready-to-use JsonSerializerOptions that:
- keeps property names as-is (
PropertyNamingPolicy = null) - ignores nulls
- registers all required converters:
- Domain converters (
DateRangeConverter,EnumerationConverter, permissions, type resolvers, etc.) - Time-series converters (
TimeSeriesDataConverter<T>,TimeSeriesDataWFlagConverter<...>) - Dictionary resolvers for
Dictionary<string, IModelDataReader>andDictionary<string, Scenario>
- Domain converters (
Use it when adding controllers (see section 3). If you skip it, polymorphic deserialization (e.g., readers) and time-series payloads will be painful.
7) Reader & worker implementations (host responsibilities)¶
- Readers: must implement both
IModelInputReaderandIModelOutputReader, and must have a public parameterless constructor (the wrapperModelDataReader<T>usesActivator.CreateInstance<T>()). If your adapter needs a connection string, consider:- reading from environment/config inside your reader, or
- creating a tiny “locator” singleton you can reach from the parameterless ctor.
- Workers: implement
IScenarioWorker. Return theSimulation.Idas soon as the run is accepted; update simulation state using your own job engine/repo (the Web API does not persist simulation updates by itself).
8) Security¶
Both controllers are decorated with [Authorize]. The package does not fix policies/roles; wire your own:
- Add JWT Bearer (or your auth scheme).
- Add policies (
EditorsOnly/AdminsOnly) and apply them if you want to restrictPOST/PUT/DELETE.
Swagger is supported (annotations present). Add Swashbuckle and a bearer scheme as usual, then browse /swagger.
9) Error handling & status codes¶
With UseExceptionHandling() from DHI.Services.WebApiCore:
- Not found (
KeyNotFoundException) -> 404 - Validation (
ArgumentException,JsonException) -> 400 - Not implemented/unsupported -> 501
- Unhandled -> 500
Controllers also return the usual explicit codes:
POST-> 201 Created (Location header points to theGETroute)PUT-> 200 OKDELETE-> 204 No Content
10) Troubleshooting¶
- “Missing TypeName discriminator” / cannot resolve reader type
Your request payload must include
TypeName(full type name) and the assembly with that type must be loaded by the host. Reference the project/package that defines the reader, or callModelDataReaderTypeRegistry.Register("Full.Name", typeof(ModelDataReader<YourReader>))at startup before you accept requests. - Readers need configuration but only have a parameterless ctor Read configuration from the environment or a static provider. If you truly need ctor args, you’ll have to extend the wrapper/provider stack (out of scope for this package).
- Time-series payloads look odd
Ensure you used
SerializerOptionsDefault.Options.Converters. - IDs with slashes
Encode as
group|sub|namein URLs; decode mirrors are automatic. Escape|as||. - Derived scenarios return 500
Your
IScenarioRepositorymust implementIDerivedScenarioFactory. The default JSON repo in the host example shows how to do this (ScenarioRepositoryWithFakeFactory).
11) Quick start (host) – complete snippet¶
// Startup.cs / Program.cs
services.AddControllers().AddJsonOptions(o =>
{
o.JsonSerializerOptions.WriteIndented = true;
o.JsonSerializerOptions.DefaultIgnoreCondition = SerializerOptionsDefault.Options.DefaultIgnoreCondition;
o.JsonSerializerOptions.PropertyNamingPolicy = SerializerOptionsDefault.Options.PropertyNamingPolicy;
o.JsonSerializerOptions.AddConverters(SerializerOptionsDefault.Options.Converters);
});
services.AddApiVersioning();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer();
services.AddAuthorization();
app.UseExceptionHandling();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
AppDomain.CurrentDomain.SetData("DataDirectory",
Path.Combine(env.ContentRootPath, "App_Data"));
var modelSvc = new ModelDataReaderService(
new ModelDataReaderRepository("[AppData]models.json".Resolve(),
SerializerOptionsDefault.Options.Converters));
ServiceLocator.Register(modelSvc, "models-json");
IScenarioWorker worker = new MyScenarioWorker();
var scenSvc = new ScenarioService(
new ScenarioRepository("[AppData]scenarios.json".Resolve(),
SerializerOptionsDefault.Options.Converters),
modelSvc, worker);
ServiceLocator.Register(scenSvc, "scenarios-json");
12) Reference: endpoint cheat sheet¶
Readers (/api/models/readers/{cid}):
GET /{id}-> 200, 404GET /-> 200GET /count-> 200GET /ids-> 200POST /-> 201 (ModelDataReaderDtoRequest)PUT /-> 200 (ModelDataReaderDtoRequest)DELETE /{id}-> 204, 404
Scenarios (/api/models/scenarios/{cid}):
POST /-> 201 (Scenario)PUT /-> 200 (Scenario)DELETE /{id}-> 204GET /{id}-> 200, 404GET /-> 200GET /{id}/simulations-> 200, 404GET /{id}/simulations/{simulationId}/data/{timeSeriesId}-> 200, 404POST /execute/{id}-> 200POST /{id}/derived?derivedName=...&simulationId=...&...-> 201, 404
With the wiring above in place, your API is ready to:
- register model readers (adapters),
- define/validate scenarios against those readers,
- run scenarios via your worker, and
- serve simulation time-series data in a consistent JSON shape.