Comment renvoyer la valeur calculée DbGeography.Distance dans Code First Entity Framework sans perdre le typage fort?

c# ef-code-first entity-framework expression-trees sqlgeography

Question

Actuellement, j'ai une entité "géolocalisable" via une colonne SqlGeography que je peux utiliser via des expressions pour le filtrage et le tri. Je suis déjà capable d’obtenir toutes les entités situées à une distance x du point y et de les trier par entités les plus proches (ou les plus éloignées) du point y . Cependant, pour renvoyer la distance de l'entité à y, je dois recalculer la distance dans l'application car je n'ai pas encore déterminé comment matérialiser le résultat du calcul de la distance de la base de données aux entités de IQueryable. Il s’agit d’une entité mappée et une grande partie de la logique d’application entoure le type d’entité renvoyée. Son projet dans un objet dynamique n’est donc pas une option viable pour cette implémentation (bien que je comprenne comment cela fonctionnerait). J'ai également essayé d'utiliser un objet non mappé qui hérite de l'entité mappée mais qui souffre des mêmes problèmes. Si j'ai bien compris, je devrais pouvoir définir le getter d'une propriété non mappée pour attribuer une valeur calculée dans une extension interrogeable SI je modifie l'arborescence des expressions qui représente l'IQueryable mais le comment m'échappe. J'ai déjà écrit des expressions de cette manière auparavant, mais je pense que je dois pouvoir modifier l'actuel select plutôt que simplement enchaîner sur un nouveau Expression.Call qui est pour moi un territoire inexploré.

Le code suivant devrait illustrer correctement le problème:

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;
    }
}

Réponse acceptée

Le champ Distance fait logiquement pas partie de votre table, car il représente une distance par rapport à un point spécifié dynamiquement. En tant que tel, il ne devrait pas faire partie de votre entité.

À ce stade, si vous souhaitez que le calcul soit effectué sur la base de données, vous devez créer une procédure stockée ou un fichier TVF (ou sg else) qui renvoie votre entité étendue avec la distance. De cette façon, vous pouvez mapper le type de retour sur une entité. C'est une conception plus claire pour moi d'ailleurs.


Réponse populaire

Qu'en est-il de remplacer la ligne

var results = query.ToList();

avec

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();


Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi