DHI.Services.TimeSteps.WebApi — Internal Developer Guide¶
Expose time-stepped data over HTTP using the TimeSteps Web API package. This guide documents the controller behavior, routes, payloads, error semantics, wiring, and a working host you can clone.
Source sample (ready to run): GitHub – TimeSteps Host https://github.com/DHI/DomainServicesSamples/tree/main/Host/TimeSteps
1) What you get¶
- Route base:
api/timesteps/{connectionId}(API v1) - Controller:
TimeStepsController - Auth:
[Authorize]on the controller (configure JWT Bearer in your host) - Data types on the wire:
itemId: string,value: object(your provider decides the JSON shape)
The controller resolves a server through the Domain Services runtime:
HTTP -> TimeStepsController
-> Services.Get<ITimeStepService<string, object>>(connectionId)
-> your ITimeStepServer<string, object> (provider)
2) Endpoints (quick matrix)¶
Base: /api/timesteps/{connectionId}
| Method | Path | Query/body | Response | Notes |
|---|---|---|---|---|
| GET | /{itemId}/data/{date} |
— | 200 object or 404 |
Exact value at date. Throws → mapped (see §5). |
| POST | /list |
{"id":[date,...], ...} |
200 Dictionary<string,Dictionary<DateTime,object>> |
Bulk read; skips missing pairs. |
| GET | /datetimes |
— | 200 DateTime[] |
Ascending timeline. |
| GET | /datetime/first |
— | 200 DateTime? |
null if empty. |
| GET | /datetime/last |
— | 200 DateTime? |
null if empty. |
| GET | /{itemId}/data/firstafter/{date} |
— | 200 object or 400 / 200 null |
Next value after date. Range-checked. |
| GET | /{itemId}/data/lastbefore/{date} |
— | 200 object or 400 / 200 null |
Previous value before date. Range-checked. |
| GET | /items |
— | 200 Item<string>[] |
Each item has Id and Name. |
Path params
{connectionId}— key registered inServiceLocatoror Connections (see §6).{itemId}— single URL segment; if your item IDs contain reserved characters, URL-encode them and avoid/.{date}— ASP.NET Core:datetimeroute; prefer ISO-8601 (2024-06-03T12:34:56Z).
3) Request/response examples¶
3.1 Get a value at an exact time¶
GET /api/timesteps/dfs2/station:001%7Ctemp/data/2014-10-01T00:30:00Z
Authorization: Bearer <jwt>
200 OK (example JSON shaped by your provider):
{
"Value": 12.34,
"Unit": "°C"
}
404 Not Found If the timestamp isn’t in the timeline, or the item has no value at that time.
3.2 Bulk read for many items and times¶
POST /api/timesteps/dfs2/list
Authorization: Bearer <jwt>
Content-Type: application/json
{
"station:001|temp": ["2014-10-01T00:00:00Z", "2014-10-01T00:30:00Z"],
"station:001|sal" : ["2014-10-01T00:00:00Z"]
}
200 OK
{
"station:001|temp": {
"2014-10-01T00:00:00Z": { "Value": 11.9, "Unit": "°C" },
"2014-10-01T00:30:00Z": { "Value": 12.3, "Unit": "°C" }
},
"station:001|sal": {
"2014-10-01T00:00:00Z": { "Value": 29.7, "Unit": "PSU" }
}
}
Missing pairs are simply omitted. DateTime dictionary keys are serialized as ISO-8601 strings.
3.3 Timeline & items¶
GET /api/timesteps/dfs2/datetimes
["2014-10-01T00:00:00Z","2014-10-01T00:30:00Z","2014-10-01T01:00:00Z"]
GET /api/timesteps/dfs2/items
[
{ "Id": "station:001|temp", "Name": "Temperature @ Station 001" },
{ "Id": "station:001|sal", "Name": "Salinity @ Station 001" }
]
3.4 Neighbors¶
GET /api/timesteps/dfs2/station:001%7Ctemp/data/firstafter/2014-10-01T00:05:00Z
- 200 OK value for the first time step after 00:05.
- 400 Bad Request if the query time is at/after the last timestamp.
- 200 OK (null) if the series is empty.
GET /api/timesteps/dfs2/station:001%7Ctemp/data/lastbefore/2014-10-01T00:05:00Z
- Symmetric rules (before first → 400; empty → 200 null).
4) Data shape (object payload)¶
The Web API is generic on purpose: it exposes values as object. Your provider (ITimeStepServer<string, object>) should return JSON-serializable POCOs or primitives:
- Simple numeric sample:
{ "Value": 12.34, "Unit": "°C" } - Vector sample:
{ "U": 0.12, "V": -0.03, "Unit": "m/s" } - Complex: any JSON that
System.Text.Jsoncan serialize
The sample host disables camelCase and writes properties as declared (
PropertyNamingPolicy = null). If you need a different casing, change the host’s JSON options.
5) Error semantics¶
The controller returns Ok(...) and relies on the standard DHI exception middleware to map domain exceptions:
| Source error from service | Meaning | Typical HTTP |
|---|---|---|
KeyNotFoundException |
Missing timestamp or item value at exact time | 404 Not Found |
ArgumentOutOfRangeException |
Neighbor query outside the timeline bounds | 400 Bad Request |
| Other unhandled | Unexpected provider/host failure | 500 Internal Server Error |
If your host uses the sample middleware (app.UseExceptionHandling(); or equivalent), these mappings are automatic.
6) Wiring the service¶
You can wire a time-step server imperatively with ServiceLocator, or declaratively via the Connections module.
6.1 Imperative (one-liner)¶
// Program/Startup after building the app
ServiceLocator.Register(
new TimeStepService<string, object>(
new DHI.Services.Provider.MIKECore.Dfs2TimeStepServer("[AppData]R20141001.dfs2".Resolve())
),
"dfs2" // <- your {connectionId}
);
Then call:
GET /api/timesteps/dfs2/datetimes
The
[AppData]placeholder is resolved by the Web API connection variant (see below) or by your own code if you call.Resolve().
6.2 Declarative (Connections module)¶
connections.json
{
"$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",
"dfs2": {
"$type": "DHI.Services.TimeSteps.WebApi.TimeStepServiceConnection, DHI.Services.TimeSteps.WebApi",
"ConnectionString": "[AppData]R20141001.dfs2",
"ServerType": "DHI.Services.Provider.MIKECore.Dfs2TimeStepServer, DHI.Services.Provider.MIKECore",
"Name": "Dfs2 timestep connection",
"Id": "dfs2"
}
}
At startup:
// Resolve [AppData] to <contentroot>/App_Data
AppDomain.CurrentDomain.SetData("DataDirectory",
Path.Combine(builder.Environment.ContentRootPath, "App_Data"));
// Enable if you use the Connections module globally
// Services.Configure(new ConnectionRepository("[AppData]connections.json".Resolve()), lazyCreation: true);
TimeStepServiceConnection(WebApi variant) automatically resolves[AppData]when constructing your server: it callsConnectionString.Resolve().
7) Minimal host (JWT + Swagger)¶
Here’s the essential shape (see the repo for a complete version).
var builder = WebApplication.CreateBuilder(args);
// AuthN/AuthZ (JWT bearer)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => { /* TokenValidationParameters ... */ });
builder.Services.AddAuthorization();
// MVC + JSON + API versioning
builder.Services.AddControllers()
.AddJsonOptions(o =>
{
o.JsonSerializerOptions.PropertyNamingPolicy = null;
o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
builder.Services.AddApiVersioning(o =>
{
o.ReportApiVersions = true;
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
});
// Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(o =>
{
o.SwaggerDoc("v1", new OpenApiInfo { Title = "TimeSteps API", Version = "1" });
o.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "DHI.Services.TimeSteps.WebApi.xml"));
o.EnableAnnotations();
// Bearer definition omitted for brevity
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("../swagger/v1/swagger.json", "TimeSteps API v1");
});
// Optional: exception handling middleware (maps exceptions -> HTTP)
app.UseExceptionHandling();
app.MapControllers();
// Set [AppData]
AppDomain.CurrentDomain.SetData("DataDirectory",
Path.Combine(app.Environment.ContentRootPath, "App_Data"));
// Register a provider (imperative example)
ServiceLocator.Register(
new TimeStepService<string, object>(
new DHI.Services.Provider.MIKECore.Dfs2TimeStepServer("[AppData]R20141001.dfs2".Resolve())),
"dfs2");
app.Run();
8) Provider expectations (server side)¶
When you implement ITimeStepServer<string, object> for the Web API:
- Timeline:
GetDateTimesmust be sorted ascending and deduplicated. - Items: return stable
Idand human-readableName. - Exact get: return
Maybe.Empty<object>()for missing values (the service translates to 404). - Neighbors: implement
GetFirstAfter/GetLastBeforeefficiently (binary search); Web API converts out-of-range into 400. - Auth (optional): inspect
ClaimsPrincipal userto enforce item/timestamp access if your backend requires it. - Objects: ensure your value objects are System.Text.Json serializable.
9) Practical tips¶
- Dates in URLs: Prefer UTC (
Zsuffix). Keep your provider’s timeline and lookups in the same time zone. - Item IDs in URLs: They’re single path segments. If your IDs contain characters like
:,|, or spaces, URL-encode them; avoid/. - Large bulk reads: The
/listendpoint lets clients fetch sparse matrices without repeated roundtrips. - Empty series: Neighbor endpoints return 200 null when the underlying series is empty; exact
GET …/data/{date}will 404 for missing data.
10) Troubleshooting¶
- 404 on exact GET: The timestamp isn’t part of the provider’s timeline, or the item has no value at that time.
- 400 on neighbor queries: Your
dateis outside the provider’s first/last bounds. - Item IDs with slashes: Encode or remap; the controller treats
{itemId}as one segment. - Unexpected
application/jsonnull: Series is empty (by design for first/last/neighbor endpoints). - Custom JSON shape: If your provider returns complex objects, ensure your host’s JSON options (converters, casing) match your clients’ expectations.
11) Reference: Controller actions¶
GET {itemId}/data/{date}→timeStepService.Get(itemId, date, user)POST list→timeStepService.Get(ids, user)GET datetimes→timeStepService.GetDateTimes(user)GET datetime/first→timeStepService.GetFirstDateTime(user)GET datetime/last→timeStepService.GetLastDateTime(user)GET {itemId}/data/firstafter/{date}→timeStepService.GetFirstAfter(itemId, date, user)GET {itemId}/data/lastbefore/{date}→timeStepService.GetLastBefore(itemId, date, user)GET items→timeStepService.GetItems(user)
12) See also¶
- Core guide: DHI.Services.TimeSteps
- Working sample host: https://github.com/DHI/DomainServicesSamples/tree/main/Host/TimeSteps