Skip to content

DHI.Services.MIKECore for Meshes – Internal Guide (DFSU)

What this provider does

DfsuMeshRepository adapts MIKE Core DFSU files into the Meshes domain interfaces:

  • Implements IGroupedMeshRepository<string> → you get the standard Meshes capabilities:
    • Discover meshes and metadata (MeshInfo)
    • Extract time series at a point
    • Compute spatial aggregations (whole mesh or within polygon[s])
    • Return instantaneous aggregated values at a timestamp
    • Provide mesh + element values for contouring
  • Adds grouping by file-system path: a “group” is a folder under your configured path/prefix.

You can use it:

  • Directly (instantiate the repository; or wrap with MeshService / GroupedMeshService), or
  • Via Web API (register the service in the host with a connectionId, then call REST endpoints).

Constructor & dependencies

public class DfsuMeshRepository : BaseGroupedMeshRepository<string>
{
    public DfsuMeshRepository(IFileSource fileSource,
                              string filePathOrPrefix = null,
                              Parameters parameters = null)
}
  • IFileSource fileSource: abstraction for file systems (local disk, cloud, etc.). The repository uses it to enumerate .dfsu files and open them.
  • filePathOrPrefix: optional subfolder under the IFileSource root (used for grouping).
  • Parameters: a typed key–value bag (currently not used by the DFSU implementation but available for future options, see §11).

The provider relies on MIKE Core APIs (Generic.MikeZero.DFS, Generic.MikeZero.DFS.dfsu) and utility types (SMeshData, SMeshIntersectionCalculator) for mesh geometry and spatial operations.


How DFSU maps to our domain

Domain concept DFSU source
Mesh ID / Name Full file path returned by IFileSource
Items dfsuFile.ItemInfo (name, unit, quantity). Exposed as Item with string id (item number)
Time axis dfsuFile.GetDateTimes()
Mesh projection dfsuFile.Projection.WKTString → exposed as MeshInfo.Projection
Mesh bbox min/max of dfsuFile.X, dfsuFile.Y
Elements (2D/3D) dfsuFile.GetPolygons() or GetPolygons3D() → NTS polygons
Delete/missing value dfsuFile.DeleteValueFloat

When a list of polygons is provided, element area weights (or intersection weights) are computed once and then reused across timesteps to avoid IO thrashing.


Discovery APIs

GetAll(user)

Enumerates all .dfsu files under filePathOrPrefix (recursively). For each file:

Builds MeshInfo<string> (inherits BaseGroupedEntity<string>)

  • Id → the full file path as returned by IFileSource (stable identifier).
  • Name → the file name only (e.g., mesh01.dfsu).
  • Group → the relative folder under filePathOrPrefix (if any). (You don’t need to parse it yourself; it’s provided via BaseGroupedEntity.)
  • ItemsIEnumerable<Spatial.Data.Item> created from DFSU item metadata:
    • Id = ItemNumber.ToString()
    • Quantity = DFSU item quantity (e.g., “Water Level”)
    • Unit = DFSU unit (e.g., “m”)
    • PhysicalDimension = DFSU physical dimension if available (fallback: same as quantity or empty)

      Note: Item does not have a Name property. If you need a human-friendly label, use the DFSU item’s own name/quantity when presenting data. When services return “all items” time series keyed by item name, that refers to the DFSU item’s display name, not Item.Name (which doesn’t exist).

  • DateRange → from the first and last timestamps in the file.
  • Projection → DFSU WKT string (MeshInfo.Projection).
  • BoundingBox → min/max of X/Y (MeshInfo.BoundingBox).

Construction sketch (what the repo actually returns):

var info = new MeshInfo<string>(
    id: fullPath,
    name: Path.GetFileName(fullPath),
    group: relativeFolder,                 // may be null/empty
    items: itemsFromDfsu,                  // IEnumerable<Item>
    dateRange: new DateRange(first, last)
)
{
    Projection = dfsuFile.Projection.WKTString,
    BoundingBox = new BoundingBox(xmin, ymin, xmax, ymax)
};

GetByGroup(group)

Same as GetAll but scoped to folder Path.Combine(filePathOrPrefix, group).

