实体框架核心3.1存储过程可选参数不起作用 编辑:2020年8月24日已解决的原因 FromSqlRaw方法 FromSqlInterpolated

问题描述

[已解决](请参阅结尾)

我一天中的大部分时间都在寻找一种没有运气的解决方案。

  • EF Core 3.1
  • Visual Studio 2019
  • sql Server 2017
  • .Net Core 3.1

我一直无法找到一种解决方案,该解决方案可以使用可选参数从.Net Core 3.1调用存储过程。我已经阅读了数十篇从SO到博客文章,但没有任何效果

这是我的C#代码,用于通过所有尝试来完成此工作的方法调用存储过程:

public List<BudgetWorkflowStatusByDepartment> GetBudgetWorkflowStatusByDepartment(int? companyId,int? departmentId,int? locationId,int? sublocationId)
{
        //var compId = new sqlParameter("@CompanyId",companyId);
        //compId.Value = (object)companyId ?? sqlInt32.Null;

        //var deptId = new sqlParameter("@DepartmentId",departmentId);
        //deptId.Value = (object)departmentId ?? sqlInt32.Null;

        //var Locid = new sqlParameter("@LocationId",locationId);
        //Locid.Value = (object)locationId ?? sqlInt32.Null;

        //var subId = new sqlParameter("@SubLocationId",sublocationId);
        //subId.Value = (object)sublocationId ?? sqlInt32.Null;

        var compId = new sqlParameter("@CompanyID",companyId);
        compId.Value = (object)companyId ?? dbnull.Value;

        var deptId = new sqlParameter("@DepartmentID",departmentId);
        deptId.Value = (object)departmentId ?? dbnull.Value;

        var Locid = new sqlParameter("@LocationID",locationId);
        Locid.Value = (object)locationId ?? dbnull.Value;

        var subId = new sqlParameter("@SubLocationID",sublocationId);
        subId.Value = (object)sublocationId ?? dbnull.Value;

        //var result = context.BudgetWorkflowStatusByDepartment
        //    .FromsqlRaw("EXEC mis.BudgetWorkflowStatusByDepartment @CompanyID,@DepartmentID,@LocationID,@SubLocationID",compId,deptId,Locid,subId).ToList();

        //var result = context.BudgetWorkflowStatusByDepartment
        //    .FromsqlRaw("EXEC mis.BudgetWorkflowStatusByDepartment {0},{1},{2},{3}",companyId,departmentId,locationId,companyId).ToList();

        // Error Must declare the scalar variable "@CompanyID".
        //var result = context.BudgetWorkflowStatusByDepartment
        //    .FromsqlRaw($"EXEC mis.BudgetWorkflowStatusByDepartment @CompanyID={compId},@DepartmentID={deptId},@LocationID={Locid},@SubLocationID={subId}").ToList();

        // Error Must declare the scalar variable "@CompanyID".
        var result = context.BudgetWorkflowStatusByDepartment
            .FromsqlRaw($"EXEC mis.BudgetWorkflowStatusByDepartment {compId},{deptId},{Locid},{subId}").ToList();

        //var result = context.BudgetWorkflowStatusByDepartment
        //        .FromsqlInterpolated($"EXEC mis.BudgetWorkflowStatusByDepartment @CompanyID={compId},@SubLocationID={subId}").ToList();

        return result;
}

这些尝试均无效。错误范围为:

数据为空。不能在Null值上调用方法属性

...到...

必须声明标量变量“ @compId”。

...在我尝试了一些很可能是不正确的尝试以使其正常工作时遇到的其他错误

这是存储过程本身的签名,表明所有参数都可以为空。

ALTER PROCEDURE [mis].[BudgetWorkflowStatusByDepartment] 
    (@CompanyID int = null,@DepartmentID int = null,@LocationID int = null,@SubLocationID int = null)

令人沮丧的是,此存储过程在带或不带参数的sql Server中都可以在LinqPad中使用。而且它仅在使用所有参数的情况下在我的代码中起作用,但是如果任何参数为null,则将引发错误

对我如何使功能与可选参数一起使用的任何见解,均表示赞赏。

编辑:2020年8月24日

基于新建议,我尝试了这些建议,由于某些原因它们仍然无法使用。

我已经使用所有四个参数直接在SSMS中测试了该存储过程,并且它可以正常工作。然后我一个一个删除了最后一个参数,并在删除每个参数时对其进行了测试,它可以返回正确的数据。

