Skip to content

DHI.Services.GIS.WebApi (Maps) — Internal Developer Guide

Packages (NuGet):

  • DHI.Services.GIS.WebApi (controllers, connection wrappers, DTOs, serializer defaults)
  • Your provider(s) that implement IMapSource / IGroupedMapSource (e.g. DHI.Services.Provider.MIKECore, DHI.Services.Provider.MCLite, etc.)
  • SkiaSharp (image encode)

This guide shows how to host the WebApi, wire connections (code or JSON), and call each endpoint.


1) What the WebApi exposes

The WebApi wraps the GIS Maps Core services and offers:

  • GET /api/mapsWMS-like (limited) endpoint:
    • request=GetMap → returns image/png (SKBitmap encoded)
    • request=GetLegendGraphic → returns image/png legend for a style
  • GET /api/maps/{connectionId}/datetimes/{id} — available time steps for a data source
  • POST /api/maps/{connectionId} — render a stack of maps (time → base64 png)
  • Grouped layers (only if the connection is grouped):
    • GET /api/maps/{connectionId}/layers — list layers (optionally by group)
    • GET /api/maps/{connectionId}/layers/{id} — get a layer by fullname
    • GET /api/maps/{connectionId}/layers/fullnames — list fullnames (optionally by group)
    • GET /api/maps/{connectionId}/layers/{id}/stream/ascii — download a layer artifact (stream)
  • Map styles (predefined):
    • GET /api/mapstyles / GET /api/mapstyles/{id} / GET /api/mapstyles/{id}/palette
    • POST /api/mapstyles (Editors only)
    • DELETE /api/mapstyles/{id} (Editors only)

All controllers are API versioned (v1) and ship Swagger annotations.


2) Hosting the WebApi

2.1 Minimal Startup (excerpt)

public void ConfigureServices(IServiceCollection services)
{
  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
          .AddJwtBearer(o => o.TokenValidationParameters = new TokenValidationParameters
          {
            ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true,
            ValidIssuer = Configuration["Tokens:Issuer"],
            ValidAudience = Configuration["Tokens:Audience"],
            IssuerSigningKey = RSA.BuildSigningKey(Configuration["Tokens:PublicRSAKey"].Resolve())
          });

  services.AddAuthorization(options =>
  {
    options.AddPolicy("AdministratorsOnly",
      p => p.RequireClaim(ClaimTypes.GroupSid, "Administrators"));
    options.AddPolicy("EditorsOnly",
      p => p.RequireClaim(ClaimTypes.GroupSid, "Editors"));
  });

  services.AddApiVersioning(o =>
  {
    o.ReportApiVersions = true;
    o.AssumeDefaultVersionWhenUnspecified = true;
    o.DefaultApiVersion = new ApiVersion(1, 0);
    o.ApiVersionReader = ApiVersionReader.Combine(
      new QueryStringApiVersionReader("api-version","version","ver"),
      new HeaderApiVersionReader("api-version"));
  });

  services
    .AddCors()
    .AddResponseCompression()
    .AddControllers()
    .AddJsonOptions(o =>
    {
      o.JsonSerializerOptions.WriteIndented = true;
      o.JsonSerializerOptions.DefaultIgnoreCondition =
        DHI.Services.GIS.WebApi.SerializerOptionsDefault.Options.DefaultIgnoreCondition;
      o.JsonSerializerOptions.AddConverters(
        DHI.Services.GIS.WebApi.SerializerOptionsDefault.Options.Converters);
    });

  services.AddSwaggerGen(sw =>
  {
    sw.SwaggerDoc(Configuration["Swagger:SpecificationName"],
      new OpenApiInfo { Title = Configuration["Swagger:DocumentTitle"], Version = "1",
                        Description = File.ReadAllText(Configuration["Swagger:DocumentDescription"].Resolve())});
    sw.EnableAnnotations();
    sw.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "DHI.Services.GIS.WebApi.xml"));
    sw.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
      Description = "Enter the word 'Bearer' followed by a space and the JWT.",
      Name = "Authorization", In = ParameterLocation.Header,
      Type = SecuritySchemeType.ApiKey, Scheme = "Bearer"
    });
    sw.AddSecurityRequirement(new OpenApiSecurityRequirement {
      { new OpenApiSecurityScheme { Reference = new OpenApiReference {
            Type = ReferenceType.SecurityScheme, Id = "Bearer"}}, new List<string>() }
    });
  });

  // Infrastructure for styles (used by MapStylesController and /api/maps?request=GetLegendGraphic)
  services.AddScoped<IMapStyleRepository>(_ =>
      new DHI.Services.GIS.WebApi.MapStyleRepository("styles.json",
        DHI.Services.GIS.WebApi.SerializerOptionsDefault.Options));

  // Optional: ConnectionType discovery (if you expose an endpoint using it)
  services.AddScoped(_ => new DHI.Services.WebApiCore.ConnectionTypeService(AppContext.BaseDirectory));
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); else app.UseHsts();

  app.UseAuthentication();
  app.UseHttpsRedirection();
  app.UseSwagger();
  app.UseSwaggerUI(ui =>
  {
    var spec = Configuration["Swagger:SpecificationName"];
    ui.SwaggerEndpoint($"../swagger/{spec}/swagger.json", Configuration["Swagger:DocumentName"]);
    ui.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
  });
  app.UseExceptionHandling();
  app.UseResponseCompression();
  app.UseRouting();
  app.UseAuthorization();
  app.UseEndpoints(e => e.MapControllers());

  // Set [AppData]
  var contentRoot = Configuration.GetValue("AppConfiguration:ContentRootPath", env.ContentRootPath);
  var appData = Path.Combine(contentRoot, "App_Data");
  AppDomain.CurrentDomain.SetData("DataDirectory", appData);

  // Register services (either manual or via connections.json)
  RegisterServices(appData);
}

