Skip to content

DHI.Services.Places.WebApi — Internal Developer Guide

Expose places, indicators, GIS features, and status colors over HTTP using the DHI.Services.Places.WebApi package. This guide tracks the existing controller behavior precisely and mirrors the structure of the core guide.

Sample host app & wiring:

https://github.com/DHI/DomainServicesSamples/tree/main/Host/Places


What you get

  • Route base: api/places/{connectionId} (API v1)
  • Controller: PlacesController
    • Places
      • Add (POST …/)
      • Get one (GET …/{id})
      • List (GET …?group=…)
      • Full names (GET …/fullnames?group=…)
      • Delete (DELETE …/{id})
    • Indicators (per place)
      • Add (POST …/{id}/indicator/{type})
      • Update (PUT …/{id}/indicator/{type})
      • Delete (DELETE …/{id}/indicator/{type})
      • Get one (GET …/{id}/indicators/{type})
      • Get all at place (GET …/{id}/indicators)
    • Status & thresholds
      • Status of one indicator (GET …/{id}/indicators/{type}/status)
      • Status of all indicators of a type (optionally by group) (GET …/indicators/{type}/status?group=…)
      • Thresholds for all indicators at place (GET …/{id}/thresholds)
      • Thresholds for one indicator at place (GET …/{id}/thresholds/{indicatorType})
    • Mapping
      • GIS features of places, with optional indicator status evaluation (GET …/features + query flags)
      • Palette image for an indicator (GET …/{id}/indicators/{type}/palette)

How it resolves services: every call resolves a PlaceService by {connectionId} from the Domain Services ServiceLocator.


DTOs & serialization

The API uses simple DTOs:

  • PlaceDTO<TCollectionId> — carries FullName, FeatureId, Indicators, Metadata
  • IndicatorDTO — carries DataSource, StyleCode, optional TimeInterval, optional AggregationType (DisplayName string), PaletteType, and optional Quantile
  • IndicatorStatusDTOIndicatorDTO + Status (SKColor)
  • TimeIntervalDTOType, Start, End

AggregationType in requests/responses is the display name (e.g., "Average", "Maximum"). We map it via Enumeration.FromDisplayName<AggregationType>(). Spelling & casing must match the display name.

JSON options SerializerOptionsDefault.Options is wired in Startup.AddJsonOptions. It registers the same converters as the core module so API payloads are stable:

new PlaceConverter(),
new DataSourceConverter(),
new IndicatorConverter(),
new TimeIntervalConverter(),
new FeatureIdConverter<string>(),
new DictionaryTypeResolverConverter<string, Place>()

Endpoints (quick matrix)

Base: /api/places/{connectionId}

Method Path Query/body Auth Response Notes
POST / JSON PlaceDTO [Authorize] 201 Created + PlaceDTO Create a place. Validates GIS collection + feature, and each indicator. Location header points to GET /{id}.
DELETE /{id} [Authorize] 204 No Content Removes the place. 404 via middleware if not found.
POST /{id}/indicator/{type} JSON IndicatorDTO [Authorize] 201 Created + IndicatorDTO Adds an indicator to a place. Fails if it already exists.
PUT /{id}/indicator/{type} JSON IndicatorDTO [Authorize] 200 OK + IndicatorDTO Replaces/updates an existing indicator.
DELETE /{id}/indicator/{type} [Authorize] 204 No Content Removes the indicator from the place.
GET / group=… (optional) [Authorize] PlaceDTO[] Without group: all places. With group: places inside that group (recursive).
GET /fullnames group=… (optional) [Authorize] string[] Full name strings. With group: recursive subset.
GET /{id} [Authorize] PlaceDTO Returns one place or 404 if missing.
GET /{id}/indicators [Authorize] Dictionary<string, IndicatorDTO> All indicators at a place (by type).
GET /indicators/{type} group=… (optional) [Authorize] Dictionary<string, IndicatorDTO> All indicators of a given type. If group provided, only within that group. Keys are place ids.
GET /{id}/indicators/{type} [Authorize] IndicatorDTO One indicator at a place.
GET /{id}/thresholds [Authorize] Dictionary<string, double[]> Thresholds for each indicator at the place.
GET /{id}/thresholds/{indicatorType} [Authorize] double[] Thresholds for one indicator at the place.
GET /{id}/indicators/{type}/status dateTime=… (for RelativeToDateTime), path=… [Authorize] IndicatorStatusDTO or 204 Returns 204 No Content if no data can be computed (missing/corrupt).
GET /indicators/{type}/status group=… (optional), dateTime=…, path=… [Authorize] Dictionary<string, IndicatorStatusDTO> One status per place id.
GET /features group=…, from=…, to=…, dateTime=…, path=…, includeIndicatorStatus=true | false [Authorize] IFeature[] Returns features for places; optionally includes status colors merged into the features.
GET /{id}/indicators/{type}/palette width, height, numberOfDecimals=1, horizontal=false [Authorize] image/png Renders the indicator’s palette as a PNG.

Feature & status retrieval logic

