【问题标题】:EF Core decimal precision for Always Encrypted columnAlways Encrypted 列的 EF Core 十进制精度
【发布时间】:2026-02-06 05:30:01
【问题描述】:

您好,我有设置始终加密功能的 SQL 服务器,我还设置了 EF 以使用始终加密的列,但是当我尝试添加/更新时,对于 Db 操作,我使用 DbContext,在我的 Db 中输入我得到跟随错误:

Operand type *: decimal(1,0) encrypted with (encryption_type = 'DETERMINISTIC', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256', column_encryption_key_name = '****', column_encryption_key_database_name = '****') is incompatible with decimal(6,2) encrypted with (encryption_type = 'DETERMINISTIC', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256', column_encryption_key_name = '*****', column_encryption_key_database_name = '****')

我使用的模型

public class Model
{
    /// <summary>
    /// Payment method name
    /// </summary>
    [Column(TypeName = "nvarchar(MAX)")]
    public string Name { get; set; }

    /// <summary>
    /// Payment method description
    /// </summary>
    [Column(TypeName = "nvarchar(MAX)")]
    public string Description { get; set; }

    /// <summary>
    /// Fee charges for using payment method
    /// </summary>
    [Column(TypeName = "decimal(6,2)")]
    public decimal Fee { get; set; }
}

我也尝试在 OnModelCreating 方法中指定十进制格式

 builder.Entity<Model>().Property(x => x.Fee).HasColumnType("decimal(6,2)");

我错过了什么? 感谢您的任何建议

【问题讨论】:

  • 如果您查看this comment,我认为它给出了一个提示:“如果您更新单行,则不会出现这种情况的原因是我们在这种情况下生成了完全不同的 SQL” t 依赖于一个表变量。精度和比例实际上从未在参数中设置,因此我们让提供者(SqlClient)在这种情况下根据传递的值来决定使用哪些参数方面。不幸的是,在加密列的情况下这样做是错误的(并且可以说总是——计划缓存污染)。
  • another issue 对 Always Encrypted 的跟踪支持。似乎没有发生太多事情,但如果 EF Core 中的事情仍然如此,那么上述问题将是一个问题。

标签: sql sql-server entity-framework always-encrypted


【解决方案1】:

我和我的同事使用 DiagnosticSource 找到了解决该问题的方法。

你必须知道:

  • Entity Framework Core 将自身挂接到 DiagnosticSource。
  • DiagnosticSource 使用观察者模式通知其观察者。

这个想法是填充命令对象(由 EFCore 创建)的“Precision”和“Scale”字段,这样对 Sql 的调用将包含正确执行查询所需的所有信息。

首先,创建监听器:

namespace YOUR_NAMESPACE_HERE
{
    public class EfGlobalListener : IObserver<DiagnosticListener>
    {
        private readonly CommandInterceptor _interceptor = new CommandInterceptor();

        public void OnCompleted()
        {
        }
        public void OnError(Exception error)
        {
        }
        public void OnNext(DiagnosticListener value)
        {
            if (value.Name == DbLoggerCategory.Name)
            {
                value.Subscribe(_interceptor);
            }
        }
    }
}

CommandInterceptor 在哪里:

namespace YOUR_NAMESPACE_HERE
{
    public class CommandInterceptor : IObserver<KeyValuePair<string, object>>
    {
        // This snippet of code is only as example, you could maybe use Reflection to retrieve Field mapping instead of using Dictionary
        private Dictionary<string, (byte Precision, byte Scale)> _tableMapping = new Dictionary<string, (byte Precision, byte Scale)>
        {
            { "Table1.DecimalField1", (18, 2) },
            { "Table2.DecimalField1", (12, 6) },
            { "Table2.DecimalField2", (10, 4) },
        };

