Skip to content

DHI.Services.GIS.WebApi — Internal Developer Guide

This guide explains how to host and use the GIS Web API endpoints that sit on top of DHI.Services.GIS. It covers:

  • how the Web API resolves connections and instantiates GIS services
  • how to configure authentication, versioning, and JSON serialization
  • a complete endpoint reference with request/response shapes
  • how to filter, download, and edit GIS data
  • examples for both ungrouped and grouped repositories

The Web API returns and consumes the spatial types from DHI.Spatial (e.g., IFeature, IGeometry, IFeatureCollection) using the GeoJSON-compatible converters from DHI.Spatial.GeoJson. See the Spatial and GIS docs from previous section; this guide builds on top of those.


1) Architecture in one minute

  • Connection objects (GisServiceConnection, GroupedGisServiceConnection, UpdatableGisServiceConnection, GroupedUpdatableGisServiceConnection) hold:
    • a repository type (string) that is constructed via reflection
    • a connection string that supports [AppData] token resolution
  • Each connection’s Create() method instantiates the repository and wraps it in the appropriate service (GisService, GroupedGisService, UpdatableGisService, GroupedUpdatableGisService).
  • The controller (GISController) exposes REST endpoints under /api/.... Read-only endpoints are available to authenticated users; editing endpoints require the EditorsOnly authorization policy.
  • JSON serialization uses System.Text.Json + a curated set of converters (see SerializerOptionsDefault) so spatial interfaces and maps serialize cleanly.

2) Hosting & configuration

2.1 Add the Web API to an ASP.NET Core host

Register authentication/authorization, versioning, MVC, and JSON converters:

builder.Services
  .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(opts => { /* your TokenValidationParameters */ });

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

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

builder.Services.AddControllers().AddJsonOptions(o =>
{
  o.JsonSerializerOptions.WriteIndented = true;
  o.JsonSerializerOptions.DefaultIgnoreCondition = SerializerOptionsDefault.Options.DefaultIgnoreCondition;
  // Crucial: add all default converters so spatial types serialize properly
  o.JsonSerializerOptions.AddConverters(DHI.Services.GIS.WebApi.SerializerOptionsDefault.Options.Converters);
});

// It's important to have this injected
builder.Services.AddScoped(_ => new ConnectionTypeService(AppContext.BaseDirectory));

Versioning All endpoints are versioned (v1). Clients can set ?api-version=1 or a header api-version: 1. The controller route root is /api.

2.2 JSON converters (what you get by default)

SerializerOptionsDefault.Options includes converters for:

  • DHI.Spatial.GeoJson: GeometryConverter, GeometryCollectionConverter, PositionConverter, FeatureConverter, FeatureCollectionConverter, FeatureInfoConverter, AttributeConverter, AssociationConverter
  • GIS DTOs returned by the services (e.g., FeatureCollectionInfo<string>)
  • A general ObjectToInferredTypeConverter
  • Helpers for maps (safe to keep registered even if you don’t call map endpoints)

It also sets NumberHandling = AllowReadingFromString, so numeric values encoded as strings are accepted.

2.3 Connection resolution & the [AppData] token

All connection classes resolve the [AppData] token inside connection strings. In Startup.Configure, point App_Data to your content root and set the DataDirectory:

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

Now strings like "[AppData]shp" become {ContentRoot}/App_Data/shp.

2.4 Registering services

You can register services in two ways:

a) Programmatic (at startup)

ServiceLocator.Register(
  new GroupedGisService(
    new DHI.Services.Provider.MCLite.FeatureRepository("database=mc2014.2")
  ),
  "mc-ws1"
);

ServiceLocator.Register(
  new GisService(
    new DHI.Services.Provider.ShapeFile.FeatureRepository(Path.Combine(appData, "shp"))
  ),
  "shape"
);

// Updatable, grouped example
ServiceLocator.Register(
  new GroupedUpdatableGisService(
    new DHI.Services.Provider.MCLite.FeatureRepository($"database={Path.Combine(appData, "MCSQLiteTest.sqlite")};dbflavour=SQLite")
  ),
  "mclite"
);

