Skip to content

DHI.Services.Meshes.WebApi – Internal Guide

1) What this API does

The Meshes Web API is a read-only HTTP facade over DHI.Services.Meshes. It lets clients:

  • Discover available meshes and metadata (MeshInfo)
  • Query time series at a point (for one item or all items)
  • Compute spatial aggregations (whole mesh or within polygons), optionally grouped by a temporal period
  • Get instantaneous aggregated values at a specific timestamp
  • Generate contour lines (as GeoJSON features) for a specific timestamp or hourly over a time range

This layer is storage-agnostic. Which repository/provider is used is decided by the {connectionId} part of the route and the service registration in the host (see §6).


2) High-level architecture

HTTP Controller (MeshesController)
          │
          ▼
Services.Get<IMeshService>(connectionId)
          │              (or IGroupedMeshService for grouped operations)
          ▼
  MeshService / GroupedMeshService<TId>
          │
          ▼
       IMeshRepository<TId> (provider-specific)
  • Authentication: [Authorize] on the controller (JWT Bearer).
  • Versioning: [ApiVersion("1")] → all routes are v1 of this controller.
  • Serialization: custom JsonSerializerOptions with Mesh/Spatial/TimeSeries converters (SerializerOptionsDefault).
  • Swagger/OpenAPI: configured in the host with annotations on actions.

3) Routing, shapes & examples

Base path

/api/meshes/{connectionId}

{connectionId} selects a registered mesh service (e.g., "dfsu" in the host example). See §6 for registering more.

IDs & Groups using “FullName” Many endpoints accept mesh id or group that represent a FullName (Group/Name). Use standard URL encoding (e.g., Coastal%2FWestBay) — the controller uses FullNameString.FromUrl(...) to decode.

Conventions

  • Times are UTC; pass ISO-8601 (YYYY-MM-DDTHH:mm:ssZ) unless you’re sure the client attaches Z.
  • GeoJSON bodies:
    • Point{"type":"Point","coordinates":[x,y]}
    • Polygon{"type":"Polygon","coordinates":[ [[x1,y1],[x2,y2],...,[x1,y1]] ]} (first ring only shown; use RFC 7946 format)
  • AggregationType path segment: one of Minimum, Maximum, Average, Sum (case-insensitive display names parsed via Enumeration.FromDisplayName<AggregationType>).
  • Period path segment: any supported Period value (e.g., Hour, Day, Month, Year).

3.1 Catalog & discovery

GET /api/meshes/{connectionId}

  • Optional group query string (?group=Project%2FA).
  • Returns IEnumerable<MeshInfo> (group-aware entries with id, name, group, items[], dateRange, optional projection, boundingBox).

Example

GET /api/meshes/dfsu?group=Scenarios%2FRiverMouth HTTP/1.1
Authorization: Bearer <JWT>
Accept: application/json

GET /api/meshes/{connectionId}/count

Returns the number of meshes.

GET /api/meshes/{connectionId}/{id}

Returns one MeshInfo. id may be a “FullName”, e.g. Scenarios%2FRiverMouth%2Fmesh-001.

GET /api/meshes/{connectionId}/ids

Returns IEnumerable<string> of mesh IDs.

GET /api/meshes/{connectionId}/fullnames

Optional group query. These are convenience strings "{group}/{name}".

Only meaningful for grouped providers; it’s fine to call on non-grouped providers (you’ll just get name).


3.2 Availability

GET /api/meshes/{connectionId}/{id}/datetimes

Returns IEnumerable<DateTime> available for queries (sorted). Use this to validate timestamps for contours/instant aggregates.


3.3 Values at a point

POST /api/meshes/{connectionId}/{id}/{item}/values

Body: GeoJSON Point. Query: optional from, to.

  • Returns ITimeSeriesData<double> for one item and Point in a DateRange(from,to).

Example

POST /api/meshes/dfsu/Scenarios%2FRiverMouth%2Fmesh-001/WaterLevel/values?from=2025-05-01T00:00:00Z&to=2025-05-02T00:00:00Z
Content-Type: application/json
Authorization: Bearer <JWT>

{"type":"Point","coordinates":[12.345,55.678]}

Response (shape via TimeSeriesDataConverter<double>, camelCase)

[
  {"time":"2025-05-01T00:00:00Z","value":0.23},
  {"time":"2025-05-01T01:00:00Z","value":0.25}
]

POST /api/meshes/{connectionId}/{id}/values

Body: GeoJSON Point. Query: optional from, to.

  • Returns Dictionary<string, ITimeSeriesData<double>> keyed by item for all items at the point.
{
  "WaterLevel":[{"time":"...","value":0.23}, ...],
  "Salinity":[{"time":"...","value":28.1}, ...]
}

3.4 Spatial aggregation over a range

GET /api/meshes/{connectionId}/{id}/{item}/{aggregation}

Query: optional from, to. Returns aggregated time series over the whole mesh.

POST /api/meshes/{connectionId}/{id}/{item}/{aggregation}

Body: GeoJSON Polygon. Query: optional from, to. Returns aggregated time series within polygon.

