Spatial Queries In Entity Framework Core

Spatial Queries In Entity Framework Core

Spatial data, also known as geospatial data, is information about a physical object that can be represented by numerical values in a geographic coordinate system. Many applications nowadays make heavy use of spatial data for analysis and to make important business decisions. Consider the examples of online taxi apps, like Uber or Ola. Their whole business model is running based on location-based data.

A spatial database is a database that is optimized for storing and querying data that represents objects defined in a geometric space. Most spatial databases allow the representation of simple geometric objects, such as points, lines, and polygons. Microsoft introduced two spatial data types with SQL Server 2008: geometry and geography. Similarly, databases like PostgreSQL and MySQL also provide spatial data types.

With the release of Entity Framework Core 2.2, Microsoft brought support for spatial types and queries in EF Core. With the introduction of spatial types, we could do queries like whether a location falls in a certain area, or queries based on the distance between two points, etc.

EF Core supports mapping to spatial data types using the NetTopologySuite spatial library.

We will create a sample ASP.NET Core MVC application to learn how to use spatial types in EF Core. Note that we need EF Core version 2.2 and above to support spatial types. This MVC application will collect a latitude and longitude as user input and list a set of tourist attraction places in the ascending order of distance from the input location. I will be using Visual Studio Code and DotNet CLI for development.

Create a directory SpatialSample. Open Visual Studio Code and run the command dotnet new mvc --name SpatialSample in the terminal. This shall create an ASP.NET Core MVC application for us.

To use spatial data with EF Core, we need to install the appropriate supporting NetTopologySuite package from Nuget based on the SQL provider we use. Since I am using SQL Server as my database I need to add Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite package. Run the command dotnet add package Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite --version 2.2.4 in the terminal to add the package.

Now, let's add our tourist attraction entity. Create a folder Entities and add a class TouristAttractions.cs.

using System.ComponentModel.DataAnnotations.Schema;  
using GeoAPI.Geometries;  

namespace SpatialSample.Entities  
{  
    public class TouristAttraction  
    {  
        public int Id { get; set; }  
        public string Name { get; set; }  

        [Column(TypeName = "geometry")]  
        public IPoint Location { get; set; }  
    }  
}

Observe the Location property in the entity. It is of type IPoint. It is a NetTopologySuite type that is used to represent a location as a point. Note that I have configured the Location properties column type as geometry. This is because, in SQL Server, spatial properties are mapped to geography type by default.

Now, let's add our DbContext class. Create a class called SpatialDbContext.cs.

using GeoAPI.Geometries;  
using Microsoft.EntityFrameworkCore;  
using NetTopologySuite;  
using SpatialSample.Entities;  

namespace SpatialSample  
{  
    public class SpatialDbContext : DbContext  
    {  
        public DbSet<TouristAttraction> TouristAttractions { get; set; }  

        public SpatialDbContext(DbContextOptions options) : base(options) { }  
    }  
}

Now, in the Startup.cs class, we need to register our DbContext. Since we are using NetTopologySuite for mapping spatial data, we need to use the UseNetTopologySuite method on the provider’s DbContext options builder. So, in the ConfigureServices method, add the following code.

services.AddDbContext<SpatialDbContext>(opts =>  
{  
    opts.UseSqlServer(  
        "<Connection string goes here>",  
        x => x.UseNetTopologySuite()  
    );  
});

We need to add the migration to create our database. Run the command dotnet ef migrations add AddTouristAttractionTable. After the migrations are created, run the command dotnet ef database update to apply the migrations on the database.

We need to add some seed data for the application. We will add some tourist attraction destinations to the database. We can override the OnModelCreating method of DbContext class to add seed data.

protected override void OnModelCreating(ModelBuilder modelBuilder)  
{  
    var geometryFactory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326);  

    modelBuilder.Entity<TouristAttraction>()  
        .HasData(  
            new TouristAttraction  
            {  
                Id = 1,  
                Name = "Taj Mahal",  
                Location = geometryFactory.CreatePoint(new Coordinate(27.175015, 78.042155))  
            },  
            new TouristAttraction  
            {  
                Id = 2,  
                Name = "The Golden Temple of Amritsar",  
                Location = geometryFactory.CreatePoint(new Coordinate(31.619980, 74.876485))  
            },  
            new TouristAttraction  
            {  
                Id = 3,  
                Name = "The Red Fort, New Delhi",  
                Location = geometryFactory.CreatePoint(new Coordinate(28.656159, 77.241020))  
            },  
            new TouristAttraction  
            {  
                Id = 4,  
                Name = "The Gateway of India, Mumbai",  
                Location = geometryFactory.CreatePoint(new Coordinate(18.921984, 72.834654))  
            },  
            new TouristAttraction  
            {  
                Id = 5,  
                Name = "Mysore Palace",  
                Location = geometryFactory.CreatePoint(new Coordinate(12.305025, 76.655753))  
            },  
            new TouristAttraction  
            {  
                Id = 6,  
                Name = "Qutb Minar",  
                Location = geometryFactory.CreatePoint(new Coordinate(28.524475, 77.185521))  
            }  
        );  
}

We need to create a geometry object from the latitude and longitude of a location to save in our database. For creating geometries we can create a geometry factory using CreateGeometryFactory method of NetTopologySuite. Then we use the CreatePoint method of the geometry factory to create a point from a set of coordinates. Note that CreateGeometryFactory method accepts a parameter called srid. An SRID or spatial reference identifier is a unique identifier associated with a specific coordinate system. 4326 is a commonly used SRID.

