Wie DbGeography.Distance berechneten Wert in Code First Entity Framework zurückgeben, ohne starke Typisierung zu verlieren?

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

Frage

Gegenwärtig habe ich eine Entität, die über eine SqlGeography-Spalte "geolocatable" ist, die ich über Ausdrücke zum Filtern und Sortieren verwenden kann. Ich bin schon in der Lage, alle Entitäten innerhalb der Entfernung x von Punkt y zu erhalten und nach Entitäten zu sortieren, die am nächsten (oder am weitesten von) y entfernt sind . Um jedoch den Abstand von der Entität zu y zurückzugeben, muss ich die Entfernung in der Anwendung neu berechnen, da ich noch nicht festgestellt habe, wie das Ergebnis der Abstandsberechnung von der Datenbank zu den Entitäten in IQueryable umgesetzt werden soll. Dies ist eine abgebildete Entität, und eine große Menge Anwendungslogik umgibt den Typ der zurückgegebenen Entität, so dass die Projektion in ein dynamisches Objekt keine praktikable Option für diese Implementierung ist (obwohl ich verstehe, wie das funktionieren würde). Ich habe auch versucht, ein nicht zugeordnetes Objekt zu verwenden, das von der zugeordneten Entität erbt, aber die gleichen Probleme aufweist. Im Wesentlichen, so wie ich es verstehe, sollte ich in der Lage sein, den Getter einer nicht zugeordneten Eigenschaft zu definieren, um einen berechneten Wert in einer abfragbaren Erweiterung zuzuweisen, wenn ich den Ausdrucksbaum modifiziere, der das IQueryable darstellt, aber wie es mir entgeht. Ich habe schon früher Ausdrücke auf diese Weise geschrieben, aber ich denke, dass ich in der Lage sein muss, das vorhandene select zu modifizieren, anstatt nur einen neuen Expression.Call zu verketten, was für mich unerforschtes Gebiet ist.

Der folgende Code sollte das Problem richtig veranschaulichen:

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

Akzeptierte Antwort

Das Feld Distance ist logisch nicht Teil Ihrer Tabelle, da es eine Entfernung zu einem dynamisch angegebenen Punkt darstellt. Als solches sollte es nicht Teil Ihrer Einheit sein.

An dieser Stelle sollten Sie eine Stored Procedure oder eine TVF (oder sg else) erstellen, die Ihr Entity um den Abstand erweitert. Auf diese Weise können Sie den Rückgabetyp einer Entität zuordnen. Es ist ein klarer Entwurf zu mir BTW.


Beliebte Antwort

Was ist mit dem Ersetzen der Linie

var results = query.ToList();

mit

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


Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum
Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum