Параметр таблицы значений хранимых процедур Entity Framework
Я пытаюсь вызвать хранимую процедуру, которая принимает параметр табличного значения. Я знаю, что это еще не поддерживается напрямую в Entity Framework, но насколько я понимаю, вы можете сделать это, используя ExecuteStoreQuery
командовать из ObjectContext
, У меня есть общий репозиторий структуры сущностей, где у меня есть следующее 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 Code First и использую DbContext
для ObjectContext
чтобы получить ExecuteStoreQuery
метод доступен. Это необходимо или я могу сделать это против DbContext
также?
6 ответов
ОБНОВИТЬ
Я добавил поддержку для этого в пакете Nuget - https://github.com/Fodsuk/EntityFrameworkExtras (EF4,EF5,EF6)
Проверьте репозиторий GitHub для примеров кода.
Немного не вопрос, но тем не менее полезен для людей, пытающихся передать пользовательские таблицы в хранимую процедуру. Поиграв с примером Ника и другими постами Stackru, я придумал это:
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
должны быть в порядке с параметрами в вашей хранимой процедуре - Вы должны передать каждый столбец в вашу пользовательскую таблицу, даже если они имеют значения по умолчанию. Так что, похоже, у меня не было столбца IDENTITY(1,1) NOT NULL в моем UDT
Итак, вот обновление 2018: сквозное решение, которое описывает, как вызвать хранимую процедуру с параметром таблицы из Entity Framework без пакетов 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# для использования этой хранимой процедуры
И вот как вы можете вызвать эту хранимую процедуру из 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);
}
}
}
Я надеюсь, что это помогает!
Я хочу поделиться своим решением по этой проблеме:
У меня есть хранимые процедуры с несколькими параметрами табличных значений, и я обнаружил, что если вы называете это так:
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;
}
Код, безусловно, может быть более оптимизирован, но я надеюсь, что это поможет.
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();
Подход DataTable - единственный способ, но создание DataTable и его заполнение вручную - это бесполезно. Я хотел определить свой DataTable непосредственно из моего IEnumerable в стиле, похожем на беглое построение моделей 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 (обратите внимание, что вы не можете запустить его там, потому что не все сборки загружены в скрипку)
Измените код конкатенации строк, чтобы получить что-то вроде:
EXEC someStoredProcedureName @p0,@p1,@p2,@p3,@p4,@p5,@p6,@p7