How can i get #rErr / Output Parameter value in c#? - c#

I create a SQL stored procedure
create proc p1
(
#name1 nvarchar(50),
#rErr int OUTPUT
)
as
begin Transaction
insert into test (name1)
values (#name1)
if #name1 = 'n100'
Begin
Rollback Transaction
Set #rErr=50001
Return #rErr
End
commit Transaction
How can I get the #rErr value in C#?

Accessing output parameters is a little awkward; you need to add them to the parameters collection in the usual way, with with a .Direction of Output. Then you can read the .Value of the parameter object after executing the method. However: it is much easier to use select and process it as a result. Note that return values can be done in a similar way, but with the appropriate .Direction. The fact that you both output and return it, in this case, makes it even more fun... I'd just use the output part, personally. Or ... throw an exception (raiserrror).
Something like:
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "p1";
cmd.CommandType = CommandType.StoredProcedure;
var name = cmd.CreateParameter();
name.ParameterName = "#name1";
name.Value = "abc"; // etc
cmd.Parameters.Add(name);
var err = cmd.CreateParameter();
err.ParameterName = "#rErr";
err.Direction = ParameterDirection.Output;
cmd.Parameters.Add(err);
cmd.ExecuteNonQuery();
if (err.Value is int i)
{
// error i happened
}
}
However: if you'd just used:
raiserror (50001, 16, 1) -- severity needs to be at least 16 here; typically = 16
or (in more recent SQL Server versions):
throw 50001, 'oops', 1
you can get a similar result much more easily; this will result in an Exception direction from the ADO.NET layer.
(note that you should add the custom error message formally to sysmessages when using raiserror - throw doesn't require that step)
If you used the throw (or raiserror) approach, and removed the output parameter, this entire piece of code could become, with some help from Dapper:
conn.Execute("p1", new { name1 = "abc" }, commandType: CommandType.StoredProcedure);
which is a lot easier to get right!

You don't need transaction for a single insert, you can refactor your SP like following. To return the code as output parameter, you can simply use RETURN(50001)
CREATE PROC P1 (#name1 NVARCHAR(50),
#rErr INT output)
AS
IF #name1 <> 'n100'
BEGIN
INSERT INTO test(name1)
VALUES (#name1)
RETURN(0)
END
RETURN(50001)

Related

How to insert statement in SQL with variable containing quotes from a procedure fired in C#?

I need help, I have a problem while inserting a statement in SQL.
I call a SQL statement from my ASP.NET program, some variables contain quotes so when the insert is fired I have an exception like:
Exception Details: System.Data.SqlClient.SqlException: Incorrect syntax near 'xxxxx'.
Unclosed quotation mark after the character string ''.
I don't want the content of my variable to be changed...
Any idea how to handle this?
The C# part :
SqlCommand cmdInsertAssessment = new SqlCommand("xxxxxxx", sqlCnx);
cmdInsertAssessment.CommandType = CommandType.StoredProcedure;
cmdInsertAssessment.Parameters.AddWithValue("#templateID", templateID);
cmdInsertAssessment.Parameters.AddWithValue("#companyID", companyID);
cmdInsertAssessment.Parameters.AddWithValue("#userID",userID);
cmdInsertAssessment.Parameters.AddWithValue("#opn",opn);
cmdInsertAssessment.Parameters.AddWithValue("#mn",Mm);
cmdInsertAssessment.Parameters.AddWithValue("#max",max);
cmdInsertAssessment.Parameters.AddWithValue("#remarque",remarque);
cmdInsertAssessment.Parameters.AddWithValue("#templateTheme",templateTheme);
cmdInsertAssessment.Parameters.AddWithValue("#name", sName);
cmdInsertAssessment.Parameters.AddWithValue("#finished", iFinished);
cmdInsertAssessment.Parameters.AddWithValue("#datenow", dtNow);
try
{
cmdInsertAssessment.ExecuteNonQuery();
}
catch (Exception e)
{
}
SQL part :
CREATE PROCEDURE ["xxxxxxx"] #templateID int,
#companyID int,
#userID int,
#opn nvarchar(255),
#mn nvarchar(255),
#max int,
#remarque nvarchar(255),
#templateTheme nvarchar(255),
#name nvarchar(255),
#finished int,
#datenow datetime
AS
BEGIN
DECLARE
#points AS FLOAT
SET #points=0
IF(#mn='M')
BEGIN
IF(#opn='O')
BEGIN
SET #points=10
END
IF(#opn='P')
BEGIN
SET #points=2
END
END
IF(#mn!='M')
BEGIN
IF(#opn='O')
BEGIN
SET #points=2
END
if(#opn='P')
BEGIN
SET #points=1
END
END
IF(#remarque=NULL)
BEGIN
SET #remarque='nothing'
END
MERGE INTO [dbo].[Assessment] as target
USING (SELECT #templateID,#companyID,#userID,#opn,#points,#max,#remarque,#templateTheme,#datenow,#name,#finished)
As source (_templateID,_companyID,_userID,_opn,_points,_max,_remarque,_templateTheme,_datenow,_name,_finished)
ON target.TemplateID=source._templateID
AND target.TemplateTheme=source._templateTheme
AND target.NameAssessment=source._name
WHEN MATCHED THEN
UPDATE SET Points = source._points, Remarque = source._remarque, FillDate= source._datenow, Finished = source._finished, OPN = source._opn
WHEN NOT MATCHED THEN
INSERT (TemplateID, CompanyID, UserID, OPN, Points, Max, Remarque, TemplateTheme, FillDate, NameAssessment,Finished)
VALUES (source._templateID,source._companyID,source._userID,source._opn,source._points,source._max,source._remarque,source._templateTheme,source._datenow,source._name,source._finished);
END
GO
Thanks :)
Taking things from the begining ! Your procedure calculates a number of points, based on parameters you supply (#mn, #opn), then inserts or updates table Assessment. The first thing to say is that this is not a job for Merge. Merge is intended to operate on two tables, and using it for a row and a table is using a sledgehammer to crack a nut. You should really use
IF EXISTS( SELECT ID FROM ASSESSMENT WHERE... )
then write a classic insert and a classic update. Your procedure will be easier to understand, easier to maintain, and likely run much faster.
If you're still reading, I'll keep going. The calculation of points, business logic that uses nothing from the DB, will be much happier in the C#. Wherever you put it, you can use ternary operators to shorten those either-or choices. The following replaces 20 lines in your procedure.
var points = (mn == 'm')?(opn == 'O'?10:2):(opn == 'O'?2:1);
The assignment starting IF( #remarque = null ) can be done with a null coalescing operator ISNULL() in sql, ?? in C#.
And if you're still still reading, grab QueryFirst. You'll get a v.clean separation between SQL and C# and all your parameter creation will be done for you.
Because you said you wanted to use stored procedures
using (SqlConnection cnn = new SqlConnection(/*Connection String*/))
{
using (SqlCommand cmd = new SqlCommand("MyStoredProcedure", cnn))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("#param1", "Value 1");
cmd.Parameters.AddWithValue("#param2", "xxxxxx");
cnn.Open();
}
}

Logging not Persisting When Exception Occurs in Method Executed in a Trigger

I have been stuck all day on this issue and cannot seem to find anything online pointing me to what might be causing it.
I have the below logging method in a Logger class and the below code calling the logger. When no exception occurs all the log statements work perfectly, however when an exception occurs the log statements do not run at all (however they do run from the web service call).
Logger Log Method:
public static Guid WriteToSLXLog(string ascendId, string masterDataId, string masterDataType, int? status,
string request, string requestRecieved, Exception ex, bool isError)
{
var connection = ConfigurationManager.ConnectionStrings["AscendConnectionString"];
string connectionString = "context connection=true";
// define INSERT query with parameters
var query =
"INSERT INTO " + AscendTable.SmartLogixLogDataTableName +
" (LogID, LogDate, AscendId, MasterDataId, MasterDataType, Status, Details, Request, RequestRecieved, StackTrace, IsError) " +
"VALUES (#LogID, #LogDate, #AscendId, #MasterDataId, #MasterDataType, #Status, #Details, #Request, #RequestRecieved, #StackTrace, #IsError)";
var logId = Guid.NewGuid();
using (var cn = new SqlConnection(connectionString))
{
if (!cn.State.Equals(ConnectionState.Open))
{
cn.Open();
}
// create command
using (var cmd = new SqlCommand(query, cn))
{
try
{
// define parameters and their values
cmd.Parameters.Add("#LogID", SqlDbType.UniqueIdentifier).Value = logId;
cmd.Parameters.Add("#LogDate", SqlDbType.DateTime).Value = DateTime.Now;
if (ascendId != null)
{
cmd.Parameters.Add("#AscendId", SqlDbType.VarChar, 24).Value = ascendId;
}
else
{
cmd.Parameters.Add("#AscendId", SqlDbType.VarChar, 24).Value = DBNull.Value;
}
cmd.Parameters.Add("#MasterDataId", SqlDbType.VarChar, 50).Value = masterDataId;
cmd.Parameters.Add("#MasterDataType", SqlDbType.VarChar, 50).Value = masterDataType;
if (ex == null)
{
cmd.Parameters.Add("#Status", SqlDbType.VarChar, 50).Value = status.ToString();
}
else
{
cmd.Parameters.Add("#Status", SqlDbType.VarChar, 50).Value = "2";
}
if (ex != null)
{
cmd.Parameters.Add("#Details", SqlDbType.VarChar, -1).Value = ex.Message;
if (ex.StackTrace != null)
{
cmd.Parameters.Add("#StackTrace", SqlDbType.VarChar, -1).Value =
ex.StackTrace;
}
else
{
cmd.Parameters.Add("#StackTrace", SqlDbType.VarChar, -1).Value = DBNull.Value;
}
}
else
{
cmd.Parameters.Add("#Details", SqlDbType.VarChar, -1).Value = "Success";
cmd.Parameters.Add("#StackTrace", SqlDbType.VarChar, -1).Value = DBNull.Value;
}
if (!string.IsNullOrEmpty(request))
{
cmd.Parameters.Add("#Request", SqlDbType.VarChar, -1).Value = request;
}
else
{
cmd.Parameters.Add("#Request", SqlDbType.VarChar, -1).Value = DBNull.Value;
}
if (!string.IsNullOrEmpty(requestRecieved))
{
cmd.Parameters.Add("#RequestRecieved", SqlDbType.VarChar, -1).Value = requestRecieved;
}
else
{
cmd.Parameters.Add("#RequestRecieved", SqlDbType.VarChar, -1).Value = DBNull.Value;
}
if (isError)
{
cmd.Parameters.Add("#IsError", SqlDbType.Bit).Value = 1;
}
else
{
cmd.Parameters.Add("#IsError", SqlDbType.Bit).Value = 0;
}
// open connection, execute INSERT, close connection
cmd.ExecuteNonQuery();
}
catch (Exception e)
{
// Do not want to throw an error if something goes wrong logging
}
}
}
return logId;
}
My Method where the logging issues occur:
public static void CallInsertTruckService(string id, string code, string vinNumber, string licPlateNo)
{
Logger.WriteToSLXLog(id, code, MasterDataType.TruckType, 4, "1", "", null, false);
try
{
var truckList = new TruckList();
var truck = new Truck();
truck.TruckId = code;
if (!string.IsNullOrEmpty(vinNumber))
{
truck.VIN = vinNumber;
}
else
{
truck.VIN = "";
}
if (!string.IsNullOrEmpty(licPlateNo))
{
truck.Tag = licPlateNo;
}
else
{
truck.Tag = "";
}
if (!string.IsNullOrEmpty(code))
{
truck.BackOfficeTruckId = code;
}
truckList.Add(truck);
Logger.WriteToSLXLog(id, code, MasterDataType.TruckType, 4, "2", "", null, false);
if (truckList.Any())
{
// Call SLX web service
using (var client = new WebClient())
{
var uri = SmartLogixConstants.LocalSmartLogixIntUrl;
uri += "SmartLogixApi/PushTruck";
client.Headers.Clear();
client.Headers.Add("content-type", "application/json");
client.Headers.Add("FirestreamSecretToken", SmartLogixConstants.FirestreamSecretToken);
var serialisedData = JsonConvert.SerializeObject(truckList, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Serialize
});
// HTTP POST
var response = client.UploadString(uri, serialisedData);
var result = JsonConvert.DeserializeObject<SmartLogixResponse>(response);
Logger.WriteToSLXLog(id, code, MasterDataType.TruckType, 4, "3", "", null, false);
if (result == null || result.ResponseStatus != 1)
{
// Something went wrong
throw new ApplicationException("Error in SLX");
}
Logger.WriteToSLXLog(id, code, MasterDataType.TruckType, result.ResponseStatus, serialisedData,
null, null, false);
}
}
}
catch (Exception ex)
{
Logger.WriteToSLXLog(id, code, MasterDataType.TruckType, 4, "4", "", null, false);
throw;
}
finally
{
Logger.WriteToSLXLog(id, code, MasterDataType.TruckType, 4, "5", "", null, false);
}
}
As you can see I have added several log statements throughout the method. All of these log statements except the one in the catch block are successful if no exception is thrown. If an exception is thrown then none of them are successful. For most of them the values are exactly the same whether or not there is an exception so I know its not an issue with the values being passed. I am thinking something weird is happening that causes a rollback or something, but I am not using a transaction or anything here. One last thing this DLL is being run through the SQL CLR which is why I am using "context connection=true" for my connection string.
Thanks in advance.
Edit:
I tried adding the following as my connection string but I get an exception when trying to .Open the connection now that says "Transaction context in use by another session". I am thinking this has to do with me calling this SQL CLR procedure through a trigger. The connection string I tried is
connectionString = "Trusted_Connection=true;Data Source=(local)\\AARONSQLSERVER;Initial Catalog=Demo409;Integrated Security=True;";
Also here is the trigger:
CREATE TRIGGER [dbo].[PushToSLXOnVehicleInsert]
ON [dbo].[Vehicle] AFTER INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #returnValue int
DECLARE #newLastModifiedDate datetime = null
DECLARE #currentId bigint = null
DECLARE #counter int = 0;
DECLARE #maxCounter int
DECLARE #currentCode varchar(24) = null
DECLARE #currentVinNumber varchar(24)
DECLARE #currentLicPlateNo varchar(30)
declare #tmp table
(
id int not null
primary key(id)
)
insert #tmp
select VehicleID from INSERTED
SELECT #maxCounter = Count(*) FROM INSERTED GROUP BY VehicleID
BEGIN TRY
WHILE (#counter < #maxCounter)
BEGIN
select top 1 #currentId = id from #tmp
SELECT #currentCode = Code, #currentVinNumber = VINNumber, #currentLicPlateNo = LicPlateNo FROM INSERTED WHERE INSERTED.VehicleID = #currentId
if (#currentId is not null)
BEGIN
EXEC dbo.SLX_CallInsertTruckService
#id = #currentId,
#code = #currentCode,
#vinNumber = #currentVinNumber,
#licPlateNo = #currentLicPlateNo
END
delete from #tmp where id = #currentId
set #counter = #counter + 1;
END
END TRY
BEGIN CATCH
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT
#ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
IF (#ErrorMessage like '%Error in SLX%')
BEGIN
SET #ErrorMessage = 'Error in SLX. Please contact SLX for more information.'
END
RAISERROR (#ErrorMessage, -- Message text.
#ErrorSeverity, -- Severity.
#ErrorState -- State.
);
END CATCH;
END
GO
The main issue here is that the SQLCLR Stored Procedure is being called from within a Trigger. A Trigger always runs within the context of a Transaction (to bind it to the DML operation that initiated the Trigger). A Trigger also implicitly sets XACT_ABORT to ON which cancels the Transaction if any error occurs. This is why none of the logging statements persist when an exception is thrown: the Transaction is auto-rolled-back, taking with it any changes made in the same Session, including the logging statements (because the Context Connection is the same Session), as well as the original DML statement.
You have three fairly simple options, though they leave you with an overall architectural problem, or a not-so-difficult-but-a-little-more-work option that solves the immediate issue as well as the larger architectural problem. First, the three simple options:
You can execute SET XACT_ABORT OFF; at the beginning of the Trigger. This will allow the TRY ... CATCH construct to work as you are expecting it to. HOWEVER, this also shifts the responsibility to you issue a ROLLBACK (usually in the CATCH block), unless you want the original DML statement to succeed no matter what, even if the Web Service calls and logging fail. Of course, if you issue a ROLLBACK, then none of the logging statements will persist, even if the Web Service still registers all of the calls that were successful, if any were.
You can leave SET XACT_ABORT alone and use a regular / external connection to SQL Server. A regular connection will be an entirely separate Connection and Session, hence it can operate independantly with regards to the Transaction. Unlike the SET XACT_ABORT OFF; option, this would allow the Trigger to operate "normally" (i.e. any error would roll-back any changes made natively in the Trigger as well as the original DML statement) while still allowing the logging INSERT statements to persist (since they were made outside of the local Transaction).
You are already calling a Web Service so the Assembly already has the necessary permissions to do this without making any additional changes. You just need to use a proper connection string (there are a few errors in your syntax), probably something along the lines of:
connectionString = #"Trusted_Connection=True; Server=(local)\AARONSQLSERVER; Database=Demo409; Enlist=False;";
The "Enlist=False;" part (scroll to the far right) is very important: without it you will continue to get the "Transaction context in use by another session" error.
If you want to stick with the Context Connection (it is a little faster) and allow for any errors outside of the Web Service to roll-back the original DML statement and all logging statements, while ignoring errors from the Web Service, or even from the logging INSERT statements, then you can simply not re-throw the exception in the catch block of CallInsertTruckService. You could instead set a variable to indicate a return code. Since this is a Stored Procedure, it can return SqlInt32 instead of void. Then you can get that value by declaring an INT variable and including it in the EXEC call as follows:
EXEC #ReturnCode = dbo.SLX_CallInsertTruckService ...;
Just declare a variable at the top of CallInsertTruckService and initialize it to 0. Then set it to some other value in the catch block. And at the end of the method, include a return _ReturnCode;.
That being said, no matter which of those choices you pick, you are still left with two fairly large problems:
The DML statement and its system-initiated Transaction are impeded by the Web Service calls. The Transaction will be left open for much longer than it should be, and this could at the very least increase blocking related to the Vehicle Table. While I am certainly an advocate of doing Web Service calls via SQLCLR, I would strongly recommend against doing so within a Trigger.
If each VehicleID that is inserted should be passed over to the Web Service, then if there is an error in one Web Service call, the remaining VehicleIDs will be skipped, and even if they aren't (option # 3 above would continue processing the rows in #tmp) then at the very least the one that just had the error won't ever be retried later.
Hence the ideal approach, which solves these two rather important issues as well the initial logging issue, is to move to a disconnected asynchronous model. You can set up a queue table to hold the Vehile info to process based on each INSERT. The Trigger would do a simple:
INSERT INTO dbo.PushToSLXQueue (VehicleID, Code, VINNumber, LicPlateNo)
SELECT VehicleID, Code, VINNumber, LicPlateNo
FROM INSERTED;
Then create a Stored Procedure that reads an item from the queue table, calls the Web Service, and if successful, then deletes that entry from the queue table. Schedule this Stored Procedure from a SQL Server Agent job to run every 10 minutes or something like that.
If there are records that will never process, then you can add a RetryCount column to the queue table, default it to 0, and upon the Web Service getting an error, increment RetryCount instead of removing the row. Then you can update the "get entry to process" SELECT query to include WHERE RetryCount < 5 or whatever limit you want to set.
There are a few issues here, with various levels of impact:
Why is id a BIGINT in the T-SQL code yet a string in the C# code?
Just FYI, the WHILE (#counter < #maxCounter) loop is inefficient and error prone compared to using an actual CURSOR. I would get rid of the #tmp Table Variable and #maxCounter.
At the very least change SELECT #maxCounter = Count(*) FROM INSERTED GROUP BY VehicleID to be just SET #maxCounter = ##ROWCOUNT; ;-). But swapping out for a real CURSOR would be best.
If the CallInsertTruckService(string id, string code, string vinNumber, string licPlateNo) signature is the actual method decorated with [SqlProcedure()], then you really should be using SqlString instead of string. Get the native string value from each parameter using the .Value property of the SqlString parameter. You can then set the proper size using the [SqlFacet()] attribute as follows:
[SqlFacet(MaxSize=24)] SqlString vinNumber
For more info on working with SQLCLR in general, please see the series that I am writing on this topic over at SQL Server Central: Stairway to SQLCLR (free registration is required to read content on that site).

How to Identify whether SQL job is successfully executed or not in C#

I have an C# method to execute a SQL job. It executes the SQL job successfully.
And the code works perfect.
And I'm using standard SQL stored procedure msdb.dbo.sp_start_job for this.
Here is my code..
public int ExcecuteNonquery()
{
var result = 0;
using (var execJob =new SqlCommand())
{
execJob.CommandType = CommandType.StoredProcedure;
execJob.CommandText = "msdb.dbo.sp_start_job";
execJob.Parameters.AddWithValue("#job_name", "myjobname");
using (_sqlConnection)
{
if (_sqlConnection.State == ConnectionState.Closed)
_sqlConnection.Open();
sqlCommand.Connection = _sqlConnection;
result = sqlCommand.ExecuteNonQuery();
if (_sqlConnection.State == ConnectionState.Open)
_sqlConnection.Close();
}
}
return result;
}
Here is the sp which executing inside the job
ALTER PROCEDURE [Area1].[Transformation]
AS
BEGIN
SET NOCOUNT ON;
SELECT NEXT VALUE FOR SQ_COMMON
-- Transform Master Data
exec [dbo].[sp_Transform_Address];
exec [dbo].[sp_Transform_Location];
exec [dbo].[sp_Transform_Product];
exec [dbo].[sp_Transform_Supplier];
exec [dbo].[sp_Transform_SupplierLocation];
-- Generate Hierarchies and Product References
exec [dbo].[sp_Generate_HierarchyObject] 'Area1',FGDemand,1;
exec [dbo].[sp_Generate_HierarchyObject] 'Area1',RMDemand,2;
exec [dbo].[sp_Generate_Hierarchy] 'Area1',FGDemand,1;
exec [dbo].[sp_Generate_Hierarchy] 'Area1',RMDemand,2;
exec [dbo].[sp_Generate_ProductReference] 'Area1',FGDemand,1;
exec [dbo].[sp_Generate_ProductReference] 'Area1',RMDemand,2;
-- Transform Demand Allocation BOM
exec [Area1].[sp_Transform_FGDemand];
exec [Area1].[sp_Transform_FGAllocation];
exec [Area1].[sp_Transform_RMDemand];
exec [Area1].[sp_Transform_RMAllocation];
exec [Area1].[sp_Transform_BOM];
exec [Area1].[sp_Transform_RMDemand_FK];
-- Transform Purchasing Document Data
exec [dbo].[sp_Transform_PurchasingDoc];
exec [dbo].[sp_Transform_PurchasingItem];
exec [dbo].[sp_Transform_ScheduleLine];
exec [dbo].[sp_CalculateRequirement] 'Area1'
exec [dbo].[sp_Create_TransformationSummary] 'Area1'
-- Trauncate Integration Tables
exec [dbo].[sp_TruncateIntegrationTables] 'Area1'
END
The problem is, even the job is executed successfully or not it always returns -1. How can I identify whether job is successfully executed or not.
After running msdb.dbo.sp_start_job the return code is mapped to an output parameter. You have the opportunity to control the parameter's name prior to execution:
public int StartMyJob( string connectionString )
{
using (var sqlConnection = new SqlConnection( connectionString ) )
{
sqlConnection.Open( );
using (var execJob = sqlConnection.CreateCommand( ) )
{
execJob.CommandType = CommandType.StoredProcedure;
execJob.CommandText = "msdb.dbo.sp_start_job";
execJob.Parameters.AddWithValue("#job_name", "myjobname");
execJob.Parameters.Add( "#results", SqlDbType.Int ).Direction = ParameterDirection.ReturnValue;
execJob.ExecuteNonQuery();
return ( int ) sqlCommand.Parameters["results"].Value;
}
}
}
You need to know the datatype of the return code to do this - and for sp_start_job, it's SqlDbType.Int.
However, this is only the results of starting the job, which is worth knowing, but isn't the results of running your job. To get the results running of your job, you can periodically execute:
msdb.dbo.sp_help_job #jobName
One of the columns returned by the procedure is last_run_outcome and probably contains what you're really interested in. It will be 5 (unknown) while it's still running.
A job is usually the a number of steps - where each step may or may not be executed according to the outcome of previous steps. Another procedure called sp_help_jobhistory supports a lot of filters to specify which specific invocation(s) and/or steps of the job you're interested in.
SQL likes to think about jobs as scheduled work - but there's nothing to keep you from just starting a job ad-hoc - although it doesn't really provide you with much support to correlate your ad-hoc job with an instance is the job history. Dates are about as good as it gets (unless somebody knows a trick I don't know.)
I've seen where the job is created ad-hoc job just prior to running it, so the current ad-hoc execution is the only execution returned. But you end up with a lot of duplicate or near-duplicate jobs laying around that are never going to be executed again. Something you'll have to plan on cleaning up afterwards, if you go that route.
A note on your use of the _sqlConnection variable. You don't want to do that. Your code disposes of it, but it was apparently created elsewhere before this method gets called. That's bad juju. You're better off just creating the connection and disposing of it the same method. Rely on SQL connection pooling to make the connection fast - which is probably already turned on.
Also - in the code you posted - it looks like you started with execJob but switched to sqlCommand - and kinda messed up the edit. I assumed you meant execJob all the way through - and that's reflected in the example.
From MSDN about SqlCommand.ExecuteNonQuery Method:
For UPDATE, INSERT, and DELETE statements, the return value is the number of rows affected by the command. When a trigger exists on a table being inserted or updated, the return value includes the number of rows affected by both the insert or update operation and the number of rows affected by the trigger or triggers. For all other types of statements, the return value is -1. If a rollback occurs, the return value is also -1.
In this line:
result = sqlCommand.ExecuteNonQuery();
You want to return the number of rows affected by the command and save it to an int variable but since the type of statement is select so it returns -1. If you test it with INSERT or DELETE or UPDATE statements you will get the correct result.
By the way if you want to get the number of rows affected by the SELECT command and save it to an int variable you can try something like this:
select count(*) from jobs where myjobname = #myjobname
And then use ExecuteScalar to get the correct result:
result = (int)execJob.ExecuteScalar();
You need to run stored proceedure msdb.dbo.sp_help_job
private int CheckAgentJob(string connectionString, string jobName) {
SqlConnection dbConnection = new SqlConnection(connectionString);
SqlCommand command = new SqlCommand();
command.CommandType = System.Data.CommandType.StoredProcedure;
command.CommandText = "msdb.dbo.sp_help_job";
command.Parameters.AddWithValue("#job_name", jobName);
command.Connection = dbConnection;
using (dbConnection)
{
dbConnection.Open();
using (command){
SqlDataReader reader = command.ExecuteReader();
reader.Read();
int status = reader.GetInt32(21); // Row 19 = Date Row 20 = Time 21 = Last_run_outcome
reader.Close();
return status;
}
}
}
enum JobState { Failed = 0, Succeeded = 1, Retry = 2, Cancelled = 3, Unknown = 5};
Keep polling on Unknown, until you get an answer. Lets hope it is succeeded :-)

Error adding parameters to sql execution from c#

Sorry if someone has already given an answer to this, I have been browsing the site and trying tips and bits of code for hours, to no avail.
I have a stored procedure which basically retrieves the id of an element, if this element exists, and zero if it doesn't.
Here we go:
PROCEDURE [dbo].[ChecksProductExistence]
#Product_Name varchar(50),
#Return_Value Int output
AS
BEGIN
IF EXISTS (SELECT prod.ProductID
FROM dbo.Products prod
WHERE prod.ProductName = #Product_Name)
SET #Return_Value = 1;
ELSE
SET #Return_Value = 0;
END
Okay, so far so good. Execute that in my database, and it works like a charm... not that it's such complex code.
Then, in Visual Studio, I coded the following:
private static bool checkExistingProduct(ShoppingListContext ctx, Product product)
{
var cmd = ctx.Database.Connection.CreateCommand();
cmd.CommandText = "[dbo].[ChecksProductExistence]";
ctx.Database.Connection.Open();
int bla = 0;
SqlParameter inParameter = new SqlParameter();
inParameter.ParameterName = "#Product_Name";
inParameter.SqlDbType = SqlDbType.NVarChar;
inParameter.Direction = ParameterDirection.Input;
inParameter.Value = product.ProductName;
SqlParameter outParameter = new SqlParameter();
outParameter.ParameterName = "#Return_Value";
outParameter.SqlDbType = SqlDbType.Int;
outParameter.Direction = ParameterDirection.Output;
cmd.Parameters.Add(inParameter);
cmd.Parameters.Add(outParameter);
bla = cmd.ExecuteNonQuery();
Console.WriteLine(outParameter.Value);
Console.ReadLine();
if ((int)outParameter.Value == 1) return true; else return false;
}
As you can see... simple enough. I create a command, add parameters, execute it, try some decision making on the output.
But... I get an error on the
cmd.ExecuteNonQuery();
line. I have changed it to ExecuteScalar, for example, but still no luck. I have executed it without the "bla = ...", and still the same.
The error I get is
An unhandled exception of type 'System.Data.SqlClient.SqlException' occurred in System.Data.dll
Additional information: Procedure or function 'ChecksProductExistence' expects parameter '#Product_Name', which was not supplied.
But then again, not only can you see clearly in the code that I AM IN FACT SUPPLYING IT, but also on the debug session I can explore the cmd object and browse the parameters, and I can see that IT IS THERE, and the values are correct...
So what else can I do?? Am I making some silly mistake I can't see...?
As stated in the documentation for SqlCommand.CommandType you have to set it to CommandType.StoredProcedure and set the CommandText to the name of the stored procedure. It defaults to CommandType.Text where you have to include the named parameters in the text of the query.
When you set the CommandType property to StoredProcedure, you should
set the CommandText property to the name of the stored procedure. The
command executes this stored procedure when you call one of the
Execute methods.
The Microsoft .NET Framework Data Provider for SQL
Server does not support the question mark (?) placeholder for passing
parameters to a SQL Statement or a stored procedure called with a
CommandType of Text. In this case, named parameters must be used. For
example:
SELECT * FROM Customers WHERE CustomerID = #CustomerID
So, just add the following
cmd.CommandType = CommandType.StoredProcedure;
somewhere before executing it.

Output variable not getting set in SQL Server stored procedure

I hope this is not one of those questions where I slap myself afterwards, but this is really confusing me. I have this working for another one of my stored procedures which is why this is so confusing. It's basically the same setup in both. Here's what's happening.
Here's an example of my stored procedure:
ALTER PROCEDURE [dbo].[CreateRecord]
-- Add the parameters for the stored procedure here
#Link1Id INT = NULL,
#Link2Id INT = NULL,
#Amount MONEY,
#Output int out
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SET #Output = 0
-- Insert statements for procedure here
IF #Link1Id = NULL
BEGIN
IF NOT EXISTS(SELECT * FROM dbo.Records WHERE Link2Id = #Link2Id)
INSERT INTO [dbo].[Records]
([Link1Id]
,[Link2Id])
VALUES
(#Link1Id
,#Link2Id)
SET #Output = (SELECT RecordId FROM dbo.Records WHERE Link2Id = #Link2Id)
END
ELSE
BEGIN
IF NOT EXISTS(SELECT * FROM dbo.Records WHERE Link1Id = #Link1Id)
INSERT INTO [dbo].[Records]
([Link1Id]
,[Link2Id])
VALUES
(#Link1Id
,#Link2Id)
SET #Output = (SELECT RecordId FROM dbo.Records WHERE Link1Id = #Link1Id)
END
END
Now, I have created a unit test that basically runs this procedure, and tries to Assert that the returned #Output is greater than 0, but the #Output parameter never has a value on the SqlCommand in the code. Here's some of the C# code:
private int ExecuteNonQueryWithOutput(string procedureName, SqlParameter[] parameters)
{
SqlCommand command = this.GenerateCommand(procedureName, parameters);
connection.Open();
command.ExecuteNonQuery();
int retval = (int)command.Parameters[OUTPUT].Value;
connection.Close();
return retval;
}
Now, I can step over the line that calls ExecuteNonQuery(), and verify in the database that the new (and correct) record is there, but then on the next line, it throws an exception when it calls (int)command.Parameters[OUTPUT].Value; as the Value is not there.
This is working perfectly for another procedure that I have which is setup in the same exact fashion. Do you know why it wouldn't be working here?
Thanks, I'm kind of stumped. I've debugged for a while now with no luck.
Edit:
Code that generates the parameters array:
List<SqlParameter> parameters = new List<SqlParameter>();
parameters.Add(new SqlParameter { ParameterName = "#Link1Id", SqlDbType = SqlDbType.Int, Direction = ParameterDirection.Input, Value = link1Val });
parameters.Add(new SqlParameter { ParameterName = "#Link2Id", SqlDbType = SqlDbType.Int, Direction = ParameterDirection.Input, Value = link2Val });
parameters.Add(new SqlParameter { ParameterName = OUTPUT, SqlDbType = SqlDbType.Int, Direction = ParameterDirection.Output });
return this.ExecuteNonQueryWithOutput("CreateRecord", parameters.ToArray());
I don't see where you've declared #Output. Did you mean:
ALTER PROCEDURE [dbo].[CreateRecord]
-- Add the parameters for the stored procedure here
#Link1Id INT = NULL,
#Link2Id INT = NULL,
#Amount MONEY,
#Output INT = NULL OUTPUT
AS
Also I'm not 100% sure you have the syntax right for retrieving a named output parameter. But the parameter has to exist before you can reference it anyway. How did you save that stored procedure without declaring #Output?
There are numerous things wrong with the code that go beyond the output parameter issue.
To answer the actual question, you are likely passing a NULL value back as the Output. When it tries to convert this to an Int you are getting an error.
Also the sql line:
IF #Link1ID = null
Will ALWAYS fail. In SQL parlance, null is an indeterminate value, so (null != null). The way to test for null values is to use IS. For example:
IF (#Link1ID is null)
Which leads me to believe that you are actually getting a primary key violation in the sql code.
Now, onto the bigger issue. Your C# code is flawed. The command object is never disposed of and if there are any issues your connection object won't be disposed of either. This will lead to fun sql errors due running out of available sql connections..
It should look something like the following:
private int ExecuteNonQueryWithOutput(string procedureName, SqlParameter[] parameters)
{
int retval = 0;
using (SqlConnection conn = new SqlConnection("connection string here"))
using (SqlCommand command = this.GenerateCommand(procedureName, parameters)) {
connection.Open();
command.ExecuteNonQuery();
retval = (int)command.Parameters[OUTPUT].Value;
}
return retval;
}
Note that this declares, uses and disposes of your connection and command objects locally. If there is a problem this will make sure the resources are properly disposed of.
Also note that it does not use a global "connection" object. Connection pooling offered by the operating system is incredibly efficient at opening/closing connections as needed. Because of this the best practice is to instantiate and keep them around only long enough to deal with the current operation. The longer it's open the more likely you'll run into issues.

Categories

Resources