T3.0
T3.0

Reputation: 496

How do I map a MySql user defined function with ASP.NET EF Core Pomelo

I'm trying to map a custom MySQL function to a method on my dbcontext as shown in this article. As suggested in the article I've stubbed out a method with the same signature of my user-defined function.

public int CalcOffset(DateTime input_date, int id)
    => throw new InvalidOperationException();

protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDbFunction(typeof(DbContext).GetMethod(nameof(CalcOffset), new[] { typeof(DateTime), typeof(int) }),
            b =>
                {
                    b.HasName("fn_test");
                    b.HasParameter("input_date");
                    b.HasParameter("id");
                });
        }

The MySql function I'm attempting to map to:

CREATE DEFINER=`myuser`@`localhost` FUNCTION `fn_test`(input_date datetime, project_id int) RETURNS int(11)
BEGIN

RETURN 1;
END

finally I call the function via the context

var offset = _context.CalcOffset(DateTime.Now, id);

When I run the code above I'm getting the error message thrown by the function I stubbed out. However per the article the method should be mapped the database function, not the actual method:

The body of the CLR method is not important. The method will not be invoked client-side, unless EF Core can't translate its arguments. If the arguments can be translated, EF Core only cares about the method signature

Versions: ASP.NET Core 3.1 Pomelo Entity Framework Core 3.2 MySql 5.7

Upvotes: 1

Views: 1387

Answers (1)

lauxjpn
lauxjpn

Reputation: 5254

UDFs work fine in my tests. Here is a fully working console program:

using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
using Pomelo.EntityFrameworkCore.MySql.Storage;

namespace IssueConsoleTemplate
{
    public class IceCream
    {
        public int IceCreamId { get; set; }
        public string Name { get; set; }
    }

    public class Context : DbContext
    {
        public virtual DbSet<IceCream> IceCreams { get; set; }

        public int IncrementInteger(int value)
            => throw new InvalidOperationException();

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var connectionString = "server=127.0.0.1;port=3306;user=root;password=;database=So66857783";

            optionsBuilder.UseMySql(
                    connectionString,
                    options => options.CharSetBehavior(CharSetBehavior.NeverAppend)
                        .ServerVersion(ServerVersion.AutoDetect(connectionString)))
                .UseLoggerFactory(
                    LoggerFactory.Create(
                        configure => configure
                            .AddConsole()
                            .AddFilter(level => level >= LogLevel.Information)))
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<IceCream>(
                entity =>
                {
                    modelBuilder.HasDbFunction(
                        typeof(Context).GetMethod(nameof(IncrementInteger)),
                        b => b.HasName("udf_increment")
                            .HasParameter("value"));

                    entity.HasData(
                        new IceCream {IceCreamId = 1, Name = "Vanilla"},
                        new IceCream {IceCreamId = 2, Name = "Chocolate"},
                        new IceCream {IceCreamId = 3, Name = "Matcha"}
                    );
                });
        }
    }

    internal static class Program
    {
        private static void Main(string[] args)
        {
            using var context = new Context();

            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            SetupDatabase(context);

            var iceCreams = context.IceCreams
                .OrderBy(i => i.IceCreamId)
                .Select(
                i => new
                {
                    IceCreamId = i.IceCreamId,
                    IcrementedValue = context.IncrementInteger(i.IceCreamId)
                })
                .ToList();
            
            Trace.Assert(iceCreams.Count == 3);
            Trace.Assert(iceCreams[0].IceCreamId == 1);
            Trace.Assert(iceCreams[0].IcrementedValue == 2);
        }

        private static void SetupDatabase(DbContext context)
        {
            context.Database.OpenConnection();

            var connection = context.Database.GetDbConnection();
            using var command = connection.CreateCommand();

            command.CommandText = @"CREATE FUNCTION `udf_increment`(value int) RETURNS int
    DETERMINISTIC
BEGIN
    RETURN value + 1;
END";
            command.ExecuteNonQuery();
        }
    }
}

It generates the following SQL query:

SELECT `i`.`IceCreamId`, `udf_increment`(`i`.`IceCreamId`) AS `IcrementedValue`
FROM `IceCreams` AS `i`
ORDER BY `i`.`IceCreamId`

However, just calling the function without an entity context, as you do in your OP, will not work because you would just execute your function body, instead of letting EF Core translating an expression tree to SQL:

// This does not work, because Context.IncrementInteger(int) is being directly called
// here, instead of being part of an expression tree, that is then translated to SQL
// by EF Core.
var answer = context.IncrementInteger(41);
Trace.Assert(answer == 42);

Of course you can always execute some good old SQL yourself, if you need to:

// Executing the UDF using god old SQL.
// Use parameters for input values in the real world.
context.Database.OpenConnection();
using var command = context.Database.GetDbConnection().CreateCommand();
command.CommandText = @"SELECT `udf_increment`(41)";
var answerToEverything = (int)command.ExecuteScalar();

Trace.Assert(answerToEverything == 42);

Upvotes: 2

Related Questions