I am using Dapper's QueryMultipleAsync method to read multiple result set. My stored procedure checks some conditions (for example a user might be trying to get someone else's data by sending an Id to my API) to determine whether the data should return or not.
I, on C# side, first need to read the return value (not result set) to determine if data is returned or SP simply returned 3 (which means insufficient rights). To illustrate the case:
IF #UserRole < 10
BEGIN
RETURN 3; -- Insufficient rights.
END
IF #IsCurrentUserOwner = 0
BEGIN
RETURN 5; -- Not owner.
END
-- Get users.
SELECT
Id
, [Name]
, LastName
FROM
Users
-- WHERE ...
-- Get chat messages.
SELECT
*
FROM
ChatMessages
-- WHERE ...
I know that output and return values are written at the end of reader so I can only read return parameter when all the data (result set) is read. So, I always have to read the result set first then return/output parameters.
What if my SP looked like this:
-- ...some code above...
IF #IsCurrentUserOwner = 0
BEGIN
RETURN 5; -- Not owner.
END
DECLARE #RType TINYINT = NULL
-- some other code here to get #RType value here...
SELECT #RType -- To make this a result so QueryMultiple's reader can read this.
-- ...some other code to get users and chat messages...
To describe the problem:
#RType variable could be 5 as well as SP's return value. When I read the result first (because output/return parameters are at the end of reader), how do I know the value I just read (which is 5 in this case) is the #RType or return value? (they are convertible)
This is how my C# code roughly looks like:
var parameters = new DynamicParameters();
parameters.Add("#RetVal", dbType: DbType.Int32, direction: ParameterDirection.ReturnValue);
var dbResult = await Context.SqlConnection.QueryMultipleAsync(sql: "my_sp_name", param: parameters, commandType: CommandType.StoredProcedure)
// Some code here...
using (var reader = dbResult)
{
var rType = reader.Read<byte>(); // <----- How to know if I read #RType or just return value of SP since they are convertible to each other?
var users = reader.Read<User>();
var chatMessages = reader.Read<ChatMessage>();
// ...
}
var returnValue = parameters.Get<int>("#RetVal");
if (returnValue == 5)
{
return "You are not allowed to see this data";
}
How do you recommend me to handle the case?
Your C# code uses async - await (QueryMultipleAsync) and currently Dapper doesn't support for Async methods that no perform a SELECT statement.
There is an open issue about that in Github:
https://github.com/StackExchange/Dapper/issues/591
Option 1
You can split your stored procedure into 2 parts:
1. Check the Return value.
2. Getting the select statements.
Option 2
ALTER PROCEDURE dbo.sp
... your params..
#Output INT OUTPUT
AS
IF #UserRole < 10
BEGIN
SET #Output = 3; -- Insufficient rights.
END
IF #IsCurrentUserOwner = 0
BEGIN
SET #Output = 5; -- Not owner.
END
-- Get users.
SELECT
Id
, [Name]
, LastName
FROM
Users
-- WHERE ...
-- Get chat messages.
SELECT
*
FROM
ChatMessages
-- WHERE ...
Option 3
Use ADO.NET
I wasn't really clear on your question, but the snippet below should work. Since you are defining the #RetVal as ParameterDirection.ReturnValue you should be able to access it before accessing the actual readers. I was able to verify this works in SQL SERVER.
var returnValue = parameters.Get<int>("#RetVal");
if (returnValue == 5 || returnValue == 3)
{
return "You are not allowed to see this data";
}
using (var reader = dbResult)
{
var rType = reader.Read<byte>(); // <----- How to know if I read #RType or just return value of SP since they are convertible to each other?
var users = reader.Read<User>();
var chatMessages = reader.Read<ChatMessage>();
// ...
}
here is my stored procedure
ALTER PROCEDURE mysp_TestOne
#p1 int
AS
BEGIN
SET NOCOUNT ON;
if #p1 > 0
return 5;
SELECT 1 F1
union
Select 2 F1;
SELECT 1 F1
union
Select 2 F1;
END
GO
and more complete snippet of c#;
using (SqlConnection cn = new SqlConnection(connectionString))
{
var sql = #"mysp_TestOne";
var parameters = new DynamicParameters();
// #p1 triggers whether to return query results or just the returnvalue
parameters.Add("p1", 0, direction: ParameterDirection.Input);
parameters.Add("#RetVal", 0, direction: ParameterDirection.ReturnValue);
var dbResult = await cn.QueryMultipleAsync(sql, parameters, commandType: CommandType.StoredProcedure);
var returnValue = parameters.Get<int>("#RetVal");
if (returnValue == 5 || returnValue == 3)
{
return "You are not allowed to see this data";
}
using (var reader = dbResult)
{
var rType = reader.Read<byte>(); // <----- How to know if I read #RType or just return value of SP since they are convertible to each other?
var users = reader.Read<User>();
var chatMessages = reader.Read<ChatMessage>();
// ...
}
}
HTH
Related
I have a procedure which returns the identity of the record added. I am using Entity Framework to call the procedure and retrieve the value, but it is always 0.
This is the code - can you figure out why it is not returning the identity value?
C# Entity Framework domain code:
var cNumber = new SqlParameter("CNumber", acctSl.cNumber);
var fId = new SqlParameter("FId", acctSl.FId);
var splAmt = new SqlParameter("SplAmt", acctSl.SplAmt);
var frDt = new SqlParameter("FrDt", acctSl.FrDate);
var toDt = new SqlParameter("ToDt", acctSl.ToDate);
var user = new SqlParameter("User", acctSl.User);
var id = new SqlParameter("Id", "")
{
Direction = ParameterDirection.Output,
SqlDbType = SqlDbType.VarChar
};
var sql = "EXECUTE [dbo].[InsertAcctSpl] #CNumber, #FID, #SplAmt, #FrDt, #ToDt, #User, #Id OUTPUT";
var result = DbContext.Database.ExecuteSqlRaw(sql, cNumber, fId, splAmt, frDt, toDt, user, id);
int rowsAffected;
var yourOutput = Convert.ToInt32(id.Value);
if (result > 0)
{
acctSl.AcctId = yourOutput;
}
else
{
acctSl.AcctId = 0;
}
SQL Server procedure:
ALTER PROCEDURE [dbo].[InsertAccountsSpend]
#CNumber varchar(15),
#FId bigint,
#SplAmt money,
#FrDt date,
#ToDt date,
#User bigint,
#Id bigint OUTPUT
AS
INSERT INTO AcctSpend (CNmbr, FID, SplAmt, FrDt, ToDt,
Cr8Dt, Cr8User_ID, UpdtDt, UpdtUser_ID)
VALUES (#CNumber, #FId, #splAmt, #FroDt,# ToDt,
GETDATE(), #User, GETDATE(), #User)
SET #id = SCOPE_IDENTITY()
RETURN #id
The issue was with the data type it needed be long
var id = new SqlParameter("Id", "")
{
Direction = ParameterDirection.Output,
**SqlDbType = SqlDbType.BigInt**
};
Your problem seems to be with this section of your code:
int rowsAffected;
var yourOutput = Convert.ToInt32(id.Value);
if (rowsAffected > 0)
{
acctSl.AcctId = yourOutput;
}
else
{
acctSl.AcctId = 0;
}
You are basing your if-else logic off of the rowsAffected variable, but that variable is never assigned the value from your stored procedures output. Since rowsAffected is declared as an int type variable, it cannot be null, so it is automatically set to 0.
To get the actual value for rowsAffected you will need to utilize the data returned in your result variable that you have declared here:
var result = DbContext.Database.ExecuteSqlRaw(sql, cNumber, fId, splAmt, frDt, toDt, user, id);
EDIT
It appears that the syntax around your id SQL parameter object is incorrect. Try creating this object in the following manner:
var id = new SqlParameter
{
ParameterName = "Id",
DbType = System.Data.DbType.String,
Direction = System.Data.ParameterDirection.Output
};
I have a stored procedure for a travel booking system that takes in a TripID and identifies whether the trip is domestic or not (e.g. whether the origin and destination countries are the same for all trip legs). When I run the procedure from SSMS, it correctly returns 1 for domestic and 0 for international. However, when I try to access the data in my application through DataReader, it inappropriately returns 0 for domestic trips.
That being said, I don't think the problem lies purely with the DataReader because when I alter my stored procedure to return 1 immediately, DataReader will correctly detect this value.
Can anyone suggest changes to my code to fix this behavior?
Here is the stored procedure, pared down:
SET NOCOUNT ON;
-- EXEC CheckIsDomestic 6343
Declare #HomeOffice INT = (SELECT TOP 1 o.DestinationID
FROM TR_Trips t
JOIN TR_Travelers ta ON t.TravelerID = ta.TravelerID
JOIN TR_OfficeLocations o ON ta.OfficeID = o.Office_Loc_Id
WHERE t.TripID = #TripID)
SELECT l.Destination_ID AS DestinationID
INTO #TempDest
FROM TR_Trips t JOIN TR_Legs l ON t.TripID = l.TripID
WHERE t.TripID = #TripID
--Check whether there is a destination in the list that is different than the home country
DECLARE #CountRows int = (SELECT COUNT(*)
FROM #TempDest t
WHERE DestinationID <> #HomeOffice )
IF #CountRows > 0
BEGIN
SELECT 0
RETURN --tried with and without RETURN; no change
END
ELSE
BEGIN SELECT 1
RETURN
END
And here are the applicable parts of my application:
public bool IsDomestic(int TripID)
{
bool ReturnValue = true;
NewStoredProcedureCommand("CheckIsDomestic");
AddParameter("#TripID", TripID, System.Data.SqlDbType.Bit);
ReturnValue = Execute_ReturnValueBool();
return ReturnValue;
}
public Boolean Execute_ReturnValueBool()
{
if (sqlCommand == null)
NewCommand();
if (sqlCommand.Connection.State != ConnectionState.Open)
sqlCommand.Connection.Open();
bool ReturnValue = false;
SqlDataReader DR = sqlCommand.ExecuteReader();
if (DR.HasRows)
{
DR.Read();
System.Diagnostics.Debug.WriteLine(DR[0]);
ReturnValue = Convert.ToBoolean(DR[0]);
}
DR.Close();
sqlCommand.Connection.Close();
return ReturnValue;
}
Why are you using the BIT type for your TripID parameter in the application code? Try setting it to INT.
Problem:
When values are provided to the following script then executed using a setup in C# like below (or in SQL Server environment) the values do not update in the database.
Stored procedure:
-- Updates the Value of any type of PropertyValue
-- (Type meaining simple Value, UnitValue, or DropDown)
CREATE PROCEDURE [dbo].[usp_UpdatePropertyValue]
#PropertyValueID int,
#Value varchar(max) = NULL,
#UnitValue float = NULL,
#UnitOfMeasureID int = NULL,
#DropDownOptionID int = NULL
AS
BEGIN
-- If the Property has a #Value, Update it.
IF #Value IS NOT NULL
BEGIN
UPDATE [dbo].[PropertyValue]
SET
Value = #Value
WHERE
[dbo].[PropertyValue].[ID] = #PropertyValueID
END
-- Else check if it has a #UnitValue & UnitOfMeasureID
ELSE IF #UnitValue IS NOT NULL AND #UnitOfMeasureID IS NOT NULL
BEGIN
UPDATE [dbo].[UnitValue]
SET
UnitValue = #UnitValue,
UnitOfMeasureID = #UnitOfMeasureID
WHERE
[dbo].[UnitValue].[PropertyValueID] = #PropertyValueID
END
-- Else check if it has just a #UnitValue
ELSE IF #UnitValue IS NOT NULL AND #UnitOfMeasureID IS NULL
BEGIN
UPDATE [dbo].[UnitValue]
SET
UnitValue = #UnitValue
WHERE
[dbo].[UnitValue].[PropertyValueID] = #PropertyValueID
END
-- Else check if it has a #DropDownSelection to update.
ELSE IF #DropDownOptionID IS NULL
BEGIN
UPDATE [dbo].[DropDownSelection]
SET
SelectedOptionID = #DropDownOptionID
WHERE
[dbo].[DropDownSelection].[PropertyValueID] = #PropertyValueID
END
END
When I do an execution of this script, like below, it does not update any values.
Example execution:
String QueryString = "EXEC [dbo].[usp_UpdatePropertyValue] #PropertyValueID, #Value, #UnitValue, #UnitOfMeasureID, #DropDownOptionID";
SqlCommand Cmd = new SqlCommand(QueryString, this._DbConn);
Cmd.Parameters.Add(new SqlParameter("#PropertyValueID", System.Data.SqlDbType.Int));
Cmd.Parameters.Add(new SqlParameter("#Value", System.Data.SqlDbType.Int));
Cmd.Parameters.Add(new SqlParameter("#UnitValue", System.Data.SqlDbType.Int));
Cmd.Parameters.Add(new SqlParameter("#UnitOfMeasureID", System.Data.SqlDbType.Int));
Cmd.Parameters.Add(new SqlParameter("#DropDownOptionID", System.Data.SqlDbType.Int));
Cmd.Parameters["#PropertyValueID"].Value = Property.Value.ID; // 1
Cmd.Parameters["#Value"].IsNullable = true;
Cmd.Parameters["#Value"].Value = DBNull.Value;
Cmd.Parameters["#UnitValue"].IsNullable = true;
Cmd.Parameters["#UnitValue"].Value = DBNull.Value;
Cmd.Parameters["#UnitOfMeasureID"].IsNullable = true;
Cmd.Parameters["#UnitOfMeasureID"].Value = DBNull.Value;
Cmd.Parameters["#DropDownOptionID"].IsNullable = true;
Cmd.Parameters["#DropDownOptionID"].Value = 2; // Current Value in DB: 3
Details:
After running an execute (via C# code or SQL Server environment) it does not update dbo.DropDownSelection.SelectedOptionID. I'm guessing that it might be because dbo.DropDownSelection.SelectedOptionID is non-nullable and the parameter I'm using to set it is nullable (despite that when setting it shouldn't ever be null). Upon execution the return value is 0. If I run one of the Updates outside of the procedure they work perfectly, hence my suspicion that it has to do with null-able types.
Question(s):
Could this be because the parameters to the stored procedure are nullable and the fields I'm setting aren't?
If not, what could it be?
It looks like you're passing in Null for every argument except for PropertyValueID and DropDownOptionID, right? I don't think any of your IF statements will fire if only these two values are not-null. In short, I think you have a logic error.
Other than that, I would suggest two things...
First, instead of testing for NULL, use this kind syntax on your if statements (it's safer)...
ELSE IF ISNULL(#UnitValue, 0) != 0 AND ISNULL(#UnitOfMeasureID, 0) = 0
Second, add a meaningful PRINT statement before each UPDATE. That way, when you run the sproc in MSSQL, you can look at the messages and see how far it's actually getting.
You can/should set your parameter to value to DBNull.Value;
if (variable == "")
{
cmd.Parameters.Add("#Param", SqlDbType.VarChar, 500).Value = DBNull.Value;
}
else
{
cmd.Parameters.Add("#Param", SqlDbType.VarChar, 500).Value = variable;
}
Or you can leave your server side set to null and not pass the param at all.
In a SQL Server 2008 I have a simple stored procedure moving a bunch of records to another table:
CREATE PROCEDURE [dbo].MyProc(#ParamRecDateTime [datetime])
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO [dbo].Table2
SELECT
...,
...
FROM [dbo].Table1
WHERE RecDateTime <= #ParamRecDateTime
DELETE FROM [dbo].Table1
WHERE RecDateTime <= #ParamRecDateTime
END
Running it from within SQL Server Management Studio, I get the job done and return value = 0
DECLARE #return_value int
EXEC #return_value = dbo.MyProc #ParamRecDateTime = '2011-06-25 11:00:00.000'
SELECT 'Return Value' = #return_value
But when I call the same stored procedure from an app using Entity framework, I also get the job done but the return value is "-1":
int result = myrepository.MyProc(datetimePar);
MessageBox.Show(result.ToString());
I didn't manage to find an explanation for this error, but found this discouraging post, where it's said that there is no standard for this type of return codes in SQL Server.
What is the good, reliable way of getting know of a Stored Procedure execution result when calling it from Entity Framework and when the Stored Procedure doesn't return any entities?
One way to do it is to call ExecuteStoreCommand, and pass in a SqlParameter with a direction of Output:
var dtparm = new SqlParameter("#dtparm", DateTime.Now);
var retval = new SqlParameter("#retval", SqlDbType.Int);
retval.Direction = ParameterDirection.Output;
context.ExecuteStoreCommand("exec #retval = MyProc #dtparm", retval, dtparm);
int return_value = (int)retval.Value;
Originally I tried using a direction of ReturnValue:
retval.Direction = ParameterDirection.ReturnValue;
context.ExecuteStoreCommand("MyProc #dtparm", retval, dtparm);
but retval.Value would always be 0. I realized that retval was the result of executing the MyProc #dtparm statement, so I changed it to capture the return value of MyProc and return that as an output parameter.
using (dbContext db = new dbContext())
{
var parameters = new[]
{
new SqlParameter("#1","Input Para value"),
new SqlParameter("#2",SqlDbType.VarChar,4){ Value = "default if you want"},
new SqlParameter("#3",SqlDbType.Int){Value = 0},
new SqlParameter("#4","Input Para Value"),
new SqlParameter("#5",SqlDbType.VarChar,10 ) { Direction = ParameterDirection.Output },
new SqlParameter("#6",SqlDbType.VarChar,1000) { Direction = ParameterDirection.Output }
};
db.ExecuteStoreCommand("EXEC SP_Name #1,#2,#3,#4,#5 OUT,#6 OUT", parameters);
ArrayList ObjList = new ArrayList();
ObjList.Add(parameters[1].Value);
ObjList.Add(parameters[2].Value);
}
See OUTPUT attribute for SQL param of store procedure,
here
For future reference: I had the same issue but needed multiple OUTPUT variables. The solution was a combination of both answers. Below is a complete sample.
public void MyStoredProc(int inputValue, out decimal outputValue1, out decimal outputValue2)
{
var parameters = new[] {
new SqlParameter("#0", inputValue),
new SqlParameter("#1", SqlDbType.Decimal) { Direction = ParameterDirection.Output },
new SqlParameter("#2", SqlDbType.Decimal) { Direction = ParameterDirection.Output }
};
context.ExecuteStoreCommand("exec MyStoredProc #InParamName=#0, #OutParamName1=#1 output, #OutParamName2=#2 output", parameters);
outputValue1 = (decimal)parameters[1].Value;
outputValue2 = (decimal)parameters[2].Value;
}
Please note the Types used (decimal.) If another type is needed, remember to not only change it in the method argument list but also the SqlDbType.XXX.
I'm writing a sql script, and I'd like to use Management Studio to develop the query, and a C# program to run it in production.
My query contains parameters, like so;
SELECT * FROM TABLE
WHERE id = #id
I can feed in a value for #id in the C# program, and that works nicely. However, I also want to declare default values for testing in Management Studio. So I really want to write something like this pseudocode;
if not declared #id
declare #id int
set #id=43
end if
SELECT * FROM TABLE
WHERE id = #id
Is there any way to check to see if a variable name has already been taken?
You can't do exactly what you're after. I'd suggest either:
1) wrap the script up as a sproc and give defaults for the params
2) include a comment block at the top of the script that you can then uncomment when running in SSMS:
/*
-- Default variables for testing
DECLARE #Id INTEGER
SET #Id = 43
*/
SELECT * FROM Table WHERE id = #Id
I've managed to make some progress by marking out the default variables in the script, like so;
/** DEFAULTS **/
declare #id int
set #id = 43
/** END DEFAULTS **/
Then preprocessing the script in my C# program, like so;
script = RemoveBlock(script, "DEFAULTS");
And implementing the function like so;
public static string RemoveBlock(string script, string blockName)
{
if (script == null) { return null; }
var startTag = string.Format("/** {0} **/", blockName);
var endTag = string.Format("/** END {0} **/", blockName);
var startTagIdx = script.IndexOf(startTag);
if (startTagIdx == -1) { return script; }
var endTagIdx = script.IndexOf(endTag, startTagIdx + startTag.Length);
if (endTagIdx == -1) { return script; }
var endOfEndTag = endTagIdx + endTag.Length;
var beforeBlock = script.Substring(0, startTagIdx);
var afterBlock = script.Substring(endOfEndTag);
return beforeBlock + afterBlock;
}
So the C# program runs a version without the variables but with parameters.