Grouped by period

  • Whole mesh: GET /api/meshes/{connectionId}/{id}/{item}/{aggregation}/period/{period}?from=...&to=...
  • Within polygon: POST /api/meshes/{connectionId}/{id}/{item}/{aggregation}/period/{period}?from=...&to=... (GeoJSON Polygon in body)

Response shape is ITimeSeriesData<double> as in §3.3.


3.5 Instantaneous aggregation

GET /api/meshes/{connectionId}/{id}/{item}/{aggregation}/{dateTime}

Returns a single aggregated value (whole mesh) at the timestamp. May be null if no data exactly at that instant.

POST /api/meshes/{connectionId}/{id}/{item}/{aggregation}/{dateTime}

Body: GeoJSON Polygon. Returns a single aggregated value within polygon.

Note: The controller action returns ActionResult<double> but the underlying service returns double?. If you expect null (no data) in your provider, prefer changing the signature to ActionResult<double?> in the controller (see §7).


3.6 Contours

POST /api/meshes/{connectionId}/contours/{id}/{item}/{dateTime}

Body: double[] threshold values.

  • Returns a GeoJSON-like IFeatureCollection with contour polygons. Geometry/feature serialization is handled by the custom converters so it looks like standard GeoJSON:
{
  "type": "FeatureCollection",
  "features": [
    {
      "type":"Feature",
      "geometry":{"type":"Polygon","coordinates":[[ [x,y], ... ]]},
      "properties":{"value": 0.5}
    }
  ]
}

POST /api/meshes/{connectionId}/contours/{id}/{item}?from=...&to=...

Body: double[] threshold values. Returns a list of IFeatureCollection — one per hour between from and to (inclusive).

Time stepping is hourly. If you need a different cadence, adjust the range enumeration in this action.


4) Error handling & status codes

  • The controller is decorated with ProducesResponseType(404) on endpoints that may raise “not found”. Internally, services throw KeyNotFoundException when a mesh id does not exist and the global exception handler (app.UseExceptionHandling()) maps this to 404.
  • Invalid arguments (e.g., missing body for POST, invalid GeoJSON, unknown aggregation token) will bubble up to the exception handler; how these are mapped depends on WebApiCore’s middleware. Aim to return 400 for client errors; throw ArgumentException/ArgumentNullException in services/providers to help mapping.
  • Authentication failures yield 401; missing/invalid bearer token, or 403 if user not allowed depending on repository authorization.

Developer tip: If your provider can’t produce a value at an instant, return Maybe.Empty<double>() from the repository. The service will convert that to null safely.


5) Serialization (JSON)

SerializerOptionsDefault.Options configures:

  • DefaultIgnoreCondition = WhenWritingNull
  • PropertyNamingPolicy = CamelCase
  • PropertyNameCaseInsensitive = true
  • ReferenceHandler = IgnoreCycles
  • JsonNumberHandling = AllowReadingFromString
  • Converters added:
    • Spatial: PositionConverter, GeometryConverter, FeatureCollectionConverter
    • Time series: TimeSeriesDataConverter<double>
    • Utilities: ObjectToInferredTypeConverter, DateRangeConverter

These are injected in Startup:

services.AddControllers().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.DefaultIgnoreCondition = SerializerOptionsDefault.Options.DefaultIgnoreCondition;
    options.JsonSerializerOptions.WriteIndented = true;
    options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
    options.JsonSerializerOptions.AddConverters(SerializerOptionsDefault.Options.Converters);
});

Implication for clients: time series serialize as arrays of { time, value }, and features serialize as GeoJSON, using camelCase everywhere.


6) Hosting & wiring providers

The Host project shows the minimal wiring:

// 1) JWT Bearer
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ... }; });

// 2) Controllers + JSON (see §5)

// 3) Swagger (security scheme "Bearer" added)

// 4) Pipeline
app.UseAuthentication();
app.UseSwagger();
app.UseSwaggerUI(...);
app.UseExceptionHandling();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapControllers());

// 5) Register a mesh service under a connection id
var meshRepository = new DfsuMeshRepository(new FileSource("[AppData]\\dfsu".Resolve()));
var meshService = new GroupedMeshService(meshRepository);
ServiceLocator.Register(meshService, "dfsu");   // ← connectionId

Adding another source (e.g., NetCDF):

var ncRepo = new NetCdfMeshRepository(new FileSource("[AppData]\\netcdf".Resolve()));
var ncService = new MeshService(ncRepo); // or GroupedMeshService if grouped
ServiceLocator.Register(ncService, "netcdf");

Consumers then hit /api/meshes/netcdf/....

JWT settings expected in configuration:

"Tokens": {
  "Issuer": "dhigroup.com",
  "Audience": "dhigroup.com",
  "PublicRSAKey": "[env:PublicRSAKey]"
},

7) Notes

7.1 Concurrent dictionary bug in GetContoursInRange

This action fills results from a Parallel.ForEach but uses a plain Dictionary<long, IFeatureCollection>, which is not thread-safe.

Fix:

var results = new ConcurrentDictionary<long, IFeatureCollection>();

