实体框架存储过程表值参数

Posted

技术标签:

【中文标题】实体框架存储过程表值参数【英文标题】:Entity Framework Stored Procedure Table Value Parameter 【发布时间】:2011-12-30 17:41:07 【问题描述】:

我正在尝试调用一个接受表值参数的存储过程。我知道实体框架尚未直接支持此功能,但据我了解,您可以使用ObjectContext 中的ExecuteStoreQuery 命令来实现。我有一个通用实体框架存储库,其中有以下ExecuteStoredProcedure 方法:

public IEnumerable<T> ExecuteStoredProcedure<T>(string procedureName, params object[] parameters)

    StringBuilder command = new StringBuilder();
    command.Append("EXEC ");
    command.Append(procedureName);
    command.Append(" ");

    // Add a placeholder for each parameter passed in
    for (int i = 0; i < parameters.Length; i++)
    
        if (i > 0)
            command.Append(",");

        command.Append("" + i + "");
    

    return this.context.ExecuteStoreQuery<T>(command.ToString(), parameters);

命令字符串的结尾是这样的:

EXEC someStoredProcedureName 0,1,2,3,4,5,6,7

我尝试在接受表值参数的存储过程上运行此方法,但它会中断。我读到here,参数需要是SqlParameter 类型,表值参数需要将SqlDbType 设置为Structured。所以我这样做了,我收到一条错误消息:

The table type parameter p6 must have a valid type name

所以,我将 SqlParameter.TypeName 设置为我在数据库上创建的用户定义类型的名称,然后当我运行查询时,我得到以下真正有用的错误:

Incorrect syntax near '0'.

如果我恢复到 ADO.NET 并执行数据读取器,我可以运行查询,但我希望使用数据上下文让它工作。

有没有办法使用ExecuteStoreQuery 传递表值参数?此外,我实际上首先使用实体​​框架代码并将DbContext 转换为ObjectContext 以获得ExecuteStoreQuery 方法可用。这是必要的吗?或者我也可以针对DbContext 这样做吗?

【问题讨论】:

目前看来不可能:***.com/questions/2837350/… 我很害怕,但根据以下答案,尽管没有给出示例,但似乎有可能。我留下了一条评论,并附有这个问题的链接,看看他是否会回复:***.com/questions/6084061/… 你最终让这个工作了吗?可以粘贴一个完整的例子吗? 【参考方案1】:

更新

我在 Nuget 包上添加了对此的支持 - https://github.com/Fodsuk/EntityFrameworkExtras#nuget (EF4,EF5,EF6)

查看GitHub 存储库以获取代码示例。


有点问题,但对于试图将用户定义的表传递到存储过程的人来说仍然有用。在玩弄了 Nick 的示例和其他 *** 帖子之后,我想出了这个:

class Program

    static void Main(string[] args)
    
        var entities = new NewBusinessEntities();

        var dt = new DataTable();
        dt.Columns.Add("WarningCode");
        dt.Columns.Add("StatusID");
        dt.Columns.Add("DecisionID");
        dt.Columns.Add("Criticality");

        dt.Rows.Add("EO01", 9, 4, 0);
        dt.Rows.Add("EO00", 9, 4, 0);
        dt.Rows.Add("EO02", 9, 4, 0);

        var caseId = new SqlParameter("caseid", SqlDbType.Int);
        caseId.Value = 1;

        var userId = new SqlParameter("userid", SqlDbType.UniqueIdentifier);
        userId.Value = Guid.Parse("846454D9-DE72-4EF4-ABE2-16EC3710EA0F");

        var warnings = new SqlParameter("warnings", SqlDbType.Structured);
        warnings.Value= dt;
        warnings.TypeName = "dbo.udt_Warnings";

        entities.ExecuteStoredProcedure("usp_RaiseWarnings_rs", userId, warnings, caseId);
    


public static class ObjectContextExt

    public static void ExecuteStoredProcedure(this ObjectContext context, string storedProcName, params object[] parameters)
    
        string command = "EXEC " + storedProcName + " @caseid, @userid, @warnings";

        context.ExecuteStoreCommand(command, parameters);
    

存储过程如下所示:

ALTER PROCEDURE [dbo].[usp_RaiseWarnings_rs]
    (@CaseID int, 
     @UserID uniqueidentifier = '846454D9-DE72-4EF4-ABE2-16EC3710EA0F', --Admin
     @Warnings dbo.udt_Warnings READONLY
)
AS

