J'essaie d'appeler une procédure stockée qui accepte un paramètre de valeur de table. Je sais que cela n'est pas encore directement pris en charge dans Entity Framework, mais d'après ce que je comprends, vous pouvez le faire en utilisant la commande ExecuteStoreQuery
du ObjectContext
. J'ai un référentiel framework d'entités génériques où j'ai la méthode suivante 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);
}
La chaîne de commande se termine comme ceci:
EXEC someStoredProcedureName {0},{1},{2},{3},{4},{5},{6},{7}
J'ai essayé d'exécuter cette méthode sur une procédure stockée qui accepte un paramètre de valeur table et qui se rompt. J'ai lu ici que les paramètres devaient être de type SqlParameter
et que le paramètre table doit avoir le SqlDbType
défini sur Structured
. Donc j'ai fait ceci et j'ai une erreur qui dit:
The table type parameter p6 must have a valid type name
Ainsi, j'ai défini SqlParameter.TypeName sur le nom du type défini par l'utilisateur que j'ai créé sur la base de données. Lorsque j'exécute la requête, j'obtiens l'erreur réellement utile suivante:
Incorrect syntax near '0'.
La requête peut s'exécuter si je retourne à ADO.NET et exécute un lecteur de données, mais j'espérais que cela fonctionne avec le contexte de données.
Est-il possible de passer un paramètre de valeur de table en utilisant ExecuteStoreQuery
? En outre, j'utilise réellement Entity Framework Code First et transforme le DbContext
en un ObjectContext
pour obtenir la méthode ExecuteStoreQuery
disponible. Est-ce nécessaire ou est-ce que je peux le faire aussi contre le DbContext
?
[~ # ~] met à jour [~ # ~]
J'ai ajouté un support pour cela sur le package Nuget - https://github.com/Fodsuk/EntityFrameworkExtras#nuget (EF4, EF5, EF6)
Consultez le référentiel GitHub pour des exemples de code.
Question légèrement délicate, mais néanmoins utile pour les personnes essayant de passer des tables définies par l'utilisateur dans une procédure stockée. Après avoir joué avec l'exemple de Nick et d'autres publications de Stackoverflow, je suis arrivé à ceci:
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);
}
}
et la procédure stockée ressemble à ceci:
ALTER PROCEDURE [dbo].[usp_RaiseWarnings_rs]
(@CaseID int,
@UserID uniqueidentifier = '846454D9-DE72-4EF4-ABE2-16EC3710EA0F', --Admin
@Warnings dbo.udt_Warnings READONLY
)
AS
et la table définie par l'utilisateur ressemble à ceci:
CREATE TYPE [dbo].[udt_Warnings] AS TABLE(
[WarningCode] [nvarchar](5) NULL,
[StatusID] [int] NULL,
[DecisionID] [int] NULL,
[Criticality] [int] NULL DEFAULT ((0))
)
Les contraintes que j'ai trouvées incluent:
ExecuteStoreCommand
doivent être en ordre avec les paramètres de votre procédure stockée.Ok, voici donc une solution mise à jour 2018: complète qui explique comment appeler une procédure stockée avec le paramètre table d’Entity Framework sans les packages de nuget
J'utilise EF 6.xx, SQL Server 2012 et VS2017
Disons que vous avez un type de table simple défini comme ceci (juste une colonne)
go
create type GuidList as table (Id uniqueidentifier)
et une procédure stockée avec plusieurs paramètres tels que:
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
En SQL, vous utiliseriez quelque chose comme ça:
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'
Et voici comment appeler cette procédure stockée depuis Entity Framework (dans 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);
}
}
}
J'espère que ça aide! ????
Je veux partager ma solution sur ce problème:
J'ai stocké des procédures avec plusieurs paramètres de valeur de table et j'ai découvert que si vous l'appelez ainsi:
var query = dbContext.ExecuteStoreQuery<T>(@"
EXECUTE [dbo].[StoredProcedure] @SomeParameter, @TableValueParameter1, @TableValueParameter2", spParameters[0], spParameters[1], spParameters[2]);
var list = query.ToList();
vous obtenez une liste sans enregistrements.
Mais j'ai joué avec plus et cette ligne m'a donné une idée:
var query = dbContext.ExecuteStoreQuery<T>(@"
EXECUTE [dbo].[StoredProcedure] 'SomeParameterValue', @TableValueParameter1, @TableValueParameter2", spParameters[1], spParameters[2]);
var list = query.ToList();
J'ai changé mon paramètre @ SomeParameter avec sa valeur réelle 'SomeParameterValue' dans le texte de la commande. Et cela a fonctionné :) Cela signifie que si nous avons autre chose que SqlDbType.Structured dans nos paramètres, il ne les passe pas tous correctement et nous n’obtenons rien. Nous devons remplacer les paramètres actuels par leurs valeurs.
Donc, ma solution ressemble à ceci:
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;
}
Le code pourrait sûrement être plus optimisé mais j'espère que cela aidera.
L’approche DataTable est le seul moyen, mais la construction d’un DataTable et son remplissage manuel sont fugaces. Je voulais définir mon DataTable directement à partir de mon IEnumerable dans un style similaire à celui du modélisateur de modèles couramment utilisé par EF. Alors:
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()
);
J'ai posté la chose sur dontnetfiddle: https://dotnetfiddle.net/ZdpYM (notez que vous ne pouvez pas l'exécuter là-bas car tous les assemblys ne sont pas chargés dans le violon)
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();
Changez votre code de concaténation de chaîne pour produire quelque chose comme:
EXEC someStoredProcedureName @p0,@p1,@p2,@p3,@p4,@p5,@p6,@p7