Параметр Значения Таблицы Хранимых Процедур Entity Framework
я пытаюсь вызвать хранимую процедуру, которая принимает параметр таблицы значений. Я знаю, что это не поддерживается напрямую в Entity Framework, но из того, что я понимаю, вы можете сделать это с помощью С ObjectContext. У меня есть общий репозиторий Entity framework, где у меня есть следующее 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}
я попытался запустить этот метод в хранимой процедуре, которая принимает таблица значение параметра и он ломается. Я читаю здесь что параметры должны быть типа SqlParameter и возвращающий табличное значение параметр должен иметь SqlDbType значение Structured. Поэтому я сделал это, и я получаю сообщение об ошибке:
The table type parameter p6 must have a valid type name
Итак, я установил SqlParameter.TypeName к имени пользовательского типа, который я создал в базе данных, а затем, когда я запускаю запрос, я получаю следующую действительно полезную ошибку:
Incorrect syntax near '0'.
я могу получить запрос для запуска, если я вернуться назад ADO.NET и выполнить средство чтения данных, но я надеялся, что он будет работать с использованием контекста данных.
есть ли способ передать параметр значения таблицы с помощью ExecuteStoreQuery? Кроме того, я на самом деле сначала использую код Entity Framework и бросаю DbContext до ObjectContext для получения ExecuteStoreQuery способ доступен. Это необходимо или я могу сделать это против DbContext так же?
6 ответов:
обновление
Я добавил поддержку для этого на пакет Nuget -https://github.com/Fodsuk/EntityFrameworkExtras#nuget (EF4, EF5, EF6)
Проверьте GitHub репозиторий для примеров кода.
немного не вопрос, но тем не менее полезно для людей, пытающихся передать пользовательские таблицы в хранимую процедуру. После игры с примером Ника и другими сообщениями 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); } }и хранимая процедура выглядит так:
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должны быть в порядке с параметрами в вашей хранимой процедуре- вы должны пройти каждый столбец в пользовательской таблице, даже если они имеют значения по умолчанию. Так что, кажется, я не мог иметь идентичность(1,1) не Нулевой столбец на моем UDT
Я хочу поделиться своим решением по этой проблеме:
у меня есть хранимые процедуры с несколькими табличные параметры и я узнал, что если вы называете это так:
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.Структурированный в наших параметрах он не передает их все правильно, и мы ничего не получаем. Нам нужно заменить фактические параметры их значениями.
Итак, мое решение выглядит следующим образом:
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; }код, безусловно, может быть более оптимизирован, но я надеюсь, что это поможет.
хорошо, так вот обновить 2018: сквозное решение, которое описывает, как вызвать хранимую процедуру с параметром таблицы из Entity Framework без пакетов nuget
Я использую EF 6.xx, SQL Server 2012 и VS2017
1. Ваше табличное значение prameter
допустим, у вас есть простой тип таблицы, определенный следующим образом (только один столбец)
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 go3. 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# для использования этой хранимой процедуры
и вот как вы можете вызвать эту хранимую процедуру из Entity Framework (внутри 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); } } }Я надеюсь, что это помогает!
подход DataTable является единственным способом, но построение DataTable и заполнение его вручную-это fugly. Я хотел определить свой DataTable непосредственно из моего IEnumerable в стиле, похожем на fluent model builder thingy от 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 (Обратите внимание, что вы не можете запустить его там, потому что не все сборки загружаются в скрипку)
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();
измените код конкатенации строк, чтобы создать что-то вроде:
EXEC someStoredProcedureName @p0,@p1,@p2,@p3,@p4,@p5,@p6,@p7
Comments