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 fromDHI.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 theEditorsOnlyauthorization 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=1or a headerapi-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 themoutSpatialReference(string) – e.g.,EPSG:4326- Any other
key=valuebecomes 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
groupquery 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}ReturnsIEnumerable<FeatureCollectionInfo<string>>.
Geometry only¶
GET /api/geometrycollections/{connectionId}/{featureCollectionId}
- Returns
GeometryCollectionwith 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:4326Body:["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 toList<QueryCondition>. Typical fields areItem,Operator,Value(exact operator names depend on your provider; common ones includeEqual,NotEqual,Like,GreaterThan,In).
3.2 Feature collections (write) — EditorsOnly¶
Requires
EditorsOnlypolicy. 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
Locationheader (points toGet)
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=falsereturns 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
QueryConditionseparately.
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=valuepair that is notassociationsoroutSpatialReferenceis turned into aQueryCondition(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
TokenValidationParametersfor your issuer, audience, and key. - Policies:
- read endpoints:
[Authorize] - write endpoints:
[Authorize(Policy = "EditorsOnly")]
- read endpoints:
- Versioning:
?api-version=1orapi-version: 1header. - Errors:
404for missing collections/features- write endpoints return
201,200, or204appropriately - 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 usesFullNameString.FromUrl()internally. - CRS reprojection: if your provider supports it, pass
outSpatialReference=EPSG:XXXX. - Feature ids: write APIs assume
Guidfeature 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-scopedproperties, 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>fromIGisRepository<string>GroupedGisServiceConnection->GroupedGisService<string>fromIGroupedGisRepository<string>UpdatableGisServiceConnection->UpdatableGisService<string, Guid>fromIUpdatableGisRepository<string, Guid>GroupedUpdatableGisServiceConnection->GroupedUpdatableGisService<string, Guid>fromIGroupedUpdatableGisRepository<string, Guid>
Each takes Id and Name (metadata), a RepositoryType (assembly-qualified), and a ConnectionString.