2

Currently I have an entity that is "geolocatable" via a SqlGeography column that I can use via expressions for filtering and sorting. I am already able to get all entities within distance x of point y and sort by entities closest to (or furthest from) point y. However, in order to return the distance from the entity to y I have to recalculate the distance in the application because I have not yet determined how to materialize the result of the distance calculation from the database to the entities in the IQueryable. This is a mapped entity and a great deal of application logic surrounds the type of entity returned so projecting it into a dynamic object is not a viable option for this implementation (though I understand how that would work). I have also tried using an unmapped object that inherits from the mapped entity but that suffers the same problems. Essentially, as I understand it, I should be able to define the getter of an unmapped property to assign a computed value in a queryable extension IF I modify the expression tree that represents the IQueryable but the how escapes me. I've written expressions in this manner before but I think I need to be able to modify the existing select rather than just chaining on a new Expression.Call which is unexplored territory for me.

The following should code should properly illustrate the problem:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration;
using System.Data.Entity.Spatial; // from Microsoft.SqlServer.Types (Spatial) NuGet package
using System.Linq;

public class LocatableFoo
{
    [Key]
    public int Id { get; set; }

    public DbGeography Geolocation { get; set; }

    [NotMapped]
    public double? Distance { get; set; }
}

public class PseudoLocatableFoo : LocatableFoo
{
}

public class LocatableFooConfiguration : EntityTypeConfiguration<LocatableFoo>
{
    public LocatableFooConfiguration()
    {
        this.Property(foo => foo.Id).HasColumnName("id");
        this.Property(foo => foo.Geolocation).HasColumnName("geolocation");
    }
}

public class ProblemContext : DbContext
{
    public DbSet<LocatableFoo> LocatableFoos { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new LocatableFooConfiguration());

        base.OnModelCreating(modelBuilder);
    }
}

public class Controller
{
    public Controller(ProblemContext context) // dependency injection
    {
        this.Context = context;
    }

    private ProblemContext Context { get; set; }

    /* PROBLEM IN THIS METHOD:
     * Do not materialize results (ie ToList) and then calculate distance as is done currently <- double calculation of distance in DB and App I am trying to solve
     * Must occur prior to materialization
     * Must be assignable to "query" that is to type IQueryable<LocatableFoo>
     */
    public IEnumerable<LocatableFoo> GetFoos(decimal latitude, decimal longitude, double distanceLimit)
    {
        var point = DbGeography.FromText(string.Format("Point({0} {1})", longitude, latitude), 4326); // NOTE! This expects long, lat rather than lat, long.
        var query = this.Context.LocatableFoos.AsQueryable();

        // apply filtering and sorting as proof that EF can turn this into SQL
        query = query.Where(foo => foo.Geolocation.Distance(point) < distanceLimit);
        query = query.OrderBy(foo => foo.Geolocation.Distance(point));

        //// this isn't allowed because EF doesn't allow projecting to mapped entity
        //query = query.Select( foo => new LocatableFoo { Id = foo.Id, Geolocation = foo.Geolocation, Distance = foo.Geolocation.Distance(point) });

        //// this isn't allowed because EF doesn't allow projecting to mapped entity and PseudoLocatableFoo is considered mapped since it inherits from LocatableFoo
        //query = query.Select( foo => new PseudoLocatableFoo { Id = foo.Id, Geolocation = foo.Geolocation, Distance = foo.Geolocation.Distance(point) });

        //// this isn't allowed because we must be able to continue to assign to query, type must remain IQueryable<LocatableFoo>
        //query = query.Select( foo => new { Id = foo.Id, Geolocation = foo.Geolocation, Distance = foo.Geolocation.Distance(point) });

        // this is what I though might work
        query = query.SelectWithDistance(point);

        this.Bar(query);
        var results = query.ToList(); // run generated SQL
        foreach (var result in results) //problematic duplicated calculation
        {
            result.Distance = result.Geolocation.Distance(point);
        }

        return results;
    }