用户定义的表格如下所示:

CREATE TYPE [dbo].[udt_Warnings] AS TABLE(
    [WarningCode] [nvarchar](5) NULL,
    [StatusID] [int] NULL,
    [DecisionID] [int] NULL,
    [Criticality] [int] NULL DEFAULT ((0))
)

我发现的约束包括:

    传递给ExecuteStoreCommand 的参数必须与存储过程中的参数顺序一致 您必须将每一列传递到用户定义的表中,即使它们具有默认值。所以看来我的 UDT 上没有 IDENTITY(1,1) NOT NULL 列

【讨论】:

这正是我们最终要做的。抱歉,我没有用解决方案更新帖子。感谢您花时间去做!我已经给你正确的答案了。 谢谢,您的问题帮助我走上了正确的道路 :) 我不确定 ssilas777,我会假设 DataContext 的底层数据访问组件类似于 DbContext 或 ObjectContext。我会考虑用我在 Nuget 上安装的 NuGet 包来支持这一点:) 是 Db 上下文还是实体上下文?有人可以帮我吗? 解决了! TResult 约定是使用设置器而不是字段。以下链接很有帮助..谢谢! msdn.microsoft.com/en-us/library/vstudio/…【参考方案2】:

好的,这里是 2018 年更新:端到端解决方案,它描述了如何使用实体框架中的表参数调用存储过程没有 nuget 包

我正在使用 EF 6.xx、SQL Server 2012 和 VS2017

1。您的表值参数

假设您有一个像这样定义的简单表类型(只有一列)

go
create type GuidList as table (Id uniqueidentifier)

2。你的存储过程

还有一个带有多个参数的存储过程,例如:

go
create procedure GenerateInvoice
    @listIds GuidList readonly,
    @createdBy uniqueidentifier,
    @success int out,
    @errorMessage nvarchar(max) out
as
begin
    set nocount on;

    begin try
    begin tran;  

    -- 
    -- Your logic goes here, let's say a cursor or something:
    -- 
    -- declare gInvoiceCursor cursor forward_only read_only for
    -- 
    -- bla bla bla
    --
    --  if (@brokenRecords > 0)
    --  begin
    --      RAISERROR(@message,16,1);
    --  end
    -- 


    -- All good!
    -- Bonne chance mon ami!

    select @success = 1
    select @errorMessage = ''

    end try
    begin catch  
        --if something happens let's be notified
        if @@trancount > 0 
        begin
            rollback tran;  
        end

        declare @errmsg nvarchar(max)
        set @errmsg =       
            (select 'ErrorNumber: ' + cast(error_number() as nvarchar(50))+
            'ErrorSeverity: ' + cast(error_severity() as nvarchar(50))+
            'ErrorState: ' + cast(error_state() as nvarchar(50))+
            'ErrorProcedure: ' + cast(error_procedure() as nvarchar(50))+
            'ErrorLine: ' + cast(error_number() as nvarchar(50))+
            'error_message: ' + cast(error_message() as nvarchar(4000))
            )
        --save it if needed

        print @errmsg

        select @success = 0
        select @errorMessage = @message

        return;
    end catch;

    --at this point we can commit everything
    if @@trancount > 0 
    begin
        commit tran;  
    end

end
go

3。使用此存储过程的 SQL 代码

在 SQL 中你会使用类似的东西:

declare @p3 dbo.GuidList
insert into @p3 values('f811b88a-bfad-49d9-b9b9-6a1d1a01c1e5')
exec sp_executesql N'exec GenerateInvoice @listIds, @CreatedBy, @success',N'@listIds [dbo].[GuidList] READONLY,@CreatedBy uniqueidentifier',@listIds=@p3,@CreatedBy='FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF'

4。使用此存储过程的 C# 代码

