{"id":4857,"date":"2022-02-22T02:24:30","date_gmt":"2022-02-22T09:24:30","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/odata\/?p=4857"},"modified":"2022-02-22T02:24:30","modified_gmt":"2022-02-22T09:24:30","slug":"customizing-filter-for-spatial-data-in-asp-net-core-odata-8","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/odata\/customizing-filter-for-spatial-data-in-asp-net-core-odata-8\/","title":{"rendered":"Customizing $filter for spatial data in ASP.NET Core OData 8"},"content":{"rendered":"<h2 id=\"background\" class=\"x-hidden-focus\">Background<\/h2>\n<p>The OData URI parser parses the $filter query to a <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/microsoft.odata.uriparser.filterclause\">FilterClause<\/a>.<\/p>\n<p>The <code>FilterBinder<\/code> translates an OData <code>$filter<\/code> parse tree represented by a <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/microsoft.odata.uriparser.filterclause\">FilterClause<\/a> to an <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/system.linq.expressions.expression\">Expression<\/a>. The <code>Expression<\/code> can be applied to an <code>IQueryable<\/code> and passed to an ORM (e.g <a href=\"https:\/\/docs.microsoft.com\/en-us\/ef\/core\/\">Entity Framework Core<\/a>) for processing.<\/p>\n<p>The <code>FilterBinder<\/code> contains the default implementation on how the Expressions should be created. This creates a few issues:<\/p>\n<ul>\n<li>\u00a0Not everyone uses EF Core so in some cases, the expressions generated by the <code>FilterBinder<\/code> may not be compatible with the target entity-relational mapper (O\/RM) framework.<\/li>\n<li>Users may want to enhance performance of some of the created Expressions e.g flexibility on how they handle constant parameterizations.<\/li>\n<\/ul>\n<p>In this blog post, we will demonstrate how to implement support for the OData spatial <code>geo.distance<\/code> built-in function. The current ASP.NET Core OData 8 doesn&#8217;t support it.<\/p>\n<p>EF Core enables mapping to spatial data types in the database by using <code>NetTopologySuite<\/code> types in the model. These types are unknown to the <code>IEdmModel<\/code>. We will be customizing our logic to work with both <code>OData spatial<\/code> and <code>NetTopologySuite<\/code> types.<\/p>\n<h3>Overview of spatial data types<\/h3>\n<p>Spatial types are supported in OData using the <code>Microsoft.Spatial<\/code> library. OData defines 8 spatial data types for <code>geography<\/code> &amp; <code>geometry<\/code> respectively.<\/p>\n<p>EF Core supports mapping to spatial data types using the <code>NetTopologySuite<\/code> spatial library.\nPreviously in EF classic, <code>System.Data.Spatial<\/code> library was used.<\/p>\n<p><a href=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2022\/02\/spatial-types-updated.png\"><img decoding=\"async\" class=\"alignnone size-full wp-image-4902\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2022\/02\/spatial-types-updated.png\" alt=\"odata and nettopologysuite spatial types\" width=\"821\" height=\"447\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2022\/02\/spatial-types-updated.png 821w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2022\/02\/spatial-types-updated-300x163.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2022\/02\/spatial-types-updated-768x418.png 768w\" sizes=\"(max-width: 821px) 100vw, 821px\" \/><\/a><\/p>\n<h3>Building the service<\/h3>\n<p>In ASP.NET Core OData\u00a0 v8.0.5 we added ability to inject a custom implementation of the <code>FilterBinder<\/code>.<\/p>\n<p>This tutorial assumes that you already have the knowledge to build an ASP.NET Core OData 8.x service. If not, start by reading <a href=\"https:\/\/devblogs.microsoft.com\/odata\/tutorial-creating-a-service-with-odata-8-0\/\" target=\"_blank\" rel=\"noopener\">Tutorial: Creating a Service with ASP.NET Core OData 8.0 for .NET 5<\/a>. Let\u2019s get started.<\/p>\n<p>In this sample service, we will add capability to filter spatial data in <strong>AspNetCore OData 8<\/strong>, which is currently not supported.<\/p>\n<p>We need to install 2 nuget packages:<\/p>\n<ul>\n<li><a href=\"https:\/\/www.nuget.org\/packages\/NetTopologySuite\/\">NetTopologySuite<\/a> &#8211; used for manipulating 2-dimensional linear geometry.<\/li>\n<li><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.EntityFrameworkCore.InMemory\">Microsoft.EntityFrameworkCore.InMemory<\/a> &#8211; In-memory database provider for Entity Framework Core.<\/li>\n<\/ul>\n<h5>Model<\/h5>\n<p>Creating the <code>Customer<\/code> entity model.<\/p>\n<pre class=\"prettyprint\">public class Customer\r\n{\r\n\u00a0 \u00a0 public int Id { get; set; }\r\n\u00a0 \u00a0 public string Name { get; set; }\r\n\u00a0 \u00a0 public Point Loc { get; set; }\r\n}<\/pre>\n<h5>Database Context<\/h5>\n<p>Creating the database context.<\/p>\n<pre class=\"prettyprint\">public class CustomerContext : DbContext\r\n{\r\n    public CustomerContext(DbContextOptions&lt;CustomerContext&gt; options)\r\n      : base(options)\r\n    {\r\n    }\r\n\r\n    public DbSet&lt;Customer&gt; Customers { get; set; }\r\n}<\/pre>\n<h5>Data Source<\/h5>\n<p>We can then add a <code>DataSource.cs<\/code> file to prepopulate data whenever we run the service.<\/p>\n<pre class=\"prettyprint\">public class DataSource\r\n{\r\n    private static DataSource instance = new DataSource();\r\n    public static DataSource Instance =&gt; instance;\r\n    public List&lt;Customer&gt; Customers { get; set; }\r\n\r\n    private DataSource()\r\n    {\r\n        this.Reset();\r\n        this.Initialize();\r\n    }\r\n    public void Reset()\r\n    {\r\n        this.Customers = new List&lt;Customer&gt;();\r\n    }\r\n\r\n    public void Initialize()\r\n    {\r\n        Point seattle = new Point(-122.333056, 47.609722) { SRID = 4326 };\r\n        Point redmond = new Point(-122.123889, 47.669444) { SRID = 4326 };\r\n\r\n        this.Customers.AddRange(new List&lt;Customer&gt;()\r\n        {\r\n            new Customer()\r\n            {\r\n                Id = 1,\r\n                Name = \"Customer 1\",\r\n                Loc = seattle\r\n            },\r\n            new Customer()\r\n            {\r\n                Id = 2,\r\n                Name = \"Customer 2\",\r\n                Loc = redmond\r\n            }\r\n        });\r\n    }\r\n}<\/pre>\n<h5>Other Configurations<\/h5>\n<p>In <code>Startup.cs<\/code>, we replace the <code>ConfigureServices<\/code> method with the following code:<\/p>\n<pre class=\"prettyprint\">public void ConfigureServices(IServiceCollection services)\r\n{\r\n    services.AddDbContext&lt;CustomerContext&gt;(opt =&gt; opt.UseInMemoryDatabase(\"CustomerGeoContext\"));\r\n    services.AddControllers().AddOData(opt =&gt; opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5)\r\n                                                    .AddRouteComponents(\"odata\", GetEdmModel()));\r\n}\r\n\r\nprivate static IEdmModel GetEdmModel()\r\n{\r\n    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();\r\n    builder.EntitySet&lt;Customer&gt;(\"Customers\");\r\n    return builder.GetEdmModel();\r\n}<\/pre>\n<p class=\"prettyprint\">If we run our service and try to get the <code>Customers<\/code> data, we get a truncated response.<\/p>\n<pre class=\"prettyprint\">GET ~\/odata\/Customers<\/pre>\n<pre class=\"prettyprint\">{\r\n    \"@odata.context\": \"http:\/\/localhost:5000\/odata\/$metadata#Customers\",\r\n    \"value\": [\r\n        {\r\n            \"Id\": 1,\r\n            \"Name\": \"Customer 1\"<\/pre>\n<p>Let&#8217;s modify the code in <code>GetEdmModel<\/code> method as follows:<\/p>\n<pre class=\"prettyprint\">        public static IEdmModel GetEdmModel()\r\n        {\r\n            var builder = new ODataModelBuilder();\r\n\r\n            builder.EntitySet&lt;Customer&gt;(\"Customers\");\r\n            var customer = builder.EntityType&lt;Customer&gt;();\r\n            customer.Property(c =&gt; c.Id);\r\n            customer.Property(c =&gt; c.Name);\r\n            customer.ComplexProperty(c =&gt; c.Loc);\r\n            customer.HasKey(c =&gt; c.Id);\r\n\r\n            var point = builder.ComplexType&lt;Point&gt;();\r\n            point.Property(p =&gt; p.X);\r\n            point.Property(p =&gt; p.Y);\r\n            point.Property(p =&gt; p.Z);\r\n            point.Property(p =&gt; p.M);\r\n            point.ComplexProperty(p =&gt; p.CoordinateSequence);\r\n            point.Property(p =&gt; p.NumPoints);\r\n\r\n            var coordinateSequence = builder.ComplexType&lt;CoordinateSequence&gt;();\r\n            coordinateSequence.Property(c =&gt; c.Dimension);\r\n            coordinateSequence.Property(c =&gt; c.Measures);\r\n            coordinateSequence.Property(c =&gt; c.Spatial);\r\n            coordinateSequence.Property(c =&gt; c.HasZ);\r\n            coordinateSequence.Property(c =&gt; c.HasM);\r\n            coordinateSequence.Property(c =&gt; c.ZOrdinateIndex);\r\n            coordinateSequence.Property(c =&gt; c.MOrdinateIndex);\r\n            coordinateSequence.Property(c =&gt; c.Count);\r\n\r\n            return builder.GetEdmModel();\r\n        }<\/pre>\n<p>In the above changes, we are including the spatial properties in the model so that we can get the correct response.<\/p>\n<p class=\"prettyprint\">If we re-run <code>GET ~\/odata\/Customers<\/code>, we will get the correct response.<\/p>\n<pre class=\"prettyprint\">{\r\n    \"@odata.context\": \"http:\/\/localhost:5000\/odata\/$metadata#Customers\",\r\n    \"value\": [\r\n        {\r\n            \"Id\": 1,\r\n            \"Name\": \"Customer 1\",\r\n            \"Loc\": {\r\n                \"X\": -122.333056,\r\n                \"Y\": 47.609722,\r\n                \"Z\": \"NaN\",\r\n                \"M\": \"NaN\",\r\n                \"NumPoints\": 1,\r\n                \"CoordinateSequence\": {\r\n                    \"Dimension\": 2,\r\n                    \"Measures\": 0,\r\n                    \"Spatial\": 2,\r\n                    \"HasZ\": false,\r\n                    \"HasM\": false,\r\n                    \"ZOrdinateIndex\": -1,\r\n                    \"MOrdinateIndex\": -1,\r\n                    \"Count\": 1\r\n                }\r\n            }\r\n        },\r\n        {\r\n            \"Id\": 2,\r\n            \"Name\": \"Customer 2\",\r\n            \"Loc\": {\r\n                \"X\": -122.123889,\r\n                \"Y\": 47.669444,\r\n                \"Z\": \"NaN\",\r\n                \"M\": \"NaN\",\r\n                \"NumPoints\": 1,\r\n                \"CoordinateSequence\": {\r\n                    \"Dimension\": 2,\r\n                    \"Measures\": 0,\r\n                    \"Spatial\": 2,\r\n                    \"HasZ\": false,\r\n                    \"HasM\": false,\r\n                    \"ZOrdinateIndex\": -1,\r\n                    \"MOrdinateIndex\": -1,\r\n                    \"Count\": 1\r\n                }\r\n            }\r\n        }\r\n    ]\r\n}<\/pre>\n<p>Let&#8217;s inspect the metadata: <code>GET ~\/odata\/$metadata<\/code><\/p>\n<pre class=\"prettyprint\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\r\n&lt;edmx:Edmx Version=\"4.0\" xmlns:edmx=\"http:\/\/docs.oasis-open.org\/odata\/ns\/edmx\"&gt;\r\n    &lt;edmx:DataServices&gt;\r\n        &lt;Schema Namespace=\"GeometryWebAPI.Models\" xmlns=\"http:\/\/docs.oasis-open.org\/odata\/ns\/edm\"&gt;\r\n            &lt;EntityType Name=\"Customer\"&gt;\r\n                &lt;Key&gt;\r\n                    &lt;PropertyRef Name=\"Id\" \/&gt;\r\n                &lt;\/Key&gt;\r\n                &lt;Property Name=\"Id\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"Name\" Type=\"Edm.String\" \/&gt;\r\n                &lt;Property Name=\"Loc\" Type=\"NetTopologySuite.Geometries.Point\" Nullable=\"false\" \/&gt;\r\n            &lt;\/EntityType&gt;\r\n        &lt;\/Schema&gt;\r\n        &lt;Schema Namespace=\"NetTopologySuite.Geometries\" xmlns=\"http:\/\/docs.oasis-open.org\/odata\/ns\/edm\"&gt;\r\n            &lt;ComplexType Name=\"Point\"&gt;\r\n                &lt;Property Name=\"X\" Type=\"Edm.Double\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"Y\" Type=\"Edm.Double\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"Z\" Type=\"Edm.Double\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"M\" Type=\"Edm.Double\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"CoordinateSequence\" Type=\"NetTopologySuite.Geometries.CoordinateSequence\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"NumPoints\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n            &lt;\/ComplexType&gt;\r\n            &lt;ComplexType Name=\"CoordinateSequence\"&gt;\r\n                &lt;Property Name=\"Dimension\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"Measures\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"Spatial\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"HasZ\" Type=\"Edm.Boolean\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"HasM\" Type=\"Edm.Boolean\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"ZOrdinateIndex\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"MOrdinateIndex\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"Count\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n            &lt;\/ComplexType&gt;\r\n        &lt;\/Schema&gt;\r\n        &lt;Schema Namespace=\"Default\" xmlns=\"http:\/\/docs.oasis-open.org\/odata\/ns\/edm\"&gt;\r\n            &lt;EntityContainer Name=\"Container\"&gt;\r\n                &lt;EntitySet Name=\"Customers\" EntityType=\"GeometryWebAPI.Models.Customer\" \/&gt;\r\n            &lt;\/EntityContainer&gt;\r\n        &lt;\/Schema&gt;\r\n    &lt;\/edmx:DataServices&gt;\r\n&lt;\/edmx:Edmx&gt;<\/pre>\n<p>The <code>Loc<\/code> property has a type\u00a0<span class=\"atv\"><code>NetTopologySuite.Geometries.Point<\/code>. This is not an OData type. If we filter against this property, an <code>ODataException<\/code> will be thrown\u00a0since the Odata Uri Parser doesn&#8217;t understand this type.\u00a0<\/span><\/p>\n<pre class=\"prettyprint\">{\r\n    \"error\": {\r\n        \"code\": \"\",\r\n        \"message\": \"The query specified in the URI is not valid. No function signature for the function with name 'geo.distance' matches the specified arguments. The function signatures considered are: geo.distance(Edm.GeographyPoint Nullable=true, Edm.GeographyPoint Nullable=true); geo.distance(Edm.GeometryPoint Nullable=true, Edm.GeometryPoint Nullable=true).\",\r\n        \"details\": [],\r\n        \"innererror\": {}\r\n    }\r\n}<\/pre>\n<p>The error above is occurring because <code>geo.distance<\/code> spatial function expects arguments of the type <code>Edm.GeographyPoint<\/code> or <code>Edm.GeometryPoint<\/code>. Instead we are passing arguments of the type <span class=\"atv\"><code>NetTopologySuite.Geometries.Point<\/code>.<\/span><\/p>\n<h5>Wrapping the Spatial Data types<\/h5>\n<p>To circumvent the issue described above, we will write custom wrappers to convert <code>NetTopologySuite<\/code> types to OData spatial types.<\/p>\n<pre class=\"prettyprint\">    public class GeographyPointWrapper\r\n    {\r\n        private readonly Point _dbPoint;\r\n        protected GeographyPointWrapper(Point point)\r\n        {\r\n            _dbPoint = point;\r\n        }\r\n\r\n        public static implicit operator GeographyPoint(GeographyPointWrapper wrapper)\r\n        {\r\n            return GeographyPointConverter.ConvertPointTo(wrapper._dbPoint);\r\n        }\r\n\r\n        public static implicit operator GeographyPointWrapper(GeographyPoint pt)\r\n        {\r\n            return new GeographyPointWrapper(GeographyPointConverter.ConvertFrom(pt));\r\n        }\r\n\r\n        public static implicit operator Point(GeographyPointWrapper wrapper)\r\n        {\r\n            return wrapper._dbPoint;\r\n        }\r\n\r\n        public static implicit operator GeographyPointWrapper(Point point)\r\n        {\r\n            return new GeographyPointWrapper(point);\r\n        }\r\n    }<\/pre>\n<pre class=\"prettyprint\">    public class GeographyPointConverter\r\n    {\r\n        public static GeographyPoint ConvertPointTo(Point dbPoint)\r\n        {\r\n            Debug.Assert(dbPoint.GeometryType == \"Point\");\r\n            double lat = dbPoint.X;\r\n            double lon = dbPoint.Y;\r\n            double? alt = dbPoint.Z;\r\n            double? m = dbPoint.M;\r\n            return GeographyPoint.Create(lat, lon, alt, m);\r\n        }\r\n\r\n        public static Point ConvertFrom(GeographyPoint geographyPoint)\r\n        {\r\n            return new Point(geographyPoint.Latitude, geographyPoint.Longitude, (double)geographyPoint.Z);\r\n        }\r\n    }<\/pre>\n<h5>Modify the Edm Model<\/h5>\n<p>We make the following modifications to the <code>Customer<\/code> model so that we can apply the wrapper on the model properties.<\/p>\n<pre class=\"prettyprint\">    public class Customer\r\n    {\r\n        private GeographyPointWrapper _ptWrapper;\r\n        public int Id { get; set; }\r\n        public string Name { get; set; }\r\n\r\n        public Point Loc\r\n        {\r\n            get { return _ptWrapper; }\r\n            set { _ptWrapper = value; }\r\n        }\r\n\r\n        [NotMapped]\r\n        public GeographyPoint EdmLoc\r\n        {\r\n            get { return _ptWrapper; }\r\n            set { _ptWrapper = value; }\r\n        }\r\n    }<\/pre>\n<p>The <code>[NotMapped]<\/code> attribute ensures the <code>EdmLoc<\/code> is not mapped to <code>EFcore<\/code> since <code>EFCore<\/code> doesn&#8217;t understand <code>GeographyPoint<\/code> which is an OData type.<\/p>\n<h5>Build the modified Edm Model<\/h5>\n<p>We are using the model builder to build the modified <code>EdmModel<\/code>.<\/p>\n<p>We are ignoring <code>Loc<\/code> property which is of type <code>Point<\/code> (a <code>NetTopologySuite<\/code> type).<\/p>\n<p>We are adding the <code>EdmLoc<\/code> property (which is of type <code>Edm.GeographyPoint<\/code>) to the model and renaming it to <code>Loc<\/code>.<\/p>\n<pre class=\"prettyprint\">public static IEdmModel GetEdmModel()\r\n{\r\n    var builder = new ODataModelBuilder();\r\n    builder.EntitySet&lt;Customer&gt;(\"Customers\");\r\n\r\n    var customerType = builder.EntityType&lt;Customer&gt;();\r\n    customerType.HasKey(c =&gt; c.Id).Ignore(c =&gt; c.Loc);\r\n    customerType.Property(c =&gt; c.Name);\r\n\r\n    var customer = builder.StructuralTypes.First(t =&gt; t.ClrType == typeof(Customer));\r\n    customer.AddProperty(typeof(Customer).GetProperty(\"EdmLoc\")).Name = \"Loc\";\r\n    return builder.GetEdmModel();\r\n}<\/pre>\n<p>Now if we inspect the service metadata, we will find the property OData types.<\/p>\n<p><code> GET ~\/odata\/$metadata<\/code><\/p>\n<pre class=\"prettyprint\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\r\n&lt;edmx:Edmx Version=\"4.0\" xmlns:edmx=\"http:\/\/docs.oasis-open.org\/odata\/ns\/edmx\"&gt;\r\n    &lt;edmx:DataServices&gt;\r\n        &lt;Schema Namespace=\"GeometryWebAPI.Models\" xmlns=\"http:\/\/docs.oasis-open.org\/odata\/ns\/edm\"&gt;\r\n            &lt;EntityType Name=\"Customer\"&gt;\r\n                &lt;Key&gt;\r\n                    &lt;PropertyRef Name=\"Id\" \/&gt;\r\n                &lt;\/Key&gt;\r\n                &lt;Property Name=\"Id\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n                &lt;Property Name=\"Name\" Type=\"Edm.String\" \/&gt;\r\n                &lt;Property Name=\"Loc\" Type=\"Edm.GeographyPoint\" \/&gt;\r\n            &lt;\/EntityType&gt;\r\n        &lt;\/Schema&gt;\r\n        &lt;Schema Namespace=\"Default\" xmlns=\"http:\/\/docs.oasis-open.org\/odata\/ns\/edm\"&gt;\r\n            &lt;EntityContainer Name=\"Container\"&gt;\r\n                &lt;EntitySet Name=\"Customers\" EntityType=\"GeometryWebAPI.Models.Customer\" \/&gt;\r\n            &lt;\/EntityContainer&gt;\r\n        &lt;\/Schema&gt;\r\n    &lt;\/edmx:DataServices&gt;\r\n&lt;\/edmx:Edmx&gt;<\/pre>\n<p>We can see the <code>Loc<\/code> property now has an <code>Edm.GeographyPoint<\/code> type.<\/p>\n<p>Let&#8217;s now filter against the <code>Loc<\/code> property<\/p>\n<pre class=\"prettyprint\">GET odata\/Customers?$filter=geo.distance(Loc, geography'POINT(10 30)') gt 20<\/pre>\n<p>We get an error<\/p>\n<pre class=\"prettyprint\">{\r\n    \"error\": {\r\n        \"code\": \"\",\r\n        \"message\": \"The query specified in the URI is not valid. Unknown function 'geo.distance'.\",\r\n        \"details\": [],\r\n        \"innererror\": {\r\n            \"message\": \"Unknown function 'geo.distance'.\",\r\n            \"type\": \"System.NotImplementedException\",\r\n            \"stacktrace\": \"...\"\r\n        }\r\n    }\r\n}<\/pre>\n<p>This means that <code>geo.distance<\/code> (a spatial function) is not supported in AspNetCore OData 8.<\/p>\n<h3>Customizing FilterBinder<\/h3>\n<p>We can customize the <code>FilterBinder<\/code> in 2 ways<\/p>\n<ul>\n<li>Implement <code>IFilterBinder<\/code> interface.<\/li>\n<li>Inherit from the <code>FilterBinder<\/code> class.<\/li>\n<\/ul>\n<p>If we implement <code>IFilterBinder<\/code> interface, we will have to write all implementations of <code>Bind<\/code> methods from scratch. This can be time consuming but offers the most flexibility.<\/p>\n<p>If we inherit from the <code>FilterBinder<\/code> class, we can override the specific <code>Bind<\/code> methods that we want to modify while re-using the existing implementations of <code>Bind<\/code> methods.<\/p>\n<p>Some of the methods we can override include:<\/p>\n<pre class=\"prettyprint\">public override Expression BindCollectionNode(Microsoft.OData.UriParser.CollectionNode node, QueryBinderContext context)\r\n\r\npublic override Expression BindConstantNode(Microsoft.OData.UriParser.ConstantNode constantNode, QueryBinderContext context)\r\n\r\npublic override Expression BindCountNode(Microsoft.OData.UriParser.CountNode node, QueryBinderContext context)\r\n\r\npublic override Expression BindNavigationPropertyNode(Microsoft.OData.UriParser.QueryNode sourceNode, IEdmNavigationProperty navigationProperty, string propertyPath, QueryBinderContext context)\r\n\r\npublic override Expression Expression BindSingleValueNode(Microsoft.OData.UriParser.SingleValueNode node, QueryBinderContext context)<\/pre>\n<p>In this example, we will inherit from the <code>FilterBinder<\/code> class and override the <code>BindSingleValueFunctionCallNode(SingleValueFunctionCallNode node, QueryBinderContext context)<\/code> method. This is the method that binds function calls. We will modify this method to support <code>geo.distance<\/code> spatial function.<\/p>\n<p>Note: <span style=\"color: #52595e; font-family: Arimo, 'Helvetica Neue', Arial, sans-serif; font-size: 1rem;\">In OData, a <code>SingleValueFunctionCallNode<\/code> is a node that represents a function call that returns a single value.<\/span><\/p>\n<h5>CustomFilterBinder<\/h5>\n<pre class=\"prettyprint\">    public class CustomFilterBinder : FilterBinder\r\n    {\r\n        internal const string GeoDistanceFunctionName = \"geo.distance\";\r\n\r\n        private static readonly MethodInfo distanceMethodDb = typeof(Geometry).GetMethod(\"Distance\");\r\n\r\n        public override Expression BindSingleValueFunctionCallNode(SingleValueFunctionCallNode node, QueryBinderContext context)\r\n        {\r\n            switch (node.Name)\r\n            {\r\n                case GeoDistanceFunctionName:\r\n                    return BindGeoDistance(node, context);\r\n\r\n                default:\r\n                    return base.BindSingleValueFunctionCallNode(node, context);\r\n            }\r\n        }\r\n\r\n        public Expression BindGeoDistance(SingleValueFunctionCallNode node, QueryBinderContext context)\r\n        {\r\n            Expression[] arguments = BindArguments(node.Parameters, context);\r\n\r\n            string propertyName = null;\r\n\r\n            foreach(var queryNode in node.Parameters)\r\n            {\r\n                if(queryNode.GetType() == typeof(SingleValuePropertyAccessNode))\r\n                {\r\n                    SingleValuePropertyAccessNode svpan = queryNode as SingleValuePropertyAccessNode;\r\n                    propertyName = svpan.Property.Name;\r\n                }\r\n            }\r\n\r\n            GetPointExpressions(arguments, propertyName, out MemberExpression memberExpression, out ConstantExpression constantExpression);\r\n            var ex = Expression.Call(memberExpression, distanceMethodDb, constantExpression);\r\n\r\n            return ex;\r\n        }\r\n\r\n        private static void GetPointExpressions(Expression[] expressions, string propertyName, out MemberExpression memberExpression, out ConstantExpression constantExpression)\r\n        {\r\n            memberExpression = null;\r\n            constantExpression = null;\r\n\r\n            foreach (Expression expression in expressions)\r\n            {\r\n                var memberExpr = expression as MemberExpression;\r\n                var constantExpr = memberExpr.Expression as ConstantExpression;\r\n\r\n                if (constantExpr != null)\r\n                {\r\n                    GeographyPoint point = GetGeographyPointFromConstantExpression(constantExpr);\r\n                    constantExpression = Expression.Constant(CreatePoint(point.Latitude, point.Longitude));\r\n                }\r\n                else\r\n                {\r\n                    memberExpression = Expression.Property(memberExpr.Expression, propertyName);\r\n                }\r\n            }\r\n        }\r\n\r\n        private static GeographyPoint GetGeographyPointFromConstantExpression(ConstantExpression expression)\r\n        {\r\n            GeographyPoint point = null;\r\n            if (expression != null)\r\n            {\r\n                PropertyInfo constantExpressionValuePropertyInfo = expression.Type.GetProperty(\"Property\");\r\n                point = constantExpressionValuePropertyInfo.GetValue(expression.Value) as GeographyPoint;\r\n            }\r\n\r\n            return point;\r\n        }\r\n\r\n        private static Point CreatePoint(double latitude, double longitude)\r\n        {\r\n            \/\/ 4326 is most common coordinate system used by GPS\/Maps\r\n            var geometryFactory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326);\r\n\r\n            \/\/ see https:\/\/docs.microsoft.com\/en-us\/ef\/core\/modeling\/spatial\r\n            \/\/ Longitude and Latitude\r\n            var newLocation = geometryFactory.CreatePoint(new Coordinate(longitude, latitude));\r\n\r\n            return newLocation;\r\n        }\r\n    }<\/pre>\n<p class=\"prettyprint\">Let&#8217;s look at what each method in the above code does:<\/p>\n<ul>\n<li><code>BindSingleValueFunctionCallNode<\/code> &#8211; checks if a function name is <code>geo.distance<\/code>. If true call <code>BindGeoDistance<\/code>. If not call the base method to handle other function calls.<\/li>\n<li><code>BindGeoDistance<\/code> &#8211; Creates an expression from the <code>SingleValueFunctionCallNode<\/code>.<\/li>\n<li><code>GetPointExpressions<\/code> &#8211; Transforms <code>SingleValueFunctionCallNode<\/code> arguments&#8217; expressions and outputs a <code>MemberExpression<\/code> and a <code>ConstantExpression<\/code>. This will be used to create a <code>MethodCallExpression<\/code> in <code>BindGeoDistance<\/code>. The Expressions from the <code>BindArguments<\/code> method in <code>BindGeoDistance<\/code> have OData spatial types. However EFCore only understand <code>NetTopologySuite<\/code> types. So we recreate the expressions with NetTopologySuite types.<\/li>\n<li><code>GetGeographyPointFromConstantExpression<\/code> &#8211; Extracts the <code>GeographyPoint<\/code> property from the <code>ConstantExpression<\/code>.<\/li>\n<li><code>CreatePoint<\/code> &#8211; Create <code>Point<\/code> value, which is a <code>NetTopologySuite<\/code> type.<\/li>\n<\/ul>\n<h5>Inject CustomFilterBinder into DI container<\/h5>\n<p class=\"prettyprint\"><span style=\"color: #52595e; font-family: Arimo, 'Helvetica Neue', Arial, sans-serif; font-size: 1rem;\">In<code> Startup.cs<\/code>, w<\/span>e inject the <code>CustomFilterBinder<\/code> as a Singleton.<\/p>\n<pre class=\"prettyprint\">public void ConfigureServices(IServiceCollection services)\r\n{\r\n    services.AddControllers().AddOData(opt =&gt; opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5)\r\n                                           .AddRouteComponents(\"odata\", GetEdmModel(), svcs =&gt;\r\n                                           {\r\n                                               svcs.AddSingleton&lt;IFilterBinder, CustomFilterBinder&gt;();\r\n                                           }));\r\n}\r\n\r\n<\/pre>\n<p>If we re-run the filter query, we will get the correct results.<\/p>\n<pre class=\"prettyprint\">{\r\n    \"@odata.context\": \"http:\/\/localhost:5000\/odata\/$metadata#Customers\",\r\n    \"value\": [\r\n        {\r\n            \"Id\": 1,\r\n            \"Name\": \"Customer 1\",\r\n            \"Loc\": {\r\n                \"type\": \"Point\",\r\n                \"coordinates\": [\r\n                    47.609722,\r\n                    -122.333056,\r\n                    \"NaN\",\r\n                    \"NaN\"\r\n                ],\r\n                \"crs\": {\r\n                    \"type\": \"name\",\r\n                    \"properties\": {\r\n                        \"name\": \"EPSG:4326\"\r\n                    }\r\n                }\r\n            }\r\n        },\r\n        {\r\n            \"Id\": 2,\r\n            \"Name\": \"Customer 2\",\r\n            \"Loc\": {\r\n                \"type\": \"Point\",\r\n                \"coordinates\": [\r\n                    47.669444,\r\n                    -122.123889,\r\n                    \"NaN\",\r\n                    \"NaN\"\r\n                ],\r\n                \"crs\": {\r\n                    \"type\": \"name\",\r\n                    \"properties\": {\r\n                        \"name\": \"EPSG:4326\"\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    ]\r\n}<\/pre>\n<h3>Final thoughts<\/h3>\n<p>We have created an OData service that handles spatial data. We have customized the <code>FilterBinder<\/code> to allows us filter against <code>geo.distance<\/code> which is an OData spatial function currently not supported by AspNetCore OData 8.<\/p>\n<ul>\n<li>There are so many benefits of having the capabilities to customize the expression binders.<\/li>\n<li>We will continue evolving and adding more capabilities based on customer feedback.<\/li>\n<\/ul>\n<h5>Useful links<\/h5>\n<p>AspNetCore OData repo: <a href=\"https:\/\/github.com\/OData\/AspNetCoreOData\">https:\/\/github.com\/OData\/AspNetCoreOData<\/a><\/p>\n<p>AspNetCore OData issues page: <a href=\"https:\/\/github.com\/OData\/AspNetCoreOData\/issues\">https:\/\/github.com\/OData\/AspNetCoreOData\/issues<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Background The OData URI parser parses the $filter query to a FilterClause. The FilterBinder translates an OData $filter parse tree represented by a FilterClause to an Expression. The Expression can be applied to an IQueryable and passed to an ORM (e.g Entity Framework Core) for processing. The FilterBinder contains the default implementation on how the [&hellip;]<\/p>\n","protected":false},"author":20598,"featured_media":3253,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1472,1],"tags":[],"class_list":["post-4857","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-asp-net-core","category-odata"],"acf":[],"blog_post_summary":"<p>Background The OData URI parser parses the $filter query to a FilterClause. The FilterBinder translates an OData $filter parse tree represented by a FilterClause to an Expression. The Expression can be applied to an IQueryable and passed to an ORM (e.g Entity Framework Core) for processing. The FilterBinder contains the default implementation on how the [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4857","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/users\/20598"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/comments?post=4857"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4857\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media\/3253"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media?parent=4857"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/categories?post=4857"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/tags?post=4857"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}