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
- Discover meshes and metadata (
- 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.dfsufiles and open them.filePathOrPrefix: optional subfolder under theIFileSourceroot (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 byIFileSource(stable identifier).Name→ the file name only (e.g.,mesh01.dfsu).Group→ the relative folder underfilePathOrPrefix(if any). (You don’t need to parse it yourself; it’s provided viaBaseGroupedEntity.)Items→IEnumerable<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:
Itemdoes not have aNameproperty. 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, notItem.Name(which doesn’t exist).
DateRange→ from the first and last timestamps in the file.Projection→ DFSUWKTstring (MeshInfo.Projection).BoundingBox→ min/max ofX/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
fromandtoindices must exist in the file list (IndexOfon exactDateTime). - 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
Pointis expected in WGS84 lon/lat (RFC 7946).- Z extraction is not supported (will throw if
position.Z.HasValue).
Flow
- Convert Geo lon/lat → mesh projection using
Cartography.Geo2Proj. - Find element index containing the point; we build NTS polygons for elements and run
Contains(). If not found → throws (likely outside mesh). - Iterate timesteps
[from..to], read the item timestep array, and pick the value atelementIndex.
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
itemInfosbyItemNumberto 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
aggregationTypeisAverage→ compute weighted average by area. - Else → compute
Minimum/Maximum/SumusingaggregationType.GetValue(...).
Polygon(s)¶
GetAggregatedValues(id, aggregationType, item, polygons, dateRange)
- If any polygon is provided (non-null), we create
SMeshDataand anSMeshIntersectionCalculatoronce, 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.
- For each polygon, compute the list of (element index, intersection weight). This is parallelized with
- 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 tonull).
Contouring support¶
GetMeshData(id, item, dateTime?) → (Mesh mesh, float[] elementData)¶
- Builds a
DfsContouring.Meshfrom the DFSU file and returns the raw element data (float array) for:timeStepInd = 0ifdateTime == null- otherwise the index of the provided
dateTimeinGetDateTimes(id)
- Consumers (MeshService) call
BuildFeatureCollection(mesh, elementData, thresholds)to produce GeoJSON-like contours.
The
MeshServiceandGroupedMeshServicevalidate 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 invalidtimeStepInd.
Geometry, CRS, and 2D/3D meshes¶
- Element polygons are created via:
- 2D:
dfsuFile.GetPolygons() - 3D:
dfsuFile.GetPolygons3D()
- 2D:
- 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
Cartographyusing 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 usingSMeshIntersectionCalculator.
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
GetParameterhelpers (int, double, DateTime, TimeSpan, Guid, bool, string)
Potential future keys (examples):
Layer=top|bottom|<index>TimeTolerance=PT30S(select nearest time within tolerance)ExcludeDeleteValues=true|falseResample=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"))
);
2) Via Web API (recommended for apps)¶
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
DfsuMeshRepositorycaches 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
ItemNumberto 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 useGetPolygons3D()for element geometries whenNumberOfLayers > 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
ArgumentExceptionwith both ranges in the message. - Missing timestep: for instant queries we return
Maybe.Empty<double>()(service becomesnull); 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,
IndexOfwill be-1→ DFSU read will fail.
Known pitfalls (please read)¶
-
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 (viaParameters) if your data are not perfectly aligned. -
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
GetPolygons3Dpath)
- Point exactly on element boundary (ensure consistent
How grouping works (folders → “FullName”)¶
GetAll()enumerates.dfsuunderfilePathOrPrefixrecursively;Id/Namebecome the full path (you can treatfolder/file.dfsuas a FullNameGroup/Nameif it suits your app).GetByGroup(group)filters by joining the prefix andgroupas a subfolder.
Example
[AppData]\dfsu\
├─ Scenarios\RiverMouth\mesh01.dfsu
└─ Scenarios\Coast\mesh02.dfsu
GetByGroup("Scenarios\\RiverMouth")→ onlymesh01.- 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>orIGroupedMeshRepository<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¶
- Drop DFSU files under
[AppData]\dfsu(or your chosen folder). - Register the DFSU service:
var repo = new DfsuMeshRepository(new FileSource("[AppData]\\dfsu".Resolve())); ServiceLocator.Register(new GroupedMeshService(repo), "dfsu"); - Hit the Web API:
/api/meshes/dfsu/ids/api/meshes/dfsu/{id}/datetimes- Point values, polygon aggregations, contours.