        public void OnCompleted()
        {
        }
        public void OnError(Exception error)
        {
        }
        public void OnNext(KeyValuePair<string, object> value)
        {
            if (value.Key == RelationalEventId.CommandExecuting.Name)
            {
                // After that EF Core generates the command to send to the DB, this method will be called

                // Cast command object
                var command = ((CommandEventData)value.Value).Command;

                // command.CommandText -> contains SQL command string
                // command.Parameters -> contains all params used in sql command

                // ONLY FOR EXAMPLE PURPOSES
                // This code may contain errors.
                // It was written only as an example.

                string table = null;
                string[] columns = null;
                string[] parameters = null;

                var regex = new Regex(@"^INSERT INTO \[(.+)\] \((.*)\)|^VALUES \((.*)\)|UPDATE \[(.*)\] SET (.*)$", RegexOptions.Multiline);
                var matches = regex.Matches(command.CommandText);

                foreach (Match match in matches)
                {
                    if(match.Groups[1].Success)
                    {
                        // INSERT - TABLE NAME
                        table = match.Groups[1].Value;
                    }
                    if (match.Groups[2].Success)
                    {
                        // INSERT - COLS NAMES
                        columns = match.Groups[2].Value.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(c => c.Replace("[", string.Empty).Replace("]", string.Empty).Trim()).ToArray();
                    }
                    if (match.Groups[3].Success)
                    {
                        // INSERT - PARAMS VALUES
                        parameters = match.Groups[3].Value.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(c => c.Trim()).ToArray();
                    }
                    if (match.Groups[4].Success)
                    {
                        // UPDATE - TABLE NAME
                        table = match.Groups[4].Value;
                    }
                    if (match.Groups[5].Success)
                    {
                        // UPDATE - COLS/PARAMS NAMES/VALUES
                        var colParams = match.Groups[5].Value.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(p => p.Replace("[", string.Empty).Replace("]", string.Empty).Trim()).ToArray();
                        columns = colParams.Select(cp => cp.Split('=', StringSplitOptions.RemoveEmptyEntries)[0].Trim()).ToArray();
                        parameters = colParams.Select(cp => cp.Split('=', StringSplitOptions.RemoveEmptyEntries)[1].Trim()).ToArray();
                    }
                }

                // After taking all the necessary information from the sql command
                // we can add Precision and Scale to all decimal parameters
                foreach (var item in command.Parameters.OfType<SqlParameter>().Where(p => p.DbType == DbType.Decimal))
                {
                    var index = Array.IndexOf<string>(parameters, item.ParameterName);
                    var columnName = columns.ElementAt(index);

                    var key = $"{table}.{columnName}";

                    // Add Precision and Scale, that fix our problems w/ always encrypted columns
                    item.Precision = _tableMapping[key].Precision;
                    item.Scale = _tableMapping[key].Scale;
                }
            }
        }
    }
}

最后在 Startup.cs 中添加下面这行代码来注册监听器:

DiagnosticListener.AllListeners.Subscribe(new EfGlobalListener());