2.2 Service registration (manual)

void RegisterServices(string appData)
{
  // Grouped map (MCLite)
  var sqliteConn = $"database={Path.Combine(appData,"MCSQLiteTest.sqlite")};dbflavour=SQLite";
  ServiceLocator.Register(
    new DHI.Services.GIS.Maps.GroupedMapService(
      new DHI.Services.Provider.MCLite.GroupedMapSource(sqliteConn),
      new DHI.Services.GIS.Maps.MapStyleService(
        new DHI.Services.GIS.WebApi.MapStyleRepository(Path.Combine(appData,"styles.json"),
                                                       DHI.Services.GIS.WebApi.SerializerOptionsDefault.Options))),
    "mc-groupedmap");

  // Dfs2 single map source
  var dfs2 = Path.Combine(appData, "dfs2", "R20141001.dfs2");
  ServiceLocator.Register(
    new DHI.Services.GIS.Maps.MapService(
      new DHI.Services.Provider.MIKECore.Dfs2MapSource(dfs2),
      new DHI.Services.GIS.Maps.MapStyleService(
        new DHI.Services.GIS.WebApi.MapStyleRepository(Path.Combine(appData,"styles.json"),
                                                       DHI.Services.GIS.WebApi.SerializerOptionsDefault.Options))),
    "dfs2-map");

  // Dfsu single map source
  ServiceLocator.Register(
    new DHI.Services.GIS.Maps.MapService(
      new DHI.Services.Provider.MIKECore.DfsuMapSource(appData),
      new DHI.Services.GIS.Maps.MapStyleService(
        new DHI.Services.GIS.WebApi.MapStyleRepository(Path.Combine(appData,"styles.json"),
                                                       DHI.Services.GIS.WebApi.SerializerOptionsDefault.Options))),
    "dfsu-map");
}

2.3 Service registration (config-driven)

Place a connections.json (example below) and hydrate connection classes (they resolve [AppData] via .Resolve()):

  • DHI.Services.GIS.WebApi.MapServiceConnection
  • DHI.Services.GIS.WebApi.CachedMapServiceConnection
  • DHI.Services.GIS.WebApi.FileCachedMapServiceConnection
  • DHI.Services.GIS.WebApi.GroupedMapServiceConnection
{
  "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",
  "dfs2-map": {
    "$type": "DHI.Services.GIS.WebApi.MapServiceConnection, DHI.Services.GIS.WebApi",
    "MapSourceConnectionString": "[AppData]dfs2\\R20141001.dfs2",
    "MapSourceType": "DHI.Services.Provider.MIKECore.Dfs2MapSource, DHI.Services.Provider.MIKECore",
    "MapStyleConnectionString": "[AppData]styles.json",
    "MapStyleRepositoryType": "DHI.Services.GIS.Maps.MapStyleRepository, DHI.Services.GIS",
    "Name": "Dfs2 map connection",
    "Id": "dfs2-map"
  },
  "mc-groupedmap": {
    "$type": "DHI.Services.GIS.WebApi.GroupedMapServiceConnection, DHI.Services.GIS.WebApi",
    "MapSourceConnectionString": "database=[AppData]MCSQLiteTest.sqlite;dbflavour=SQLite",
    "MapSourceType": "DHI.Services.Provider.MCLite.GroupedMapSource, DHI.Services.Provider.MCLite",
    "Name": "Local MCLite gis connection to workspace1",
    "Id": "mc-groupedmap"
  },
  "dfs2-map-cached": {
    "$type": "DHI.Services.GIS.WebApi.CachedMapServiceConnection, DHI.Services.GIS.WebApi",
    "MapSourceConnectionString": "[AppData]dfs2\\R20141001.dfs2",
    "MapSourceProperties": {
      "$type": "DHI.Services.Parameters, DHI.Services",
      "CachedImageWidth": 100,
      "CacheExpirationInMinutes": 2
    },
    "MapSourceType": "DHI.Services.Provider.MIKECore.Dfs2MapSource, DHI.Services.Provider.MIKECore",
    "MapStyleConnectionString": "[AppData]styles.json",
    "MapStyleRepositoryType": "DHI.Services.GIS.Maps.MapStyleRepository, DHI.Services.GIS",
    "Name": "Cached Dfs2 map service connection",
    "Id": "dfs2-map-cached"
  }
}

