用于自定义 MySQL 函数、文字与列的实体框架核心

发布于 2025-01-11 11:07:03 字数 8476 浏览 0 评论 0原文

我正在尝试在 EF Core 3.1、.NET Core 3.1 中映射 MySQL 函数。更具体地说,我正在尝试映射 params object[] 类型的参数。

下面的示例代码使用 CONCAT() 进行说明。

经过一番摆弄 HasTranslation() 和自定义 RelationalTypeMapping 后,它可以使用文字。这意味着它会生成正确的 SQL 查询并将成功执行该查询。

一旦我传递一个列,它就无法生成正确的 SQL 查询,并且在执行时会抛出 NotSupportedException

在此阶段,我试图解决两个问题:

  1. 传递任何列都永远不会影响我的函数配置,进而影响我的翻译。

  2. 传递任何列都会导致其他参数出现各种(且奇怪的)行为。在解决#1 后,这个问题可能很容易解决,但它仍然很有趣。 (即 "asdf"(object)"asdf"

用于调试的 ToSql() 扩展方法来自 https://stackoverflow.com/a/67901042/1048799

输出在 代码。

<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="MySql.EntityFrameworkCore" Version="3.1.17" />

EfTestService.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace EfTest
{
    public interface IEfTestService
    {
        Task RunAsync();
    }

    public class EfTestService : IEfTestService
    {
        private readonly ILogger<IEfTestService> logger;
        private readonly EfTestDbContext dbContext;

        public EfTestService(ILogger<IEfTestService> logger, EfTestDbContext dbContext)
        {
            this.logger = logger;
            this.dbContext = dbContext;
        }

        public async Task RunAsync()
        {
            var concat0 = dbContext.TestRecords.Select(r => dbContext.Concat(0, 1, "2", 3.0d, 4.0m, "five"));
            var concat0Sql = concat0.ToSql();
            //var concat0Result = await concat0.FirstAsync();
            Console.WriteLine(concat0Sql);

            /*
             * works as expected
             * 
                SELECT `CONCAT`(0, 1, '2', 3, 4.0, 'five')
                FROM `TestRecords` AS `t`
            */

            var concat1 = dbContext.TestRecords.Select(r => dbContext.Concat(r.TestRecordId));
            var concat1Sql = concat1.ToSql();
            //var concat1Result = await concat1.FirstAsync();
            Console.WriteLine(concat1Sql);

            /* 
             * not CONCAT
             * 
                SELECT `t`.`TestRecordId`
                FROM `TestRecords` AS `t`
            */

            var concat2 = dbContext.TestRecords.Select(r => dbContext.Concat(r.TestRecordId, 0.1)); 
            var concat2Sql = concat2.ToSql();
            //var concat2Result = await concat2.FirstAsync();
            Console.WriteLine(concat2Sql);

            /* 
             * not CONCAT, 0.1 is included
             * 
                SELECT `t`.`TestRecordId`, 0.1
                FROM `TestRecords` AS `t`
            */

            var concat3 = dbContext.TestRecords.Select(r => dbContext.Concat(r.TestRecordId, "asdf")); 
            var concat3Sql = concat3.ToSql();
            //var concat3Result = await concat3.FirstAsync();
            Console.WriteLine(concat3Sql);

            /* 
             * not CONCAT, asdf is NOT included
             * 
                SELECT `t`.`TestRecordId`
                FROM `TestRecords` AS `t`
            */

            var concat4 = dbContext.TestRecords.Select(r => dbContext.Concat(r.TestRecordId, (object)"asdf")); 
            var concat4Sql = concat4.ToSql();
            //var concat4Result = await concat4.FirstAsync();
            Console.WriteLine(concat4Sql);

            /* 
             * not CONCAT, asdf is included
             * 
                SELECT `t`.`TestRecordId`, 'asdf'
                FROM `TestRecords` AS `t`
            */


        }
    }
}

EfTestDbContext.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using System;
using System.Linq;
using System.Linq.Expressions;

namespace EfTest
{
    public class EfTestDbContext : DbContext
    {
        public EfTestDbContext(DbContextOptions options) : base(options) { }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder
                .HasDbFunction(() => Concat(default))
                .HasTranslation(expressions =>
                { /* breakpoint, hit when passing only literals */
                    if (expressions.First() is SqlConstantExpression expression)
                    {
                        if (expression.Value is object[] @params)
                        {
                            var args = @params.Select(p => new SqlConstantExpression(Expression.Constant(p), ObjectTypeMapping.Instance));

                            return SqlFunctionExpression.Create("`CONCAT`", args, typeof(string), null);
                        }
                    }

                    throw new InvalidOperationException();
                })
                .HasParameter("vals").Metadata.TypeMapping = RelationalTypeMapping.NullMapping;
        }

