Skip to content

DHI.Services.DS for Scalars — Internal Developer Guide

Call another Domain Services (DS) server’s Scalars Web API as if it were a local repository. This package ships an HTTP-backed repository that implements the same abstractions as the core Scalars module, so you can plug it into your services with zero behavioral changes.

  • Repository: DHI.Services.Provider.DS.ScalarRepository
  • Models: convenience wrappers Provider.DS.Scalar and Provider.DS.ScalarData
  • Works with: ScalarService<TId,TFlag> / GroupedScalarService<TId,TFlag> from DHI.Services.Scalars
  • Retries: built-in (Polly), bearer auth via IAccessTokenProvider

See DS fundamentals here: DS Core


Install

dotnet add package DHI.Services.Provider.DS
dotnet add package DHI.Services.Scalars

When to use this provider

  • You want to consume scalars hosted by another DS service over HTTP.
  • You still want to program against the repository/service interfaces (IGroupedScalarRepository, IScalarService, …).
  • You prefer connection strings / connections.json wiring instead of manually composing HttpClient + auth.

What the HTTP provider expects

Point it at the remote Scalars Web API collection route, including the connection id, for example:

https://remote.host/api/scalars/{connectionId}

The provider maps its methods to the Web API routes:

  • GetAllGET {baseUrl}
  • Get(id)GET {baseUrl}/{idUrl}
  • CountGET {baseUrl}/count
  • GetByGroup(group)GET {baseUrl}?group={groupUrl}
  • GetFullNames(group)GET {baseUrl}/fullnames?group={groupUrl}
  • AddPOST {baseUrl}
  • UpdatePUT {baseUrl}
  • Remove(id)DELETE {baseUrl}/{idUrl}
  • SetData(id)PUT {baseUrl}/{idUrl}/data
  • SetLocked(id)PUT {baseUrl}/{idUrl}/locked

idUrl and groupUrl are URL-safe forms of FullName/group (internally via FullNameString.ToUrl(...)).


Quick start (code)

1) Construct the repository

Choose one of these constructors:

using DHI.Services.Provider.DS;
using DHI.Services.Scalars;

// A) Parse a connection string (baseUrl + token + optional retry)
var repo = new ScalarRepository(
    "baseUrl=https://remote.host/api/scalars/prod;token=[env:SCALARS_API_TOKEN];retry=5");

// B) Provide baseUrl + a token provider instance
var repo2 = new ScalarRepository(
    baseUrl: "https://remote.host/api/scalars/prod",
    accessTokenProvider: myAccessTokenProvider,   // implements IAccessTokenProvider
    retryCount: 5);

// C) Use an external HttpClient (e.g., from IHttpClientFactory)
var http = httpClientFactory.CreateClient("ds");
var repo3 = new ScalarRepository(
    http, 
    "baseUrl=https://remote.host/api/scalars/prod;token=[env:SCALARS_API_TOKEN]");

Connection string keys

  • baseUrl (required): the full collection URL (/api/scalars/{connectionId})
  • token (required): either a literal token or a placeholder like [env:SCALARS_API_TOKEN]
  • retry (optional): integer retry count for transient HTTP failures (default 5)

Tokens are sent as Authorization: Bearer {token}.

using DHI.Services;

// grouped service since the repo is group-aware
var svc = new GroupedScalarService<string,int>(repo);

// now you can use the regular service API:
svc.Add(new DHI.Services.Scalars.Scalar<string,int>(
    id: "ProjectA/Salinity Threshold", 
    name: "Salinity Threshold",
    valueTypeName: "System.Double",
    group: "ProjectA"));

svc.SetData("ProjectA/Salinity Threshold",
    new DHI.Services.Scalars.ScalarData<int>(35.0, DateTime.UtcNow, flag: 1));

var last = svc.GetByGroup("ProjectA").FirstOrDefault();

You can also plug the repo into ScalarService<string,int> if you do not use grouping semantics.


Registration options

Exactly like the rest of the Domain Services ecosystem, you can wire the DS provider three ways.

A) Quick registration (code)

ServiceLocator.Register(
    new GroupedScalarService<string,int>(
        new DHI.Services.Provider.DS.ScalarRepository(
            "baseUrl=https://remote.host/api/scalars/prod;token=[env:SCALARS_API_TOKEN];retry=5")),
    "scalars-ds");

B) Via connection object (code)

Use the Web API connection type but point it at the DS provider repo:

using DHI.Services.Scalars.WebApi;

