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

C# 实体框架存储过程表值参数,c#,entity-framework,stored-procedures,ef-code-first,table-valued-parameters,C#,Entity Framework,Stored Procedures,Ef Code First,Table Valued Parameters,我试图调用一个接受表值参数的存储过程。我知道实体框架还不直接支持这一点,但据我所知,您可以使用ObjectContext下的ExecuteStoreQuery命令来实现这一点。我有一个通用实体框架存储库,其中我有以下ExecuteStoredProcedure方法: public IEnumerable<T> ExecuteStoredProcedure<T>(string procedureName, params object[] parameters) {

我试图调用一个接受表值参数的存储过程。我知道实体框架还不直接支持这一点,但据我所知,您可以使用
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);
}
我尝试在一个接受表值参数的存储过程上运行此方法,但它会中断。我了解到参数需要是
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
执行此操作?

更改字符串连接代码以生成类似以下内容:

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

更新

我已经在Nuget软件包上添加了对此的支持-(EF4、EF5、EF6)

查看存储库中的代码示例


这个问题有点离题,但对于试图将用户定义的表传递到存储过程中的人来说仍然很有用。在研究了Nick的示例和其他Stackoverflow帖子后,我得出了以下结论:

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);
    }
}
存储过程如下所示:

EXEC someStoredProcedureName {0},{1},{2},{3},{4},{5},{6},{7}
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))
)
用户定义的表如下所示:

EXEC someStoredProcedureName {0},{1},{2},{3},{4},{5},{6},{7}
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上似乎没有标识(1,1)NOTNULL列

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

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

    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(@”
    执行[dbo]。[StoredProcess]@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();
    
    var query=dbContext.ExecuteStoreQuery(@”
    执行[dbo]。[StoredProcess]“SomeParameterValue”、@TableValueParameter1、@TableValueParameter2”、spParameters[1]、spParameters[2]);
    var list=query.ToList();
    
    我用命令文本中的实际值'SomeParameterValue'更改了参数@SomeParameter。 它成功了:) 这意味着,如果我们的参数中除了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;
    }
    
    公共静态列表ExecuteStoreProcedure(此ObjectContext dbContext、string storedProcedureName、params SqlParameter[]参数)
    {
    var spSignature=新StringBuilder();
    对象参数;
    bool hasTableVariables=parameters.Any(p=>p.SqlDbType==SqlDbType.Structured);
    AppendFormat(“执行{0}”,storedProcedureName);
    var length=parameters.Count()-1;
    if(hasTableVariables)
    {
    var tableValueParameters=新列表();
    对于(int i=0;i

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

    数据表方法是唯一的方法,但是构建数据表并手动填充它是很麻烦的。我想直接从m定义我的数据表
    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
    
    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'
    
        [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);
            }
        }
    }