DHI.Services.ShapeFile — Provider Guide (GIS Features)¶
Provider for Esri Shapefiles (
.shp/.shx/.dbf, optional.prj,.cpg) into our unifiedFeatureCollection<string>model. Covers: repository behavior, path/ID rules, read/write/update/delete, filtering, projection handling, attribute null sentinels, DBF defaults, wiring behind the Web API “Connections” layer.
What this provider gives you¶
| Scope | Class | Base type | Read | Write | Update | Delete | Notes |
|---|---|---|---|---|---|---|---|
| Shapefile | FeatureRepository |
BaseGisRepository<string> + IUpdatableRepository<FeatureCollection<string>, string> |
✓ | ✓ | ✓ | ✓ | Thin adapter over NetTopologySuite Esri IO; works on a file or a folder root. |
Key traits
- Works in single-file or folder mode (recursive).
- Reads/writes geometry + DBF attributes; preserves/creates
.prjwhen provided. - Filters on read:
Equalonly, OR-combined across conditions. - Uses sentinel values for nulls when writing DBF (config-free, consistent with other repos).
- No reprojection;
.prjWKT is passed through verbatim.
Quick start¶
using DHI.Services.Provider.ShapeFile;
using Spatial;
// Folder mode: ids are relative to this root
var repo = new FeatureRepository(@"C:\data\shp");
// Read (OR filter on two fields)
var filter = new[]
{
new QueryCondition("CLASS", QueryOperator.Equal, "Highway"),
new QueryCondition("LANES", QueryOperator.Equal, 4)
};
var fc = repo.Get(@"roads\primary.shp", filter, associations:false).GetOrThrow();
Console.WriteLine($"{fc.Id}: {fc.Features.Count} features");
// Write (single-file mode)
var outRepo = new FeatureRepository(@"C:\out\roads.shp");
var outFc = new FeatureCollection<string>("roads.shp", "roads");
outFc.Attributes.Add(new Attribute("ID", typeof(int), 10));
outFc.Attributes.Add(new Attribute("NAME", typeof(string), 80));
outFc.Metadata["Projection"] = @"PROJCS[""WGS_1984_UTM_Zone_32N"", ...]";
var geom = Geometry.FromWKT("LINESTRING(12.1 55.7, 12.2 55.8)");
var f = new Spatial.Feature(geom);
f.AttributeValues["ID"] = 1;
f.AttributeValues["NAME"] = "Main St";
outFc.Features.Add(f);
outRepo.Add(outFc); // creates .shp/.shx/.dbf and .prj
Path & ID semantics¶
Construct the repository with a path; two modes:
-
Single-file mode (
pathpoints to a.shp) -
Get(id)ignoresidand reads that file. -
Add/Update/Removetarget that file location. -
Folder mode (
pathpoints to a directory) -
idis a relative path under the folder (e.g.,roads/primary.shp). - Slashes/backslashes are normalized.
GetAll()recursively enumerates*.shp.
API surface & behavior¶
// Existence
bool Contains(string id);
// Read
Maybe<FeatureCollection<string>> Get(string id);
Maybe<FeatureCollection<string>> Get(string id, IEnumerable<QueryCondition> filter, bool associations, string outSpatialReference = null);
IEnumerable<FeatureCollection<string>> GetAll();
bool ContainsAttribute(string id, string name);
// Write/update/delete
void Add(FeatureCollection<string> featureCollection);
void Update(FeatureCollection<string> featureCollection); // Remove + Add
void Remove(string id);
Behavior details
Get(...)builds aFeatureCollection<string>:- Schema inference from the first feature’s DBF attributes (name, .NET type, default length).
- Features are materialized with geometry via WKB (
Geometry.FromWkb(...)). - If
<file>.prjexists, its WKT is stored inMetadata["Projection"].
ContainsAttribute(...)inspects the first feature; if NTS throws, you’ll get: >The file {file} is corrupted, please repair geometry. (...)Add(...)writes all features through NTS Shapefile IO and creates.prjif Projection WKT is present and not"NON-UTM"/"Local Coordinates".Update(...)= Remove then Add.Remove(...)deletes:.shp,.shx,.dbf,.prj,.cpg(if present).
Not implemented (will throw if called):
GetFootprint,GetEnvelope,GetIds.
Filtering (read)¶
Get(id, filter, ...) supports only QueryOperator.Equal.
Multiple filters are OR combined.
Comparison logic: attribute value .ToString() equals filter Value.ToString().
var filter = new[]
{
new QueryCondition("RoadType", QueryOperator.Equal, "Primary"),
new QueryCondition("Status", QueryOperator.Equal, "Open")
};
// includes a feature if RoadType == "Primary" OR Status == "Open"
Attributes & null handling (write)¶
When writing DBF values, nulls are coerced by IgnoreNullAttribute(...) to sentinel values:
| Declared .NET type | Null sentinel written to DBF |
|---|---|
double, float |
-1e+20 |
short, int, long |
int.MinValue |
ushort, uint, ulong |
0 |
string |
"" (empty) |
bool |
false |
DateTime |
new DateTime(0, 1, 1) (sentinel) |
We don’t emit DBF “true nulls”. If you need a different placeholder strategy, prefill values before calling
Add.
DBF defaults (schema inference)¶
When inferring field lengths from a source shapefile, we use:
| Type family | Length | Decimals |
|---|---|---|
double, float |
18 | 8 |
short, int, uint |
10 | 0 |
long, ulong |
18 | 0 |
string |
254 | 0 |
bool |
1 | 0 |
DateTime |
8 | 0 |
Interoperability tips
- Keep DBF field names ≤ 10 chars for maximum compatibility (classic readers).
- We delete
.cpgonRemovebut do not create it onAdd. If you need a specific code page, write your own.cpgafterAdd.
Projection (.prj)¶
- Read: if
<file>.prjexists, its WKT is placed intofeatureCollection.Metadata["Projection"]. - Write: if
Metadata["Projection"]is present and not"NON-UTM"/"Local Coordinates", we create<file>.prjwith that WKT.
No reprojection is performed.
Error handling & caveats¶
- Parsing errors (geometry/DBF) bubble as:
>
The file {file} is corrupted, please repair geometry. (...) - Schema inference looks at the first feature only; empty shapefiles produce an empty schema.
- Concurrency: no extra locking beyond .NET handles; avoid concurrent writes to the same file.
- Encoding: If your workflow requires DBF code pages, manage
.cpgalongside.
REST routes (when hosted via Web API)¶
Once wired (see below), standard routes are available:
GET /api/featurecollections/{connectionId}→ list feature collection IDs (folder mode)GET /api/featurecollections/{connectionId}/{id}→ read shapefilePOST /api/featurecollections/{connectionId}→ add (updatable)PUT /api/featurecollections/{connectionId}/{id}→ update (updatable)DELETE /api/featurecollections/{connectionId}/{id}→ delete (updatable)
id uses forward slashes; we normalize internally.
Wiring (Connections)¶
connections.json
"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": "Shapefile",
"Id": "shape"
}
- If
ConnectionString→ folder, the service can enumerate many shapefiles. - If it → file, the service exposes that single dataset.
- Because the repo implements
IUpdatableRepository, the Web API exposes write/update/delete endpoints.
Programmatic (ServiceLocator)
using DHI.Services.GIS.WebApi;
using DHI.Services.Provider.ShapeFile;
// Folder mode
ServiceLocator.Register(new GisService(new FeatureRepository(@"C:\data\shp")), "shape");
// Single file
ServiceLocator.Register(new GisService(new FeatureRepository(@"C:\data\shp\roads.shp")), "roads");