Skip to content

DHI.Services.Scalars.WebApi – Internal Guide

Expose Scalars (key–value pairs with timestamp and optional quality flag) over HTTP. This package wraps the Scalars core services with:

  • A ready-made ASP.NET Core controller (ScalarsController)
  • DTOs for clean wire formats
  • Connection classes to instantiate services from connections.json (with [AppData] resolution)
  • Small helpers for DTO mapping

It’s designed for internal DHI hosts and services that want a thin, uniform HTTP facade for Scalar<TId,TFlag>.


Install

dotnet add package DHI.Services.Scalars.WebApi

You’ll typically also reference your chosen repository provider (e.g., the built-in JSON repo from DHI.Services.Scalars, or your own).

Sample host app & wiring:

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


API surface

All endpoints are versioned (api-version=1) and secured via [Authorize].

Base route:

/api/scalars/{connectionId}

Where {connectionId} maps to a connection in connections.json (or a service registered via ServiceLocator).

Endpoints

Method & Route Purpose Notes
GET /api/scalars/{connectionId}/{id} Get a single scalar id is the URL form of the scalar identifier. For grouped connections, pass FullNameString.ToUrl(fullName).
GET /api/scalars/{connectionId} List scalars Optional group query only for grouped connections.
GET /api/scalars/{connectionId}/count Count
GET /api/scalars/{connectionId}/ids All IDs
GET /api/scalars/{connectionId}/fullnames All FullNames Grouped connections only. Optional group query for recursive listing within a group.
POST /api/scalars/{connectionId} Create Body: ScalarDTO.
PUT /api/scalars/{connectionId} Update whole scalar Body: ScalarDTO. Respects Locked.
PUT /api/scalars/{connectionId}/{id}/data?logging={true | false} Set data Body: ScalarDataDTO. Respects Locked. Optional logging=false to suppress change log.
PUT /api/scalars/{connectionId}/{id}/locked Set locked/unlocked Body: LockedDTO.
DELETE /api/scalars/{connectionId}/{id} Delete

Versioning Clients can provide the version via query or header:

  • ?api-version=1
  • Header: api-version: 1

Auth Controller is [Authorize]; the actual scheme/policies are configured by the host (see sample Startup below). Typical requests use a bearer token:

Authorization: Bearer <JWT>

DTOs (wire format)

ScalarDTO

{
  "FullName": "ProjectA/Salinity Threshold",
  "ValueTypeName": "System.Double",
  "Description": "Max allowed salinity",
  "Locked": false,
  "Value": "35.0",
  "DateTime": "2025-01-10T08:00:00Z",
  "Flag": 1
}
  • FullName = "Group/Name" for grouped connections (returned by the API). For non-grouped connections, the server still uses FullName but Group may be null — the path id is then the entity Id.
  • Value is a string on the wire. The server converts it to the actual .NET type indicated by ValueTypeName (e.g., "System.Double") using a ToObject() helper.

ScalarDataDTO

{
  "Value": "36.5",
  "DateTime": "2025-01-10T08:15:00Z",
  "Flag": 2
}

LockedDTO

{ "Locked": true }

Request/response examples (cURL)

Assume a grouped connection with ID fake (see registration options below).

Note: Always include a token if your host requires it: -H "Authorization: Bearer $TOKEN"

Create a scalar

curl -X POST "https://localhost:5001/api/scalars/fake?api-version=1" \
  -H "Content-Type: application/json" \
  -d '{
    "FullName": "ProjectA/Salinity Threshold",
    "ValueTypeName": "System.Double",
    "Description": "Max allowed salinity",
    "Locked": false
  }'

Response: 201 Created with Location header pointing to:

/api/scalars/fake/{id-from-FullNameString.ToUrl}

Set data

curl -X PUT "https://localhost:5001/api/scalars/fake/ProjectA%7CSalinity%20Threshold/data?api-version=1&logging=true" \
  -H "Content-Type: application/json" \
  -d '{ "Value": "35.0", "DateTime": "2025-01-10T08:00:00Z", "Flag": 1 }'

