Skip to content

Using NTS.IO.GeoJSON4STJ with ASP.NET Core MVC

Joe Amenta edited this page Nov 24, 2022 · 10 revisions

The package: https://www.nuget.org/packages/NetTopologySuite.IO.GeoJSON4STJ

Enabling the feature

With WebApplicationBuilder (default in .NET 6.0+)

builder.Services.AddControllers()
	.AddJsonOptions(options =>
		{
			// this constructor is overloaded.  see other overloads for options.
			var geoJsonConverterFactory = new GeoJsonConverterFactory();
			options.JsonSerializerOptions.Converters.Add(geoJsonConverterFactory);
		});

// nothing to do with NTS.IO.GeoJSON4STJ specifically, but a recommended
// best-practice is to inject this instead of using the global variable:
builder.Services.AddSingleton(NtsGeometryServices.Instance);

With Explicit Startup class

public void ConfigureServices(IServiceCollection services)
{
	services
		.AddControllers()
		.AddJsonOptions(options =>
		{
			// this constructor is overloaded.  see other overloads for options.
			var geoJsonConverterFactory = new GeoJsonConverterFactory();
			options.JsonSerializerOptions.Converters.Add(geoJsonConverterFactory);
		});

	// nothing to do with NTS.IO.GeoJSON4STJ specifically, but a recommended
	// best-practice is to inject this instead of using the global variable:
	services.AddSingleton(NtsGeometryServices.Instance);
}

Either Way...

According to some users' experiences with the Newtonsoft.Json version, you may need to tweak your AddControllers call like so:

		.AddControllers(options =>
		{
			// Prevent the following exception: 'This method does not support GeometryCollection arguments' 
			// See: https://github.com/npgsql/Npgsql.EntityFrameworkCore.PostgreSQL/issues/585 
			options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(Point))); 
			options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(Coordinate))); 
			options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(LineString))); 
			options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(MultiLineString))); 
		})

Sample controller

Here is a modified version of the controller that gets created when you run dotnet new webapi in .NET SDK 7.0.100, meant to demonstrate how to accept and return GeoJSON objects, including some slightly fancy features.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

using NetTopologySuite;
using NetTopologySuite.Features;
using NetTopologySuite.Geometries;
using NetTopologySuite.IO.Converters;

namespace YourProjectNamespace.Controllers;

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    private readonly IOptions<JsonOptions> _jsonOptions;

    private readonly NtsGeometryServices _geometryServices;

    public WeatherForecastController(ILogger<WeatherForecastController> logger, IOptions<JsonOptions> jsonOptions, NtsGeometryServices geometryServices)
    {
        _logger = logger;

        // if you need to use TryGetJsonObjectPropertyValue, then you will
        // want to inject this in order for it to work correctly:
        _jsonOptions = jsonOptions;

        // in Startup.ConfigureServices:
        // services.AddSingleton(NtsGeometryServices.Instance);
        _geometryServices = geometryServices;
    }

    // sample: emit GeoJSON objects as output
    [HttpGet]
    public FeatureCollection Get()
    {
        var geometryFactory = _geometryServices.CreateGeometryFactory();

        var result = new FeatureCollection();
        for (int index = 0; index < 5; index++)
        {
            result.Add(new Feature
            {
                Geometry = geometryFactory.CreatePoint(new Coordinate(Random.Shared.NextDouble() * 340 - 170, Random.Shared.NextDouble() * 160 - 80)),
                Attributes = new AttributesTable
                {
                    // by default, an attribute with this property name
                    // will be written as the Feature's "id", instead of
                    // storing it in the "properties" of the Feature.
                    // you can change the name of the "special" property by
                    // using a different GeoJsonConverterFactory constructor
                    // (remember to update other code that uses it, though)
                    { GeoJsonConverterFactory.DefaultIdPropertyName, Guid.NewGuid() },

                    // for anything that you want to be able to parse back,
                    // make sure to nest it at least one level.
                    {
                        "forecast", new AttributesTable
                        {
                            { "date", DateOnly.FromDateTime(DateTime.Now.AddDays(index)) },
                            { "temperatureC", Random.Shared.Next(-20, 55) },
                            { "summary", Summaries[Random.Shared.Next(Summaries.Length)] },
                        }
                    },
                },
            });
        }

        return result;
    }

    // sample: accept GeoJSON objects as input
    // POST data is identical to the GET data.
    [HttpPost]
    public IEnumerable<WeatherForecastFeatureDetail> Post(FeatureCollection forecastFeatures)
    {
        foreach (var forecastFeature in forecastFeatures)
        {
            if (!(forecastFeature.GetOptionalId(GeoJsonConverterFactory.DefaultIdPropertyName) is string forecastIdString && Guid.TryParse(forecastIdString, out Guid id)))
            {
                throw new ArgumentException("missing 'id' on a feature, or it isn't a GUID");
            }

            if (forecastFeature.Geometry is not Point point)
            {
                throw new ArgumentException("missing 'geometry' on a feature, or it isn't a Point");
            }

            // this library always deserializes attributes to this concrete type
            if (forecastFeature.Attributes is not JsonElementAttributesTable forecastAttributes)
            {
                throw new ArgumentException("missing 'properties' on a feature");
            }

            if (!forecastAttributes.TryGetJsonObjectPropertyValue("forecast", _jsonOptions.Value.JsonSerializerOptions, out WeatherForecast forecast))
            {
                throw new ArgumentException("'forecast' property is not a WeatherForecast");
            }

            yield return new WeatherForecastFeatureDetail
            {
                Id = id,
                Location = point,
                Forecast = forecast,
            };
        }
    }

    public sealed record WeatherForecastFeatureDetail
    {
        public required Guid Id { get; init; }

        public required Point Location { get; init; }

        public required WeatherForecast Forecast { get; init; }
    }
}