Skip to content

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 like DataUtility, 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 AddGeometryColumn and standard ST_* functions.
    • Supports KML streaming (GetStream).
  • SQLite + SpatiaLite
    • Loads mod_spatialite dynamically on first connection (enables ST_* functions).
    • No KML streaming.
  • SQL Server (Geometry)
    • Uses geometry::STGeomFromText/STAsBinary.
    • Requires at least one feature on collection creation to infer geometry type.
    • No KML streaming.

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 of IFeature (Geometry + AttributeValues + Associations)
    • Metadata: dictionary with entries like "Projection", symbology, etc.
  • IFeature

    • Geometry: an IGeometry from Spatial.GeoAPI
    • AttributeValues: dictionary (id, version are reserved)
    • Associations: list of Association<T>
      • Association<TimeSeries<string,double>> (time series)
      • Association<Stream> (document)
      • Association<Spreadsheet<string>> (spreadsheet)
  • FeatureLayerDefinition

    • LayerStyleDefinition and SLDLayerStyleDefinition nodes.
    • The repository will store SLD; if a legacy style is present, it converts Point/Line/Polygon styles to SLD on the fly.
  • FeatureCollectionField Convenience for column name/type/length discovery.


Quick start

Wiring options

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 (All or Geometry).
  • Full connection-string grammar is in MCLite Providers.

Once registered, your Web API exposes the usual routes, e.g. GET /api/featurecollections/{connectionId}/{id} where connectionId = 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 GroupedUpdatableGisService to expose grouped + updatable endpoints (Add, Update*, Remove*, etc.).
  • If you only need read-only routes, you can register the repository with GisService instead.

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 (with id, 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: true populates 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 FeatureLayerDefinition styles to SLD when needed and stores the result under Metadata["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, uses Name if 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"). See ContainsGroup/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 associations flag.
  • 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 (see Db.FeatureCacheMode in 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 generic Metadata (from XML).
    • DefaultSymbology:
      • If already SLD: returned as-is.
      • If legacy style: the provider converts to SLD using _ConvertToSLDPoint/Line/Polygon based on geometry type.
  • FeatureLayerDefinition allows 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 with Projections.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:

  • stringvarchar(n) (Postgres: character varying(n); varchar(max) on SQL Server when n==0 or >8000)
  • double/floatdouble precision (Postgres), real (SQLite), float (SQL Server)
  • int16/32/64integer
  • DateTimetimestamp (Postgres/SQLite) or datetime (SQL Server)
  • boolboolean (Postgres/SQLite) or bit (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 Equal is 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 IDbConnection when inserting many things in a transaction.

Error conditions

  • SQLite / SQL Server: GetStream (KML) throws NotSupportedException.
  • SQL Server: ensure your initial Add has at least one feature (we must register a geometry type).
  • Filtering: only QueryOperator.Equal is supported in this provider’s server-side filtering.
  • SpatiaLite: the provider attempts to EnableExtensions and LoadExtension("mod_spatialite"). Ensure deployment includes the native module (see team deployment notes).
  • Attribute indices are based on the projected column order returned by GetColumnNames (excluding geometry). Double-check the index you pass to UpdateAttributeValues/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 Equal is supported; ensure the attribute name matches the stored column (sanitizer handles cases/escaping, but spelling still matters).
  • SpatiaLite loads fail: ensure mod_spatialite is present and loadable by the process; the provider adds the current directory to PATH, 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.