dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.63k stars 3.15k forks source link

Add support for value converters to HasDbFunction in order to better support value objects #33788

Open jscarle opened 3 months ago

jscarle commented 3 months ago

As a follow up to https://github.com/dotnet/efcore/issues/13752, I'm attempting to use HasDbFunction with my GeoCoordinate value object to call the database function STDistance.

I already have a value converter as defined below:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NetTopologySuite;
using NetTopologySuite.Geometries;

internal sealed class GeoCoordinateConverter : ValueConverter<GeoCoordinate, Point>
{
    private static readonly GeometryFactory _geoFactory =
        NtsGeometryServices.Instance.CreateGeometryFactory(4326);

    public GeoCoordinateConverter()
        : base(valueObject => ConvertToValue(valueObject), value => ConvertToGeoCoordinate(value))
    {
    }

    private static Point ConvertToValue(GeoCoordinate geoCoordinate)
    {
        var coordinate = new Coordinate(geoCoordinate.Longitude.ToDouble(), geoCoordinate.Latitude.ToDouble());
        var point = _geoFactory.CreatePoint(coordinate);
        return point;
    }

    private static GeoCoordinate ConvertToGeoCoordinate(Point point)
    {
        var coordinate = point.Coordinate;
        return GeoCoordinate.Convert(coordinate.Y, coordinate.X);
    }
}

internal static class GeoCoordinateExtensions
{
    public static void ConfigureGeoCoordinate(this ModelConfigurationBuilder configurationBuilder)
    {
        configurationBuilder.Properties<GeoCoordinate>()
            .HaveConversion<GeoCoordinateConverter>()
            .HaveColumnType("geography");
    }
}

The value converter does work when reading and writing entities that contain the GeoCoordinate value object. However, when defining the database function as below:

public static double DistanceInMeters(GeoCoordinate coordinate1, GeoCoordinate coordinate2)
    => throw new NotImplementedException();
modelBuilder.HasDbFunction(typeof(MyDbContext).GetMethod(nameof(DistanceInMeters), new[]
{
    typeof(GeoCoordinate), typeof(GeoCoordinate)
})!, 
    b => b.HasTranslation(
        e =>
            new SqlFunctionExpression(
                e.First(),
                "STDistance",
                false,
                false,
                typeof(double),
                null)));

The following exception is thrown when the DbContext is instantiated:

System.InvalidOperationException: The parameter 'coordinate1' for the DbFunction 
'MyDbContext.DistanceInMeters(GeoCoordinate,GeoCoordinate)' has an invalid type 'GeoCoordinate'.
Ensure the parameter type can be mapped by the current provider.
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.
      ValidateDbFunctions(IModel model, IDiagnosticsLogger`1 logger)

It seems that HasDbFunction does not take value converters into consideration.

Note: This is being developed and tested in .NET 8.0 with EF Core 8.0.

jscarle commented 3 months ago

Seems related to https://github.com/dotnet/efcore/issues/28393

jscarle commented 2 months ago

I confirmed in a different project that there seems to be absolutely no way to configure a User Defined Function in such a way to make it possible to use parameters that are not a C# primitive type such as int, decimal, or string.

alandryz83 commented 2 months ago

Bye