Skip to content

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: ModelDataReaderServiceConnection and ScenarioServiceConnection to 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"
}

TypeName must 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:

  • ModelDataReaderId must exist.
  • ParameterValues keys must be in the reader’s GetParameterList() and type-compatible (numeric types auto-convert).
  • InputTimeSeriesValues keys must exist in GetInputTimeSeriesList().
  • If a DiscreteTimeSeriesService is 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>&copyInputs=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> and Dictionary<string, Scenario>

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 IModelInputReader and IModelOutputReader, and must have a public parameterless constructor (the wrapper ModelDataReader<T> uses Activator.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 the Simulation.Id as 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 restrict POST/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 the GET route)
  • PUT -> 200 OK
  • DELETE -> 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 call ModelDataReaderTypeRegistry.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|name in URLs; decode mirrors are automatic. Escape | as ||.
  • Derived scenarios return 500 Your IScenarioRepository must implement IDerivedScenarioFactory. 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, 404
  • GET / -> 200
  • GET /count -> 200
  • GET /ids -> 200
  • POST / -> 201 (ModelDataReaderDtoRequest)
  • PUT / -> 200 (ModelDataReaderDtoRequest)
  • DELETE /{id} -> 204, 404

Scenarios (/api/models/scenarios/{cid}):

  • POST / -> 201 (Scenario)
  • PUT / -> 200 (Scenario)
  • DELETE /{id} -> 204
  • GET /{id} -> 200, 404
  • GET / -> 200
  • GET /{id}/simulations -> 200, 404
  • GET /{id}/simulations/{simulationId}/data/{timeSeriesId} -> 200, 404
  • POST /execute/{id} -> 200
  • POST /{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.