DHI.Services.MCLite for GIS — Internal Developer Guide¶
This page documents the GIS provider for MCLite:
DHI.Services.Provider.MCLite.FeatureRepository. If you need deeper background on the MCLite runtime (connection strings,DbFlavour, schema resolution, shared helpers likeDataUtility, etc.), see MCLite Providers.
What this provider gives you¶
FeatureRepository is a grouped, updatable GIS repository that persists vector datasets (“feature collections”) in an MCLite database (PostgreSQL/PostGIS, SQLite/SpatiaLite, or SQL Server). It handles:
- Creating/deleting feature collections (tables) with a geometry column
- Bulk insert of features with attributes, with automatic Multi-* normalization
- CRUD on features and attributes
- Associations: link a feature to Time Series, Documents, and Spreadsheets
- Spatial envelopes/footprints (extent/convex hull)
- Metadata & symbology (reads SLD or converts legacy symbology to SLD)
- KML streaming (PostgreSQL only)
- Caching of features with version-based invalidation
Supported databases & key differences¶
- PostgreSQL + PostGIS (recommended)
- Uses
AddGeometryColumnand standardST_*functions. - Supports KML streaming (
GetStream).
- Uses
- SQLite + SpatiaLite
- Loads
mod_spatialitedynamically on first connection (enablesST_*functions). - No KML streaming.
- Loads
- SQL Server (Geometry)
- Uses
geometry::STGeomFromText/STAsBinary. - Requires at least one feature on collection creation to infer geometry type.
- No KML streaming.
- Uses
See MCLite Providers for database connection string details, DbFlavour, and schema resolution.
Core types surfaced by the provider¶
-
FeatureCollection<string>A named collection (path-like full name/group1/subgroup/name) containing:Attributes: list of attribute definitions (name,Type,Length)Features: list ofIFeature(Geometry+AttributeValues+Associations)Metadata: dictionary with entries like"Projection", symbology, etc.
-
IFeatureGeometry: anIGeometryfromSpatial.GeoAPIAttributeValues: dictionary (id,versionare reserved)Associations: list ofAssociation<T>Association<TimeSeries<string,double>>(time series)Association<Stream>(document)Association<Spreadsheet<string>>(spreadsheet)
-
FeatureLayerDefinitionLayerStyleDefinitionandSLDLayerStyleDefinitionnodes.- The repository will store SLD; if a legacy style is present, it converts Point/Line/Polygon styles to SLD on the fly.
-
FeatureCollectionFieldConvenience for column name/type/length discovery.
Quick start¶
Wiring options¶
A) Using the Connections module (recommended for Web API hosts)¶
Add an entry to your connections.json for MC Lite (grouped, updatable):
"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": "MC Lite (grouped, updatable)",
"Id": "mclite"
}
- For PostgreSQL, include
host,port,database,username,password(plus optional extras). - For SQL Server, include
host,port,database,username,password. - Optional parameters:
workspace,cachemode(AllorGeometry). - Full connection-string grammar is in MCLite Providers.
Once registered, your Web API exposes the usual routes, e.g.
GET /api/featurecollections/{connectionId}/{id}whereconnectionId = mclite.
B) Programmatic registration (ServiceLocator)¶
Register the MCLite repository manually via ServiceLocator:
// SQLite example
ServiceLocator.Register(
new GroupedUpdatableGisService(
new DHI.Services.Provider.MCLite.FeatureRepository(
"database=C:\\data\\MCSQLiteTest.sqlite;dbflavour=SQLite"
)
),
"mclite"
);
// PostgreSQL example
ServiceLocator.Register(
new GroupedUpdatableGisService(
new DHI.Services.Provider.MCLite.FeatureRepository(
"dbflavour=PostgreSQL;host=localhost;port=5432;database=mc_ws;username=dss_admin;password=secretdss_admin;workspace=workspace1"
)
),
"mclite"
);
// SQL Server example
ServiceLocator.Register(
new GroupedUpdatableGisService(
new DHI.Services.Provider.MCLite.FeatureRepository(
"dbflavour=SqlServer;host=localhost;port=1433;database=mc_ws;username=sa;password=SecretPassword!;workspace=workspace1"
)
),
"mclite"
);
- Use
GroupedUpdatableGisServiceto expose grouped + updatable endpoints (Add,Update*,Remove*, etc.). - If you only need read-only routes, you can register the repository with
GisServiceinstead.
2) Create the repository¶
var repo = new DHI.Services.Provider.MCLite.FeatureRepository(
"database=[AppData]MCSQLiteTest.sqlite;dbflavour=SQLite");
// or e.g. PostgreSQL:
// "database=mydb;host=localhost;port=5432;username=dss_admin;password=secretdss_admin;dbflavour=PostgreSQL"
3) Create a feature collection¶
using Spatial; // Attribute
using Spatial.GeoAPI; // Geometry wrappers
using GIS;
// Prepare attributes (do NOT include id/version; those are reserved)
var attrs = new List<IAttribute>
{
new Attribute("Name", typeof(string), 256),
new Attribute("Status", typeof(string), 32),
new Attribute("Flow", typeof(double), 0),
new Attribute("InstalledOn", typeof(DateTime), 0),
new Attribute("Active", typeof(bool), 0),
};
// Build features
var features = new List<IFeature>();
// Assume you have a helper to build IGeometry from WKT (common in our stack):
IGeometry g1 = Geometry.FromWkt("POINT(12.570 55.675)"); // Copenhagen-ish
var f1 = new Feature(g1);
f1.AttributeValues["Name"] = "Sensor A";
f1.AttributeValues["Status"] = "OK";
f1.AttributeValues["Flow"] = 42.3;
f1.AttributeValues["InstalledOn"] = DateTime.UtcNow.Date;
f1.AttributeValues["Active"] = true;
// Attach a time series association (by full name or later-resolved id)
f1.Associations.Add(new Association<TimeSeries<string,double>>("/Telemetry/Sensors/TimeseriesA"));
features.Add(f1);
// Create the collection object
var fc = new FeatureCollection<string>("/Hydro/Sensors/Stations", "Stations", "/Hydro/Sensors");
// Optional: set projection metadata (SRID is resolved at create time)
// Default SRID is 4326 (WGS84)
fc.Metadata["Projection"] = "EPSG:4326";
// Attach attributes + features
foreach (var a in attrs) fc.Attributes.Add(a);
foreach (var f in features) fc.Features.Add(f);
// Persist (creates the feature table + geometry column + bulk inserts)
repo.Add(fc);
What happens under the hood:
- A new row is inserted in the master feature class table; a dedicated table
fc_<guid>is created for your features (withid,version,geometry, and attribute columns). - The geometry column is created with the collection’s SRID (defaults to 4326 or inferred from
Metadata["Projection"]). - LineString and Polygon features are normalized to MultiLineString/MultiPolygon at insert time for schema consistency.
- Bulk insert runs in batches (SQL Server respects the 2100-parameter limit automatically).
- All values are sanitized and parameterized.
Common operations¶
Read a collection¶
var maybe = repo.Get("/Hydro/Sensors/Stations", associations: true);
if (maybe.HasValue)
{
var collection = maybe.Value;
foreach (var feat in collection.Features)
{
var id = (Guid)feat.AttributeValues["id"];
var name = (string)feat.AttributeValues["Name"];
var geometry = feat.Geometry; // IGeometry
var hasTimeseries = feat.Associations.Any(a => a.Type.Name.Contains("TimeSeries"));
}
}
associations: truepopulates feature associations (time series, document, spreadsheet).- Cache behavior (see Caching section) may return fast responses when unchanged.
Read collection info (attributes, metadata) without loading geometries¶
var info = repo.GetInfo("/Hydro/Sensors/Stations");
if (info.HasValue)
{
var meta = info.Value.Metadata; // IsPublic, DisplayField, Srid, Metadata, DefaultSymbology (SLD)
var attrs = info.Value.Attributes; // name/type/length, includes id/version
}
The provider converts legacy
FeatureLayerDefinitionstyles to SLD when needed and stores the result underMetadata["DefaultSymbology"].
Get a single feature¶
var featureId = /* some Guid from earlier */;
var f = repo.GetFeature("/Hydro/Sensors/Stations", featureId, associations: true);
if (f.HasValue)
{
var feature = f.Value;
var geom = feature.Geometry;
var flow = (double)feature.AttributeValues["Flow"];
}
Query feature IDs by attribute filters¶
var ids = repo.GetFeatureIds(
"/Hydro/Sensors/Stations",
new [] { new QueryCondition("Status", QueryOperator.Equal, "OK") }
);
// Only QueryOperator.Equal is supported in this provider’s filtering.
Envelope / footprint¶
var envelope = repo.GetEnvelope("/Hydro/Sensors/Stations"); // polygon rectangle (extent)
var footprint = repo.GetFootprint("/Hydro/Sensors/Stations"); // convex hull polygon
You can also request a combined footprint of many collections:
var multiFootprint = repo.GetFootprint(new [] {
"/Hydro/Sensors/Stations",
"/Hydro/Sensors/Intakes"
});
Add a feature to an existing collection¶
IGeometry g2 = Geometry.FromWkt("POINT(12.58 55.68)");
var f2 = new Feature(g2);
f2.AttributeValues["Name"] = "Sensor B";
f2.AttributeValues["Status"] = "OK";
f2.AttributeValues["Flow"] = 18.9;
f2.AttributeValues["InstalledOn"] = DateTime.UtcNow.Date;
f2.AttributeValues["Active"] = true;
// Optional: associations
f2.Associations.Add(new Association<Stream>("/Docs/Installations/SensorB.pdf"));
f2.Associations.Add(new Association<Spreadsheet<string>>("/Sheets/SensorB"));
repo.AddFeature("/Hydro/Sensors/Stations", f2);
- If
f2.AttributeValues["id"]is not set, a new Guid is generated. - Associations are persisted in the relevant association tables.
Update a feature (replace)¶
var existing = repo.GetFeature("/Hydro/Sensors/Stations", featureId).Value;
// modify attributes / geometry
existing.AttributeValues["Status"] = "Service";
existing.Geometry = Geometry.FromWkt("POINT(12.60 55.69)");
// Update removes the old feature and re-inserts the new one
repo.UpdateFeature("/Hydro/Sensors/Stations", existing);
Update attribute definitions (DDL) and values (DML)¶
// Add new attribute column
repo.AddAttribute("/Hydro/Sensors/Stations", new Attribute("Owner", typeof(string), 128));
// Change an attribute’s type/length
repo.UpdateAttribute("/Hydro/Sensors/Stations", new Attribute("Owner", typeof(string), 256));
// Drop an attribute by index (0-based across non-geometry columns)
repo.RemoveAttribute("/Hydro/Sensors/Stations", attributeIndex: 5);
// Update values for specific features
repo.UpdateAttributeValues("/Hydro/Sensors/Stations",
new [] { featureId1, featureId2 },
new Dictionary<int, object> {
// map: attributeIndex -> value
{ /* index of Status */, "OK" },
{ /* index of Flow */, 25.0 }
});
// Update values by filter (only Equality supported)
repo.UpdateAttributeValues("/Hydro/Sensors/Stations",
new [] { new QueryCondition("Owner", QueryOperator.Equal, "Ops") },
new Dictionary<int, object> { /* ... */ });
Delete feature(s) or entire collection¶
repo.RemoveFeature("/Hydro/Sensors/Stations", featureId);
// Or delete many
repo.RemoveFeatures("/Hydro/Sensors/Stations", new [] { featureId1, featureId2 });
// Or drop the whole collection (removes table + associations + entity description)
repo.Remove("/Hydro/Sensors/Stations");
Export to KML (PostgreSQL only)¶
var (maybeStream, fileType, fileName) = repo.GetStream("/Hydro/Sensors/Stations");
if (maybeStream.HasValue)
{
using var fs = File.Create(fileName);
maybeStream.Value.CopyTo(fs);
}
KML outputs a
<Placemark>per feature, usesNameif present, and places all non-geometry attributes in<ExtendedData>.
Groups & discovery¶
Collections live in a hierarchical group tree. Useful methods:
// All collections
var all = repo.GetAll();
// Collections by group (recursive by default)
var groupCollections = repo.GetByGroup("/Hydro/Sensors");
// Non-recursive group listing
var infos = repo.GetInfoByGroup("/Hydro/Sensors;nonrecursive");
// Full paths of all collections
var names = repo.GetFullNames();
// Geometry types per collection
var types = repo.GetGeometryTypes();
// Per-group geometry types (non-recursive)
var groupTypes = repo.GetGeometryTypes("/Hydro/Sensors;nonrecursive");
You can pass tree options via suffixes on the group:
;nonrecursive,;groupsonly, or both (e.g.,"/Group;nonrecursive"). SeeContainsGroup/GetFullNames(string group, …)for details.
Caching & consistency¶
- The repository keeps an in-memory cache of feature collections and filtered results, keyed by:
- the collection full name,
- the filter (item, operator, value triplets),
- the
associationsflag.
- Every write (insert/update/delete) bumps a version on the collection row; the cache is invalidated if the in-db version differs from the cached version.
- Cache mode is controlled by the connection string:
cachemode=All|Geometry(seeDb.FeatureCacheModein MCLite Providers).All: cache full features.Geometry: cache minimal geometry-only reads and build associations on demand.
Symbology & metadata¶
GetInfo(id)enriches the collection with:IsPublic,DisplayField,Srid, and genericMetadata(from XML).DefaultSymbology:- If already SLD: returned as-is.
- If legacy style: the provider converts to SLD using
_ConvertToSLDPoint/Line/Polygonbased on geometry type.
FeatureLayerDefinitionallows you to submit a simple style (fill/stroke/size/bitmap) that the repo will turn into SLD and store.
SRID & projections¶
- Default SRID is 4326 unless you set
fc.Metadata["Projection"]. The provider resolves SRID withProjections.GetSrid(...)at create time. - KML export transforms to EPSG:4326 internally.
Type system & DDL mapping¶
When you add columns (AddAttribute, UpdateAttribute), the provider maps .NET types to database types:
string→varchar(n)(Postgres:character varying(n);varchar(max)on SQL Server whenn==0or>8000)double/float→double precision(Postgres),real(SQLite),float(SQL Server)int16/32/64→integerDateTime→timestamp(Postgres/SQLite) ordatetime(SQL Server)bool→boolean(Postgres/SQLite) orbit(SQL Server)
Reserved columns:
id,version,geometry. Don’t add attributes with these names.
Security & input hygiene¶
All entry points sanitize:
- Collection & feature payloads (
UserInputSanitizer.SanitizeFeatureCollection/Feature) - Column names (
SanitizeColumnName+EscapeColumnName) - Filters (only
Equalis allowed; values parameterized) - All SQL uses parameters. This provider is safe against SQL injection when used through its public API.
Performance notes¶
- Bulk insert is batched (default 1000 rows). On SQL Server, batch size is auto-adjusted to keep parameters
< 2100. - Multi-* normalization guarantees a single geometry type in the table; it reduces DDL churn and improves spatial function performance.
- Prefer overloads that accept an existing
IDbConnectionwhen inserting many things in a transaction.
Error conditions¶
- SQLite / SQL Server:
GetStream(KML) throwsNotSupportedException. - SQL Server: ensure your initial
Addhas at least one feature (we must register a geometry type). - Filtering: only
QueryOperator.Equalis supported in this provider’s server-side filtering. - SpatiaLite: the provider attempts to
EnableExtensionsandLoadExtension("mod_spatialite"). Ensure deployment includes the native module (see team deployment notes). - Attribute indices are based on the projected column order returned by
GetColumnNames(excludinggeometry). Double-check the index you pass toUpdateAttributeValues/RemoveAttribute.
API surface (most-used)¶
-
Collections
Add(FeatureCollection<string>)Update(FeatureCollection<string>)Remove(string id)Get(string id, bool associations=false, …)→Maybe<FeatureCollection<string>>GetInfo(string id)→Maybe<FeatureCollectionInfo<string>>GetAll(),GetByGroup(string group),GetFullNames([group])GetGeometryTypes([group])GetEnvelope(string id),GetFootprint(string id|IEnumerable<string> ids)GetStream(string id)→(Maybe<Stream>, fileType, fileName)(PostgreSQL only)
-
Attributes
AddAttribute(string collectionId, IAttribute attribute)UpdateAttribute(string collectionId, IAttribute attribute)RemoveAttribute(string collectionId, int attributeIndex)GetAttributeIndexByName(string collectionId, string attributeName)ContainsAttribute(string id, string name)
-
Features
AddFeature(string id, IFeature feature [, IDbConnection])UpdateFeature(string id, IFeature feature)RemoveFeature(string id, Guid featureId)RemoveFeatures(string id, IEnumerable<Guid> featureIds)ContainsFeature(string id, Guid featureId)GetFeature(string collectionId, Guid featureId, bool associations=false)GetFeatureInfo(string collectionId, Guid featureId, bool associations=false)GetFeatureIds(string collectionId, IEnumerable<QueryCondition> filter)UpdateAttributeValues(string collectionId, IEnumerable<Guid> featureIds, IDictionary<int,object> attributes)UpdateAttributeValues(string collectionId, IEnumerable<QueryCondition> filter, IDictionary<int,object> attributes)
Advanced: feature associations¶
Associations are written automatically when you call AddFeature:
- Time series: attach
Association<TimeSeries<string,double>>("/path/to/timeseries") - Documents: attach
Association<Stream>("/Docs/Folder/File.pdf") - Spreadsheets: attach
Association<Spreadsheet<string>>("/Sheets/Workbook")
At read-time (associations: true), these are rehydrated by looking up full names by id.
Advanced: style input¶
You can write symbology as an XML FeatureLayerDefinition:
<FeatureLayerDefinition>
<LayerStyleDefinition>
<SymbolType>Circle</SymbolType>
<Color>Color [R=16,G=110,B=190]</Color>
<OuterlineColor>Color [R=0,G=0,B=0]</OuterlineColor>
<OuterlineThickness>1.0</OuterlineThickness>
<Size>12</Size>
</LayerStyleDefinition>
</FeatureLayerDefinition>
On read, the repository:
- Detects if it’s SLD or a simple style;
- Converts to SLD (Point/Line/Polygon) when needed and exposes the result via
GetInfo(...).Metadata["DefaultSymbology"].
Troubleshooting checklist¶
- “not found”: most methods first look up the collection’s Guid. Ensure the full path is correct (e.g.,
"/Hydro/Sensors/Stations"). - KML not supported: you’re on SQLite/SQL Server — use PostgreSQL/PostGIS for streaming.
- Filters ignored: only
Equalis supported; ensure the attribute name matches the stored column (sanitizer handles cases/escaping, but spelling still matters). - SpatiaLite loads fail: ensure
mod_spatialiteis present and loadable by the process; the provider adds the current directory toPATH, but you may still need deployment tweaks.
Appendix — tiny helper snippets¶
Check existence
if (!repo.Contains("/Hydro/Sensors/Stations")) { /* create */ }
if (repo.ContainsFeature("/Hydro/Sensors/Stations", featureId)) { /* ... */ }
Get a feature that has the exact same geometry as a given IGeometry
using (var conn = DbConnectionFactory.CreateConnection(new Db(connStr)))
{
conn.Open();
var same = repo.GetAssociatedFeature("/Hydro/Sensors/Stations", inputGeom, conn);
}
If you need deeper details about shared infrastructure, like Db construction, parsed connection strings, DbConnectionFactory, DataUtility helpers (GetId, GetGroupId, path resolution), and the constants behind table/field names—follow the links in MCLite Providers.