ContainsGroup(group)

True if GetByGroup(group) returns any meshes.

GetDateTimes(id)

All timestamps for a single file (SortedSet<DateTime> → returned sorted ascending).


Time axis & indexing

The repository frequently translates a DateRange to file timestep indices:

(from, to) = GetTimeStepIndexes(dateTimes, dateRange);
  • Intersects the client range with the file range. If they don’t overlap → throws ArgumentException.
  • Both from and to indices must exist in the file list (IndexOf on exact DateTime).
  • For instant queries (dateTime), the caller (service layer) should ensure it’s one of the available file timestamps (the Web API services already do this check and will 400/404 otherwise).

Queries at a point

GetValues(id, item, Point, DateRange) and

GetValues(id, Point, DateRange) (all items)

Inputs

  • Point is expected in WGS84 lon/lat (RFC 7946).
  • Z extraction is not supported (will throw if position.Z.HasValue).

Flow

  1. Convert Geo lon/lat → mesh projection using Cartography.Geo2Proj.
  2. Find element index containing the point; we build NTS polygons for elements and run Contains(). If not found → throws (likely outside mesh).
  3. Iterate timesteps [from..to], read the item timestep array, and pick the value at elementIndex.

Output

  • One or many TimeSeriesData<double> (keyed by item name when “all items”).
  • Missing DFSU values (DeleteValueFloat) are stored as-is in the series; consumers can ignore/clean later.

Implementation note

  • When “all items”, we order itemInfos by ItemNumber to minimize seeking when reading the DFSU file.

Spatial aggregation (whole mesh & polygons)

Whole mesh

GetAggregatedValues(id, aggregationType, item, dateRange)

  • Precompute per-element weights = polygon area.
  • For each timestep:
    • Read the item’s element values.
    • Discard delete values.
    • If aggregationType is Average → compute weighted average by area.
    • Else → compute Minimum / Maximum / Sum using aggregationType.GetValue(...).

Polygon(s)

GetAggregatedValues(id, aggregationType, item, polygons, dateRange)

  • If any polygon is provided (non-null), we create SMeshData and an SMeshIntersectionCalculator once, then:
    • For each polygon, compute the list of (element index, intersection weight). This is parallelized with Parallel.For.
    • For each timestep, read once and apply to each polygon’s weight list.
  • If a specific polygon is null, we interpret it as “whole mesh” and default to area weights.

Weights

  • By default, whole mesh weights = element polygon area.
  • For polygons, weights = fractional area inside the polygon returned by the searcher.

Warning: Delete values are filtered out before aggregation.

Return type

  • Whole mesh: TimeSeriesData<double>.
  • Polygons: IEnumerable<TimeSeriesData<double>> aligned by input polygon order.

Instantaneous aggregation

Whole mesh / Polygon

GetAggregatedValue(id, aggregationType, item, dateTime) GetAggregatedValue(id, aggregationType, item, polygon, dateTime)

Implementation reuses the time-range methods by setting DateRange(dateTime, dateTime) and then taking the first datapoint. Returns Maybe<double>:

  • Maybe.Empty<double>() if the DFSU value was missing or the time didn’t exist (service layer converts to null).

Contouring support

GetMeshData(id, item, dateTime?) → (Mesh mesh, float[] elementData)

  • Builds a DfsContouring.Mesh from the DFSU file and returns the raw element data (float array) for:
    • timeStepInd = 0 if dateTime == null
    • otherwise the index of the provided dateTime in GetDateTimes(id)
  • Consumers (MeshService) call BuildFeatureCollection(mesh, elementData, thresholds) to produce GeoJSON-like contours.

The MeshService and GroupedMeshService validate the timestamp exists before calling the repository; if you bypass them and call the repository directly, pass a valid timestamp or you’ll end up with an invalid timeStepInd.


Geometry, CRS, and 2D/3D meshes

  • Element polygons are created via:
    • 2D: dfsuFile.GetPolygons()
    • 3D: dfsuFile.GetPolygons3D()
  • We wrap each polygon into an NTS Polygon (ensuring a closed ring).
  • For point-in-polygon, incoming lon/lat is transformed to the mesh projection via Cartography using DFSU’s projection WKT, geographic origin, and orientation.
  • For polygon aggregation, the polygon you pass in (Spatial.Polygon) is converted to an NTS polygon and intersected with the mesh elements using SMeshIntersectionCalculator.

