Skip to content

Geospatial Types

Chronicle has built-in support for geospatial types from Cratis.Fundamentals: Point, LineString, and Polygon. You can use them directly in events, read models, reducers, and projections without any extra setup.

The three geospatial types are records defined in the Cratis.Geospatial namespace:

using Cratis.Geospatial;
public static class GeospatialTypeExamples
{
public static Point CreatePoint() => new(10.456, 42.123);
public static LineString CreatePath() => new([
new Point(10.456, 42.123),
new Point(11.789, 43.456)
]);
public static Polygon CreateBoundary() => new(
Shell: new LinearRing([
new Point(0, 0),
new Point(10, 0),
new Point(10, 10),
new Point(0, 10),
new Point(0, 0)
]),
Holes: []);
}
  • Point represents a single geographic location in longitude-then-latitude order (GeoJSON standard).
  • LineString represents a connected series of points forming a line or path.
  • Polygon represents an enclosed area with a shell (outer boundary) and optional holes (inner boundaries).
  • LinearRing is an array of points that form a closed loop (used by Polygon).

Using Geospatial Types in Events and Read Models

Section titled “Using Geospatial Types in Events and Read Models”

Import the namespace and use the geospatial types as any other property type:

using Cratis.Chronicle.Events;
using Cratis.Geospatial;
[EventType]
public record GeospatialAssetLocationUpdated(Point Location);
[EventType]
public record GeospatialRouteCreated(LineString Path);
[EventType]
public record GeospatialZoneEstablished(Polygon Boundaries);
public record GeospatialAssetReadModel(Guid Id, Point Location);
public record GeospatialRouteReadModel(Guid Id, LineString Path);
public record GeospatialZoneReadModel(Guid Id, Polygon Boundaries);

Projections and reducers handle geospatial types like any other value type. AutoMap picks them up automatically:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
using Cratis.Geospatial;
[EventType]
public record GeospatialProjAssetLocationUpdated(Point Location);
public record GeospatialProjAssetReadModel(Guid Id, Point Location);
public class GeospatialProjAssetProjection : IProjectionFor<GeospatialProjAssetReadModel>
{
// AutoMap picks up the Point-typed property automatically — no manual mapping needed
public void Define(IProjectionBuilderFor<GeospatialProjAssetReadModel> builder) => builder
.From<GeospatialProjAssetLocationUpdated>();
}

Chronicle describes a geospatial value as a leaf: an object carrying only its format, with no sub-properties.

{ "type": "object", "format": "point" }
{ "type": "object", "format": "linestring" }
{ "type": "object", "format": "polygon" }

The format is the entire description, and that is deliberate. A geospatial value is stored and materialized as a single typed CLR value — Point, LineString, Polygon — so the members you see on the wire belong to the type’s own GeoJSON converter, not to your event or read model. Were the schema to flatten them into properties, the sink would treat the coordinates as fields of your model and drop them on the way through.

That format annotation is what every reader keys off: the storage sinks use it to materialize the typed value, and tools like the Workbench use it to recognize a geographic field and display it as one.

On the wire, the value itself is GeoJSON:

{
"pickedUpAt": { "type": "Point", "coordinates": [10.7522, 59.9139] }
}

Longitude comes first, then latitude — the GeoJSON convention, and the same order the Point constructor takes.

The two features meet more often than you might expect: a delivery has a customer name and a drop-off point, an incident report has a reporter and a location. As soon as anything in an event or read model is marked [PII], Chronicle runs its compliance pass over the whole document — not just the personal values — so the geospatial value gets visited too.

Nothing happens to it. Chronicle stops at a geospatial value rather than descending into its GeoJSON members: those members belong to the type’s converter, and there is no schema property under them for a [PII] marker to sit on. The location is written and read back verbatim, in the clear — which is what you want, because a venue is public data even when the organizer’s name is not.

Chronicle stores geospatial values in BSON format:

Point: Flat document with longitude and latitude

{ "longitude": 10.456, "latitude": 42.123 }

LineString: Array of points

[
{ "longitude": 10.456, "latitude": 42.123 },
{ "longitude": 11.789, "latitude": 43.456 }
]

Polygon: Document with shell (outer ring) and holes (inner rings)

{
"shell": [
{ "longitude": 0, "latitude": 0 },
{ "longitude": 10, "latitude": 0 },
{ "longitude": 10, "latitude": 10 },
{ "longitude": 0, "latitude": 10 },
{ "longitude": 0, "latitude": 0 }
],
"holes": []
}

BSON serializers are registered automatically — no configuration required.

Geospatial types generate as JSON objects in the schema, so the SQL sink stores them as JSON columns. No migration or configuration changes are required.

The Chronicle Workbench recognizes geospatial format markers in event schemas and renders them as human-readable strings rather than expanding nested structures:

Point: lat: 42.123, long: 10.456
LineString: [lat: 42.123, long: 10.456], [lat: 43.456, long: 11.789]
Polygon: shell with 5 points, 0 holes