After adding the seed data, we need to create another migration as data seeding has become part of EF migrations in .NET Core. Run the command dotnet ef migrations add AddSeedData. After the migrations are created run the command dotnet ef database update to apply the migrations on the database.

We can verify this by checking the TouristAttractions table in the database.

1.jpg

We need to create the web part of our application next. We shall create View Model classes first. We create a View Model to bind the user input and a view model to bind the list of location information based on the user search.

namespace SpatialSample.Models  
{  
    public class TouristAttractionViewModel  
    {  
        public double Latitude { get; set; }  
        public double Longitude { get; set; }  
        public string Name { get; set; }  
        public double Distance { get; set; }  
    }  

    public class SearchInputModel  
    {  
        public double Latitude { get; set; }  
        public double Longitude { get; set; }  
    }  

    public class IndexPageViewModel  
    {  
        public SearchInputModel SearchInput { get; set; }  
        public List<TouristAttractionViewModel> TouristAttractions { get; set; }  
    }  
}

Now, let's create a view for user interaction. In the Index.cshtml file in the Views\Home folder, replace the existing code with the following code.

@{  
    ViewData["Title"] = "Spatial Sample";  
}  

@model IndexPageViewModel    

<form     
    asp-controller="Home"     
    asp-action="Search"    
    method="post"     
    class="form-horizontal"     
    role="form">    

    <div class="form-group">    
        <label for="Title">Latitude</label>    
        <input     
            class="form-control"     
            placeholder="Enter Latitude"    
            asp-for="SearchInput.Latitude">       
    </div>   
    <div class="form-group">    
    <label for="Title">Longitude</label>    
    <input     
        class="form-control"     
        placeholder="Enter Longitude"    
        asp-for="SearchInput.Longitude">       
    </div>   

    <button type="submit" class="btn btn-primary">Submit</button>    
</form>    

@{  
    if(Model?.TouristAttractions?.Any() == true)  
    {  
        <br>          
        <br>  

        <table class="table table-bordered table-responsive table-hover">    
        <tr>    
            <th>Name</th>    
            <th>Distance</th>    
            <th>Coordinates</th>    
        </tr>    
        @foreach (var t in Model.TouristAttractions)    
        {    
            <tr>    
                <td>@t.Name</td>    
                <td>@t.Distance</td>    
                <td>@t.Latitude, @t.Longitude</td>     
            </tr>    
        }    
        </table>  
    }  
    else  
    {  
        <br>          
        <br>  
        <div class="alert alert-warning" role="alert">    
            <strong>Please enter Latitude and Longitude of the location</strong>     
        </div>   
    }  
}

The View contains a form that accepts a latitude and longitude value from the user and submits the input to a controller action. Let's create the controller next. Replace the code in the HomeController.cs with the following.

using System;  
using System.Linq;  
using Microsoft.AspNetCore.Mvc;  
using NetTopologySuite.Geometries;  
using SpatialSample.Models;  

namespace SpatialSample.Controllers  
{  
    public class HomeController : Controller  
    {  
        private readonly SpatialDbContext _spatialDbContext;  

        public HomeController(SpatialDbContext spatialDbContext)  
        {  
            this._spatialDbContext = spatialDbContext;  
        }  

        public IActionResult Index()  
        {  
            return View();  
        }  

        [HttpPost]  
        public IActionResult Search([FromForm] IndexPageViewModel indexModel)  
        {  
            var indexViewModel = new IndexPageViewModel  
            {  
                SearchInput = indexModel.SearchInput  
            };  

            // Convert the input latitude and longitude to a Point  
            var location = new Point(indexModel.SearchInput.Latitude, indexModel.SearchInput.Longitude) { SRID = 4326 };  

            // Fetch the tourist attractions and their  
            // distances from the input location   
            // using spatial queries.  
            var touristAttractions = _spatialDbContext  
                .TouristAttractions  
                .Select(t => new { Place = t, Distance = t.Location.Distance(location) })  
                .ToList();  

            // Ordering the result in the ascending order of distance  
            indexViewModel.TouristAttractions = touristAttractions  
                .OrderBy(x => x.Distance)  
                .Select(t => new TouristAttractionViewModel  
                {  
                    Distance = Math.Round(t.Distance, 6),  
                    Latitude = t.Place.Location.X,  
                    Longitude = t.Place.Location.Y,  
                    Name = t.Place.Name  
                }).ToList();  

            return View("Index", indexViewModel);  
        }  
    }  
}

The latitude and longitude submitted by the user are passed to the Search action. The latitude and longitude are first converted to a point object and are passed to the spatial query to be run by EF Core. The query will compare every location in the database and return the distance of each with the input location. Then we order the result set in ascending order of the distances and return the result to the view.

The distance calculated by NetTopologySuite is not the actual distance in kilometers between two locations. The distance is calculated based on a Cartesian plane between two coordinates. If we need to get the actual distance between two locations in kilometers we need to perform some advanced spatial queries and transforms.

Now run the application using the command dotnet run. The application will be loaded in the browser with a form to accept a latitude and longitude value. Enter any coordinates and click the *Submit *button. Let's try New Delhi (28.613939, 77.209023). We shall get a screen like below.

1.jpg

Summary

In this article, we have looked at the spatial types and query support in Entity Framework core. For more information on spatial support in EF Core, check out this link.