Skip to content

DHI.Services.MIKECore for Rasters – Internal Guide (DFS2)

This guide explains the Rasters Providers module with emphasis on the MIKECore (Dfs2TimeStepServer) provider. The MIKECore provider implements a time-step server over DFS2 files (MIKE Zero 2D time-varying rasters), turning them into in-memory Raster objects with geospatial metadata and optional bitmap rendering.


Key types (mental model)

  • Dfs2TimeStepServer The main entry point. Opens a DFS2 file, lists available items (variables/layers), lists datetimes, and materializes a raster for a specific (itemId, dateTime).

  • BaseTimeStepServer<TKey, TResult> Contract implemented by servers; here TKey = string (item id), TResult = object (a boxed Raster).

  • IFileSource / FileSource Abstraction over storage. Default implementation reads from local disk but you can provide your own (e.g., network share, object storage). The server never assumes local I/O—it always goes through IFileSource.

  • Item<string> Describes a DFS2 variable (sometimes called “item” in MIKE). The provider enriches Item.Metadata with spatial extent, grid size, units, and projection.

  • Maybe<T> A tiny optional container. Get(...) returns Maybe<object>: it’s empty when the item/date is not found; value when the raster exists.

  • Raster (provider’s nested class) A concrete BaseRaster with pixel values (IList<float>), grid size, pixel size, WKT projection, and helpers (min/max, ToBitmap(), etc.).

  • MIKE Zero / DFS dependencies Uses Generic.MikeZero.DFS (dfs123, Dfs2File, IDfsAxisEqD2, etc.) to read files and metadata.


What the provider does (and how)

1) File opening & validation

  • Constructor validates the DFS2 file exists in the IFileSource root.
  • All reads go through IFileSource.ReadDfs2(path, func) which safely opens/dispenses Dfs2File to your lambda.

2) Time axis handling

  • If the DFS2 time axis is equidistant, it enumerates via TimeAxis.GetDateTimes().
  • If non-equidistant, it reads each timestep to compute UTC datetimes from StartDateTime + secondsFromAxis.

3) Spatial axis & extents

  • Only equidistant 2D axis (EqD2) is fully supported. For EqD2:
    • Reads cell size (Dx, Dy) and counts (XCount, YCount).
    • Computes the geographic bbox:
      • Lower-left geo anchor: (Longitude, Latitude) from the DFS2 projection header.
      • Projects that to the projected space, offsets by grid span |Dx| * (XCount-1) and |Dy| * (YCount-1), then transforms back to geo for the upper-right corner.
      • Expands bbox by half a pixel on all sides so the reported x0,y0,x1,y1 describe outer cell edges, not centers.
  • For non-EqD2 axes: the provider returns local coordinates (no geodetic transform). A TODO is left in code to implement full handling.

4) Delete values & missing data

  • DFS2 DeleteValueFloat cells are replaced with 0 (conservative default). If you need “null semantics”, see “Customization” below.

5) Item metadata surfaced

For each DFS2 item, you get:

  • dataType, valueType (DFS metadata)
  • quantity, unit (human-readable descriptions)
  • x0,y0,x1,y1 (bbox in degrees for EqD2; local units otherwise)
  • dx,dy (cell size; units follow the DFS projection)
  • nx,ny (grid size)
  • projection (full WKT)

6) Bitmap rendering

  • Raster.ToBitmap() generates an SKBitmap using a simple linear gradient from LightSkyBlueDarkBlue across [min, max]. This is intentionally neutral; render styling is typically owned by the caller/UI. You can override this (see “Customization”).

Public API (you can call this as-is)

Construction

// simplest: local file
var server = new Dfs2TimeStepServer(@"C:\data\mike\rain.dfs2");

// advanced: custom file source root + relative file path
IFileSource fs = new FileSource(@"\\nas\dfs2"); // or your own implementation
var server = new Dfs2TimeStepServer(fs, "rain_202501.dfs2");

Exceptions

  • ArgumentNullException if args are null.
  • ArgumentException if the DFS2 file doesn’t exist in the given IFileSource.

GetDateTimes(ClaimsPrincipal user = null) : IList<DateTime>

Returns all available timesteps (UTC DateTime).

IList<DateTime> stamps = server.GetDateTimes();