b) Via connections.json (declarative)

{
  "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[DHI.Services.IConnection, DHI.Services]], mscorlib",
  "mc-ws1": {
    "$type": "DHI.Services.GIS.WebApi.GroupedGisServiceConnection, DHI.Services.GIS.WebApi",
    "ConnectionString": "database=mc2014.2",
    "RepositoryType": "DHI.Services.Provider.MCLite.FeatureRepository, DHI.Services.Provider.MCLite",
    "Name": "Local MC gis connection to workspace1",
    "Id": "mc-ws1"
  },
  "shape": {
    "$type": "DHI.Services.GIS.WebApi.GisServiceConnection, DHI.Services.GIS.WebApi",
    "ConnectionString": "[AppData]shp",
    "RepositoryType": "DHI.Services.Provider.ShapeFile.FeatureRepository, DHI.Services.Provider.ShapeFile",
    "Name": "Shape file gis connection",
    "Id": "shape"
  },
  "mclite": {
    "$type": "DHI.Services.GIS.WebApi.GroupedUpdatableGisServiceConnection, DHI.Services.GIS.WebApi",
    "ConnectionString": "database=[AppData]MCSQLiteTest.sqlite;dbflavour=SQLite",
    "RepositoryType": "DHI.Services.Provider.MCLite.FeatureRepository, DHI.Services.Provider.MCLite",
    "Name": "Local MC Lite gis connection to workspace1",
    "Id": "mclite"
  }
}

The connection classes internally do:

var repoType = Type.GetType(RepositoryType, throwOnError: true);
var repo = Activator.CreateInstance(repoType, ConnectionString.Resolve());
return new ...GisService<string>( (I...GisRepository<string>) repo );

Any exception thrown by the repository constructor is re-thrown (preserving stack) via ExceptionDispatchInfo.Capture(ex.InnerException).Throw().


3) Endpoint reference (v1)

All routes are rooted at /api. Replace {connectionId} with your registered id (e.g., shape, mc-ws1, mclite).

3.1 Feature collections (read)

Get one collection

GET /api/featurecollections/{connectionId}/{featureCollectionId}

  • Query:
    • associations (bool) – include associations if provider supports them
    • outSpatialReference (string) – e.g., EPSG:4326
    • Any other key=value becomes a filter (QueryCondition) on attributes
  • Returns: FeatureCollection<string> (GeoJSON-like)
  • 404 if not found

Example:

GET /api/featurecollections/shape/country?associations=true&outSpatialReference=EPSG:4326&name=Denmark

Get many by id

POST /api/featurecollections/{connectionId}/list

Body: ["group/a","group/b","group/c"] (these are FullNameString values; use URL encoding for slashes when needed)

Count collections

GET /api/featurecollections/{connectionId}/count

List ids (optionally with filters)

GET /api/featurecollections/{connectionId}/ids

  • Converts query pairs into QueryConditions.
  • Example: ?keyword=hydro&owner=TeamA

List all / by group

GET /api/featurecollections/{connectionId}

  • Optional group query parameter (only for grouped connections)
  • Ungrouped returns all.

Properties (schema/info)

  • Single: GET /api/featurecollections/{connectionId}/properties/{featureCollectionId}
  • In group: GET /api/featurecollections/{connectionId}/properties?group={group} Returns IEnumerable<FeatureCollectionInfo<string>>.

Geometry only

GET /api/geometrycollections/{connectionId}/{featureCollectionId}

  • Returns GeometryCollection with just the geometries of features.
  • Filters via query string supported (same convention).

Envelope & footprint

  • Envelope: GET /api/featurecollections/{connectionId}/{featureCollectionId}/envelope?outSpatialReference=EPSG:3857
  • Footprint (single): GET /api/featurecollections/{connectionId}/{featureCollectionId}/footprint?outSpatialReference=EPSG:4326&simplifyDistance=100
  • Footprint (many): POST /api/featurecollections/{connectionId}/footprint?outSpatialReference=EPSG:4326 Body: ["a","b","c"] returns a union footprint.

