Skip to content

Using NTS.IO.GeoJSON4STJ with ASP.NET Core Minimal APIs

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

In keeping with the "minimal" theme that you'll find in most minimal API documentation, this provides a complete example that does not include any workarounds for model validation weirdness that shows up in more involved circumstances. See the full MVC page for details.

The following is modified from the template that you get from dotnet new webapi -minimal --no-openapi in .NET SDK 7.0.100 to do exactly the same as the ASP.NET Core MVC sample does. Note that OpenAPI is not supported for GeoJSON types (see #68).

using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.Options;

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

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.ConfigureHttpJsonOptions(options =>
{
    // this constructor is overloaded.  see other overloads for options.
    var geoJsonConverterFactory = new GeoJsonConverterFactory();
    options.SerializerOptions.Converters.Add(geoJsonConverterFactory);
});

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

// sample: emit GeoJSON objects as output
app.MapGet("/weatherforecast", () =>
{
    var geometryFactory = NetTopologySuite.NtsGeometryServices.Instance.CreateGeometryFactory();
    var forecast = new FeatureCollection();
    for (int index = 0; index < 5; index++)
    {
        forecast.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 forecast;
});

// sample: accept GeoJSON objects as input
// POST data is identical to the GET data.
// if you need to use TryGetJsonObjectPropertyValue, then you will want to use
// the JsonOptions that you configured earlier, in case of nested GeoJSON
app.MapPost("/weatherforecast", (FeatureCollection forecastFeatures, IOptions<JsonOptions> jsonOptions) =>
{
    return forecastFeatures.Select(forecastFeature =>
    {
        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.SerializerOptions, out WeatherForecast forecast))
        {
            throw new ArgumentException("'forecast' property is not a WeatherForecast");
        }

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

app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

sealed record WeatherForecastFeatureDetail
{
    public required Guid Id { get; init; }
    public required Point Location { get; init; }
    public required WeatherForecast Forecast { get; init; }
}