Parameters (extensibility hook)

The Parameters object is injected but not currently used by the DFSU implementation. It’s a typed dictionary with:

  • Parsing from "k1=v1;k2=v2;..." strings
  • Strongly-typed GetParameter helpers (int, double, DateTime, TimeSpan, Guid, bool, string)

Potential future keys (examples):

  • Layer=top|bottom|<index>
  • TimeTolerance=PT30S (select nearest time within tolerance)
  • ExcludeDeleteValues=true|false
  • Resample=Hour|Day (server-side grouping)

You can introduce such behavior without changing the public surface by checking _parameters inside this repository.


Using the provider

1) Direct usage (no Web API)

// 1) Choose a file source (local or cloud)
IFileSource fs = new FileSource(@"C:\data\meshes");

// 2) Create repo; group by prefix (folder), optionally add parameters
var repo = new DfsuMeshRepository(fs, filePathOrPrefix: "dfsu");

// 3) Wrap in a service (recommended) for guards & convenience
var svc = new GroupedMeshService<string>(repo);

// 4) Discover meshes
foreach (var info in svc.GetAll())
{
    Console.WriteLine($"{info.Id} | {info.DateRange.From:u}..{info.DateRange.To:u}");
}

// 5) Time series at a point (WaterLevel at lon=12.345, lat=55.678)
var ts = svc.GetValues(
    id: @"C:\data\meshes\dfsu\scenarioA\mesh01.dfsu",
    item: "WaterLevel",
    point: new Spatial.Point(new Spatial.Position(12.345, 55.678)),
    dateRange: new DateRange(DateTime.Parse("2025-05-01Z"), DateTime.Parse("2025-05-02Z"))
);

// 6) Area-weighted average over a polygon
var poly = new Spatial.Polygon();
poly.Coordinates.Add(new List<Spatial.Position>{
    new(12.0, 55.0), new(12.5,55.0), new(12.5,55.5), new(12.0,55.5), new(12.0,55.0)
});
var avg = svc.GetAggregatedValues(
    id: @"C:\...mesh01.dfsu",
    aggregationType: AggregationType.Average,
    item: "WaterLevel",
    polygon: poly,
    dateRange: new DateRange(DateTime.Parse("2025-05-01Z"), DateTime.Parse("2025-05-02Z"))
);

In the host’s Startup.Configure:

var meshRepository = new DfsuMeshRepository(new FileSource("[AppData]\\dfsu".Resolve()));
var meshService = new GroupedMeshService(meshRepository);
ServiceLocator.Register(meshService, "dfsu"); // connectionId

Now call the API with /api/meshes/dfsu/... (full endpoint guide was in the Web API doc).

3) Via Connections module

If your application uses the Connections module, register the MIKECloud connection like this:

{
  "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",
  "mc-meshes": {
    "$type": "DHI.Services.Meshes.WebApi.GroupedMeshServiceConnection,DHI.Services.Meshes.WebApi",
    "ConnectionString": "[AppData]\\dfsu",
    "RepositoryType": "DHI.Services.Provider.MIKECore.DfsuMeshRepository,DHI.Services.Provider.MIKECore",
    "Name": "MIKECloud providers",
    "Id": "mc-meshes"
  }
}

Performance notes & tuning

  • Avoid repeated polygon–mesh intersections DfsuMeshRepository caches weights per polygon for each request by computing them once before looping over timesteps. This is a big win when you have many timesteps.
  • Parallelization Weight computation for multiple polygons is parallelized:

    Parallel.For(0, weights.Count, i => { ... });
    

    The IO loop over timesteps remains sequential to avoid thrashing the DFSU reader. * Read order for items When returning “all items”, item infos are ordered by ItemNumber to minimize seeking. * Large meshes / long ranges If you aggregate over many hours/days, consider: * Reducing the request time window * Moving to grouped-by-period endpoints at the API level * Precomputing per-mesh area weights and keeping them in memory (provider-level optimization if memory allows) * 3D files We use GetPolygons3D() for element geometries when NumberOfLayers > 0. Today we still aggregate per element (not vertically).


