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/maps— WMS-like (limited) endpoint:request=GetMap→ returns image/png (SKBitmapencoded)request=GetLegendGraphic→ returns image/png legend for a style
GET /api/maps/{connectionId}/datetimes/{id}— available time steps for a data sourcePOST /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 fullnameGET /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}/palettePOST /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.MapServiceConnectionDHI.Services.GIS.WebApi.CachedMapServiceConnectionDHI.Services.GIS.WebApi.FileCachedMapServiceConnectionDHI.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=wmsversion=1.3.0request=GetMaporGetLegendGraphic
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 registeredIMapService(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 CRSitem(required): provider-level variable/field identifiertimestamp(optional): ISO datetime for time-varying datasetsfilepath(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 repowidth,height(required)isVertical(optional, defaulttrue)
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/FileCachedMapSourcerequireEPSG:3857and will throw otherwise.
3.2 GET /api/maps/{connectionId}/datetimes/{id}¶
- Returns available DateTime stamps for a data source.
idis URL-encoded fullname (useFullNameString.FromUrlrules in client if needed).- Optional
from,toquery 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}→ listLayerobjectsGET /api/maps/{connectionId}/layers/{id}→ singleLayerby fullnameGET /api/maps/{connectionId}/layers/fullnames?group={group}→ list of stringsGET /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. GetStreamthrowsKeyNotFoundException→ returns404.
3.5 Map styles¶
All under /api/mapstyles ([Authorize]):
GET /api/mapstyles→ all stylesGET /api/mapstyles/count→ countGET /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,..."}
- Body:
DELETE /api/mapstyles/{id}(EditorsOnly)
The Maps WMS
GetLegendGraphicendpoint 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=GetMapreserved 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(default1024)NumberOfCachedZoomLevels(default5)CacheExpirationInMinutes(memory: default20; file: defaultint.MaxValue)CacheRoot(file cache only; optional)
- CRS must be
EPSG:3857(Web Mercator); decorators enforce and will throw otherwise. - Automatic invalidation only happens when
sourceIdis a file path and itsLastWriteTimechanges.
8) Style usage in WebApi¶
- If a
MapStyleServiceis injected in yourIMapService:styles(WMS) orstyle(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
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"}'