Spatial data and Entity Framework – from real world usage
Julie Lerman created a post about spatial data usage in Entity Framework. Her conclusions are correct. But because from last three projects I did on Entity Framework two of them are working with spatial data (every customer wants a Google Maps or Bing Maps on website, it’s like cup holders in cars) I would like share my solution as well some ideas behind why I used this approach.
First and foremost Entity Framework doesn’t support spatial data now. So you have to create some workaround. The idea getting the data through blob is exactly what I created. In my tables where I need to save some coordinates (it’s mainly a point) I create simple column with this data type – I consider this clean initial work better, because maybe sometimes/somewhere/somebody will work with this structures so to have it clean. Then for the workaround I add XXX_bin column where I’ll store the binary representation of spatial data and use it for Entity Framework. Something like this:
[Location] geography Default geography::STGeomFromText('POINT EMPTY', 4326) NOT NULL,
[Location_bin] Varbinary(max) NOT NULL,
Because I’m working only through Entity Framework I need to update the Location automatically as there might be (and are) indices or other stuff working with it. To make it transparent I’m using insert or update trigger:
if (update(Location_bin))
begin
update %tablename% set
Location = geography::STGeomFromWKB(Location_bin, 4326)
where
ID in (select ID from inserted);
end
If you expect somebody updating the Location column directly too, you need to create another trigger and make sure you’ll not end up in infinite loop.
To support the – in my case – points for UI developers I create in every entity computed property turning Location_bin into my LatLong struct. This struct is using Microsoft.SqlServer.Types namespace.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Types;
namespace FooBar
{
public struct LatLong
{
public double Lat { get; set; }
public double Long { get; set; }
public static LatLong FromSqlBytes(byte[] bytes)
{
SqlGeography g = SqlGeography.STGeomFromWKB(new SqlBytes(bytes), 4326);
return new LatLong { Lat = g.Lat.Value, Long = g.Long.Value };
}
public byte[] ToSqlBytes()
{
return SqlGeography.Point(this.Lat, this.Long, 4326).STAsBinary().Buffer;
}
}
}
public LatLong Location
{
get
{
return LatLong.FromSqlBytes(this.Location_bin);
}
set
{
this.Location_bin = value.ToSqlBytes();
}
}
This creates almost seamless support for other developers when using spatial data in Entity Framework – inserting, updating, deleting all works without notice from others (if you’re not looking into internal implementation).
There are only two problems, both solvable.
First one is that developer cannot filter/sort/… using Location property. This isn’t probably a big problem. As (s)he doesn’t have spatial methods (like i.e. Distance) available, so the queries will not make sense. Which brings me to the other problem and that’s the querying. With EFv1 I created stored procedures for these queries and returned data through these. Same works for EFv4. If you have strict set of results you need, this is probably easiest solution. The other one, working in EFv4 is LINQ function imports via EdmFunction attribute. For computations I’m going to use I create function in T-SQL doing the work and I import it into my solution thru above noted attribute. The simple Distance method may look like:
create function Distance ( @Location_bin1 varbinary(max), @Location_bin2 varbinary(max) ) returns int as begin return geography::STGeomFromWKB(@Location_bin1, 4326).STDistance(geography::STGeomFromWKB(@Location_bin2, 4326)); end
And
[EdmFunction("model.Store", "Distance")]
internal static int Distance(this byte[] location1, byte[] location2)
{
throw new NotSupportedException();
}
Sure, when calling this function for a big resultsets it may be a performance bottleneck (I recommend doing there as much work as possible, even at the price of more functions doing similar stuff, to not create a deep function calls.), but with additional filters it works well.
protected IQueryable<Something> SomethingInDistance(LatLong point, int distance)
{
byte[] spatialData = point.ToSqlBytes();
return this.Somethings.Where(o => o.Location_bin.Distance(spatialData) < distance);
}
Another way could be to create a view (if you know the parameters in advance) and do explicit join (table/entityset <> view/entityset) in EF. Or do the filtering on client (if reasonably small). And I’m sure I’ll find other ways how to get the data you want back, simply choose what fits your needs best.
Although it may look like a lot of work, it isn’t. In fact you’re pretty fast (there’s a high level of reusability) if you know what to do (and after one implementation you will).



