W jednym z projektów podjęliśmy się wyzwania udostępnienia API ze wsparciem OData dla każdego kto będzie chciał konsumować nasze API. Decyzja ta zapadła po tym jak sami dla naszej aplikacji musieliśmy wykonać szereg skompilowanych kwerend na danych i OData okazała się najlepszym wyjściem… Przynajmniej tak nam się zdawało.

W którymś momencie do naszych wymagań doszła opcja wykonywania zapytań spatial – na przykład dystans pomiędzy punktem a lokalizacją nie może być większy niż 10km. Tutaj OData wyszła nam naprzeciw i aż byliśmy szczęśliwi, że zdecydowaliśmy się na OData. Jak byśmy jeszcze mieli to dodawać to zapytań to już masakra by była.

Nie wiem ile czasu minęło, może dwa tygodnie, może mniej. Ale przychodzi wiadomości, że coś jest nie tak z zapytaniami spatial, jakoś nie działają:

https://localhost/odata/beaches?$filter=distanceto(GeoLocation,POINT(321720 160261)) lt 900.0

The query specified in the URI is not valid. ')' or ',' expected at position 36 in 'distanceto(GeoLocation,POINT(321720 160261)) lt 900.0'.

Od tego momentu rozpoczęła się spirala wkurzenia, przekleństw i wyrwania sobie włosów.

To co się okazało po wielu godzinach testów, prób i debugowania oraz czytania Internetu tak naprawdę można nadać się jako TL;DR i można na tym zakończyć pisanie

TL;DR

Spatial queries na OData dla Web API nie działają i działać nie będą.

To tak w skrócie :) Ale do rzeczy, o co się tutaj wszystko rozeszło.

1. Parametr POINT

Parametr POINT musi być podany jako ciąg znaków. W przeciwnym wypadku nici z konwersji ani z prasowania URL. Na walidacji się wywali. Ale to nie wszystko. Gdyż ot tak napisany string nic nie daje, i POINT jest traktowany jako… string. A on tak ma nie być.

A więc trzeba nadać jego typ. Typ ma znaczenie i mamy do wyboru dwa:

  • geography – lat long
  • geometry – easting/northing

To ma znaczenie! Jednak bez naszego wysiłku nie będzie miało żadnego znaczenia, gdyż nie dojdziemy do ostatniego kroku. Jednak warto o tym wiedzieć. Mając geometry OData zamieni to na odpowiedni typ danych, który potem może mieć duże znaczenie.

2. distanceto a OData

Metoda taka nie istnieje. Dowiemy się zresztą tym jak tylko zamienimy POINT na string:

https://localhost/odata/beaches?$filter=distanceto(GeoLocation,'POINT(321720 160261)') lt 900.0

The query specified in the URI is not valid. An unknown function with name 'distanceto' was found. This may also be a key lookup on a navigation property, which is not allowed.

Ale jako? A no, ten post co go znaleźliśmy był out of date (co z tego, że to leży na github i można to łatwo zaznaczyć…). Ten poprawny post już z poprawną metodą znajduje się tutaj. A więc distanceto to geo.distance. Schemat zresztą mówi to samo.

3. GeoLocation czyli własność entity

Tutaj też wesoło nie jest. Bo jak mamy EntityFramework to nasz typ jest DbGeometry. Niestety, OData nie rozumie tego typu (zauważcie, że już podaje geometry w POINT!):

https://localhost/odata/beaches?$filter=geo.distance(GeoLocation,geometry'POINT(321720 160261)') lt 900.0

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).

A to znaczy, że nasz GeoLocation musi być albo GeographyPoint albo GeometryPoint.

No dobra, to dało się “dość prosto” obejść, do klasy na której operowaliśmy trzeba było dodać:

public GeometryPoint Point
{
    get
    {
        if (this.GeoLocation == null)
        {
            return null;
        }

        return GeometryPoint.Create(this.GeoLocation.XCoordinate ?? 0, this.GeoLocation.YCoordinate ?? 0);
    }
    set
    {
        this.GeoLocation = DbGeometry.FromText($"POINT({value.X} {value.Y})");
    }
}

I następnie poprawnie skonfigurować zarówno EF jak i OData – nie można użyć atrybutów gdyż działają one zarówno na EF i OData jednocześnie! :)

// EF
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Beach>()
                .Ignore(e => e.Point);
}

// Odata
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Beach>("beaches");
builder.Entity<Beach>()
    .Ignore(e => e.GeoLocation);

Super!!! To teraz powinno działać prawda? PRAWDA?

4. Cała prawda i tylko prawda

Pozostawię tylko wyjątek:

message: "Unknown function 'geo.distance'.",
type: "System.NotImplementedException"

5. Ale przecież błędy?

Tak. OData ma zaimplementowaną walidację zapytań. Działa ona wyśmienicie. Ale implementacja już metody leży nie po stronie OData a po stronie paczki dla określonej technologii. Tak więc o to, Web API ląduje bez wsparcia dla geo.distance zaś WCF ze wsparciem. No właśnie… to powodowało jeszcze większe komplikacje. BO WIEDZIAŁEM, że to działa. To czemu to u mnie za cholerę nie chciało?

Sami sprawdźcie, działa?

http://services.odata.org/V3/OData/OData.svc/Suppliers?$filter=geo.distance(Location, geography'POINT(-127.89734578345 45.234534534)') lt 300

Działa.

Podsumowanie

Ale, jako, ot tak? Nie podejmujesz walki?

Podjąłem. Ale jedynym rozwiązaniem, i to rozwiązaniem które wprowadziliśmy było ściągnięcie kodu z github i modyfikacja implementacji Web API Odata tak by było wsparcie dla geo.distance. Jednak to był hack i to chamski, brzydki i baaaardzo ograniczony. Działa dla nas, dla innych nie będzie :)

Szczerze, to był powód dla którego zrezygnowałem z SharePoint – bo właśnie takie hacki trzeba było robić. Zamiast czerpać przyjemność z pisania kodu to ja spędzam dni i godziny na to by naprawić błąd MS. A nazywam to błędem bo nie ma nigdzie napisane, że brak wsparcia. Wręcz przeciwnie jest napisane, że wsparcie jest i to działa. A jednak…

Postaram się zaś jakoś opisać w jaki sposób doszedłem do tego, że to nie wina u nas, ale Web API OData bo to może być dość ciekawe. Na dziś, wiedźcie, że Wep API i spatial queries nie działają i działać nie będą gdyż są to metody nie zaimplementowane! :)

5 KOMENTARZE

Comments are closed.