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})
- Add (
- 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)
- Add (
- 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})
- Status of one indicator (
- Mapping
- GIS features of places, with optional indicator status evaluation (
GET …/features+ query flags) - Palette image for an indicator (
GET …/{id}/indicators/{type}/palette)
- GIS features of places, with optional indicator status evaluation (
- Places
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>— carriesFullName,FeatureId,Indicators,MetadataIndicatorDTO— carriesDataSource,StyleCode, optionalTimeInterval, optionalAggregationType(DisplayName string),PaletteType, and optionalQuantileIndicatorStatusDTO—IndicatorDTO+Status(SKColor)TimeIntervalDTO—Type,Start,End
AggregationTypein requests/responses is the display name (e.g.,"Average","Maximum"). We map it viaEnumeration.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=trueand eitherfromortois provided → status is evaluated over the explicit[from,to]period (overrides indicatorTimeInterval). - If
includeIndicatorStatus=trueand nofrom/to→ status is evaluated using each indicator’s declarativeTimeInterval. If any indicator usesRelativeToDateTime, passdateTime. - If
includeIndicatorStatus=false→ pure features; no status attached.
Returned features get extra attributes:
placeId,fullName,name,groupLayerindicators→{ [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/MyStation→Stations|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
IndicatorDTOis 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
IndicatorStatusDTOor204when 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/AdministratorsOnlyare 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, andTimeSeriesServiceConnectionIdsas 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.).
- GIS collection missing or the feature not found for
- 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).
- Backing service (by
- Status endpoints return
204 No Contentwhen no status can be computed for a specific indicator (no data in range, all ensemble members null, etc.). - Get →
404 Not Foundwhen 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, passdateTimeon the status endpoints; otherwise the API will reject/return no data. - Path injection for time series: When an indicator’s
EntityIdstarts with[Path], passpath=…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 viaPlaceServiceConnection.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.Optionsto 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 |