Download as file

GET /api/featurecollections/{connectionId}/{featureCollectionId}/stream/

  • Returns a file stream with content type and filename provided by the service/repository.

Query for ids (POST)

POST /api/featurecollections/{connectionId}/query

Body (example):

{
  "Filter": [
    { "Item": "country", "Operator": "Equal", "Value": "DK" },
    { "Item": "name", "Operator": "Like",  "Value": "%fjord%" }
  ]
}

Returns: IEnumerable<string> (collection ids).

About filters URL query pairs and QueryDTO<T> bodies are translated to List<QueryCondition>. Typical fields are Item, Operator, Value (exact operator names depend on your provider; common ones include Equal, NotEqual, Like, GreaterThan, In).


3.2 Feature collections (write) — EditorsOnly

Requires EditorsOnly policy. Only available for updatable connections.

Create a collection

POST /api/featurecollections/{connectionId}/{featureCollectionId}

Body: an IFeatureCollection in GeoJSON-like form (features + attributes). The API wraps it in the concrete GIS FeatureCollection including Id, Name, and Group parsed from featureCollectionId.

  • 201 Created with Location header (points to Get)

Upsert a collection

PUT /api/featurecollections/{connectionId}/{featureCollectionId}

Body: IFeatureCollection

  • 200 OK with the collection

Delete a collection

DELETE /api/featurecollections/{connectionId}/{featureCollectionId}

  • 204 No Content

3.3 Features (read & query) — Updatable connections

List feature ids

GET /api/featurecollections/{connectionId}/{featureCollectionId}/ids

  • Optional attribute filters via query string.
  • Returns IEnumerable<Guid>.

Query features (POST)

POST /api/featurecollections/{connectionId}/{featureCollectionId}/query?associations=false&outSpatialReference=EPSG:4326

Body (example):

{
  "Filter": [
    { "Item": "river", "Operator": "Equal", "Value": "Sava" },
    { "Item": "class", "Operator": "In", "Value": ["A","B"] }
  ]
}

Returns: IEnumerable<IFeature> (GeoJSON-like).


3.4 Features (write) — EditorsOnly

Create a feature

POST /api/featurecollections/{connectionId}/{featureCollectionId}/feature/

Body: an IFeature

  • Note: until the service returns the new id directly, the API looks at the diff between id sets before and after the add to determine the new Guid.
  • 201 Created with body = created feature (GetFeature)

Get one feature

GET /api/featurecollections/{connectionId}/{featureCollectionId}/feature/{featureId}?associations=false&outSpatialReference=EPSG:4326&geometry=true

  • geometry=false returns info only (attributes & associations)

Update a feature

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/feature/ Body: an IFeature

  • 200 OK

Delete a feature

DELETE /api/featurecollections/{connectionId}/{featureCollectionId}/feature/{featureId}

  • 204 No Content

Delete many features

DELETE /api/featurecollections/{connectionId}/{featureCollectionId}/feature/ Body: ["guid1","guid2", ...]


3.5 Attributes (schema & values) — EditorsOnly for writes

Create a new attribute (schema field)

POST /api/featurecollections/{connectionId}/{featureCollectionId}/attribute/

Body (example):

{ "name":"color", "dataType":"System.String", "length":32, "displayName":"Color" }
  • 201 Created

List all attributes

GET /api/featurecollections/{connectionId}/{featureCollectionId}/attribute/ Returns: IList<Attribute>

Get attribute by index

GET /api/featurecollections/{connectionId}/{featureCollectionId}/attribute-index/{attributeIndex}

The API exposes index-based access because many backends are columnar. You can also update/remove by name (see below).

Update attribute (schema)

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/attribute/ Body: IAttribute

  • 200 OK

Remove attribute (by index)

DELETE /api/featurecollections/{connectionId}/{featureCollectionId}/attribute/{attributeIndex}

  • 204 No Content