The {id} segment must match what the server expects for a FullName in URLs. Let the server tell you the correct form: use the Location header from POST or precompute with FullNameString.ToUrl(fullName) if you’re a .NET client.

Read it back

curl "https://localhost:5001/api/scalars/fake/ProjectA%7CSalinity%20Threshold?api-version=1"

Lock it

curl -X PUT "https://localhost:5001/api/scalars/fake/ProjectA%7CSalinity%20Threshold/locked?api-version=1" \
  -H "Content-Type: application/json" \
  -d '{ "Locked": true }'

List by group (grouped only)

curl "https://localhost:5001/api/scalars/fake?api-version=1&group=ProjectA"

List full names (grouped only)

curl "https://localhost:5001/api/scalars/fake/fullnames?api-version=1&group=ProjectA"

Behavior & constraints

  • Value typing: ScalarDTO.ValueTypeName must be a valid .NET type name (e.g., "System.Double", "System.Int32", "System.Guid"). The server validates on SetData/Update.
  • Locked: When Locked == true, Update, SetData, AddOrUpdate are rejected (exception → 500 by default; you can add exception middleware to translate to 423/409 if desired).
  • Grouped vs non-grouped:
    • If you pass group into GET /api/scalars/{conn}, the connection must be grouped (the controller casts to IGroupedScalarService for that call).
    • For grouped connections, id in path is the URL form of FullName.
  • Logging: PUT .../data?logging=false suppresses value-change logging for that one call (only relevant if the service was constructed with an ILogger).

Registering the service

You have 3 convenient options, mirroring the rest of Domain Services.

1) Quick registration (code)

Register an already constructed service (e.g., using the default JSON repo):

using DHI.Services;
using DHI.Services.Scalars;

ServiceLocator.Register(
    new GroupedScalarService<string,int>(
        new ScalarRepository("[AppData]scalars.json".Resolve())),
    "scalars-json");

2) Via connection object (code)

Use the connection types to build services programmatically. The Web API variants support [AppData] resolution out of the box.

using DHI.Services.Scalars.WebApi;

// Grouped JSON example
var conn = new GroupedScalarServiceConnection("scalars-json", "Scalars (JSON)")
{
    RepositoryType = "DHI.Services.Scalars.ScalarRepository, DHI.Services.Scalars",
    RepositoryConnectionString = "[AppData]scalars.json"
    // optionally:
    // LoggerType = "DHI.Services.Logging.SimpleLogger, DHI.Services",
    // LoggerConnectionString = "[AppData]scalars.log"
};

ServiceLocator.Register(conn.Create(), conn.Id);

3) Via connection module (connections.json)

Declare connections declaratively; they’ll be created on demand when you call Services.Get<T>(id).

A. “Fake” in-memory example (from the sample host)

{
  "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",
  "fake": {
    "$type": "DHI.Services.Scalars.WebApi.GroupedScalarServiceConnection, DHI.Services.Scalars.WebApi",
    "RepositoryConnectionString": "",
    "RepositoryType": "DHI.Services.Scalars.WebApi.Host.ScalarRepository, DHI.Services.Scalars.WebApi.Host",
    "LoggerConnectionString": "[AppData]scalars.log",
    "LoggerType": "DHI.Services.Logging.SimpleLogger, DHI.Services",
    "Name": "Fake in-memory scalar service connection",
    "Id": "fake"
  }
}

B. JSON repository (grouped) connection

{
  "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",
  "scalars-json": {
    "$type": "DHI.Services.Scalars.WebApi.GroupedScalarServiceConnection, DHI.Services.Scalars.WebApi",
    "RepositoryType": "DHI.Services.Scalars.ScalarRepository, DHI.Services.Scalars",
    "RepositoryConnectionString": "[AppData]scalars.json",
    "LoggerType": "DHI.Services.Logging.SimpleLogger, DHI.Services",
    "LoggerConnectionString": "[AppData]scalars.log",
    "Name": "Scalars (JSON, grouped)",
    "Id": "scalars-json"
  }
}

Startup (choose one approach):

// A) Manual service registration
ServiceLocator.Register(
    new GroupedScalarService<string,int>(new ScalarRepository("[AppData]scalars.json".Resolve())),
    "scalars-json");