There's 7 Comments So Far
March 21st, 2010 at 18:15
1st, your suggestions are very helpful and appreciated. If someone is implementing a basic “cup holder” spatial widget, these work-arounds get the job done nicely.
For anyone doing serious spatial work, I’ve concluded that the Entity Framework is a complete load of bull. I’ve exhaustively tested all the proposed work-arounds.
SQL Server spatial performs great. But the Entity Framework (for spatial) is a dead-end, and will remain a dead-end until MS fixes it.
March 22nd, 2010 at 09:10
The problem is, that EF isn’t just for MS SQL, but for 3rd party database engines as well. So the spatial data support isn’t easy, especially the datatypes mapping.
And BTW OT the MS SQL spatial data support is a good average, nothing super ultra great. At least IMO.
April 23rd, 2010 at 01:31
Cincura,
Thanks for the solution. I didn’t know we could do that in EF. It’s good to know about the functions.
In this case, to take advantage of indexes you could just write the query using dynamica SQL.
context.CreateQuery(“SELECT bla from X where …”, param1,param2);
This is probably not as elegant, but in this case I would go for the performant version.
Also I know that it’s hard to support and MS might be thinking of 3rd party DBs, but NHibernate has a really good support for UserTypes that works well accross different DBs, so well that they even support SqlSpatial and queries on top of it
April 30th, 2011 at 01:16
Thanks! What a slick solution.
I’m trying to get it to work, but my [EdmFunction] isn’t happy.
I don’t understand … it looks like Distance() is an extension method, but it’s internal? My compiler doesn’t like that. I made it public and put it in a public static class and it compiles, but I get:
System.NotSupportedException: The specified method ‘Int32 Distance(Byte[], Byte[])’ on the type ‘MyApp.DistanceExtension’ cannot be translated into a LINQ to Entities store expression.
April 30th, 2011 at 07:10
It doesn’t matter whether it’s public or internal or anything else (from EF’s POV). It’s extension method because of http://blog.cincura.net/id/230897 , but again, it’s just a to make it nicer.
What matters is properly EdmFunction “filled” attribute and you have to have this SP imported in model, of course.
June 16th, 2011 at 10:39
I used a variation on the same work-around, with some differences being that, since this app doesn’t (currently) ever have updates directly to the SQL geography column, I made that column a computed column, instead of using a trigger (I don’t particularly like using triggers unless I have to). Also, instead of using the LatLong structure, I just appended to public properties to my entity class, GeoLat() and GeoLong(). The getter just returns the lat (or long) portion of the coordinate, and the setter updates that portion of the coordinate (and then converts the coordinate back to byte[], in order to update the actual column).
For my SQL geography computed column, I used the same as from your trigger ( [geography]::STGeomFromWKB([CenterPointBytes],(4326)) ).
As an example, here is my GeoLat property (in VB.Net):
Public Property GeoLat As Double?
Get
If CenterPointBytes Is Nothing Then
Return New Nullable(Of Double)(Nothing)
Else
Return Math.Round(SqlGeography.STGeomFromWKB(New SqlBytes(CenterPointBytes), 4326).Lat.Value, 5)
End If
End Get
Set(ByVal value As Double?)
If value.HasValue AndAlso value 0 Then
Dim oldGeo As SqlGeography = SqlGeography.STGeomFromWKB(New SqlBytes(CenterPointBytes), 4326)
If CenterPointBytes Is Nothing OrElse oldGeo.IsNull OrElse oldGeo.Long.IsNull Then
Dim newGeo As SqlGeography = SqlGeography.STPointFromText(New SqlChars(New SqlString(String.Format(“POINT({0} {1})”, 0, value))), 4326)
CenterPointBytes = newGeo.STAsBinary().Value
Else
Dim sqlGeo As SqlGeography = SqlGeography.STPointFromText(New SqlChars(New SqlString(String.Format(“POINT({0} {1})”, oldGeo.Long, value))), 4326)
CenterPointBytes = sqlGeo.STAsBinary().Value
End If
End If
End Set
End Property
June 16th, 2011 at 10:46
Wondering whether it’s possible to index computed spatial column in MS SQL…
Share your thoughts, leave a comment!