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 usesFullNamebutGroupmay benull— the pathidis then the entityId.Valueis a string on the wire. The server converts it to the actual .NET type indicated byValueTypeName(e.g.,"System.Double") using aToObject()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 theLocationheader fromPOSTor precompute withFullNameString.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.ValueTypeNamemust be a valid .NET type name (e.g.,"System.Double","System.Int32","System.Guid"). The server validates onSetData/Update. - Locked: When
Locked == true,Update,SetData,AddOrUpdateare rejected (exception → 500 by default; you can add exception middleware to translate to 423/409 if desired). - Grouped vs non-grouped:
- If you pass
groupintoGET /api/scalars/{conn}, the connection must be grouped (the controller casts toIGroupedScalarServicefor that call). - For grouped connections,
idin path is the URL form of FullName.
- If you pass
- Logging:
PUT .../data?logging=falsesuppresses value-change logging for that one call (only relevant if the service was constructed with anILogger).
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(...)orServices.Configure(...)as shown above.
Mapping, conversion & URL forms¶
- DTO ↔ Entity:
ScalarDTO.ToScalar()andnew ScalarDTO(scalar)take care of mapping. Collections:ToDTOs(). - Value conversion:
ScalarDTO.Valueis converted usingValue.ToObject()fromWebApiCore, guided byValueTypeName. - FullName in URLs: When the route takes
{id}for a grouped connection, pass the URL-safe representation ofFullName. The server’s helpers useFullNameString.FromUrl(...)/ToUrl(...). Let the server guide you:- After
POST, theLocationheader contains the correct{id}to use. - If you construct clients in .NET, use
FullNameString.ToUrl(fullName)to compute the path segment.
- After
Error semantics¶
404 NotFoundwhen a scalar doesn’t exist (inGET/SetData/SetLockedafter re-read).400 BadRequestwhen DTO validation fails ([Required]attributes).5xxcan surface for:- Locked updates (service throws). Consider translating to
409 Conflict/423 Lockedusing centralized exception middleware if you want stricter REST semantics. - Type mismatches:
ValueTypeNamedoesn’t match actualValuetype.
- Locked updates (service throws). Consider translating to
End-to-end flow (typical)¶
- Create a scalar with
POST(no data yet), includingValueTypeName. - Set its data with
PUT .../data(value + timestamp + optional flag). - Read via
GET(returns aScalarDTOwithValue/DateTime/Flag). - Lock if the value must not change until explicitly unlocked.
- 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 useScalarServiceConnectioninstead of the grouped one). -
What types can
Valuehave? 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 setValueTypeNameaccordingly. -
How do I suppress change logs for bulk loads? Call
PUT .../data?logging=false. -
Permissions? Scalars inherit
Permissionsfrom 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);