EFCore执行自定义SQL时格式化错误:Input string was not in a correct format.
记录一下EFCore执行自定义SQL报System.FormatException异常的问题,这个异常可能是“Input string was not in a correct format.”,也可能是其它格式化异常,比如:System.ArgumentException:“Format of the initialization string does not conform to specification starting at index 0.”,总之就是格式化错误。
首先,说明一下,我这边使用的是.net6。
其次,我们如果要使用EFCore执行自定义的SQL,对于查询语句,不同版本的支持不同,比如我这里使用的.net6,它就没有提供使用EFCore执行自定义SQL的方法,但是.net6后续版本就有了,对于非查询语句,比如增删改,我们可以使用DbContext.Database.ExecuteSqlRaw
的方法和它的异步方法DbContext.Database.ExecuteSqlRawAsync
来来实现,但是需要注意字符串的格式化问题。
比如我执行这个SQL语句:
var sql = "INSERT INTO test(name,age,remark)VALUES('zhangsan',20,'remark:{ age is 20}')";
var result = await dbContext.Database.ExecuteSqlRawAsync(sql);
执行后报错:System.FormatException:“Input string was not in a correct format.”
通过查看源码,发现报错是在Microsoft.EntityFrameworkCore.Storage.Internal.RawSqlCommandBuilder
的Build
方法:
public class RawSqlCommandBuilder : IRawSqlCommandBuilder
{
public virtual RawSqlCommand Build(string sql, IEnumerable<object> parameters)
{
//省略部分代码...
foreach (var parameter in parameters)
{
if (parameter is DbParameter dbParameter)
{
if (string.IsNullOrEmpty(dbParameter.ParameterName))
{
dbParameter.ParameterName = _sqlGenerationHelper.GenerateParameterName(parameterNameGenerator.GenerateNext());
}
substitutions.Add(_sqlGenerationHelper.GenerateParameterName(dbParameter.ParameterName));
relationalCommandBuilder.AddRawParameter(dbParameter.ParameterName, dbParameter);
}
else
{
var parameterName = parameterNameGenerator.GenerateNext();
var substitutedName = _sqlGenerationHelper.GenerateParameterName(parameterName);
substitutions.Add(substitutedName);
relationalCommandBuilder.AddParameter(parameterName, substitutedName);
parameterValues.Add(parameterName, parameter);
}
}
// ReSharper disable once CoVariantArrayConversion
sql = string.Format(sql, substitutions.ToArray());
return new RawSqlCommand(
relationalCommandBuilder.Append(sql).Build(),
parameterValues);
}
}
从上面这段代码可以看到,它是在sql = string.Format(sql, substitutions.ToArray());
部分报错,这里做格式化,而我们的SQL语句中有花括号,自然就会报错了!
怎么解决,有三个方法。
1、使用string.Replace
替换掉花括号,因为string.Format
会将花括号当做替换的参数,比如:{0} {1}
,所以,我们要保留原有的花括号,可以对整个SQL语句执行以下替换,将SQL语句中的单花括号替换成两个花括号,两个花括号会在string.Format
的时候转义成一个,比如:
var sql = "INSERT INTO test(name,age,remark)VALUES('zhangsan',20,'remark:{ age is 20}')";
sql = sql.Replace("{", "{{").Replace("}", "}}");
var result = await dbContext.Database.ExecuteSqlRawAsync(sql);
2、使用参数化来实现,比如:
var connection = dbContext.Database.GetDbConnection();
var factory = DbProviderFactories.GetFactory(connection);
var nameParameter = factory.CreateParameter();
nameParameter.ParameterName = "name";
nameParameter.Value = "zhangsan";
var ageParameter = factory.CreateParameter();
ageParameter.ParameterName = "age";
ageParameter.Value = 20;
var remarkParameter = factory.CreateParameter();
remarkParameter.ParameterName = "remark";
remarkParameter.Value = "remark:{ age is 20 }";
var sql = "INSERT INTO test(name,age,remark)VALUES(@name,@age,@remark)";
var result = await dbContext.Database.ExecuteSqlRawAsync(sql);
3、直接使用格式化,其实从上面的代码上看,本质上也是参数化,我们想想Microsoft.EntityFrameworkCore.Storage.Internal.RawSqlCommandBuilder
的Build
方法为什么要做一个格式化?不就是给我们自定义格式化SQL使用的么,我们可以这么做:
var sql = "INSERT INTO test(name,age,remark)VALUES('{0}',{1},'{2}')";
var result = await dbContext.Database.ExecuteSqlRawAsync(sql, "zhangsan", 20, "remark:{age is 20}");
总结
其实,这个就是花括号导致的问题,但是我们不能说保存到数据库的字符串里面不能存在花括号,比如我们输入的文本、数学表达式、代码、JSON格式化的数据等都有可能出现花括号需要保存到数据库中。总之,知道为什么报错就好了,其实开发的时候尽量避免,无论哪种解决方法,都可以根据自己的情况定,是在不行,可以自行使用ADO.NET的方式来实现。