GET /features behaves as follows:

  • If includeIndicatorStatus=true and either from or to is provided → status is evaluated over the explicit [from,to] period (overrides indicator TimeInterval).
  • If includeIndicatorStatus=true and no from/to → status is evaluated using each indicator’s declarative TimeInterval. If any indicator uses RelativeToDateTime, pass dateTime.
  • If includeIndicatorStatus=false → pure features; no status attached.

Returned features get extra attributes:

  • placeId, fullName, name, groupLayer
  • indicators{ [indicatorType]: { styleCode, color } } when status is included

IDs & groups in URLs

We use FullNameString.ToUrl / FromUrl to carry IDs and groups in URLs.

  • Encode slashes / as | in route segments. Example: Stations/MyStationStations|MyStation
  • Escape a literal | as ||.

You’ll see this for both /{id} and for group when it’s part of the path (e.g., /fullnames?group=Stations|West).


Request/response shapes & Swagger notes

A few payloads deserve call-outs:

  • AggregationType in IndicatorDTO is a string display name, not an enum value.
  • Thresholds endpoints return arrays of numbers even though the action signatures use IEnumerator<double> internally.
  • Status endpoints return IndicatorStatusDTO or 204 when no status can be computed. The reason can be “no data points in period”, “all ensemble members are null”, or missing dependencies.

The project’s Startup wires SerializerOptionsDefault.Options into MVC so Swagger reflects the right shapes.


Security model (defaults)

  • The controller is annotated with [Authorize] at the class level → all endpoints require an authenticated user.
  • Fine-grained policies like EditorsOnly / AdministratorsOnly are not enforced by this controller (unlike some other modules). If you need that, apply policy attributes or protect the endpoints in your API gateway.

The sample host uses JWT bearer auth and typical versioning/HSTS/Swagger setup.


Wiring it up

You can wire the Places web API in two ways:

A) Manual registration (via ServiceLocator.Register)

See the sample host (repo path: Host/Places). Minimal essence:

// 1) Set App_Data
AppDomain.CurrentDomain.SetData("DataDirectory",
    Path.Combine(env.ContentRootPath, "App_Data"));

// 2) Register dependencies (examples)
ServiceLocator.Register(
    new DiscreteTimeSeriesService<string,double>(new TimeSeriesRepository("[AppData]".Resolve())),
    "csv");

ServiceLocator.Register(
    new GisService<string>(new FeatureRepository("[AppData]shp".Resolve())),
    "shp");

// 3) Compose Places
var ts = new Dictionary<string, IDiscreteTimeSeriesService<string,double>> {
    ["csv"] = Services.Get<IDiscreteTimeSeriesService<string,double>>("csv")
};
var scalars = new Dictionary<string, IScalarService<string,int>>(); // none in sample
var gis = Services.Get<IGisService<string>>("shp");

ServiceLocator.Register(
    new PlaceService(
        new PlaceRepository("[AppData]places.json".Resolve()),
        ts, scalars, gis),
    "json"  // <-- your {connectionId}
);

Then your API base is api/places/json.

Sample places.json (abridged)

{
  "Stations": {
    "MyStation": {
      "FeatureId": { "FeatureCollectionId": "Stationer.shp", "AttributeKey": "StatId", "AttributeValue": "ID92_M16" },
      "Indicators": {
        "WaterLevel": {
          "DataSource": { "ConnectionId": "csv", "EntityId": "timeseries.csv;TimeSeries1", "Type": "TimeSeries" },
          "TimeInterval": { "Type": "All" },
          "AggregationType": { "DisplayName": "Maximum", "Value": 1 },
          "StyleCode": "0:green|10:red",
          "PaletteType": "LowerThresholdValues"
        },
        "Rainfall": {
          "DataSource": { "ConnectionId": "csv", "EntityId": "timeseries.csv;TimeSeries2", "Type": "TimeSeries" },
          "TimeInterval": { "Type": "RelativeToDateTime", "Start": -1.0, "End": 0.0 },
          "AggregationType": { "DisplayName": "Sum", "Value": 3 },
          "StyleCode": "0~15:#800080,#5500AB,#2A00D5,#0000FF",
          "PaletteType": "LowerThresholdValues"
        }
      },
      "FullName": "Stations/MyStation",
      "Metadata": { "PointCategory": "Station", "Letter": "S", "Color": "#FFD700" }
    }
  }
}

B) Registration via Connections module (config-driven)

Drop a connections.json with entries like:

{
  "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",

  "csv": {
    "$type": "DHI.Services.TimeSeries.WebApi.DiscreteTimeSeriesServiceConnection, DHI.Services.TimeSeries.WebApi",
    "ConnectionString": "[AppData]",
    "RepositoryType": "DHI.Services.TimeSeries.CSV.TimeSeriesRepository, DHI.Services.TimeSeries",
    "Name": "CSV time series service connection",
    "Id": "csv"
  },

  "shp": {
    "$type": "DHI.Services.GIS.WebApi.GisServiceConnection, DHI.Services.GIS.WebApi",
    "ConnectionString": "[AppData]shp",
    "RepositoryType": "DHI.Services.Provider.ShapeFile.FeatureRepository, DHI.Services.Provider.ShapeFile",
    "Name": "Shape file GIS service connection",
    "Id": "shp"
  },

  "json": {
    "$type": "DHI.Services.Places.WebApi.PlaceServiceConnection, DHI.Services.Places.WebApi",
    "ConnectionString": "[AppData]places.json",
    "RepositoryType": "DHI.Services.Places.PlaceRepository, DHI.Services.Places",
    "GisServiceConnectionId": "shp",
    "TimeSeriesServiceConnectionIds": [ "csv" ],
    "Name": "Place service connection",
    "Id": "json"
  }
}