        [DbFunction("CONCAT")]
        public string Concat(params object[] vals)
            => throw new NotSupportedException(); /* thown at execution when passing a column */

        public DbSet<TestRecord> TestRecords { get; set; }
    }

    public class TestRecord
    {
        public int TestRecordId { get; set; }
    }
}

ObjectTypeMapping.cs

using Microsoft.EntityFrameworkCore.Storage;
using System;
using System.Text.RegularExpressions;

namespace EfTest
{
    public class ObjectTypeMapping : RelationalTypeMapping
    {
        public static readonly ObjectTypeMapping Instance = new ObjectTypeMapping();

        public ObjectTypeMapping() : base("object", typeof(object), System.Data.DbType.Object, true) { }

        protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
            => throw new NotImplementedException();

        public override string GenerateSqlLiteral(object value)
        {
            if (value is string strValue)
            {
                strValue = Regex.Replace(strValue, @"([\\])", @"\$1");

                if (!strValue.Contains("'"))
                {
                    return $"'{strValue}'";
                }
                else if (!strValue.Contains('"'))
                {
                    return $"\"{strValue}\"";
                }
                else
                {
                    strValue = Regex.Replace(strValue, "(['\"])", @"\$1");

                    return $"'{strValue}'";
                }
            }

            return base.GenerateSqlLiteral(value);
        }
    }
}

Program.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;

namespace EfTest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            IHost host = null;

            try
            {
                host = CreateHostBuilder(null).Build();

                var efTestService = ActivatorUtilities.GetServiceOrCreateInstance<IEfTestService>(host.Services);
                
                await host.StartAsync();
                await efTestService.RunAsync();
                await host.WaitForShutdownAsync();
            }
            finally
            {
                host?.Dispose();
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args)
        {
            var builder = Host.CreateDefaultBuilder()
                .ConfigureServices((context, services) =>
                {
                    services.AddDbContext<EfTestDbContext>(
                        options => options.UseMySQL(
                            context.Configuration.GetConnectionString("DefaultConnection")
                        )
                    );

                    services.AddSingleton<IEfTestService, EfTestService>();
                });

            return builder;
        }
    }
}

I'm attempting to map a MySQL function in EF Core 3.1, .NET Core 3.1. More specifically, I'm trying to map a parameter of type params object[].

The example code below uses CONCAT() for illustration purposes.

After some fiddling around with HasTranslation() and a custom RelationalTypeMapping it works with literals. Meaning it produces the correct SQL query and will execute that query successfully.

As soon as I pass a column it fails to produce the correct SQL query and will throw the NotSupportedException upon execution.

There are two issues I'm trying to resolve at this stage:

  1. Passing any column will never hit my function configuration, and by extension, my translation.

  2. Passing any column results in various (and weird) behaviors with other parameters. This could likely be easily resolved after solving #1, but it's interesting, nonetheless. (i.e. "asdf" vs (object)"asdf")

The ToSql() extension method used for debugging is from https://stackoverflow.com/a/67901042/1048799

The output is commented in the code.

Packages

<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="MySql.EntityFrameworkCore" Version="3.1.17" />

EfTestService.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace EfTest
{
    public interface IEfTestService
    {
        Task RunAsync();
    }

    public class EfTestService : IEfTestService
    {
        private readonly ILogger<IEfTestService> logger;
        private readonly EfTestDbContext dbContext;

        public EfTestService(ILogger<IEfTestService> logger, EfTestDbContext dbContext)
        {
            this.logger = logger;
            this.dbContext = dbContext;
        }

        public async Task RunAsync()
        {
            var concat0 = dbContext.TestRecords.Select(r => dbContext.Concat(0, 1, "2", 3.0d, 4.0m, "five"));
            var concat0Sql = concat0.ToSql();
            //var concat0Result = await concat0.FirstAsync();
            Console.WriteLine(concat0Sql);

            /*
             * works as expected
             * 
                SELECT `CONCAT`(0, 1, '2', 3, 4.0, 'five')
                FROM `TestRecords` AS `t`
            */

            var concat1 = dbContext.TestRecords.Select(r => dbContext.Concat(r.TestRecordId));
            var concat1Sql = concat1.ToSql();
            //var concat1Result = await concat1.FirstAsync();
            Console.WriteLine(concat1Sql);

            /* 
             * not CONCAT
             * 
                SELECT `t`.`TestRecordId`
                FROM `TestRecords` AS `t`
            */

            var concat2 = dbContext.TestRecords.Select(r => dbContext.Concat(r.TestRecordId, 0.1)); 
            var concat2Sql = concat2.ToSql();
            //var concat2Result = await concat2.FirstAsync();
            Console.WriteLine(concat2Sql);

            /* 
             * not CONCAT, 0.1 is included
             * 
                SELECT `t`.`TestRecordId`, 0.1
                FROM `TestRecords` AS `t`
            */

            var concat3 = dbContext.TestRecords.Select(r => dbContext.Concat(r.TestRecordId, "asdf")); 
            var concat3Sql = concat3.ToSql();
            //var concat3Result = await concat3.FirstAsync();
            Console.WriteLine(concat3Sql);

            /* 
             * not CONCAT, asdf is NOT included
             * 
                SELECT `t`.`TestRecordId`
                FROM `TestRecords` AS `t`
            */

            var concat4 = dbContext.TestRecords.Select(r => dbContext.Concat(r.TestRecordId, (object)"asdf")); 
            var concat4Sql = concat4.ToSql();
            //var concat4Result = await concat4.FirstAsync();
            Console.WriteLine(concat4Sql);

            /* 
             * not CONCAT, asdf is included
             * 
                SELECT `t`.`TestRecordId`, 'asdf'
                FROM `TestRecords` AS `t`
            */


        }
    }
}