GetItems(ClaimsPrincipal user = null) : IEnumerable<Item<string>>

Lists item descriptors (id = name). Check item.Metadata for spatial/units info (see above).

foreach (var it in server.GetItems())
{
  Console.WriteLine($"{it.Id} [{it.Metadata["unit"]}] {it.Metadata["nx"]}x{it.Metadata["ny"]}");
}

Get(string itemId, DateTime dateTime, ClaimsPrincipal user = null) : Maybe<object>

Returns a boxed Raster for the item/time or empty if not found.

var itemId = server.GetItems().First().Id;
var t = server.GetDateTimes().Last();

var maybe = server.Get(itemId, t);
if (!maybe.HasValue) { /* not found */ }

var raster = (Dfs2TimeStepServer.Raster)maybe.Value;

Console.WriteLine($"{raster.Name} @ {raster.DateTime:o}");
Console.WriteLine($"{raster.Size.Width}x{raster.Size.Height} px");
Console.WriteLine($"Center={raster.GeoCenter.X},{raster.GeoCenter.Y}  PixelSize={raster.PixelSize.Width}x{raster.PixelSize.Height}");
Console.WriteLine($"Unit={raster.PixelValueUnit}  Min={raster.MinValue} Max={raster.MaxValue}");

You can then draw a bitmap:

using var bmp = raster.ToBitmap();
using var stream = File.OpenWrite(@"C:\out\raster.png");
bmp.Encode(stream, SkiaSharp.SKEncodedImageFormat.Png, quality: 95);

Data contract of Raster (what you get back)

  • DateTime — timestep
  • Name — DFS2 item name
  • ValuesIList<float> length = nx * ny (row-major, index = y * nx + x)
  • SizeSKSizeI (nx, ny)
  • PixelSizeSKSize (dx, dy)
  • PixelValueUnit — e.g., mm/h, m, etc.
  • GeoCenterSKPoint in degrees for EqD2; local otherwise
  • GeoProjectionString — WKT text
  • Metadata — dictionary containing the extras listed earlier
  • ToBitmap()SKBitmap colorized image for visualization

Indexing: (x=0, y=0) is top-left; x increases to the right, y increases downward.


Security model (what’s enforced?)

The provider surface carries an optional ClaimsPrincipal user but does not enforce per-call authorization on its own. It’s there to compose with an upstream authorization component (e.g., an API layer that filters accessible items or masks values). Pass your user through if you intend to add such checks later.


Integration patterns

1) Use from a console/batch job

var server = new Dfs2TimeStepServer(@"C:\dfs2\precip.dfs2");

// pick an item & time
var item = server.GetItems().First(i => i.Id.Contains("Precipitation"));
var when = server.GetDateTimes().First(dt => dt.Hour == 0);

// get raster
var raster = (Dfs2TimeStepServer.Raster)server.Get(item.Id, when).Value;

// simple zonal statistic over a 3x3 around (x=100,y=200)
int nx = raster.Size.Width;
float Sum(int x0, int y0) {
   float s = 0; int n = 0;
   for (int y=y0; y<y0+3; y++)
     for (int x=x0; x<x0+3; x++) { s += raster.Values[y*nx+x]; n++; }
   return s / n;
}
Console.WriteLine($"3x3 mean @ (100,200) = {Sum(100,200)} {raster.PixelValueUnit}");

2) Serve through a Web API

You can wrap Dfs2TimeStepServer in a controller method:

[HttpGet("dfs2/{item}/{time}/png")]
public IActionResult Render(string item, DateTime time)
{
  var server = _servers.Resolve("rain2025"); // your DI/factory
  var m = server.Get(item, time);
  if (!m.HasValue) return NotFound();

  using var bmp = ((Dfs2TimeStepServer.Raster)m.Value).ToBitmap();
  using var ms = new MemoryStream();
  bmp.Encode(ms, SKEncodedImageFormat.Png, 95);
  ms.Position = 0;
  return File(ms, "image/png");
}

3) Plug a custom storage

Implement IFileSource and provide it to the constructor:

public sealed class S3FileSource : IFileSource { /* Exists, ReadDfs2, ... */ }