Remove attribute (by name)

DELETE /api/featurecollections/{connectionId}/{featureCollectionId}/attribute-by-name/{attributeName}

  • 204 No Content

3.6 Attribute values — targeted updates

All value updates accept JSON bodies representing either a single value or a dictionary of values.

Update one attribute by index for one feature

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/feature/{featureId}/attribute/{attributeIndex}

Body: {"value": 1} (or any JSON value)

Update one attribute by index for a list of features

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/attribute/{attributeIndex}

Body:

{
  "value": "red",
  "featureIds": ["2a0b...","7d1c..."]
}

Update one attribute by index for features matching a filter

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/attribute-where/{attributeIndex}

Body:

{
  "value": 1,
  "filter": [
    { "Item": "country", "Operator": "Equal", "Value": "DK" }
  ]
}

Note: The current implementation iterates each QueryCondition separately.

Update one attribute by name (single feature)

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/feature/{featureId}/attribute-by-name/{attributeName}

Body: {"value": 42}

Update one attribute by name (list of features)

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/attribute-by-name/{attributeName}

Body:

{
  "value": "closed",
  "featureIds": ["2a0b...","7d1c..."]
}

Update many attributes (index-based) for one feature

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/feature/{featureId}/attributes/

Body:

{ "attributes": { "1": "red", "2": 123 } }

Update many attributes (name-based) for one feature

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/feature/{featureId}/attributes-by-name/

Body:

{ "attributes": { "status": "ok", "rank": 3 } }

Update many attributes (index-based) for many features (by ids)

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/attributes/

Body:

{
  "attributes": { "1": "red", "2": 1 },
  "featureIds": ["2a0b...","7d1c..."]
}

Update many attributes (name-based) for many features (by ids)

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/attributes-by-name/

Body:

{
  "attributes": { "status": "ok", "priority": 5 },
  "featureIds": ["2a0b...","7d1c..."]
}

Update many attributes (index-based) for features matching a filter

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/attributes-where/

Body:

{
  "attributes": { "1": "red", "2": 1 },
  "filter": [
    { "Item": "country", "Operator": "Equal", "Value": "DK" },
    { "Item": "name",    "Operator": "Like",  "Value": "%fjord%" }
  ]
}

Update many attributes (name-based) for features matching a filter

PUT /api/featurecollections/{connectionId}/{featureCollectionId}/attributes-by-name-where/

Body:

{
  "attributes": { "status": "ok", "priority": 5 },
  "filter": [
    { "Item": "region", "Operator": "In", "Value": ["E","W"] }
  ]
}

4) Payloads — shapes at a glance

Because converters are registered, you can pass native GeoJSON-like structures for spatial content.

Geometry (example)

{
  "type": "Polygon",
  "coordinates": [
    [[12.0,55.0],[13.0,55.0],[13.0,56.0],[12.0,56.0],[12.0,55.0]]
  ],
  "crs": { "type": "name", "properties": { "name": "EPSG:4326" } }
}

Feature (example)

{
  "type": "Feature",
  "geometry": { /* see geometry above */ },
  "properties": {
    "id": "a6c0f7d7-22f4-4a88-8d16-f3f1f6a5b34a",
    "name": "Gauge 01",
    "discharge": 1234.5
  }
}

FeatureCollection (example)

{
  "type": "FeatureCollection",
  "features": [ { "type": "Feature", "...": "..." } ],
  "attributes": [
    { "name":"id","dataType":"System.Guid","length":36 },
    { "name":"name","dataType":"System.String","length":120 }
  ]
}

5) Filtering: URL vs POST bodies

  • URL: any ?key=value pair that is not associations or outSpatialReference is turned into a QueryCondition(key, value). For simple equality filters, this is the quickest way.
  • POST QueryDTO: for complex filtering (operators, lists, ranges), send a JSON body:
{
  "Filter": [
    { "Item":"status", "Operator":"Equal", "Value":"active" },
    { "Item":"priority", "Operator":"GreaterThan", "Value": 3 }
  ]
}