EfTestDbContext.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using System;
using System.Linq;
using System.Linq.Expressions;

namespace EfTest
{
    public class EfTestDbContext : DbContext
    {
        public EfTestDbContext(DbContextOptions options) : base(options) { }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder
                .HasDbFunction(() => Concat(default))
                .HasTranslation(expressions =>
                { /* breakpoint, hit when passing only literals */
                    if (expressions.First() is SqlConstantExpression expression)
                    {
                        if (expression.Value is object[] @params)
                        {
                            var args = @params.Select(p => new SqlConstantExpression(Expression.Constant(p), ObjectTypeMapping.Instance));

                            return SqlFunctionExpression.Create("`CONCAT`", args, typeof(string), null);
                        }
                    }

                    throw new InvalidOperationException();
                })
                .HasParameter("vals").Metadata.TypeMapping = RelationalTypeMapping.NullMapping;
        }

        [DbFunction("CONCAT")]
        public string Concat(params object[] vals)
            => throw new NotSupportedException(); /* thown at execution when passing a column */

        public DbSet<TestRecord> TestRecords { get; set; }
    }

    public class TestRecord
    {
        public int TestRecordId { get; set; }
    }
}

ObjectTypeMapping.cs

using Microsoft.EntityFrameworkCore.Storage;
using System;
using System.Text.RegularExpressions;

namespace EfTest
{
    public class ObjectTypeMapping : RelationalTypeMapping
    {
        public static readonly ObjectTypeMapping Instance = new ObjectTypeMapping();

        public ObjectTypeMapping() : base("object", typeof(object), System.Data.DbType.Object, true) { }

        protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
            => throw new NotImplementedException();

        public override string GenerateSqlLiteral(object value)
        {
            if (value is string strValue)
            {
                strValue = Regex.Replace(strValue, @"([\\])", @"\$1");

                if (!strValue.Contains("'"))
                {
                    return 
quot;'{strValue}'";
                }
                else if (!strValue.Contains('"'))
                {
                    return 
quot;\"{strValue}\"";
                }
                else
                {
                    strValue = Regex.Replace(strValue, "(['\"])", @"\$1");

                    return 
quot;'{strValue}'";
                }
            }

            return base.GenerateSqlLiteral(value);
        }
    }
}

Program.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;

namespace EfTest
{
    class Program
    {
        static async Task Main(string[] args)
        {
            IHost host = null;

            try
            {
                host = CreateHostBuilder(null).Build();

                var efTestService = ActivatorUtilities.GetServiceOrCreateInstance<IEfTestService>(host.Services);
                
                await host.StartAsync();
                await efTestService.RunAsync();
                await host.WaitForShutdownAsync();
            }
            finally
            {
                host?.Dispose();
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args)
        {
            var builder = Host.CreateDefaultBuilder()
                .ConfigureServices((context, services) =>
                {
                    services.AddDbContext<EfTestDbContext>(
                        options => options.UseMySQL(
                            context.Configuration.GetConnectionString("DefaultConnection")
                        )
                    );

                    services.AddSingleton<IEfTestService, EfTestService>();
                });

            return builder;
        }
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

触ぅ动初心 2025-01-18 11:07:03

传递任何列都不会影响我的函数配置,进而影响我的翻译。

不可能将 params object[] 直接映射到任何 EF 类。

每个参数都必须明确定义类型和计数。

在映射之前展开输入的变化并转换为适当的 EF 结构/类。

是的,大量的数据和映射类。

Passing any column will never hit my function configuration, and by extension, my translation.

It is impossible to map params object[] to any EF class directly.

Each parameter must be well defined with a type and count.

Unwind the variations of input and cast to the appropriate EF structure/class prior to mapping.

Yes, lots of data and mapping classes.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文