You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为EF Core添加自定义SQL转换:解决ExecuteSqlInterpolated的IN子句问题

在EF Core(Pomelo)中解决ExecuteSqlInterpolated的IN子句int[]类型映射问题

问题背景

使用ExecuteSqlInterpolatedAsync编写复杂查询时,IN子句传入int[]类型会抛出错误:

System.InvalidOperationException : The current provider doesn't have a store type mapping for properties of type 'int[]'

这是因为Pomelo MySQL提供程序未实现数组类型到SQL IN子句的自动转换,直接用ExecuteSqlRawAsync手动拼接SQL会导致复杂查询可读性差、维护困难,且ConfigureConventions中配置的类型映射对原生SQL参数不生效。

解决方案1:自定义DbCommandInterceptor拦截处理

通过拦截数据库命令,自动将数组类型参数拆分为多个单个参数,并替换SQL中的占位符,保持ExecuteSqlInterpolated的简洁语法同时实现参数化查询。

实现拦截器

public class ArrayParameterInterceptor : DbCommandInterceptor
{
    // 同步非查询命令处理(如INSERT/UPDATE/DELETE)
    public override InterceptionResult<int> NonQueryExecuting(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<int> result)
    {
        ProcessArrayParameters(command);
        return base.NonQueryExecuting(command, eventData, result);
    }

    // 异步非查询命令处理
    public override ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        ProcessArrayParameters(command);
        return base.NonQueryExecutingAsync(command, eventData, result, cancellationToken);
    }

    // 同步查询命令处理(如SELECT)
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<DbDataReader> result)
    {
        ProcessArrayParameters(command);
        return base.ReaderExecuting(command, eventData, result);
    }

    // 异步查询命令处理
    public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<DbDataReader> result,
        CancellationToken cancellationToken = default)
    {
        ProcessArrayParameters(command);
        return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
    }

    private void ProcessArrayParameters(DbCommand command)
    {
        // 筛选出数组类型的参数
        var arrayParams = command.Parameters.Cast<DbParameter>()
            .Where(p => p.Value is Array array && array.Length > 0)
            .ToList();

        foreach (var param in arrayParams)
        {
            var array = (Array)param.Value;
            var newParamNames = new List<string>();

            // 为数组每个元素创建单独参数
            for (int i = 0; i < array.Length; i++)
            {
                var newParam = command.CreateParameter();
                newParam.ParameterName = $"@{param.ParameterName}_{i}";
                newParam.Value = array.GetValue(i);
                newParam.DbType = param.DbType;
                command.Parameters.Add(newParam);
                newParamNames.Add(newParam.ParameterName);
            }

            // 替换SQL中的数组参数占位符为多个参数的集合
            command.CommandText = command.CommandText.Replace(
                param.ParameterName,
                $"({string.Join(", ", newParamNames)})");

            // 移除原数组参数
            command.Parameters.Remove(param);
        }
    }
}

注册拦截器

在DbContext的OnConfiguring方法中添加拦截器:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseMySql(
            "your_connection_string", 
            ServerVersion.AutoDetect("your_connection_string"))
        .AddInterceptors(new ArrayParameterInterceptor());
}

使用方式

直接保留ExecuteSqlInterpolated的写法即可,拦截器会自动处理数组参数:

var userIds = new int[] { 1, 3, 5 };
await context.Database.ExecuteSqlInterpolatedAsync(
    $"DELETE FROM Users WHERE Id IN ({userIds})");

解决方案2:封装自定义DbContext方法

如果不想用拦截器,可以在DbContext中封装一个自定义方法,手动处理数组参数的转换,同时保持插值语法的便利性。

实现自定义方法

public async Task<int> ExecuteSqlInterpolatedWithArraysAsync(FormattableString query)
{
    var arguments = query.GetArguments();
    var sqlTemplate = query.ToString();
    var parameters = new List<MySqlParameter>();

    for (int i = 0; i < arguments.Length; i++)
    {
        var arg = arguments[i];
        if (arg is int[] intArray)
        {
            // 处理int[]类型参数,生成多个单个参数
            var paramNames = new List<string>();
            for (int j = 0; j < intArray.Length; j++)
            {
                var paramName = $"param_{i}_{j}";
                paramNames.Add($"@{paramName}");
                parameters.Add(new MySqlParameter(paramName, intArray[j]));
            }
            // 替换原占位符
            sqlTemplate = sqlTemplate.Replace($"{{{i}}}", $"({string.Join(", ", paramNames)})");
        }
        else
        {
            // 处理普通参数
            var paramName = $"param_{i}";
            sqlTemplate = sqlTemplate.Replace($"{{{i}}}", $"@{paramName}");
            parameters.Add(new MySqlParameter(paramName, arg ?? DBNull.Value));
        }
    }

    return await Database.ExecuteSqlRawAsync(sqlTemplate, parameters.ToArray());
}

使用方式

调用自定义方法替代原生ExecuteSqlInterpolatedAsync:

var userIds = new int[] { 2, 4, 6 };
await context.ExecuteSqlInterpolatedWithArraysAsync(
    $"UPDATE Users SET IsActive = 0 WHERE Id IN ({userIds})");

说明

ConfigureConventions中配置的类型映射仅针对实体模型的属性,无法作用于原生SQL的参数传递,因此之前的尝试无效。上述两种方案均通过参数化方式处理数组,避免了SQL注入风险,同时保持了查询的可读性。

内容的提问来源于stack exchange,提问作者Issung

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 11:45:29