Then, at startup, load and Create() each connection and ServiceLocator.Register(service, id).

The WebApi connection overrides (MapServiceConnection, CachedMapServiceConnection, etc.) are identical to core ones except they resolve [AppData] in connection strings automatically.


3) Endpoint details & examples

3.1 GET /api/maps — WMS-like

Supported only:

  • service=wms
  • version=1.3.0
  • request=GetMap or GetLegendGraphic

Query parameters (GetMap):

  • width (int, required), height (int, required)
  • styles (required): single item taken from comma-separated list:
    • If a MapStyleService is configured: this is a style id in repo.
    • Else: it is a StyleCode (palette code).
  • layers (required): connectionId of a previously registered IMapService (single item taken)
  • crs (required): e.g., EPSG:3857 (many providers/caches only support Web Mercator)
  • bbox (required): "xmin,ymin,xmax,ymax" in the given CRS
  • item (required): provider-level variable/field identifier
  • timestamp (optional): ISO datetime for time-varying datasets
  • filepath (optional): provider-level sourceId (path or id)
  • Provider-specific: Anything else in the query string is forwarded as Parameters (controller excludes reserved keys)

Returns: image/png

Example (curl):

curl -G "https://host/api/maps" \
  --data-urlencode "request=GetMap" \
  --data-urlencode "service=wms" \
  --data-urlencode "version=1.3.0" \
  --data-urlencode "width=1024" \
  --data-urlencode "height=768" \
  --data-urlencode "styles=rain" \
  --data-urlencode "layers=dfs2-map" \
  --data-urlencode "crs=EPSG:3857" \
  --data-urlencode "bbox=1110000,6400000,1130000,6420000" \
  --data-urlencode "item=waterlevel" \
  --data-urlencode "timestamp=2020-10-21T10:00:00Z" \
  --output map.png

GetLegendGraphic parameters:

  • styles (required): style id in the configured repo
  • width, height (required)
  • isVertical (optional, default true)
curl -G "https://host/api/maps" \
  --data-urlencode "request=GetLegendGraphic" \
  --data-urlencode "service=wms" \
  --data-urlencode "version=1.3.0" \
  --data-urlencode "styles=rain" \
  --data-urlencode "width=400" \
  --data-urlencode "height=32" \
  --data-urlencode "isVertical=false" \
  --output legend.png

Notes

  • Only the first value is honored from comma-separated styles/layers.
  • If a style id is not found → 404 NotFound.
  • CRS support is provider-specific; cached services CachedMapSource/FileCachedMapSource require EPSG:3857 and will throw otherwise.

3.2 GET /api/maps/{connectionId}/datetimes/{id}

  • Returns available DateTime stamps for a data source.
  • id is URL-encoded fullname (use FullNameString.FromUrl rules in client if needed).
  • Optional from, to query arguments filter the range.

Example:

GET /api/maps/dfs2-map/datetimes/dataset-01?from=2020-10-21T00:00:00Z&to=2020-10-22T00:00:00Z

Response: SortedSet<DateTime> (JSON)


3.3 POST /api/maps/{connectionId} — batch render (time stack)

  • Body: JSON Dictionary<DateTime, string>{ "2020-10-21T10:00:00Z": "pathOrId1", ... }
  • Query:
    • width, height, style, item, bbox (all required)
    • Provider-specific query keys forwarded as Parameters
  • Response: SortedDictionary<DateTime, string> where the value is base64 PNG.

Example (curl):

curl -X POST "https://host/api/maps/dfs2-map?width=800&height=600&style=rain&item=hs&bbox=1110000,6400000,1130000,6420000" \
  -H "Content-Type: application/json" \
  -d '{
        "2020-10-21T10:00:00Z": "dataset-01@T0",
        "2020-10-21T11:00:00Z": "dataset-01@T1"
      }'