Operators are handled by the repository/provider. Common ones include Equal, NotEqual, Like, In, GreaterThan, LessThan. If you need a specific operator, ensure the underlying provider supports it.


6) Security, versioning & errors

  • Auth: JWT bearer. Configure TokenValidationParameters for your issuer, audience, and key.
  • Policies:
    • read endpoints: [Authorize]
    • write endpoints: [Authorize(Policy = "EditorsOnly")]
  • Versioning: ?api-version=1 or api-version: 1 header.
  • Errors:
    • 404 for missing collections/features
    • write endpoints return 201, 200, or 204 appropriately
    • model/JSON errors produce the usual ASP.NET Core validation responses

7) Tips

  • FullNameString: group-aware identifiers may contain path-like parts. Use FullNameString.ToUrl() when constructing links; the controller uses FullNameString.FromUrl() internally.
  • CRS reprojection: if your provider supports it, pass outSpatialReference=EPSG:XXXX.
  • Feature ids: write APIs assume Guid feature identifiers. Ensure your features carry a stable "id" property (for edits) that maps to the provider’s id.
  • AddFeature id retrieval: the controller derives the new id by diffing id lists before/after the add. If you control the provider, consider returning the new id directly to simplify this.
  • Numbers as strings: acceptable for inputs (thanks to AllowReadingFromString), which is handy for user-entered JSON.
  • Grouped-only endpoints: fullnames, group-scoped properties, and some hierarchy helpers require a grouped connection (IGroupedGisService).

8) End-to-end examples

8.1 Read a collection filtered by attribute

GET /api/featurecollections/shape/roads?class=primary&api-version=1
Authorization: Bearer <token>

Response: FeatureCollection containing only primary roads.

8.2 Query features by POST with multiple predicates

POST /api/featurecollections/mclite/workspace1:stations/query?outSpatialReference=EPSG:4326
Authorization: Bearer <token>
Content-Type: application/json

{
  "Filter": [
    { "Item":"river", "Operator":"Equal", "Value":"Sava" },
    { "Item":"status", "Operator":"In",   "Value":["active","planned"] }
  ]
}

8.3 Add a feature (EditorsOnly)

POST /api/featurecollections/mclite/workspace1:stations/feature/?api-version=1
Authorization: Bearer <editor-token>
Content-Type: application/json

{
  "type":"Feature",
  "geometry":{"type":"Point","coordinates":[12.34,55.67]},
  "properties":{"id":"00000000-0000-0000-0000-000000000000","name":"New Station"}
}

The API returns 201 Created and the persisted feature (including its new Guid id).

8.4 Bulk update an attribute by name for selected features

PUT /api/featurecollections/mclite/workspace1:stations/attribute-by-name/status
Authorization: Bearer <editor-token>
Content-Type: application/json

{
  "value":"inactive",
  "featureIds":[
    "8c4e7f1a-...","f0533d88-..."
  ]
}

8.5 Update many attributes by name for features matching a filter

PUT /api/featurecollections/mclite/workspace1:stations/attributes-by-name-where/
Authorization: Bearer <editor-token>
Content-Type: application/json

{
  "attributes": { "status":"ok", "priority":5 },
  "filter": [
    { "Item":"region", "Operator":"Equal", "Value":"north" }
  ]
}

9) Reference: connection classes

All resolve [AppData] before invoking repository constructors.

  • GisServiceConnection -> GisService<string> from IGisRepository<string>
  • GroupedGisServiceConnection -> GroupedGisService<string> from IGroupedGisRepository<string>
  • UpdatableGisServiceConnection -> UpdatableGisService<string, Guid> from IUpdatableGisRepository<string, Guid>
  • GroupedUpdatableGisServiceConnection -> GroupedUpdatableGisService<string, Guid> from IGroupedUpdatableGisRepository<string, Guid>

Each takes Id and Name (metadata), a RepositoryType (assembly-qualified), and a ConnectionString.