Skip to content

Building a Custom Domain


When to use this

Use the custom domain pattern when:

  • None of the existing modules (Jobs, TimeSeries, GIS, Scalars, ...) fit your data model.
  • You want a project-specific domain that integrates cleanly into the DS ecosystem (same connectionId routing, same connections.json configuration, same tooling).
  • You are building a reusable component that others in DHI can consume via NuGet.

Step 1 — Define your Entity

Choose a base class based on what fields you need:

Need Base class
ID only BaseEntity<TId>
ID + Name BaseNamedEntity<TId>
ID + Name + Group BaseGroupedEntity<TId>
ID is a file path Compose an ID type from BaseGroupedFileEntityId

Example — a Sensor with an ID, name, and unit:

using DHI.Services;

public sealed class Sensor : BaseNamedEntity<Guid>
{
    public Sensor(Guid id, string name, string unit)
        : base(Guard.Against.Null(id, nameof(id)),
               Guard.Against.NullOrWhiteSpace(name, nameof(name)))
    {
        Unit = Guard.Against.NullOrWhiteSpace(unit, nameof(unit));
    }

    public string Unit { get; }
}

Tip: Use Guard.Against in constructors to enforce invariants up front. See Core Concepts for all available guards.


Step 2 — Create the Repository

Define an interface and a JSON-backed implementation:

using DHI.Services;

public interface ISensorRepository :
    IUpdatableRepository<Sensor, Guid>,
    IDiscreteRepository<Sensor, Guid>
{ }

public sealed class SensorRepository : JsonRepository<Sensor, Guid>, ISensorRepository
{
    public SensorRepository(string filePath) : base(filePath) { }
}
  • Implement IDiscreteRepository if you need GetAll / Count.
  • Implement IUpdatableRepository if you need Add / Update / Remove.
  • For grouping, also implement IGroupedRepository<Sensor> and IGroupedUpdatableRepository.

For production, replace the JSON base class with a provider-specific implementation (PostgreSQL, etc.) that implements the same interfaces.


Step 3 — Create the Service

using DHI.Services;
using Microsoft.Extensions.Logging;

public interface ISensorService :
    IUpdatableService<Sensor, Guid>,
    IDiscreteService<Sensor, Guid>
{ }

public sealed class SensorService
    : BaseUpdatableDiscreteService<Sensor, Guid>, ISensorService
{
    public SensorService(ISensorRepository repo, ILogger<SensorService> logger = null)
        : base(repo, logger) { }

    // Add custom business logic, validation, or derived queries here
}

The base class handles Add, Update, Remove, GetAll, Count, Exists, TryGet, and automatically stamps Added/Updated timestamps on entities that implement ITraceableEntity<TId>.


Step 4 — Register and Use (Imperative)

var repo    = new SensorRepository("[AppData]sensors.json".Resolve());
var service = new SensorService(repo);
ServiceLocator.Register(service, "sensors-json");

// Later:
var svc    = ServiceLocator.Get<ISensorService>("sensors-json");
var sensor = new Sensor(Guid.NewGuid(), "CTD #1", "mS/cm");
svc.Add(sensor);

if (svc.TryGet(sensor.Id, out var found))
    Console.WriteLine($"{found.Name} — {found.Unit}");

Step 5 — Register via connections.json (Declarative)

If you have (or plan to create) a SensorServiceConnection class, add a connections.json entry:

{
  "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",
  "sensors-json": {
    "$type": "MyCompany.Services.Sensors.WebApi.SensorServiceConnection, MyCompany.Services.Sensors.WebApi",
    "ConnectionString": "[AppData]sensors.json",
    "RepositoryType": "MyCompany.Services.Sensors.SensorRepository, MyCompany.Services.Sensors",
    "Name": "Sensors (JSON)",
    "Id": "sensors-json"
  }
}

Note: Without a dedicated Web API package you won't have a SensorServiceConnection. In that case, use imperative registration (Step 4) or write your own ASP.NET Core controller that resolves ServiceLocator.Get<ISensorService>("sensors-json").


Step 6 — Expose via a Custom Controller (Optional)

If you don't want to create a full Web API NuGet package, a minimal controller wires your service to HTTP:

using DHI.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/sensors/{connectionId}")]
[Authorize]
public class SensorsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll(string connectionId)
    {
        var svc = ServiceLocator.Get<ISensorService>(connectionId);
        return Ok(svc.GetAll(User));
    }

    [HttpGet("{id:guid}")]
    public IActionResult GetById(string connectionId, Guid id)
    {
        var svc = ServiceLocator.Get<ISensorService>(connectionId);
        return svc.TryGet(id, out var sensor) ? Ok(sensor) : NotFound();
    }

    [HttpPost]
    public IActionResult Add(string connectionId, [FromBody] Sensor sensor)
    {
        var svc = ServiceLocator.Get<ISensorService>(connectionId);
        svc.Add(sensor);
        return CreatedAtAction(nameof(GetById), new { connectionId, id = sensor.Id }, sensor);
    }
}

No try/catch needed — UseExceptionHandling*() middleware maps exceptions to HTTP status codes automatically.


Creating a Reusable Web API Package

When your domain is stable and used in multiple projects, extract it into a NuGet package following the built-in module pattern:

  1. MyCompany.Services.Sensors — entity, repository interface, service, JsonRepository implementation.
  2. MyCompany.Services.Sensors.WebApi — controller, SensorServiceConnection : IConnection, SerializerOptionsDefault.

The connection class implements IConnection.Create() to construct the service from a ConnectionString and RepositoryType:

public sealed class SensorServiceConnection : IConnection
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string ConnectionString { get; set; }
    public string RepositoryType { get; set; }

    public object Create()
    {
        var repoType = Type.GetType(RepositoryType)
            ?? throw new InvalidOperationException($"Type not found: {RepositoryType}");
        var repo = (ISensorRepository)Activator.CreateInstance(repoType, ConnectionString.Resolve());
        return new SensorService(repo);
    }
}

Once published, other teams can add this package and use connections.json to configure it — exactly like DHI.Services.Jobs.WebApi or DHI.Services.TimeSeries.WebApi.


Next: Authentication & JWT Setup