Skip to content

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 in ServiceLocator or Connections (see §6).
  • {itemId}single URL segment; if your item IDs contain reserved characters, URL-encode them and avoid /.
  • {date} — ASP.NET Core :datetime route; 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.Json can 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 calls ConnectionString.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: GetDateTimes must be sorted ascending and deduplicated.
  • Items: return stable Id and human-readable Name.
  • Exact get: return Maybe.Empty<object>() for missing values (the service translates to 404).
  • Neighbors: implement GetFirstAfter/GetLastBefore efficiently (binary search); Web API converts out-of-range into 400.
  • Auth (optional): inspect ClaimsPrincipal user to 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 (Z suffix). 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 /list endpoint 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 date is outside the provider’s first/last bounds.
  • Item IDs with slashes: Encode or remap; the controller treats {itemId} as one segment.
  • Unexpected application/json null: 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 listtimeStepService.Get(ids, user)
  • GET datetimestimeStepService.GetDateTimes(user)
  • GET datetime/firsttimeStepService.GetFirstDateTime(user)
  • GET datetime/lasttimeStepService.GetLastDateTime(user)
  • GET {itemId}/data/firstafter/{date}timeStepService.GetFirstAfter(itemId, date, user)
  • GET {itemId}/data/lastbefore/{date}timeStepService.GetLastBefore(itemId, date, user)
  • GET itemstimeStepService.GetItems(user)

12) See also