Skip to content

DHI.Services.ShapeFile — Provider Guide (GIS Features)

Provider for Esri Shapefiles (.shp/.shx/.dbf, optional .prj, .cpg) into our unified FeatureCollection<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 .prj when provided.
  • Filters on read: Equal only, OR-combined across conditions.
  • Uses sentinel values for nulls when writing DBF (config-free, consistent with other repos).
  • No reprojection; .prj WKT 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:

  1. Single-file mode (path points to a .shp)

  2. Get(id) ignores id and reads that file.

  3. Add/Update/Remove target that file location.

  4. Folder mode (path points to a directory)

  5. id is a relative path under the folder (e.g., roads/primary.shp).

  6. Slashes/backslashes are normalized.
  7. 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 a FeatureCollection<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>.prj exists, its WKT is stored in Metadata["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 .prj if 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 .cpg on Remove but do not create it on Add. If you need a specific code page, write your own .cpg after Add.

Projection (.prj)

  • Read: if <file>.prj exists, its WKT is placed into featureCollection.Metadata["Projection"].
  • Write: if Metadata["Projection"] is present and not "NON-UTM" / "Local Coordinates", we create <file>.prj with 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 .cpg alongside.

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 shapefile
  • POST /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 ConnectionStringfolder, 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");