3.4 Grouped layers (only for grouped connections)

  • GET /api/maps/{connectionId}/layers?group={group} → list Layer objects
  • GET /api/maps/{connectionId}/layers/{id} → single Layer by fullname
  • GET /api/maps/{connectionId}/layers/fullnames?group={group} → list of strings
  • GET /api/maps/{connectionId}/layers/{id}/stream/ascii → streams an artifact (requires [Authorize])

Notes

  • Group values and ids are fullname strings; when calling from code, use the same encoding as FullNameString.FromUrl(...) expects.
  • GetStream throws KeyNotFoundException → returns 404.

3.5 Map styles

All under /api/mapstyles ([Authorize]):

  • GET /api/mapstyles → all styles
  • GET /api/mapstyles/count → count
  • GET /api/mapstyles/{id} → one style (404 if not found)
  • GET /api/mapstyles/{id}/palette → parsed palette (threshold → band)
  • POST /api/mapstyles (EditorsOnly) → add style
    • Body: { "Id": "...", "Name": "...", "StyleCode":"0~10:#0000FF,#00FFFF,..."}
  • DELETE /api/mapstyles/{id} (EditorsOnly)

The Maps WMS GetLegendGraphic endpoint uses the same repository/service behind the scenes.


4) Provider-specific parameters

The controllers preserve a reserved set of standard parameters and forward all others as Parameters to your provider. This is how you support specialized options without modifying the controller.

  • For /api/maps?request=GetMap reserved keys:
    • request,service,version,width,height,styles,item,layers,crs,bbox,isVertical,filepath,timestamp
  • For POST /api/maps/{connectionId} reserved keys:
    • timeSteps,width,height,style,item,bbox

Example:

GET /api/maps?request=GetMap&...&contour=true&contourLine=true&vectorEvery=3

Your provider reads parameters["contour"], parameters["vectorLine"], etc.


5) Security & policies

  • JWT auth is configured in host Startup.
  • MapsController.WmsRequest (WMS) does not require [Authorize] by default (public map tiles use-case). Add [Authorize] if required.
  • DateTimes and Layer stream endpoints require [Authorize] as shown.
  • MapStylesController is [Authorize]. Mutations require "EditorsOnly".

6) Serialization defaults

DHI.Services.GIS.WebApi.SerializerOptionsDefault.Options registers converters for:

  • GeoJSON (Position, Feature, Geometry, etc.)
  • Map types (MapStyle, MapStyleBand, Tile, TileImage)
  • Polymorphic dictionary resolver for styles

Use these options when you build repositories or serialize DTOs to keep consistent wire formats.


7) Caching notes

If you select CachedMapServiceConnection or FileCachedMapServiceConnection:

  • Supported parameters (via MapSourceProperties):
    • CachedImageWidth (default 1024)
    • NumberOfCachedZoomLevels (default 5)
    • CacheExpirationInMinutes (memory: default 20; file: default int.MaxValue)
    • CacheRoot (file cache only; optional)
  • CRS must be EPSG:3857 (Web Mercator); decorators enforce and will throw otherwise.
  • Automatic invalidation only happens when sourceId is a file path and its LastWriteTime changes.

8) Style usage in WebApi

  • If a MapStyleService is injected in your IMapService:
    • styles (WMS) or style (POST) must be a style id existing in repo; otherwise 404.
  • If no style service is injected:
    • The input is treated as a StyleCode and parsed on the fly.

Legend (GetLegendGraphic) always expects a style id (it uses the MapStyleService provided to MapsController via DI).


9) End-to-end examples

9.1 Register via JSON connections

Using the connections.json you provided, load each connection at startup and register:

foreach (var (id, connection) in LoadConnections("connections.json"))
{
  var service = connection.Create();       // MapService, GroupedMapService, etc.
  ServiceLocator.Register(service, id);
}

Then test:

  • Map image:

GET /api/maps?request=GetMap&service=wms&version=1.3.0
    &width=800&height=600
    &styles=rain
    &layers=dfs2-map
    &crs=EPSG:3857
    &bbox=1110000,6400000,1130000,6420000
    &item=waterlevel
* Legend:

GET /api/maps?request=GetLegendGraphic&service=wms&version=1.3.0
    &styles=rain&width=400&height=32&isVertical=false

9.2 Batch render

curl -X POST "https://host/api/maps/dfs2-map?width=800&height=600&style=rain&item=hs&bbox=1110000,6400000,1130000,6420000" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"2020-10-21T10:00:00Z":"dataset-01@T0","2020-10-21T11:00:00Z":"dataset-01@T1"}'