var conn = new GroupedScalarServiceConnection("scalars-ds", "Scalars via DS")
{
    RepositoryType = "DHI.Services.Provider.DS.ScalarRepository, DHI.Services.Provider.DS",
    RepositoryConnectionString = "baseUrl=https://remote.host/api/scalars/prod;token=[env:SCALARS_API_TOKEN];retry=5"
};
ServiceLocator.Register(conn.Create(), conn.Id);

C) Via connections.json (declarative)

Recommended form

{
  "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",
  "scalars-ds": {
    "$type": "DHI.Services.Scalars.WebApi.GroupedScalarServiceConnection, DHI.Services.Scalars.WebApi",
    "RepositoryType": "DHI.Services.Provider.DS.ScalarRepository, DHI.Services.Provider.DS",
    "RepositoryConnectionString": "baseUrl=https://remote.host/api/scalars/prod;token=[env:SCALARS_API_TOKEN];retry=5",
    "Name": "Scalars via DS",
    "Id": "scalars-ds"
  }
}

To activate connection-based wiring at startup:

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

// later:
var svc = Services.Get<IGroupedScalarService<string,int>>("scalars-ds");

Provider behavior & implementation notes

  • Auth: Authorization: Bearer <token> where the token is supplied by IAccessTokenProvider (from your code or via token=... in the connection string).
  • Retries: Exponential backoff on HttpRequestException using Polly (2^attempt seconds; default attempts: 5). Configure via retry=<n> in the connection string or retryCount ctor arg.
  • Serialization: Uses System.Text.Json with:
    • JsonStringEnumConverter
    • PropertyNameCaseInsensitive = true
    • Scalars converters: ScalarConverter<string,int>, DictionaryTypeResolverConverter<string, Scalar<string,int>>
    • Accepts either the DTO shape (camelCase) or the raw model shape from the remote API.
  • Threading: The repository exposes synchronous methods that wrap async HTTP using AsyncHelpers.RunSync(...). It’s intended for server/service code paths (avoid UI thread blocking).
  • HttpClient:
    • Default ctor uses a static HttpClient (reused).
    • You can pass your own HttpClient (from IHttpClientFactory) to control lifetime, proxy, TLS, etc.
  • Contains(id): Issues a GET and checks for 404. For bulk checks, prefer GetAll / GetByGroup and filter in-memory.
  • Type conversion: ValueTypeName must identify a real .NET type ("System.Double", "System.Int32", "System.Guid", …). The provider parses/serializes values using InvariantCulture.
  • Locked semantics: Honored by the remote service; update operations against locked scalars will fail according to server policy.

Example end-to-end usage

// 1) Wire the DS-backed service
var svc = new GroupedScalarService<string,int>(
    new DHI.Services.Provider.DS.ScalarRepository(
        "baseUrl=https://ds.company.net/api/scalars/prod;token=[env:SCALARS_API_TOKEN];retry=3"));

// 2) Add metadata (no value yet)
svc.Add(new DHI.Services.Scalars.Scalar<string,int>(
    id: "ProjectX/MaxLevel",
    name: "MaxLevel",
    valueTypeName: "System.Double",
    group: "ProjectX") { Description = "Highest allowed reservoir level" });

// 3) Set a value
svc.SetData("ProjectX/MaxLevel", new DHI.Services.Scalars.ScalarData<int>(42.3, DateTime.UtcNow));

// 4) Read
var s = svc.Get("ProjectX/MaxLevel").Value;
Console.WriteLine($"{s.FullName} = {s.GetData().Value}");

// 5) Lock
svc.SetLocked("ProjectX/MaxLevel", true);

// 6) List by group
foreach (var item in svc.GetByGroup("ProjectX"))
    Console.WriteLine(item.FullName);

Using with your existing Web API hosts

If you already host DHI.Services.Scalars.WebApi on Server A, clients on Server B can consume it through this DS provider simply by pointing baseUrl to:

https://serverA.company.net/api/scalars/<connectionIdOnServerA>

No changes to Server A are required as long as it exposes the standard Scalars Web API routes.


Troubleshooting

  • 401/403 → Check your token source ([env:...] set? scope/claims valid?), and that the remote API’s auth scheme matches (JWT/Bearer).
  • 404 on Get/SetData → Confirm id uses the FullName of the scalar and that it’s URL-encoded the same way the remote service expects.
  • Type errors when setting data → Ensure ValueTypeName matches the runtime type (e.g., sending "36.5" with "System.Double").
  • Slow retries → Default backoff grows 2, 4, 8, 16, 32 seconds. Lower retry for latency-sensitive apps.

See also