var fs = new S3FileSource("s3://bucket/prefix");
var server = new Dfs2TimeStepServer(fs, "rain.dfs2");

Performance notes & best practices

  • Re-use the server instance for multiple queries against the same file; it avoids repeated validation and allows your IFileSource to pool file handles.
  • Avoid loading everything: call GetDateTimes() and GetItems() first to narrow your request; Get(...) loads only one item/time slice.
  • Memory footprint: Values is a flat float[] of size nx*ny. Large grids (e.g., 10k × 10k) can consume gigabytes—plan accordingly.
  • Threading: API is synchronous; safe to call concurrently if your IFileSource is thread-safe and you open independent Dfs2File instances per call (the default pattern with ReadDfs2 is safe).
  • Delete values: currently coerced to 0. If you need “no-data”, either post-process or customize (see below).

Customization points

  • Missing values policy Change the line that maps DeleteValueFloat to 0 to your preferred behavior (e.g., float.NaN). Downstream stats/bitmaps should then handle NaNs as you see fit.

  • Color mapping Override or replace ToBitmap() to apply your corporate color ramp or log scaling. E.g., implement quantized color bins with legend ticks.

  • Non-EqD2 axis support The code currently returns local coordinates for non-equidistant axes. To add full support:

    1. Sample the axis coordinates per row/column.
    2. Build a warped transform for the bbox (or report per-cell geolocations).
    3. Extend Metadata to include per-axis arrays if needed.
  • Alternate projections The extents are computed by projecting the lower-left point to the native projected space, offsetting by span, and projecting back. If your DFS2 has unusual orientations, verify projection.Orientation handling and adjust in the Cartography calls.


Error handling & return semantics

  • Construction: throws early if the file can’t be found.
  • Get(itemId, dateTime): returns empty when:
    • the timestamp is not present (no index match)
    • the item id isn’t found in the DFS2 items
  • Metadata presence: All keys shown are present for EqD2. For other axes, projection and local x0…y1 are present but in local units.

Field reference (quick)

GetItems()Item<string>.Metadata keys

Key Type Meaning
dataType enum str DFS item data type
valueType enum str DFS item value type
quantity string Human-readable quantity
unit string Unit name (e.g., mm/h)
x0,y0,x1,y1 double Geo bbox (degrees) or local coords
dx,dy double Pixel size
nx,ny int Grid size
projection string WKT

Raster properties

  • Values (float[]) — length nx*ny
  • Size (SKSizeI) — (nx, ny)
  • PixelSize (SKSize) — (dx, dy)
  • GeoCenter (SKPoint) — (lon, lat) (EqD2)
  • PixelValueUnit (string)
  • GeoProjectionString (string WKT)
  • Metadata — same keys as above + min/max computed lazily in BaseRaster

Worked end-to-end example

Goal: Export all hourly precipitation rasters for 2025-02-01 to PNG for a dashboard.

var server = new Dfs2TimeStepServer(@"D:\dfs2\precip_2025.dfs2");
var item = server.GetItems().First(i => i.Id.Contains("Precipitation"));

// choose the day
var stamps = server.GetDateTimes().Where(dt => dt.Date == new DateTime(2025,2,1)).ToList();

Directory.CreateDirectory(@"D:\png\2025-02-01");

foreach (var dt in stamps)
{
    var m = server.Get(item.Id, dt);
    if (!m.HasValue) continue;

    var raster = (Dfs2TimeStepServer.Raster)m.Value;

    using var bmp = raster.ToBitmap();
    var outPath = $@"D:\png\2025-02-01\{dt:HHmm}.png";
    using var fs = File.Create(outPath);
    bmp.Encode(fs, SKEncodedImageFormat.Png, 95);

    Console.WriteLine($"Wrote {outPath}  [{raster.Size.Width}x{raster.Size.Height}]  Min={raster.MinValue:F2} Max={raster.MaxValue:F2} {raster.PixelValueUnit}");
}

Extending the provider family

Although this guide focuses on DFS2, the same pattern applies to other raster sources:

  • Implement a *TimeStepServer over your format.
  • Keep the file/source abstraction (IFileSource) to support alternative storage.
  • Populate the same item metadata and raster fields so downstream consumers remain format-agnostic.
  • (Optional) Provide a bitmap method for quick visualization.