Edge cases, errors, and messages

  • Point outside mesh: GetElementIndex(...) returns -1 → we throw:
    Unable to find element id for position {lon,lat}. Possibly outside mesh area.
    
  • Z not supported on point extraction: we throw NotSupportedException.
  • Date range outside file range: we throw ArgumentException with both ranges in the message.
  • Missing timestep: for instant queries we return Maybe.Empty<double>() (service becomes null); for range queries the missing element values are filtered out before aggregations.
  • Contours with unknown timestamp: service layer will reject before reaching the repo; if you call repo directly, IndexOf will be -1 → DFSU read will fail.

Known pitfalls (please read)

  1. Time indexing requires exact match We use List<DateTime>.IndexOf(target). If source timestamps have milliseconds or are not UTC-normalized, callers must pass exact values. Consider adding a tolerance (via Parameters) if your data are not perfectly aligned.

  2. Concurrency When you call the repository from your own code and fan out computation (e.g., multiple polygons), do not concurrently mutate shared collections. The repository methods themselves are thread-safe per call because DFSU reads are scoped to the lambda.


Testing tips

  • Happy path:
    • One file with 10–20 timesteps, 2–3 items
    • One point known to be inside the mesh
    • One polygon covering ~30% of the mesh
    • Compare whole-mesh Average against manual weighted average of element values and areas.
  • Edge path:
    • Point exactly on element boundary (ensure consistent Contains)
    • Polygon touching mesh border
    • DFSU with delete values sprinkled in (verify they’re filtered before aggregation)
    • 3D DFSU (validate GetPolygons3D path)

How grouping works (folders → “FullName”)

  • GetAll() enumerates .dfsu under filePathOrPrefix recursively; Id/Name become the full path (you can treat folder/file.dfsu as a FullName Group/Name if it suits your app).
  • GetByGroup(group) filters by joining the prefix and group as a subfolder.

Example

[AppData]\dfsu\
  ├─ Scenarios\RiverMouth\mesh01.dfsu
  └─ Scenarios\Coast\mesh02.dfsu
  • GetByGroup("Scenarios\\RiverMouth") → only mesh01.
  • In Web API, the route uses URL-encoded “FullName” values (e.g., Scenarios%2FRiverMouth%2Fmesh01.dfsu).

Used snippets

Area-weighted average over 3 polygons in parallel (repo-only)

var polygons = new[] { polyA, polyB, polyC };
var series = repo.GetAggregatedValues(
    id: dfsuPath,
    aggregationType: AggregationType.Average,
    item: "WaterLevel",
    polygons: polygons,
    dateRange: new DateRange(from, to)
); // returns IEnumerable<ITimeSeriesData<double>> aligned to polygons

Contours at one timestamp (via service)

var thresholds = new double[] { -1.0, 0.0, 1.0, 2.0 };
var fc = svc.GetContours(
    id: dfsuPath,
    item: "WaterLevel",
    thresholdValues: thresholds,
    dateTime: DateTime.Parse("2025-05-15T06:00:00Z")
);
// GeoJSON-like FeatureCollection<string>

When to consider a different provider

  • If your data aren’t DFSU (e.g., NetCDF, DB-stored elements), implement your own IMeshRepository<T> or IGroupedMeshRepository<T> using the same patterns:
    • Reuse the area/intersection weighting approach for consistent Average semantics.
    • Keep the contract: same behaviors for delete values, date-range intersection, and group semantics.

Checklist for adding DFSU to a new service host

  1. Drop DFSU files under [AppData]\dfsu (or your chosen folder).
  2. Register the DFSU service:
    var repo = new DfsuMeshRepository(new FileSource("[AppData]\\dfsu".Resolve()));
    ServiceLocator.Register(new GroupedMeshService(repo), "dfsu");
    
  3. Hit the Web API:
    • /api/meshes/dfsu/ids
    • /api/meshes/dfsu/{id}/datetimes
    • Point values, polygon aggregations, contours.