【讨论】:

    【解决方案2】:

    遇到了同样的问题。 调整了 @SteeBono 拦截器以处理包含多个语句的命令:

        public class AlwaysEncryptedDecimalParameterInterceptor : DbCommandInterceptor, IObserver<KeyValuePair<string, object>>
    {
        private Dictionary<string, (SqlDbType DataType, byte? Precision, byte? Scale)> _decimalColumnSettings =
            new Dictionary<string, (SqlDbType DataType, byte? Precision, byte? Scale)>
            {
                // MyTableDecimal
                { $"{nameof(MyTableDecimal)}.{nameof(MyTableDecimal.MyDecimalColumn)}", (SqlDbType.Decimal, 18, 6) },
    
                // MyTableMoney
                { $"{nameof(MyTableMoney)}.{nameof(MyTableMoney.MyMoneyColumn)}", (SqlDbType.Money, null, null) },
    
            };
    
        public void OnCompleted()
        {
        }
    
        public void OnError(Exception error)
        {
        }
    
        // After that EF Core generates the command to send to the DB, this method will be called
        public void OnNext(KeyValuePair<string, object> value)
        {
            if (value.Key == RelationalEventId.CommandExecuting.Name)
            {
                System.Data.Common.DbCommand command = ((CommandEventData)value.Value).Command;
    
                Regex regex = new Regex(@"INSERT INTO \[(.+)\] \((.*)\)(\r\n|\r|\n)+VALUES \(([^;]*)\);|UPDATE \[(.*)\] SET (.*)|MERGE \[(.+)\] USING \((\r\n|\r|\n)+VALUES \(([^A]*)\) AS \w* \((.*)\)");
                MatchCollection matches = regex.Matches(command.CommandText);
    
                foreach (Match match in matches)
                {
                    (string TableName, string[] Columns, string[] Params) commandComponents = GetCommandComponents(match);
                    int countOfColumns = commandComponents.Columns.Length;
    
                    // After taking all the necessary information from the sql command
                    // we can add Precision and Scale to all decimal parameters and set type for Money ones
                    for (int index = 0; index < commandComponents.Params.Length; index++)
                    {
                        SqlParameter decimalSqlParameter = command.Parameters.OfType<SqlParameter>()
                            .FirstOrDefault(p => commandComponents.Params[index] == p.ParameterName);
    
                        if (decimalSqlParameter == null)
                        {
                            continue;
                        }
    
                        string columnName = commandComponents.Columns.ElementAt(index % countOfColumns);
    
                        string settingKey = $"{commandComponents.TableName}.{columnName}";
                        if (_decimalColumnSettings.ContainsKey(settingKey))
                        {
                            (SqlDbType DataType, byte? Precision, byte? Scale) settings = _decimalColumnSettings[settingKey];
                            decimalSqlParameter.SqlDbType = settings.DataType;
    
                            if (settings.Precision.HasValue)
                            {
                                decimalSqlParameter.Precision = settings.Precision.Value;
                            }
    
                            if (settings.Scale.HasValue)
                            {
                                decimalSqlParameter.Scale = settings.Scale.Value;
                            }
                        }
                    }
                }
            }
        }
    
        private (string TableName, string[] Columns, string[] Params) GetCommandComponents(Match match)
        {
            string tableName = null;
            string[] columns = null;
            string[] parameters = null;
    
            // INSERT
            if (match.Groups[1].Success)
            {
                tableName = match.Groups[1].Value;
    
                columns = match.Groups[2].Value.Split(",", StringSplitOptions.RemoveEmptyEntries)
                    .Select(c => c.Replace("[", string.Empty)
                    .Replace("]", string.Empty)
                    .Trim()).ToArray();
    
                parameters = match.Groups[4].Value
                    .Split(",", StringSplitOptions.RemoveEmptyEntries)
                    .Select(c => c.Trim()
                        .Replace($"),{Environment.NewLine}(", string.Empty)
                        .Replace("(", string.Empty)
                        .Replace(")", string.Empty))
                    .ToArray();
    
                return (
                    TableName: tableName,
                    Columns: columns,
                    Params: parameters);
            }
    
            // UPDATE
            if (match.Groups[5].Success)
            {
                tableName = match.Groups[5].Value;
                string[] colParams = match.Groups[6].Value.Split(",", StringSplitOptions.RemoveEmptyEntries)
                    .Select(p => p.Replace("[", string.Empty).Replace("]", string.Empty).Trim())
                    .ToArray();
    
                columns = colParams.Select(cp => cp.Split('=', StringSplitOptions.RemoveEmptyEntries)[0].Trim()).ToArray();
                parameters = colParams.Select(cp => cp.Split('=', StringSplitOptions.RemoveEmptyEntries)[1].Trim()).ToArray();
    
                return (
                    TableName: tableName,
                    Columns: columns,
                    Params: parameters);
            }
    
            // MERGE
            if (match.Groups[7].Success)
            {
                tableName = match.Groups[7].Value;
                parameters = match.Groups[9].Value.Split(",", StringSplitOptions.RemoveEmptyEntries)
                    .Select(c => c.Trim()
                        .Replace($"),{Environment.NewLine}(", string.Empty)
                        .Replace("(", string.Empty)
                        .Replace(")", string.Empty))
                    .ToArray();
    
                columns = match.Groups[10].Value.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(c => c.Replace("[", string.Empty).Replace("]", string.Empty).Trim()).ToArray();
    
                return (
                    TableName: tableName,
                    Columns: columns,
                    Params: parameters);
            }
    
            throw new Exception($"{nameof(AlwaysEncryptedDecimalParameterInterceptor)} was not able to parse the command");
        }
    }
    

    【讨论】:

    • 更新了批量插入和合并命令支持