以下是如何从实体框架(在 WebAPI 内部)调用该存储过程:

    [HttpPost]
    [AuthorizeExtended(Roles = "User, Admin")]
    [Route("api/BillingToDo/GenerateInvoices")]
    public async Task<IHttpActionResult> GenerateInvoices(BillingToDoGenerateInvoice model)
    
        try
        
            using (var db = new YOUREntities())
            
                //Build your record
                var tableSchema = new List<SqlMetaData>(1)
                
                    new SqlMetaData("Id", SqlDbType.UniqueIdentifier)
                .ToArray();

                //And a table as a list of those records
                var table = new List<SqlDataRecord>();

                for (int i = 0; i < model.elements.Count; i++)
                
                    var tableRow = new SqlDataRecord(tableSchema);
                    tableRow.SetGuid(0, model.elements[i]);
                    table.Add(tableRow);
                

                //Parameters for your query
                SqlParameter[] parameters =
                
                    new SqlParameter
                    
                        SqlDbType = SqlDbType.Structured,
                        Direction = ParameterDirection.Input,
                        ParameterName = "listIds",
                        TypeName = "[dbo].[GuidList]", //Don't forget this one!
                        Value = table
                    ,
                    new SqlParameter
                    
                        SqlDbType = SqlDbType.UniqueIdentifier,
                        Direction = ParameterDirection.Input,
                        ParameterName = "createdBy",
                        Value = CurrentUser.Id
                    ,
                    new SqlParameter
                    
                        SqlDbType = SqlDbType.Int,
                        Direction = ParameterDirection.Output, // output!
                        ParameterName = "success"
                    ,
                    new SqlParameter
                    
                        SqlDbType = SqlDbType.NVarChar,
                        Size = -1,                             // "-1" equals "max"
                        Direction = ParameterDirection.Output, // output too!
                        ParameterName = "errorMessage"
                    
                ;

                //Do not forget to use "DoNotEnsureTransaction" because if you don't EF will start it's own transaction for your SP.
                //In that case you don't need internal transaction in DB or you must detect it with @@trancount and/or XACT_STATE() and change your logic
                await db.Database.ExecuteSqlCommandAsync(TransactionalBehavior.DoNotEnsureTransaction,
                    "exec GenerateInvoice @listIds, @createdBy, @success out, @errorMessage out", parameters);

                //reading output values:
                int retValue;
                if (parameters[2].Value != null && Int32.TryParse(parameters[2].Value.ToString(), out retValue))
                
                    if (retValue == 1)
                    
                        return Ok("Invoice generated successfully");
                    
                

                string retErrorMessage = parameters[3].Value?.ToString();

                return BadRequest(String.IsNullOrEmpty(retErrorMessage) ? "Invoice was not generated" : retErrorMessage);
            
        
        catch (Exception e)
        
            return BadRequest(e.Message);
        
    

希望对你有帮助! ?

【讨论】:

2018版本是粘贴在***上的2012 nuget代码:P【参考方案3】:

我想分享我对这个问题的解决方案:

我有几个表值参数的存储过程,我发现如果你这样调用它:

var query = dbContext.ExecuteStoreQuery<T>(@"
EXECUTE [dbo].[StoredProcedure] @SomeParameter, @TableValueParameter1, @TableValueParameter2", spParameters[0], spParameters[1], spParameters[2]);
var list = query.ToList();

你得到一个没有记录的列表。

但我玩得更多了,这句话给了我一个想法:

var query = dbContext.ExecuteStoreQuery<T>(@"
EXECUTE [dbo].[StoredProcedure] 'SomeParameterValue', @TableValueParameter1, @TableValueParameter2",  spParameters[1], spParameters[2]);
var list = query.ToList();

我将参数 @SomeParameter 更改为命令文本中的实际值 'SomeParameterValue'。 它奏效了:) 这意味着如果我们的参数中包含 SqlDbType.Structured 以外的其他内容,它不会正确传递它们,我们什么也得不到。 我们需要用它们的值替换实际参数。

所以,我的解决方案如下:

public static List<T> ExecuteStoredProcedure<T>(this ObjectContext dbContext, string storedProcedureName, params SqlParameter[] parameters)

    var spSignature = new StringBuilder();
    object[] spParameters;
    bool hasTableVariables = parameters.Any(p => p.SqlDbType == SqlDbType.Structured);

    spSignature.AppendFormat("EXECUTE 0", storedProcedureName);
    var length = parameters.Count() - 1;

    if (hasTableVariables)
    
        var tableValueParameters = new List<SqlParameter>();

        for (int i = 0; i < parameters.Count(); i++)
        
            switch (parameters[i].SqlDbType)
            
                case SqlDbType.Structured:
                    spSignature.AppendFormat(" @0", parameters[i].ParameterName);
                    tableValueParameters.Add(parameters[i]);
                    break;
                case SqlDbType.VarChar:
                case SqlDbType.Char:
                case SqlDbType.Text:
                case SqlDbType.NVarChar:
                case SqlDbType.NChar:
                case SqlDbType.NText:
                case SqlDbType.Xml:
                case SqlDbType.UniqueIdentifier:
                case SqlDbType.Time:
                case SqlDbType.Date:
                case SqlDbType.DateTime:
                case SqlDbType.DateTime2:
                case SqlDbType.DateTimeOffset:
                case SqlDbType.SmallDateTime:
                    // TODO: some magic here to avoid SQL injections
                    spSignature.AppendFormat(" '0'", parameters[i].Value.ToString());
                    break;
                default:
                    spSignature.AppendFormat(" 0", parameters[i].Value.ToString());
                    break;
            

            if (i != length) spSignature.Append(",");
        
        spParameters = tableValueParameters.Cast<object>().ToArray();
    
    else
    
        for (int i = 0; i < parameters.Count(); i++)
        
            spSignature.AppendFormat(" @0", parameters[i].ParameterName);
            if (i != length) spSignature.Append(",");
        
        spParameters = parameters.Cast<object>().ToArray();
    

    var query = dbContext.ExecuteStoreQuery<T>(spSignature.ToString(), spParameters);


    var list = query.ToList();
    return list;

代码当然可以更优化,但我希望这会有所帮助。

【讨论】:

呃,这不是容易被sql注入吗? @jag 我在代码中留下了一个 TODO,以便您可以开发自己的逻辑来克服注入。也不确定8年后这个解决方案是否仍然适用【参考方案4】:
var sqlp = new SqlParameter("@param3", my function to get datatable);
sqlp.SqlDbType = System.Data.SqlDbType.Structured;
sqlp.TypeName = "dbo.mytypename";

  var v = entitycontext.Database.SqlQuery<bool?>("exec [MyStorProc] @param1,@param2,@param3,@param4", new SqlParameter[]
                    
                        new SqlParameter("@param1",value here),
                        new SqlParameter("@param2",value here),

                        sqlp,
                        new SqlParameter("@param4",value here)

                    ).FirstOrDefault();

【讨论】:

请在代码周围添加一些上下文并格式化代码。 你可以直接复制粘贴。我是新来的,我不知道你在说什么。 我的意思是代码格式不正确,应该提及为什么在您看来这会起作用。 这里不知道怎么形容。实际上 param3 需要声明为 System.Data.SqlDbType.Structured;。所以我在参数声明范围之外启动它并在该范围内使用变量。【参考方案5】:

DataTable 方法是唯一的方法,但是构建一个 DataTable 并手动填充它是很麻烦的。我想直接从我的 IEnumerable 中定义我的 DataTable,其风格类似于 EF 的流利模型构建器。所以:

var whatever = new[]
            
                new
                
                    Id = 1,
                    Name = "Bacon",
                    Foo = false
                ,
                new
                
                    Id = 2,
                    Name = "Sausage",
                    Foo = false
                ,
                new
                
                    Id = 3,
                    Name = "Egg",
                    Foo = false
                ,
            ;

            //use the ToDataTable extension method to populate an ado.net DataTable
            //from your IEnumerable<T> using the property definitions.
            //Note that if you want to pass the datatable to a Table-Valued-Parameter,
            //The order of the column definitions is significant.
            var dataTable = whatever.ToDataTable(
                whatever.Property(r=>r.Id).AsPrimaryKey().Named("item_id"),
                whatever.Property(r=>r.Name).AsOptional().Named("item_name"),
                whatever.Property(r=>r.Foo).Ignore()
                );

我已经在 dontnetfiddle 上发布了这个东西:https://dotnetfiddle.net/ZdpYM3(请注意,您不能在那里运行它,因为并非所有程序集都加载到 fiddle 中)

【讨论】:

如何解决这个问题?【参考方案6】:

更改您的字符串连接代码以生成如下内容:

EXEC someStoredProcedureName @p0,@p1,@p2,@p3,@p4,@p5,@p6,@p7

【讨论】:

OP 说EXEC someStoredProcedureName 0,1,2,3,4,5,6,7 不起作用,所以参数化它也不起作用。

以上是关于实体框架存储过程表值参数的主要内容,如果未能解决你的问题,请参考以下文章

将表值参数传递给存储过程

使用 PetaPoco 将表值参数传递给存储过程

存储过程参数名称和实体框架

不使用存储过程的表值参数

执行使用表值参数的存储过程时执行超时到期

为啥需要以只读方式输入 SQL Server 存储过程的表值参数?