    // fake method representing lots of app logic that relies on knowing the type of IQueryable<T>
    private IQueryable<T> Bar<T>(IQueryable<T> foos)
    {
        if (typeof(T) == typeof(LocatableFoo))
        {
            return foos;
        }

        throw new ArgumentOutOfRangeException("foos");
    }
}

public static class QueryableExtensions
{
    public static IQueryable<T> SelectWithDistance<T>(this IQueryable<T> queryable, DbGeography pointToCalculateDistanceFrom)
    {
        /* WHAT DO?
         * I'm pretty sure I could do some fanciness with Expression.Assign but I'm not sure
         * What to get the entity with "distance" set
         */
        return queryable;
    }
}
Daniel King
  • 224
  • 1
  • 10
  • Sorry, not much help here but when you have code that does a type check on the generic parameter within a generic method, that should immediately raise alarms in your head. – Jeff Mercado Nov 11 '15 at 01:19
  • @JeffMercado, the Bar() method is really only meant to encapsulate the requirement that logic that is type dependent will be performed on the IQueryable. It is not indicative of the actual application logic as the domain of my problem is much more complex. Basically you can assume that Bar() is not representative of any real logic in practice, just in abstract. In practice, what I have reduced to LocalizableFoo is one of a number of entities all which implement interfaces (or don't) depending on how they should behave in the query composition pipeline. – Daniel King Nov 11 '15 at 18:46
  • So does your `Bar` method "need access to" your `Distance` field? Is your main worry that recalculating the distance in the application is expensive? Is it really? Or is it just "ugly"? – MBoros Nov 12 '15 at 02:30
  • @MBoros, It's expensive. The application is an API for delivering materialized data objects to clients in the quickest manner possible. We are adding compute time duplicating the computation in both the database and the app. Since the database is already forced to do the calculation for the filter/sort (only case in which we do add distance) it is 100% wasted time. – Daniel King Nov 12 '15 at 17:39
  • 1
    How can you store the distance from x to y in the database when you (presumably) wouldn't know what x is until it is requested? – Mark Homer Nov 13 '15 at 10:04
  • @Mark Homer, SQL does not store the distance - the distance is calculated. Using the Geography type for the column you are able to store a "location" in an indexable manner that you can then calculate the distance to provided another point in geographic space. You should look into the NuGet package I mention for DB spatial types to get more info. – Daniel King Nov 15 '15 at 06:04

2 Answers2

1

What about replacing the line

var results = query.ToList();

with

var results = query
                .Select(x => new {Item = x, Distance = x.Geolocation.Distance(point)}
                .AsEnumerable() // now you just switch to app execution
                .Select(x => 
                {
                    x.Item.Distance = x.Distance; // you don't need to calculate, this should be cheap
                    return x.Item;
                })
                .ToList();
MBoros
  • 1,090
  • 7
  • 19
  • This is mostly a flavor of dynamic projection which, as I stated in the question, I understand how to do but can't use. I need to select a calculated column for the mapped entity without projecting to a dynamic because the entire query pipeline is built on the concept of composability and if I must enumerate to a type other than `IQueryable` where `T` is my mapped entity, then the composability will break. Its very similar to fluent syntax where each method must return the same type as the type of the object it acts on. – Daniel King Nov 16 '15 at 22:29
  • The idea here was, that you do the projection only after you call all the methods that are typed. That is why I asked you if the `Bar` method "need access to" your `Distance` field? I assumed not, but now I guess it's not the case. – MBoros Nov 18 '15 at 07:30
0

The Distance field is logically not part of your table, since it represents a distance to a dynamically specified point. As such it should not be part of your entity.

At this point if you want it being calculated on the db, you should create a Stored procedure, or a TVF (or sg else) that returns your entity extended with the distance. This way you can map the return type to an Entity. It is a clearer design to me btw.

MBoros
  • 1,090
  • 7
  • 19
  • We have taken this tact in other places but Distance feels very much like a "calculated" column to me. I had hoped that the expression could be modified directly to support a `[DbGeneratedColumn]` but its seeming more and more unlikely. As that is the case, I'll just mark this as the answer and deal with the multiplicity of TVFs for accessing flavors of `Foo`. – Daniel King Nov 19 '15 at 22:06