Related
From my C# program, I need to call a stored procedure in an Oracle database, which has the following contract:
PKG_ENTITY.ALTER_ENTITY (VARCHAR2 NAME, VARCHAR2 FULLNAME, ATTRS_TYPE ATTRS, VARCHAR2 STATUS, INTEGER OUT RESULT, VARCHAR2 OUT ERRORMSG).
The RESULT and ERRORMSG parameters are OUT parameters.
I know about the ATTRS_TYPE type that it is specified:
TYPE ATTRS_TYPE IS TABLE OF VARCHAR2(2000) INDEX BY VARCHAR2(30);
I used to call this stored procedure as follows:
private void ExecuteNonQuery(string query, params OracleParameter[] parameters)
{
using (var connection = new OracleConnection(_connectionString))
{
var command = new OracleCommand(query, connection) { CommandType = CommandType.Text };
connection.Open();
command.Parameters.AddRange(parameters);
command.ExecuteNonQuery();
}
}
where query =
DECLARE
tNAME varchar2(100);
tATTRS PKG_ENTITY.ATTRS_TYPE;
tRESULT INTEGER;
tERRORMSG varchar2(100);
BEGIN
tNAME := :pEntityId;
tATTRS(:pPropId) := :pPropValue;
PKG_ENTITY.ALTER_ENTITY(tUSERNAME,NULL,tATTRS,NULL,tRESULT,tERRORMSG);
END;
Parameter values: pEntityId, pPropId and pPropValue are defined in the code.
Everything was fine, but then I received the requirement that it is necessary to log out values of the tRESULT and tERRORMSG parameters and with this I had great difficulties. I wanted to modify the query by adding a SELECT after calling the stored procedure. Like that:
DECLARE
tNAME varchar2(100);
tATTRS PKG_ENTITY.ATTRS_TYPE;
tRESULT INTEGER;
tERRORMSG varchar2(100);
BEGIN
tNAME := :pEntityId;
tATTRS(:pPropId) := :pPropValue;
PKG_USER.ALTER_USER(tUSERNAME,NULL,tATTRS,NULL,tRESULT,tERRORMSG);
SELECT tRESULT, tERRORMSG FROM DUAL;
END;
But such a query is not correct from the point of view of the pl/sql language. Therefore, I came to the conclusion that I need to use the stored procedure call directly and the code should look something like this:
private ProcedureResult ExecuteStoredProcedure(string procedureName)
{
using (var connection = new OracleConnection(_connectionString))
{
var command = new OracleCommand(procedureName, connection) { CommandType = CommandType.StoredProcedure };
connection.Open();
command.Parameters.Add("NAME", OracleDbType.Varchar2, "New name", ParameterDirection.Input);
command.Parameters.Add("FULLNAME", OracleDbType.Varchar2, "New fullname", ParameterDirection.Input);
var attr = new EntityAttribute() { attribute1 = "id", attribute2 = "value"};
command.Parameters.Add("ATTRS", EntityAttribute, "New fullname", ParameterDirection.Input);
command.Parameters.Add("STATUS", OracleDbType.Varchar2, "Status", ParameterDirection.Input);
command.Parameters.Add("RESULT", OracleDbType.Int32).Direction = ParameterDirection.Output;
command.Parameters.Add("ERRORMSG", OracleDbType.Varchar2).Direction = ParameterDirection.Output;
command.ExecuteNonQuery();
return new ProcedureResult()
{
StatusCode = int.Parse(command.Parameters["RESULT"].Value.ToString()),
Message = command.Parameters["ERRORMSG"].Value.ToString()
};
}
}
And here I had difficulties with the PKG_ENTITY.ATTRS_TYPE type definition.
TYPE ATTRS_TYPE IS TABLE OF VARCHAR2 (2000) INDEX BY VARCHAR2 (30);
I know that there is an IOracleCustomType interface, but I don’t understand how to implement it correctly.
For example
[OracleCustomTypeMapping("PKG_ENTITY.ATTRS_TYPE")]
public class EntityAttribute : INullable, IOracleCustomType
{
[OracleObjectMapping("ATTRIBUTE1")]
public string attribute1 { get; set; }
[OracleObjectMapping("ATTRIBUTE2")]
public string attribute2 { get; set; }
public bool IsNull => throw new System.NotImplementedException();
public void FromCustomObject(OracleConnection con, IntPtr pUdt)
{
throw new NotImplementedException();
}
public void ToCustomObject(OracleConnection con, IntPtr pUdt)
{
throw new NotImplementedException();
}
}
What should be the names of the fields of this class? I understand that 'ATTRIBUTE1' and 'ATTRIBUTE2' are not valid names.
This answer states that you cannot pass an INDEX BY VARCHAR2 associative array in C#. Instead, you can build the associative array in an anonymous PL/SQL block and call the procedure from there (as you were doing originally).
So you could use:
DECLARE
tATTRS PKG_ENTITY.ATTRS_TYPE;
BEGIN
tATTRS(:pPropId) := :pPropValue;
PKG_USER.ALTER_USER(
NAME => :pEntityId,
USERNAME => NULL,
ATTRS => tATTRS,
STATUS => NULL,
RESULT => :pResult,
ERRORMSG => :pErrorMsg
);
END;
Then pass in the parameters pPropId, pPropValue and pEntityId with the direction of ParameterDirection.Input as you were doing and pass pResult and pErrorMsg with the direction of ParameterDirection.Output.
The signature of the PL stored procedure should be
PKG_ENTITY.ALTER_ENTITY (VARCHAR2 NAME, VARCHAR2 FULLNAME, ATTRS_TYPE ATTRS, VARCHAR2 STATUS, INTEGER OUT RESULT, VARCHAR2 OUT ERRORMSG)
Do note that OUT was added to the parameter declaration.
And in the c#code you would need to make sure the parameters are marked as output
.Parameters.Add("RESULT", OracleDbType.Int32).Direction = ParameterDirection.Output;
Once done, you would just need to assign the desired value to the parameters in your PL/SQL stored procedure.
RESULT:= 0;
Calling a PL/SQL procedure is much simpler (not tested, but I assume you will get the idea):
var cmd = new OracleCommand("BEGIN PKG_ENTITY.ALTER_ENTITY(:NAME, :FULLNAME, :ATTRS, :STATUS, :RESULT, :ERRORMSG); END;"), connection);
cmd.CommandType = CommandType.Text;
// should work as well
// var cmd = new SqlCommand("PKG_ENTITY.ALTER_ENTITY(:NAME, :FULLNAME, :ATTRS, :STATUS, :RESULT, :ERRORMSG)"), connection);
// cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("NAME", OracleDbType.Varchar2, ParameterDirection.Input).Value = "New name"
cmd.Parameters.Add("FULLNAME", OracleDbType.Varchar2, ParameterDirection.Input).Value = "New fullname"
par = cmd.Parameters.Add("ATTRS", OracleDbType.Varchar2, ParameterDirection.Input);
par.CollectionType = OracleCollectionType.PLSQLAssociativeArray;
var arr string[] = new string[] {"id" , "value"};
par.Value = arr;
par.Size = arr .Count;
cmd.Parameters.Add("STATUS", OracleDbType.Varchar2, ParameterDirection.Input).Value = "Status";
cmd.Parameters.Add("RESULT", OracleDbType.Int32, ParameterDirection.Output);
cmd.Parameters("RESULT").DbType = DbType.Int32;
cmd.Parameters.Add("ERRORMSG", OracleDbType.Varchar2, 100, null, ParameterDirection.Output);
cmd.Parameters("ERRORMSG").DbType = DbType.String;
cmd.ExecuteNonQuery();
var result = System.Convert.ToInt32(cmd.Parameters("RESULT").Value);
var errmsg = cmd.Parameters("ERRORMSG").Value.ToString();
Maybe this one helps: PL/SQL Associative Array Binding
Actually, I don't remember anymore why I do cmd.Parameters("RESULT").DbType = DbType.Int32;. Perhaps it was needed to make my code compatible for ODP.NET Provider 1.x and 2.0, see Mandatory Migration of .NET 1.x Stored Procedures to .NET 2.0 or Later
ALTER PROCEDURE [dbo].[SearchMovies]
--#Year int = null,
#CategoryIds varchar(50) = null,
#Keywords nvarchar(4000) = null,
#PageIndex int = 1,
#PageSize int = 2147483644,
#TotalRecords int = null OUTPUT
As ...
EF Repository:
public class EFRepository<T> : IRepository<T> where T : BaseEntity
{
private readonly ApplicationDbContext _ctx;
private DbSet<T> entities;
string errorMessage = string.Empty;
public EFRepository(ApplicationDbContext context)
{
this._ctx = context;
entities = context.Set<T>();
}
...
public IQueryable<T> ExecuteStoredProcedureList(string commandText, params object[] parameters)
{
_ctx.Database.ExecuteSqlCommand(commandText, parameters);
return entities.FromSql(commandText, parameters);
}
}
I call this like:
var pCategoryIds = new SqlParameter()
{
ParameterName = "#CategoryIds",
Value = commaSeparatedCategoryIds,
DbType = DbType.String
};
var pKeywords = new SqlParameter()
{
ParameterName = "#Keywords",
DbType = DbType.String,
Value = name
};
var pPageIndex = new SqlParameter()
{
ParameterName = "#PageIndex",
DbType = DbType.Int32,
Value = pageIndex
};
var pPageSize = new SqlParameter()
{
ParameterName = "#PageSize",
DbType = DbType.Int32,
Value = pageSize
};
var pTotalRecords = new SqlParameter();
pTotalRecords.ParameterName = "#TotalRecords";
pTotalRecords.Direction = ParameterDirection.Output;
pTotalRecords.DbType = DbType.Int32;
var query1 = _ctx.Database.ExecuteSqlCommand("dbo.[SearchMovies] " +
"#CategoryIds, #Keywords, #PageIndex, #PageSize, #TotalRecords OUTPUT",
pCategoryIds, pKeywords, pPageIndex, pPageSize, pTotalRecords);
var query2 = _ctx.Set<MovieItem>.FromSql("dbo.[SearchMovies] " +
"#CategoryIds, #Keywords, #PageIndex, #PageSize, #TotalRecords OUTPUT",
pCategoryIds, pKeywords, pPageIndex, pPageSize, pTotalRecords);
query1 does get the output pTotalRecords fine, but no return values, and the second query2 gets the return values but no output parameter.
In EF 6, we used to have SqlQuery to do both actions in one command, how can I do the same in EF core ?
UPDATED:
Temporarily, I run 2 query, one to get the output param and one for result set.
public IQueryable<T> ExecuteStoredProcedureList(string commandText, params object[] parameters)
{
_ctx.Database.ExecuteSqlCommand(commandText, parameters);
return entities.FromSql(commandText, parameters);
}
I did something like this :-
-- My stored procedure:
CREATE PROCEDURE p1
AS
BEGIN
RETURN 29
END
GO
C# code
SqlParameter[] #params =
{
new SqlParameter("#returnVal", SqlDbType.Int) {Direction = ParameterDirection.Output}
};
_stagingContext.Database.ExecuteSqlCommand("exec #returnVal=" + storedProcName, #params);
var result = #params[0].Value; //result is 29
Hope this helps
I am working to convert some EF6 code to EF Core, and ran into this same issue. In Microsoft.EntityFrameworkCore version 2.1.0 the following use of FromSql() does return a result set and set the output parameter. You have to use .ToList() to force the proc to execute immediately, thereby returning both the results and the output param.
This is a sample from my DbContext class:
private DbQuery<ExampleStoredProc_Result> ExampleStoredProc_Results { get; set; }
public IEnumerable<ExampleStoredProc_Result> RunExampleStoredProc(int firstId, out bool outputBit)
{
var outputBitParameter = new SqlParameter
{
ParameterName = "outputBit",
SqlDbType = SqlDbType.Bit,
Direction = ParameterDirection.Output
};
SqlParameter[] parameters =
{
new SqlParameter("firstId", firstId),
outputBitParameter
};
// Need to do a ToList to get the query to execute immediately
// so that we can get both the results and the output parameter.
string sqlQuery = "Exec [ExampleStoredProc] #firstId, #outputBit OUTPUT";
var results = ExampleStoredProc_Results.FromSql(sqlQuery, parameters).ToList();
outputBit = outputBitParameter.Value as bool? ?? default(bool);
return results;
}
The stored proc returns the following entity:
public class ExampleStoredProc_Result
{
public int ID { get; set; }
public string Name { get; set; }
}
Here's the stored proc:
CREATE PROCEDURE ExampleStoredProc
#firstId int = 0,
#outputBit BIT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SET #outputBit = 1;
SELECT #firstId AS [ID], 'first' AS [NAME]
UNION ALL
SELECT #firstId + 1 AS [ID], 'second' AS [NAME]
END
GO
Hopefully anyone that comes across this post in the future finds this useful.
map into context as keyless entity
modelBuilder
.Entity<yourentity>(
eb =>
{
eb.HasNoKey();
eb.ToView("procname");
});
add a dbset
public DbSet<yourentity> yourentity { get; set; }
exclude it from migrations
modelBuilder.Entity<yourentity>().ToTable("procname", t => t.ExcludeFromMigrations());
then in context can execute it into the mapped type
var values = context.yourentity.FromSqlRaw("EXECUTE dbo.procname");
Late response, but may be useful for someone:
My stored procedure:
create procedure GetSomething(#inCode varchar(5), #outValue int OUTPUT)
as
Set #outValue = 1
end
In C#,
SqlParameter[] parameters =
{
new SqlParameter("#inCode", SqlDbType.Varchar { Direction = ParameterDirection.Input, Value = "InputValue" }),
new SqlParameter("#outValue", SqlDbType.Int { Direction = ParameterDirection.Output })
}
dbContext.Database.ExecuteSqlRaw("exec GetSomething #inCode, #outValue OUTPUT", parameters);
var result = parameters[1].Value;
The keyword OUTPUT is important while calling from .net
var results = ExampleStoredProc_Results.FromSql(sqlQuery, parameters).ToList();
DbSet.FromSql Enables you to pass in a SQL command to be executed against the database to return instances of the type represented by the DbSet.
This answer will done the trick for me.
While trying to call a stored procedure with one input and one output parameter using ExecuteSqlCommandAsync EF Core. I was avoiding using FromSql on the entity object. Below code worked great from me so just wanted to share on this old post.
SqlParameter Param1 = new SqlParameter();
Param1 .ParameterName = "#Param1";
Param1 .SqlDbType = SqlDbType.VarChar;
Param1 .Direction = ParameterDirection.Input;
Param1 .Size = 50;
Param1 .Value = "ParamValue";
SqlParameter Param2 = new SqlParameter();
Param2 .ParameterName = "#Param2";
Param2 .SqlDbType = SqlDbType.VarChar;
Param2 .Size = 2048;
Param2 .Direction = ParameterDirection.Output;
var sprocResponse = await _dbContext.Database.ExecuteSqlCommandAsync("[dbo].[StoredProcName] #Param1= #Param1, #Param2 = #Param2 OUTPUT", new object[] { Param1, Param2});
string outPutVal= urlParam.Value.ToString();
I saw many questions on stack overflow, non of them touched my own problem
procedure or function expects parameter which was not supplied
I created this SQL Server stored procedure:
CREATE proc [dbo].[spAddCustomer]
#cuName varchar(50),
#cuAddress varchar(50),
#cuMobile varchar(50),
#cuImage image,
#cityId int,
#exist int output
AS
BEGIN
IF NOT EXISTS(SELECT Cu_Mobile FROM tblCustomers WHERE Cu_Mobile = #cuMobile)
BEGIN
INSERT INTO tblCustomers (Cu_Name, Cu_Address, Cu_Mobile, Cu_Image, City_ID)
VALUES (#cuName, #cuAddress, #cuMobile, #cuImage, #cityId)
SET #exist = 1
END
ELSE
SET #exist = 0
END
In my Data Access Layer I have this method that is responsible for non-query commands:
public static int ExecuteNonQuery(string query, CommandType type, params SqlParameter[] arr)
{
int outParam;
SqlCommand cmd = new SqlCommand(query, cn);
cmd.Parameters.AddRange(arr);
cmd.CommandType = type;
int i = cmd.ExecuteNonQuery();
foreach (SqlParameter param in arr)
{
if (param.Direction == ParameterDirection.Output)
{
outParam = (int)cmd.Parameters[param.ToString()].Value;
return outParam;
}
}
return i;
}
The method responsible for creating parameters:
public static SqlParameter CreateParameter(string name, SqlDbType type, object value, ParameterDirection pd = ParameterDirection.Input)
{
SqlParameter pr = new SqlParameter();
pr.ParameterName = name;
pr.Direction = pd;
pr.SqlDbType = type;
pr.SqlValue = value;
return pr;
}
And this method in the Business Layer, pass the arguments from the Presentation Layer
public static int spAddCustomer(string cusName, string cusAddress, string cusMobile, byte[] arrImg, int cityId)
{
DataAccessLayer.Open();
int i = DataAccessLayer.ExecuteNonQuery("spAddCustomer", CommandType.StoredProcedure,
DataAccessLayer.CreateParameter("#cuName", SqlDbType.VarChar, cusName),
DataAccessLayer.CreateParameter("#cuAddress", SqlDbType.VarChar, cusAddress),
DataAccessLayer.CreateParameter("#cuMobile", SqlDbType.VarChar, cusMobile),
DataAccessLayer.CreateParameter("#cuImage", SqlDbType.Image, arrImg),
DataAccessLayer.CreateParameter("#cityId", SqlDbType.Int, cityId),
DataAccessLayer.CreateParameter("#exist", SqlDbType.Int, null, ParameterDirection.Output));
DataAccessLayer.Close();
return i;
}
When the user Click add a new record is inserted into the table (tblCustomers)
private void btnAU_Click(object sender, EventArgs e)
{
byte[] imgArr;
if (PbCustomer.Image == null)
imgArr = null;
else
{
MemoryStream ms = new MemoryStream();
PbCustomer.Image.Save(ms, PbCustomer.Image.RawFormat);
imgArr = ms.ToArray();
}
int cityId = int.Parse(cmbCities.SelectedValue.ToString());
try
{
int exist = CustomerClass.spAddCustomer(txtName.Text, txtAddress.Text, txtMobile.Text, imgArr, cityId);
if (exist == 1)
{
MessageBox.Show("A new customer has been saved");
txtAddress.Text = txtMobile.Text = txtName.Text = "";
PbCustomer.Image = null;
}
else
MessageBox.Show("A customer with the same mobile number\nalready exists, add new number!");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
But when I click the Add button (passing null image), I get this error:
procedure or function 'spAddCustomer' expects parameter '#cuImage'
which was not supplied
Despite the table tblCustomers accept null values
I just found that I can set default values for the parameter in the stored procedure:
ALTER proc [dbo].[spAddCustomer]
#cuName varchar(50)=null,
#cuAddress varchar(50)=null,
#cuMobile varchar(50)= null,
#cuImage image= null,
#cityId int= null,
#exist int output
And this solved my problem!
This is helpful specifically with null images from the PictureBox, since I have a helper method that checks for empty strings.
You need to check your input for null and use DBNull.Value when you creating the parameters. If you pass just null as a parameter - ADO.Net will ignore that.
EDIT:
You can add that check into your custom method DataAccessLayer.CreateParameter()
I have a stored procedure with an OUTPUT parameter:
CREATE PROCEDURE [dbo].[myStoredProcedure]
(#myValue NVARCHAR(100) OUTPUT)
AS
BEGIN
SET #myValue = (SELECT myValue FROM myTable WHERE something = 1)
SElECT #myValue;
END
My C# class:
public string getOutPut()
{
string shortFooter;
string sql = "myStoredProcedure";
string connStr = ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString;
using (SqlConnection DBConn = new SqlConnection(connStr))
{
using (SqlCommand DBCmd = new SqlCommand(sql, DBConn))
{
try
{
DBConn.Open();
DBCmd.CommandType = CommandType.StoredProcedure;
SqlParameter newSqlParam = new SqlParameter();
newSqlParam.ParameterName = "#myValue";
newSqlParam.SqlDbType = SqlDbType.NVarChar;
newSqlParam.Direction = ParameterDirection.Output;
DBCmd.Parameters.Add(newSqlParam);
DBCmd.ExecuteNonQuery();
shortFooter = DBCmd.Parameters["#myValue"].Value.ToString();
}
catch (Exception ex)
{
string blah = ex.ToString();
shortFooter = "";
}
}
}
return shortFooter;
}
This is the exception that I'm getting:
System.InvalidOperationException: String[0]: the Size property has an
invalid size of 0.
What I don't understand is, I have used this code before, except that in the stored procedure I'm getting the value of the output with IDENTITY() when I create a new record in the database, so the parameter type in the class is INT. But in concept it should be the same, but I guess I'm wrong, just don't know why.
What am I missing?
Thanks!!
You need to specify the size required to hold the output value. From MSDN:
For output parameters with a variable length type (nvarchar, for example), the size of the parameter defines the size of the buffer holding the output parameter.
Set the Size of the output parameter in your code to match what's in the stored procedure:
newSqlParam.Size = 100;
The default value is 0, which is what's causing the exception.
While using the using() {} (sic) blocks as shown below, and assuming that cmd1 does not live beyond the scope of the first using() {} block, why should the second block throw an exception with the message
The SqlParameter is already contained by another SqlParameterCollection
Does it mean that resources and/or handles - including the parameters (SqlParameterCollection) - attached to cmd1 are not released when its destroyed at the end of the block?
using (var conn = new SqlConnection("Data Source=.;Initial Catalog=Test;Integrated Security=True"))
{
var parameters = new SqlParameter[] { new SqlParameter("#ProductId", SqlDbType.Int ) };
using(var cmd1 = new SqlCommand("SELECT ProductName FROM Products WHERE ProductId = #ProductId"))
{
foreach (var parameter in parameters)
{
cmd1.Parameters.Add(parameter);
}
// cmd1.Parameters.Clear(); // uncomment to save your skin!
}
using (var cmd2 = new SqlCommand("SELECT Review FROM ProductReviews WHERE ProductId = #ProductId"))
{
foreach (var parameter in parameters)
{
cmd2.Parameters.Add(parameter);
}
}
}
NOTE: Doing cmd1.Parameters.Clear() just before the last brace of the first using() {} block will save you from the exception (and possible embarrassment).
If you need to reproduce you can use the following scripts to create the objects:
CREATE TABLE Products
(
ProductId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
ProductName nvarchar(32) NOT NULL
)
GO
CREATE TABLE ProductReviews
(
ReviewId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
ProductId int NOT NULL,
Review nvarchar(128) NOT NULL
)
GO
I suspect that SqlParameter "knows" which command it's part of, and that that information isn't cleared when the command is disposed, but is cleared when you call command.Parameters.Clear().
Personally I think I'd avoid reusing the objects in the first place, but it's up to you :)
Adding cmd.Parameters.Clear(); after execution should be fine.
Using blocks do not ensure that an object is "destroyed", simply that the Dispose() method is called. What that actually does is up to the specific implementation and in this case it clearly does not empty the collection. The idea is to ensure that unmanaged resources that would not be cleaned up by the garbage collector are correctly disposed. As the Parameters collection is not an unmanaged resource it is not entirely suprising it is not cleared by the dispose method.
I faced this particular error because I was using the same SqlParameter objects as part of a SqlParameter collection for calling a procedure multiple times. The reason for this error IMHO is that the SqlParameter objects are associated to a particular SqlParameter Collection and you can't use the same SqlParameter objects to create a new SqlParameter collection.
So, instead of this:
var param1 = new SqlParameter{ DbType = DbType.String, ParameterName = param1,Direction = ParameterDirection.Input , Value = "" };
var param2 = new SqlParameter{ DbType = DbType.Int64, ParameterName = param2, Direction = ParameterDirection.Input , Value = 100};
SqlParameter[] sqlParameter1 = new[] { param1, param2 };
ExecuteProc(sp_name, sqlParameter1);
/*ERROR :
SqlParameter[] sqlParameter2 = new[] { param1, param2 };
ExecuteProc(sp_name, sqlParameter2);
*/
Do this:
var param3 = new SqlParameter{ DbType = DbType.String, ParameterName = param1, Direction = ParameterDirection.Input , Value = param1.Value };
var param4 = new SqlParameter{ DbType = DbType.Int64, ParameterName = param2, Direction = ParameterDirection.Input , Value = param2.Value};
SqlParameter[] sqlParameter3 = new[] { param3, param4 };
ExecuteProc(sp_name, sqlParameter3);
using defines a scope, and does the automatic call of Dispose() for which we love it.
A reference falling out of scope will not make the object itself "disappear" if another object has a reference to it, which in this case will be the case for parameters having a reference to cmd1.
I have Also got the same issue Thanks #Jon, based on that I gave example.
When I called the below function in which 2 times same sqlparameter passed. In the first database call, it was called properly, but in the second time, it was give the above error.
public Claim GetClaim(long ClaimId)
{
string command = "SELECT * FROM tblClaim "
+ " WHERE RecordStatus = 1 and ClaimId = #ClaimId and ClientId =#ClientId";
List<SqlParameter> objLSP_Proc = new List<SqlParameter>(){
new SqlParameter("#ClientId", SessionModel.ClientId),
new SqlParameter("#ClaimId", ClaimId)
};
DataTable dt = GetDataTable(command, objLSP_Proc);
if (dt.Rows.Count == 0)
{
return null;
}
List<Claim> list = TableToList(dt);
command = "SELECT * FROM tblClaimAttachment WHERE RecordStatus = 1 and ClaimId = #ClaimId and ClientId =#ClientId";
DataTable dt = GetDataTable(command, objLSP_Proc); //gives error here, after add `sqlComm.Parameters.Clear();` in GetDataTable (below) function, the error resolved.
retClaim.Attachments = new ClaimAttachs().SelectMany(command, objLSP_Proc);
return retClaim;
}
This is the common DAL function
public DataTable GetDataTable(string strSql, List<SqlParameter> parameters)
{
DataTable dt = new DataTable();
try
{
using (SqlConnection connection = this.GetConnection())
{
SqlCommand sqlComm = new SqlCommand(strSql, connection);
if (parameters != null && parameters.Count > 0)
{
sqlComm.Parameters.AddRange(parameters.ToArray());
}
using (SqlDataAdapter da = new SqlDataAdapter())
{
da.SelectCommand = sqlComm;
da.Fill(dt);
}
sqlComm.Parameters.Clear(); //this added and error resolved
}
}
catch (Exception ex)
{
throw;
}
return dt;
}
I encountered this exception because I had failed to instantiate a parameter object. I thought it was complaining about two procedures having parameters with the same name. It was complaining about the same parameter being added twice.
Dim aParm As New SqlParameter()
aParm.ParameterName = "NAR_ID" : aParm.Value = hfCurrentNAR_ID.Value
m_daNetworkAccess.UpdateCommand.Parameters.Add(aParm)
aParm = New SqlParameter
Dim tbxDriveFile As TextBox = gvNetworkFileAccess.Rows(index).FindControl("tbxDriveFolderFile")
aParm.ParameterName = "DriveFolderFile" : aParm.Value = tbxDriveFile.Text
m_daNetworkAccess.UpdateCommand.Parameters.Add(aParm)
**aParm = New SqlParameter()** <--This line was missing.
Dim aDDL As DropDownList = gvNetworkFileAccess.Rows(index).FindControl("ddlFileAccess")
aParm.ParameterName = "AccessGranted" : aParm.Value = aDDL.Text
**m_daNetworkAccess.UpdateCommand.Parameters.Add(aParm)** <-- The error occurred here.
Issue
I was executing a SQL Server stored procedure from C# when I encountered this issue:
Exception message [The SqlParameter is already contained by another SqlParameterCollection.]
Cause
I was passing 3 parameters to my stored procedure. I added the
param = command.CreateParameter();
only once altogether. I should have added this line for each parameter, it means 3 times altogether.
DbCommand command = CreateCommand(ct.SourceServer, ct.SourceInstance, ct.SourceDatabase);
command.CommandType = CommandType.StoredProcedure;
command.CommandText = "[ETL].[pGenerateScriptToCreateIndex]";
DbParameter param = command.CreateParameter();
param.ParameterName = "#IndexTypeID";
param.DbType = DbType.Int16;
param.Value = 1;
command.Parameters.Add(param);
param = command.CreateParameter(); --This is the line I was missing
param.ParameterName = "#SchemaName";
param.DbType = DbType.String;
param.Value = ct.SourceSchema;
command.Parameters.Add(param);
param = command.CreateParameter(); --This is the line I was missing
param.ParameterName = "#TableName";
param.DbType = DbType.String;
param.Value = ct.SourceDataObjectName;
command.Parameters.Add(param);
dt = ExecuteSelectCommand(command);
Solution
Adding the following line of code for each parameter
param = command.CreateParameter();
This is how I have done it!
ILease lease = (ILease)_SqlParameterCollection.InitializeLifetimeService();
if (lease.CurrentState == LeaseState.Initial)
{
lease.InitialLeaseTime = TimeSpan.FromMinutes(5);
lease.SponsorshipTimeout = TimeSpan.FromMinutes(2);
lease.RenewOnCallTime = TimeSpan.FromMinutes(2);
lease.Renew(new TimeSpan(0, 5, 0));
}
If you're using EntityFramework
I also had this same exception. In my case, I was calling SQL via a EntityFramework DBContext. The following is my code, and how I fixed it.
Broken Code
string sql = "UserReport #userID, #startDate, #endDate";
var sqlParams = new Object[]
{
new SqlParameter { ParameterName= "#userID", Value = p.UserID, SqlDbType = SqlDbType.Int, IsNullable = true }
,new SqlParameter { ParameterName= "#startDate", Value = p.StartDate, SqlDbType = SqlDbType.DateTime, IsNullable = true }
,new SqlParameter { ParameterName= "#endDate", Value = p.EndDate, SqlDbType = SqlDbType.DateTime, IsNullable = true }
};
IEnumerable<T> rows = ctx.Database.SqlQuery<T>(sql,parameters);
foreach(var row in rows) {
// do something
}
// the following call to .Count() is what triggers the exception
if (rows.Count() == 0) {
// tell user there are no rows
}
Note: the above call to SqlQuery<T>() actually returns a DbRawSqlQuery<T>, which implements IEnumerable
Why does calling .Count() throw the exception?
I haven't fired up SQL Profiler to confirm, but I suspect that .Count() is triggering another call to SQL Server, and internally it is reusing the same SQLCommand object and trying to re-add the duplicate parameters.
Solution / Working Code
I added a counter inside my foreach, so that I could keep a row count without having to call .Count()
int rowCount = 0;
foreach(var row in rows) {
rowCount++
// do something
}
if (rowCount == 0) {
// tell user there are no rows
}
Afterthough
My project is probably using an old version of EF. The newer version may have fixed this internal bug by clearing the parameters, or disposing of the SqlCommand object.
Or maybe, there are explicit instructions that tell developers not to call .Count() after iterating a DbRawSqlQuery, and I'm coding it wrong.