In Startup (or Program for minimal hosting), enable connections:

// Services.Configure(new ConnectionRepository("connections.json", SerializerOptionsDefault.Options), lazyCreation: true);

Customize RepositoryType, ConnectionString, GisServiceConnectionId, and TimeSeriesServiceConnectionIds as needed (e.g., DB-backed repos in the future).


Practical usage (API calls)

Create a place

POST /api/places/json
Content-Type: application/json

{
  "fullName": "Stations|MyStation",
  "featureId": { "featureCollectionId": "Stationer.shp", "attributeKey": "StatId", "attributeValue": "ID92_M16" },
  "metadata": { "PointCategory": "Station", "Letter": "S" },
  "indicators": {
    "Rainfall": {
      "dataSource": { "type": "TimeSeries", "connectionId": "csv", "entityId": "timeseries.csv;TimeSeries2" },
      "styleCode": "0~15:#800080,#5500AB,#2A00D5,#0000FF",
      "timeInterval": { "type": "RelativeToDateTime", "start": -1, "end": 0 },
      "aggregationType": "Sum",
      "paletteType": "LowerThresholdValues"
    }
  }
}

Get indicator status (relative to a specific datetime)

GET /api/places/json/Stations|MyStation/indicators/Rainfall/status?dateTime=2021-06-30T12:00:00Z

Build a map layer with statuses for all places in a group

GET /api/places/json/features?group=Stations&includeIndicatorStatus=true

Explicit period status

GET /api/places/json/features?group=Stations&includeIndicatorStatus=true&from=2025-06-01T00:00:00Z&to=2025-06-30T23:59:59Z

Palette PNG

GET /api/places/json/Stations|MyStation/indicators/Rainfall/palette?width=320&height=24&numberOfDecimals=1&horizontal=true
Accept: image/png

Validation & error behavior

These are enforced by the underlying PlaceService and surface as 400/404/204:

  • Place Add fails if:
    • GIS collection missing or the feature not found for (AttributeKey == AttributeValue).
    • Place already exists.
    • Any indicator is invalid (missing service, missing time series, scalar type is not System.Double, etc.).
  • Indicator Add/Update fails if:
    • Backing service (by ConnectionId) doesn’t exist.
    • TimeSeries indicator has no AggregationType.
    • Indicator references a time series ID that does not exist (unless it uses [Path] placeholder).
  • Status endpoints return 204 No Content when no status can be computed for a specific indicator (no data in range, all ensemble members null, etc.).
  • Get404 Not Found when place/indicator is missing.

Common pitfalls

  • URL encoding: Use | instead of / inside route segments; escape | as ||. (FullNameString.ToUrl / FromUrl.)
  • RelativeToDateTime: When using indicators with TimeInterval.Type = RelativeToDateTime, pass dateTime on the status endpoints; otherwise the API will reject/return no data.
  • Path injection for time series: When an indicator’s EntityId starts with [Path], pass path=… on the status endpoints to inject the root segment at runtime.
  • AggregationType strings: Must match the display name (e.g., "Average", "Sum") — not the enum member name or numeric value.
  • Threshold endpoints: Treat them as arrays of numbers in clients.

Extensibility notes

  • Repositories: The controller is agnostic — as long as you supply a type implementing IPlaceRepository<string>. Swap in your own repository via PlaceServiceConnection.RepositoryType.
  • Auth: The controller only requires [Authorize]. Add role/policy attributes if your scenario demands write protection.
  • Serialization: If you expose these DTOs in other services, reuse SerializerOptionsDefault.Options to keep payloads consistent.

Quick reference

Need… Use… Notes
Create/read/update/delete places POST /, GET /{id}, GET /, DELETE /{id} FullName encoded with |
Manage indicators POST/PUT/DELETE /{id}/indicator/{type} AggregationType as display name
List indicators GET /{id}/indicators Map of type → indicator
Status color (one) GET /{id}/indicators/{type}/status 204 if no data
Status color (bulk by type/group) GET /indicators/{type}/status?group=… Map of place id → status
Thresholds GET /{id}/thresholds / …/{indicatorType} Arrays of numbers
Features (optional status) GET /features?includeIndicatorStatus=… Attaches { indicators: { type: { styleCode, color }}}
Palette PNG GET /{id}/indicators/{type}/palette?width=&height= Horizontal/vertical options
Wire manually ServiceLocator.Register(…) See sample Startup
Wire via connections PlaceServiceConnection in connections.json Use sample JSON; edit repository, connection strings, and dependency IDs