Polygon Conversion
The Polygon conversion feature provides automatic handling of Polygon geospatial properties from Cratis.Fundamentals in Entity Framework Core, enabling storage of complex geographic areas with optional interior boundaries (holes).
What it does
Section titled “What it does”The Polygon conversion automatically configures Entity Framework Core to handle properties of type Polygon using JSON serialization:
- PostgreSQL: Stores as
jsonbtype for efficient JSON queries - SQL Server: Stores as
nvarchar(max)with JSON serialization - SQLite: Stores as
textwith JSON serialization
This ensures Polygons (closed geographic shapes with optional holes) are stored consistently across all database providers.
Why it’s important
Section titled “Why it’s important”Using Polygon conversion provides several key benefits:
- Geographic Areas: Store service areas, regions, or any bounded geographic territory
- Complex Boundaries: Support for interior holes (e.g., a region with an excluded zone)
- Cross-Database Compatibility: Consistent Polygon handling across different database providers
- JSON Serialization: Uses standard JSON format for storage, making data human-readable
- Type Safety: Maintains strong typing with the
Polygontype from Cratis.Fundamentals - Automatic Configuration: No need for manual configuration of Polygon properties
The Polygon Type
Section titled “The Polygon Type”The Polygon type represents a closed geographic shape with a required exterior boundary (shell) and optional interior boundaries (holes):
using Cratis.Geospatial;
// Simple polygon (no holes)var shellPoints = new Point[]{ new Point(longitude: -122.4194, latitude: 37.7749), new Point(longitude: -122.4170, latitude: 37.7755), new Point(longitude: -122.4160, latitude: 37.7740), new Point(longitude: -122.4194, latitude: 37.7749) // Must close the ring};
var simplePolygon = new Polygon( new LinearRing(shellPoints), holes: []);
// Polygon with a holevar holePoints = new Point[]{ new Point(longitude: -122.4180, latitude: 37.7750), new Point(longitude: -122.4175, latitude: 37.7752), new Point(longitude: -122.4170, latitude: 37.7748), new Point(longitude: -122.4180, latitude: 37.7750) // Must close the ring};
var polygonWithHole = new Polygon( new LinearRing(shellPoints), holes: [new LinearRing(holePoints)]);The Polygon is serialized to JSON as:
{ "shell": { "coordinates": [ [-122.4194, 37.7749], [-122.4170, 37.7755], [-122.4160, 37.7740], [-122.4194, 37.7749] ] }, "holes": []}Model Usage
Section titled “Model Usage”Your entity models can use Polygon properties directly:
using Cratis.Geospatial;
public class ServiceArea{ public Guid Id { get; set; } public string AreaName { get; set; } public Polygon Boundary { get; set; } public DateTime CreatedAt { get; set; }}
public class ExclusionZone{ public Guid Id { get; set; } public Guid ServiceAreaId { get; set; } public Polygon RestrictedArea { get; set; } public Polygon? AdditionalRestriction { get; set; } // Nullable polygon}The conversion will automatically:
- Configure all
Polygonproperties to use JSON serialization - Store the shell and holes as a JSON structure in the database
- Handle conversion between .NET
Polygoninstances and JSON strings
Manual Configuration
Section titled “Manual Configuration”If you’re not using the BaseDbContext, you can manually apply Polygon conversion:
using Cratis.Arc.EntityFrameworkCore;using Cratis.Geospatial;
public class ServiceAreaDbContext(DbContextOptions options) : DbContext(options){ public DbSet<ServiceArea> Areas { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<ServiceArea>(entity => { entity.Property(e => e.Boundary) .AsPolygon(); });
base.OnModelCreating(modelBuilder); }}Note: This is automatically configured for you when using the
BaseDbContext.
Migration Usage
Section titled “Migration Usage”When creating migrations, use the PolygonColumn() extension method:
Creating a Table with Polygon Column
Section titled “Creating a Table with Polygon Column”[DbContext(typeof(ServiceAreaDbContext))][Migration($"ServiceAreas_{nameof(v1_0_0)}")]public class v1_0_0 : Migration{ protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "ServiceAreas", columns: table => new { Id = table.GuidColumn(migrationBuilder), AreaName = table.StringColumn(migrationBuilder, maxLength: 200, nullable: false), Boundary = table.PolygonColumn(migrationBuilder, nullable: false), CreatedAt = table.DateTimeOffsetColumn(migrationBuilder, nullable: false) }, constraints: table => table.PrimaryKey("PK_ServiceAreas", x => x.Id)); }
protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable(name: "ServiceAreas"); }}Adding a Polygon Column to Existing Table
Section titled “Adding a Polygon Column to Existing Table”[DbContext(typeof(ServiceAreaDbContext))][Migration($"ServiceAreas_{nameof(v1_1_0)}")]public class v1_1_0 : Migration{ protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddPolygonColumn( name: "ExpandedBoundary", table: "ServiceAreas", nullable: true); }
protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( name: "ExpandedBoundary", table: "ServiceAreas"); }}See Common Column Types for more information about column type extensions.
How it works
Section titled “How it works”The conversion system uses a ValueConverter that:
- Serializes: Converts
Polygoninstances to JSON strings usingSystem.Text.Json - Deserializes: Parses JSON strings back to
Polygoninstances when reading from database - Null handling: Properly handles nullable
Polygon?properties - Validation: Ensures rings are properly closed (first and last points match) and contain at least 4 points
The conversion is handled by the AsPolygon() extension method, which configures the property with the appropriate ValueConverter.
Polygon Requirements
Section titled “Polygon Requirements”When creating Polygon instances, ensure:
- Closed rings: Each LinearRing must have identical first and last points (the ring must close)
- Minimum points: Each ring must have at least 4 points (3 unique points + 1 closing duplicate)
- Correct order: Exterior ring (shell) vertices should follow right-hand rule for GeoJSON compatibility
- No self-intersection: Rings should not cross themselves
Database Provider Specifics
Section titled “Database Provider Specifics”The Polygon conversion adapts to different database providers:
- PostgreSQL (Npgsql): Uses
jsonbtype for efficient JSON queries - SQL Server: Uses
nvarchar(max)with JSON string storage - SQLite: Uses
textwith JSON string storage
Querying Considerations
Section titled “Querying Considerations”When querying Polygon data:
All Providers
Section titled “All Providers”For general Polygon comparison:
// Exact matchvar areas = await context.Areas .Where(a => a.Boundary == targetBoundary) .ToListAsync();
// Null checksvar areasWithBoundary = await context.Areas .Where(a => a.Boundary != null) .ToListAsync();Note: For advanced geospatial queries (point-in-polygon, intersection detection, etc.), consider using database-specific extensions or computing results in application code.
Related Topics
Section titled “Related Topics”- Common Column Types - Column type extensions
- Property Extensions - AsPolygon() and other configuration methods
- LineString Conversion - Working with routes and paths
- Point Conversion - Working with individual points
- JSON Conversion - General JSON serialization support