Parallel.ForEach(
    Partitioner.Create(dates, EnumerablePartitionerOptions.NoBuffering),
    kvp => results.TryAdd(kvp.Key, meshService.GetContours(id, item, thresholdValues, kvp.Value, user))
);

var returnObj = results.OrderBy(x => x.Key).Select(x => x.Value).ToList();

7.2 Instant aggregate return type

If providers may return null for “no value at this instant”, the controller signatures should be ActionResult<double?>:

[HttpGet("{id}/{item}/{aggregation}/{dateTime}")]
public ActionResult<double?> GetAggregatedValue(...) =>
    Ok(meshService.GetAggregatedValue(id, aggregationType, item, dateTime, user));

[HttpPost("{id}/{item}/{aggregation}/{dateTime}")]
public ActionResult<double?> GetAggregatedValueWithinPolygon(...) =>
    Ok(meshService.GetAggregatedValue(id, aggregationType, item, polygon, dateTime, user));

7.3 Consistent UTC

Services and repositories generally operate in UTC. Encourage clients to pass Z or convert to UTC server-side before calling repositories.

7.4 Aggregation tokens

Enumeration.FromDisplayName<AggregationType>(aggregation) will throw if the string doesn’t match a known display name. If you anticipate typos, add a guard translating common aliases (e.g., avg, max, min, sum) before calling it.

7.5 Group endpoints on non-grouped repos

GetFullNames and the group query parameter assume a grouped provider. We already call the grouped interface explicitly (Services.Get<IGroupedMeshService<string>>), so for non-grouped providers don’t register an IGroupedMeshService (or register a thin adapter that returns just Name as full name).

7.6 Large results

  • Time series over long ranges or many items can be big. Consider adding paging or a maxPoints downsampling query in the future.
  • Contours over long hourly ranges can be heavy. For bulk generation, prefer a streaming endpoint or server-side archiving.

8) End-to-end examples

All examples assume you registered "dfsu" and are holding a valid JWT.

8.1 List meshes in a group

curl -H "Authorization: Bearer $TOKEN" \
  "https://localhost:5001/api/meshes/dfsu?group=Scenarios%2FRiverMouth"

8.2 Inspect one mesh

curl -H "Authorization: Bearer $TOKEN" \
  "https://localhost:5001/api/meshes/dfsu/Scenarios%2FRiverMouth%2Fmesh-001"

8.3 Values at a point for one item

curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  "https://localhost:5001/api/meshes/dfsu/Scenarios%2FRiverMouth%2Fmesh-001/WaterLevel/values?from=2025-05-01T00:00:00Z&to=2025-05-01T12:00:00Z" \
  -d '{"type":"Point","coordinates":[12.345,55.678]}'

8.4 Average in a polygon, grouped monthly

curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  "https://localhost:5001/api/meshes/dfsu/mesh-001/WaterLevel/Average/period/Month?from=2025-01-01T00:00:00Z&to=2025-12-31T23:59:59Z" \
  -d '{ "type":"Polygon", "coordinates":[ [ [12.0,55.0],[12.5,55.0],[12.5,55.5],[12.0,55.5],[12.0,55.0] ] ] }'

8.5 Instantaneous max over whole mesh

curl -H "Authorization: Bearer $TOKEN" \
  "https://localhost:5001/api/meshes/dfsu/mesh-001/WaterLevel/Maximum/2025-05-15T06:00:00Z"

8.6 Contours at a timestamp

curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  "https://localhost:5001/api/meshes/dfsu/contours/mesh-001/WaterLevel/2025-05-15T06:00:00Z" \
  -d '[ -1.0, 0.0, 1.0 ]'

8.7 Hourly contours in a range

curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  "https://localhost:5001/api/meshes/dfsu/contours/mesh-001/WaterLevel?from=2025-05-01T00:00:00Z&to=2025-05-01T05:00:00Z" \
  -d '[ 0.2, 0.5, 0.8 ]'

9) Extending the API

  • Add cadence parameter to GetContoursInRange (e.g., &stepHours=3) and change the date enumeration.
  • Add multi-polygon support for aggregations: accept a FeatureCollection of polygons in the body and return a dictionary of results keyed by feature id.
  • Add downsampling for time series (e.g., maxPoints + LTTB algorithm server-side).
  • Add CRS transform: allow crs query to transform input point/polygon to mesh projection server-side using ProjNet.

10) Quick checklist when adding a new provider & exposing it

  1. Implement IMeshRepository<TId> (or IGroupedMeshRepository<TId>) in your provider (DFS(U), NetCDF, DB, …).
  2. Choose grouped or plain MeshService<TId> and register it with a connection id:
    ServiceLocator.Register(new MeshService(myRepo), "my-connection");
    
  3. Hit the API with /api/meshes/my-connection/....
  4. Validate:
    • /ids, /count, and one /{id}
    • /{id}/datetimes
    • One point time series & one polygon aggregation
    • One contour response
  5. If your provider may return “no value at instant”, update controller signatures to ActionResult<double?> (§7.2).
  6. (Optional) If grouped: wire IGroupedMeshService and test /fullnames and ?group= queries.