当我使用Api和对该方法的服务调用进行相同的测试时,四个参数有效,三个参数有效,但是2个参数,1个参数和无参数,不起作用!我不明白为什么不这么做。

这是我的新尝试:


            /// New attempts as of: 8/24/2020
            // Error Data is Null. This method or property cannot be called on Null values.
            //var compId = new sqlParameter("@CompanyID",companyId);
            //compId.Value = (object)companyId ?? dbnull.Value;

            //var deptId = new sqlParameter("@DepartmentID",departmentId);
            //deptId.Value = (object)departmentId ?? dbnull.Value;

            //var Locid = new sqlParameter("@LocationID",locationId);
            //Locid.Value = (object)locationId ?? dbnull.Value;

            //var subId = new sqlParameter("@SubLocationID",sublocationId);
            //subId.Value = (object)sublocationId ?? dbnull.Value;

            //var result = context.BudgetWorkflowStatusByDepartment
            //        .FromsqlInterpolated($"EXEC mis.BudgetWorkflowStatusByDepartment {compId},{subId}").ToList();

            // Error Data is Null. This method or property cannot be called on Null values.
            
            //var result = context.BudgetWorkflowStatusByDepartment
            //        .FromsqlInterpolated($"EXEC mis.BudgetWorkflowStatusByDepartment {companyId},{departmentId},{locationId},{sublocationId}").ToList();


            //var compId = new sqlParameter("@CompanyID",companyId) { IsNullable = true };
            //compId.Value = (object)companyId ?? dbnull.Value;

            //var deptId = new sqlParameter("@DepartmentID",departmentId) { IsNullable = true };
            //deptId.Value = (object)departmentId ?? dbnull.Value;

            //var Locid = new sqlParameter("@LocationID",locationId) { IsNullable = true };
            //Locid.Value = (object)locationId ?? dbnull.Value;

            //var subId = new sqlParameter("@SubLocationID",sublocationId) { IsNullable = true };
            //subId.Value = (object)sublocationId ?? dbnull.Value;

            //// Error Data is Null. This method or property cannot be called on Null values.
            //var result = context.BudgetWorkflowStatusByDepartment
            //                    .FromsqlRaw("EXEC mis.BudgetWorkflowStatusByDepartment @CompanyID = @CompanyID,@DepartmentID = @DepartmentID,@LocationID = @LocationID,@SubLocationID = @SubLocationID",subId)
            //                    .ToList();

            var compId = new sqlParameter("@CompanyID",companyId) { IsNullable = true };
            compId.Value = (object)companyId ?? dbnull.Value;

            var deptId = new sqlParameter("@DepartmentID",departmentId) { IsNullable = true };
            deptId.Value = (object)departmentId ?? dbnull.Value;

            var Locid = new sqlParameter("@LocationID",locationId) { IsNullable = true };
            Locid.Value = (object)locationId ?? dbnull.Value;

            var subId = new sqlParameter("@SubLocationID",sublocationId) { IsNullable = true };
            subId.Value = (object)sublocationId ?? dbnull.Value;

            // Error Data is Null. This method or property cannot be called on Null values.
            var result = context.BudgetWorkflowStatusByDepartment
                                .FromsqlRaw("EXEC mis.BudgetWorkflowStatusByDepartment @CompanyID,subId)
                                .ToList();

我认为我应该将此存储的DbContext属性设置作为附加信息。相同的模式用于其他两个sproc,它们可以工作,但是不需要可选参数。

        public DbSet<BudgetWorkflowStatusByDepartment> BudgetWorkflowStatusByDepartment { get; set; }

然后在OnModelCreating()方法中设置HasNoKey()方法

            modelBuilder.Entity<BudgetWorkflowStatusByDepartment>().HasNoKey();

我感谢每个人的建议。

我对此的解决方法是告诉DBA更改存储过程以一起删除所有参数,而我将对结果做一个.Where()子句。

幸运的是,对于此特定功能,它不会返回超过200行,但是它将返回许多列,但这仍然不应该加重负担。

解决的原因

这是我的错。我有一些int属性,这些属性需要为可为null的整数。一旦这样做,它就可以工作,并且似乎适用于大多数解决方案。

重要的是要注意那些可能为null的属性并使它们可为空,这样您就不必费心整天来解决这个问题了。


我认为,由于我经历了所有以前从未尝试过的事情(使用.Net Core 3.1存储过程使用可选参数),因此我将在处理此问题时使用各种选项记录我的结果

从.Net Core 3.1执行存储过程时,这些是一些选项,哪些有效,哪些无效。

对于这些示例,这将是.Net Core方法的签名。

public List<BudgetWorkflowStatusByDepartment> GetBudgetWorkflowStatusByDepartment
(int? companyId,int? sublocationId)
{
...
}

所有参数都是可选的。以下示例将是您可以在此方法中使用的代码

这是存储过程的签名。

ALTER procedure mis.BudgetWorkflowStatusByDepartment_Filtered (
      @CompanyID int = null,@SubLocationID int = null
)

工作示例

以下示例演示了如何使用FromsqlRawFromsqlInterpolated方法执行存储过程。

FromsqlRaw方法

此示例仅创建sqlParameter对象并将值设置为传入值或dbnull。这很重要,因为null在这种情况下不起作用。 (请参见下面的“无效示例”

// Works
var compId = new sqlParameter("@CompanyID",companyId);
compId.Value = (object)companyId ?? dbnull.Value;

var deptId = new sqlParameter("@DepartmentID",departmentId);
deptId.Value = (object)departmentId ?? dbnull.Value;

var Locid = new sqlParameter("@LocationID",locationId);
Locid.Value = (object)locationId ?? dbnull.Value;

var subId = new sqlParameter("@SubLocationID",sublocationId);
subId.Value = (object)sublocationId ?? dbnull.Value;

var result = context.BudgetWorkflowStatusByDepartment
.FromsqlRaw("EXEC mis.BudgetWorkflowStatusByDepartment_Filtered @CompanyID,subId).ToList();


下面的工作通过包含IsNullable=true对象的sqlParameter属性来实现。

// Works
var compId = new sqlParameter("@CompanyID",companyId) { IsNullable = true };
compId.Value = (object)companyId ?? dbnull.Value;

var deptId = new sqlParameter("@DepartmentID",departmentId) { IsNullable = true };
deptId.Value = (object)departmentId ?? dbnull.Value;

var Locid = new sqlParameter("@LocationID",locationId) { IsNullable = true };
Locid.Value = (object)locationId ?? dbnull.Value;

var subId = new sqlParameter("@SubLocationID",sublocationId) { IsNullable = true };
subId.Value = (object)sublocationId ?? dbnull.Value;

var result = context.BudgetWorkflowStatusByDepartment
        .FromsqlRaw("EXEC mis.BudgetWorkflowStatusByDepartment_Filtered @CompanyID = @CompanyID,subId)
        .ToList();

以下与上面类似,但语法略为简化。

// Works
var compId = new sqlParameter("@CompanyID",sublocationId) { IsNullable = true };
subId.Value = (object)sublocationId ?? dbnull.Value;

// Works
var result = context.BudgetWorkflowStatusByDepartment
        .FromsqlRaw("EXEC mis.BudgetWorkflowStatusByDepartment_Filtered @CompanyID,subId)
        .ToList();

FromsqlInterpolated

以下方法有效,因为它使用.FromsqlInterpolated()方法,然后占位符语法将起作用。

// Works
var compId = new sqlParameter("@CompanyID",sublocationId);
subId.Value = (object)sublocationId ?? dbnull.Value;

var result = context.BudgetWorkflowStatusByDepartment
    .FromsqlInterpolated($"EXEC mis.BudgetWorkflowStatusByDepartment_Filtered @CompanyID={compId},@SubLocationID={subId}").ToList();

以下内容与上面类似,但使用的语法略为简化。

// Works
var compId = new sqlParameter("@CompanyID",sublocationId);
subId.Value = (object)sublocationId ?? dbnull.Value;

var result = context.BudgetWorkflowStatusByDepartment
    .FromsqlInterpolated($"EXEC mis.BudgetWorkflowStatusByDepartment_Filtered {compId},{subId}").ToList();

以下是简写语法的最终版本。使用插值方法,您可以直接使用传入的变量,而不必创建sqlParameter对象。

// Works
var result = context.BudgetWorkflowStatusByDepartment
    .FromsqlInterpolated($"EXEC mis.BudgetWorkflowStatusByDepartment_Filtered {companyId},{sublocationId}").ToList();

非工作示例

以下代码返回一个空数组,这是不正确的。这种代码参数化只能通过“内插” sql方法以这种方式使用。


// Returns empty array
var result = context.BudgetWorkflowStatusByDepartment
.FromsqlRaw("EXEC mis.BudgetWorkflowStatusByDepartment_Filtered {0},companyId).ToList();

结果-一个空数组,这是不正确的:

[]

以下方法FromsqlRaw方法的语法中不正确。字符串中的参数要使用插值,但这是该语法的错误方法

// Error Must declare the scalar variable "@CompanyID".
var compId = new sqlParameter("@CompanyID",sublocationId);
subId.Value = (object)sublocationId ?? dbnull.Value;

var result = context.BudgetWorkflowStatusByDepartment
.FromsqlRaw($"EXEC mis.BudgetWorkflowStatusByDepartment_Filtered @CompanyID={compId},@SubLocationID={subId}").ToList();

错误

Must declare the scalar variable "@CompanyID".

以下内容也不正确。不能以此方式填充字符串中的参数。

// Error Must declare the scalar variable "@CompanyID".
var compId = new sqlParameter("@CompanyID",sublocationId);
subId.Value = (object)sublocationId ?? dbnull.Value;

var result = context.BudgetWorkflowStatusByDepartment
.FromsqlRaw($"EXEC mis.BudgetWorkflowStatusByDepartment_Filtered {compId},{subId}").ToList();

错误

Must declare the scalar variable "@CompanyID".

最后,这也将不起作用,因为??运算符正在将空值设置为null而不是dbnull.Value


var compId = new sqlParameter("@CompanyID",companyId);
compId.Value = (object)companyId ?? null;

var deptId = new sqlParameter("@DepartmentID",departmentId);
deptId.Value = (object)departmentId ?? null;

var Locid = new sqlParameter("@LocationID",locationId);
Locid.Value = (object)locationId ?? null;

var subId = new sqlParameter("@SubLocationID",sublocationId);
subId.Value = (object)sublocationId ?? null;

var result = context.BudgetWorkflowStatusByDepartment
.FromsqlRaw("EXEC mis.BudgetWorkflowStatusByDepartment_Filtered @CompanyID,subId).ToList();

错误

The parameterized query '(@CompanyID nvarchar(4000),@DepartmentID nvarchar(4000),@Locatio' expects the parameter '@CompanyID',which was not supplied.

解决方法

我刚刚重新创建了一个类似的设置(您拥有的所有版本都相同),并且以下单行代码对我有用:

public List<BudgetWorkflowStatusByDepartment> GetBudgetWorkflowStatusByDepartment(int? companyId,int? departmentId,int? locationId,int? sublocationId)
     => context.BudgetWorkflowStatusByDepartment
               .FromSqlInterpolated($"EXEC mis.BudgetWorkflowStatusByDepartment {companyId},{departmentId},{locationId},{subLocationId}").ToList();

请注意,由于我正在使用.FromSqlInterpolated()调用,因此无需创建SqlParameter-int?可以直接传递,因为这是一个安全的调用,按照docs,并正确处理了null值。

我看到您也尝试了此调用,但是看起来您正在传递SqlParameter到它,而不是数据本身-试试我的版本吗?

在进一步测试后,我将您的版本进行了一些修改,并通过将SqlParameter.IsNullable属性设置为true

public List<BudgetWorkflowStatusByDepartment> GetBudgetWorkflowStatusByDepartment(int? companyId,int? sublocationId)
{
    var compId = new SqlParameter("@CompanyID",companyId) { IsNullable = true };
    compId.Value = (object)companyId ?? DBNull.Value;

    var deptId = new SqlParameter("@DepartmentID",departmentId) { IsNullable = true };
    deptId.Value = (object)departmentId ?? DBNull.Value;

    var locId = new SqlParameter("@LocationID",locationId) { IsNullable = true };
    locId.Value = (object)locationId ?? DBNull.Value;

    var subId = new SqlParameter("@SubLocationID",sublocationId) { IsNullable = true };
    subId.Value = (object)sublocationId ?? DBNull.Value;


    var result = context.BudgetWorkflowStatusByDepartment
                        .FromSqlRaw("EXEC mis.BudgetWorkflowStatusByDepartment @CompanyID = @CompanyID,@DepartmentID = @DepartmentID,@LocationID = @LocationID,@SubLocationID = @SubLocationID",compId,deptId,locId,subId)
                        .ToList();

    return result;
}

随您选择-我个人很喜欢单线:)

,

我发现使用 dotnetcore 获取命名参数(以及可选参数)的唯一方法是使用以下方法:

_context.Database.ExecuteSqlRaw("EXEC mysproc @param1={0},@param2={1}",param1,param2);

... 其中 param1 和 param2 是 Microsoft.Data.SqlClient.SqlParameter 类型(这很重要,因为我发现 System.Data.SqlClient.SqlParamater 类型不会产生编译错误但会导致混淆运行时异常)。

虽然这始终如一,即使使用输出参数(在参数占位符后添加“out”),但当您的代码可能会使用不同的参数集提供不同的结果时,这是一种非常不灵活的创建语句的方式。我的一个用例有 23 个我可能提供的参数,其中只有三个是必需的。对 ExecuteSqlRaw 语句进行硬编码不是一种选择。

从那以后,我想出了一个有用的方法,我在控制器、页面等中使用了该方法,其中注入了 DbContext _context:

private void ExecuteStoredProcedure(
        string StoredProcedureName,List<SqlParameter> parameters)
{           
    StringBuilder stringBuilder = null;
    int paramNum = 0;

    // Build the T-SQL statement to enable named paramaters
    foreach (SqlParameter param in parameters)
    {
        string direction = param.Direction == ParameterDirection.Output ? " out" : "";
        if (stringBuilder == null)
        {                    
            stringBuilder = new StringBuilder(string.Format(CultureInfo.CurrentCulture,"EXEC {0} {1}={{{2}}}{3}",StoredProcedureName,param.ParameterName,paramNum++,direction));
        }
        else
        {
            stringBuilder.Append(string.Format(CultureInfo.CurrentCulture,",{0}={List<SqlParameter> parameters = new List<SqlParameter>();

parameters.Add(new SqlParameter()
    {
        ParameterName = "@MyOutputParam",DbType = System.Data.DbType.Int64,Direction = System.Data.ParameterDirection.Output
    });

parameters.Add(new SqlParameter()
    {
        ParameterName = "@RequiredButNullableUnicodeString",DbType = System.Data.DbType.String,Size = 50,Value = DBNull.Value
    });

parameters.Add(new SqlParameter()
   {
        ParameterName = "@SomeVarchar",DbType = System.Data.DbType.AnsiString,Size = 100,Value = "some value"
    });
}{2}",direction));
        }
    }

    try
    {
        _context.Database.ExecuteSqlRaw(
            stringBuilder.ToString(),parameters.ToArray());
    }
    catch (Exception ex)
    {
         throw;
    }
}

该方法采用任意顺序的 SqlParameters 列表。请注意,列表在传递给 ExecuteSqlRaw 之前会转换为 SqlParamater 数组。从技术上讲,它不是“参数数组”,但我还没有看到它失败。您可能还想将 DBContext 或 DatabaseFacade 传递给该方法。

我通常以下列方式构造参数列表。 ParameterName 属性必须与它将提供的存储过程参数的名称相匹配。我已经排除了包含/排除参数的任何逻辑。

{
  "msb": [
{
  "ville": "Le Mans","pays": "FRANCE","debutMatch": "2021-03-16T20:00:00+02:00","dateFin": "2021-03-16T22:00:00+02:00","meta": {
    "cree": "2021-03-10T16:22:36.223+01:00","modofie": "2021-03-12T16:29:25.223+01:00"
  },"etat": "public","titre": "MSB \"Orléans\" - 16 mars 2021","url": "https://msb.fr/index.php/component/content/article/114-competitions/jeep-elite/5107-msb-lyon-villeurbanne-105-96-ap?Itemid=101","nomOrganisation": "le-mans-sarthe-basket"
},{
  "ville": "Le Mans","titre": "MSB \"Boulazac\" - 06 avril 2021","url": "https://msb.fr","debutMatch": "2021-04-06T20:00:00+02:00","dateFin": "2021-04-06T22:00:00+02:00","logo": {
    "logo": "logo_msb_180.png","url": "https://msb.fr/images/teams/logo/logo_msb_180.png"
  },"titre": "MSB \"Orléans\" - 06 avril 2021","debutMatch": "2021-03-20T20:00:00+02:00","dateFin": "2021-03-20T22:00:00+02:00","meta": {
    "cree": "2021-03-15T16:22:36.223+01:00","modofie": "2021-03-17T16:29:25.223+01:00"
  },"titre": "MSB \"Orléans\" - 20 mars 2021","nomOrganisation": "le-mans-sarthe-basket"
}
],"page_total": {
    "pageZ": 42,"Total_compte": 215,"pageI": 1,"pageT": 6,"numeroIdentifiant": "164784656259969"
  }
}