// B) Connections.json (lazy creation)
Services.Configure(new ConnectionRepository("[AppData]connections.json".Resolve()), lazyCreation: true);
// Later: var svc = Services.Get<IGroupedScalarService<string,int>>("scalars-json");

Hosting notes (ASP.NET Core)

A typical Startup (from the sample host) configures:

  • JWT auth (AddAuthentication().AddJwtBearer(...))

  • Authorization policies (your app decides which roles/claims can call the API)

  • API versioning (query/header readers)

  • Controllers with JSON options:

    • PropertyNamingPolicy = null (keep property names as declared)
    • JsonStringEnumConverter (enums as strings)
  • HSTS, CORS, Response compression

  • Swagger with XML docs:

    setupAction.IncludeXmlComments(
        Path.Combine(AppContext.BaseDirectory, "DHI.Services.Scalars.WebApi.xml"));
    

    Ensure “Generate documentation file” is enabled for the Web API project so the XML exists.

  • [AppData] resolution: set the data directory once in Configure:

    var contentRootPath = env.ContentRootPath; // or from config
    AppDomain.CurrentDomain.SetData("DataDirectory",
        Path.Combine(contentRootPath, "App_Data"));
    
  • Service registration: either via ServiceLocator.Register(...) or Services.Configure(...) as shown above.


Mapping, conversion & URL forms

  • DTO ↔ Entity: ScalarDTO.ToScalar() and new ScalarDTO(scalar) take care of mapping. Collections: ToDTOs().
  • Value conversion: ScalarDTO.Value is converted using Value.ToObject() from WebApiCore, guided by ValueTypeName.
  • FullName in URLs: When the route takes {id} for a grouped connection, pass the URL-safe representation of FullName. The server’s helpers use FullNameString.FromUrl(...)/ToUrl(...). Let the server guide you:
    • After POST, the Location header contains the correct {id} to use.
    • If you construct clients in .NET, use FullNameString.ToUrl(fullName) to compute the path segment.

Error semantics

  • 404 NotFound when a scalar doesn’t exist (in GET/SetData/SetLocked after re-read).
  • 400 BadRequest when DTO validation fails ([Required] attributes).
  • 5xx can surface for:
    • Locked updates (service throws). Consider translating to 409 Conflict/423 Locked using centralized exception middleware if you want stricter REST semantics.
    • Type mismatches: ValueTypeName doesn’t match actual Value type.

End-to-end flow (typical)

  1. Create a scalar with POST (no data yet), including ValueTypeName.
  2. Set its data with PUT .../data (value + timestamp + optional flag).
  3. Read via GET (returns a ScalarDTO with Value/DateTime/Flag).
  4. Lock if the value must not change until explicitly unlocked.
  5. Organize using groups (FullName="Group/Name") and list by group when needed.

FAQ & tips

  • Do I need a grouped connection? Use grouped when you want hierarchical scoping (e.g., "Project/Site/Parameter"). For flat key spaces, a non-grouped service is fine (you’d use ScalarServiceConnection instead of the grouped one).

  • What types can Value have? Anything that can be represented by a .NET runtime type name and round-tripped via JSON (e.g., double, int, Guid, string, complex scalars serialized as strings). Always set ValueTypeName accordingly.

  • How do I suppress change logs for bulk loads? Call PUT .../data?logging=false.

  • Permissions? Scalars inherit Permissions from the core entity base types. Enforce at the service or controller layer as needed (or in a gateway).


Sample host (short form)

app.UseAuthentication();
app.UseAuthorization();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("../swagger/v1/swagger.json", "Scalars API");
    c.DocExpansion(DocExpansion.None);
    c.DefaultModelsExpandDepth(-1);
});

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

ServiceLocator.Register(
    new GroupedScalarService<string,int>(
        new DHI.Services.Scalars.WebApi.Host.ScalarRepository(string.Empty),
        new DHI.Services.Logging.SimpleLogger("[AppData]scalars.log".Resolve())),
    "fake");

And if you prefer connections.json, drop in the snippet shown above and switch to:

Services.Configure(new ConnectionRepository("[AppData]connections.json".Resolve()), lazyCreation: true);