4

I have a stored procedure that can return both a PRINT message and regular query results from a SELECT statement. Using the elegant solution provided in this thread, I can easily capture my PRINT message when invoking SqlCommand.ExecuteNonQuery() in my C# code. The problem is, I also want to return my result set.

When I use a SqlDataReader to get back the query results from my stored proc using SqlCommand.ExecuteReader, the event handler that captures my PRINT message never fires like it does when I use SqlCommand.ExecuteNonquery.

It's beginning to seem like I can only have one or the other. Is there a way for me to capture both my PRINT message and my query results?

Here's a snippet of my SQL stored procedure:

IF EXISTS
(
    SELECT MFG_PART_NUMBER, COUNT(*) AS RecordCount
    FROM [PartsManagement].[ManufacturerCatalogPart] 
    WHERE CatalogID = @CatalogID
    GROUP BY MFG_PART_NUMBER
    HAVING COUNT(*) > 1
)
    BEGIN

    SELECT MFG_PART_NUMBER, COUNT(*) AS RecordCount
    FROM [PartsManagement].[ManufacturerCatalogPart] 
    WHERE CatalogID = @CatalogID
    GROUP BY MFG_PART_NUMBER
    HAVING COUNT(*) > 1;

    PRINT 'The update was not executed because duplicate MFG_PART_NUMBERs were found in the catalog. Delete the duplicates and try again.';

END;

And here's a snippet from my code:

//field
private string sqlPrintMessage = "";

//event handler for sql PRINT message
void myConnection_InfoMessage(object sender, SqlInfoMessageEventArgs e)
{
    sqlPrintMessage = e.Message;
}

public void SomeMethod()
{
    using (SqlConnection sqlcon = new SqlConnection(ConnectionManager.GetConnectionString()))
    {
        sqlcon.InfoMessage += new SqlInfoMessageEventHandler(myConnection_InfoMessage);

        SqlCommand sqlcmd = new SqlCommand("[ManufacturerCatalogUpdate].[CheckCatalogForDuplicates]", sqlcon);
        sqlcmd.CommandType = CommandType.StoredProcedure;
        sqlcmd.Parameters.AddWithValue("@CatalogID", catalogID);
        sqlcon.Open();
        //SqlDataReader reader = sqlcmd.ExecuteReader();
        sqlcmd.ExecuteNonQuery();
        if (sqlPrintMessage == "") //then proceed with update
        {
            //do some logic
        }
        else
        {
            //System.Diagnostics.Debug.WriteLine(sqlPrintMessage);
            //List<DuplicatePartCount> duplicateParts = new List<DuplicatePartCount>();
            //while (reader.Read())
            //{
            //    DuplicatePartCount record = new DuplicatePartCount();
            //    record.MFG_PART_NUMBER = reader.GetString(0);
            //    record.count = reader.GetInt32(1);
            //    duplicateParts.Add(record);
            //}
        }
    }
}
Community
  • 1
  • 1
Jacob Stamm
  • 1,660
  • 1
  • 29
  • 53
  • 2
    Add an handler for the connection InfoMessage event to get informational and warning messages generated by `PRINT` and `RAISERROR` (severity 10 or less). – Dan Guzman Jun 08 '15 at 02:51
  • @DanGuzman, I'm already doing that (see my second code block). The problem is, when I use `ExecuteReader`, the event is never fired. – Jacob Stamm Jun 08 '15 at 03:20
  • Jacob, something else is going on here. I do see the event handler, but I can assure you that this should work as I have done this several times. Of course, it might be a timing issue. I know that for instance, output param values aren't available until you close the reader. I will take a look in a little while and compare to what I have working. If nobody else has an answer before then i will try to find the problem. – Solomon Rutzky Jun 08 '15 at 03:31
  • Have you tried using `DataSet` and `DataAdapter`? Check out [Multiple Result Sets section](https://msdn.microsoft.com/en-us/library/bh8kx08z%28v=vs.110%29.aspx). – yazanpro Jun 08 '15 at 04:20
  • I had some really odd behaviour regarding InfoMessages and how things were affected using either `ExecuteReader` or `ExecuteNonQuery`. It was a couple of years ago now, but my question and [answer](http://stackoverflow.com/a/12448026/685760) contain some links that might help you out. – Mr Moose Jun 08 '15 at 06:06
  • 1
    You need to navigate to the end of the reader result before the InfoMessage event handler fires. In the undermying TDS streaming protocol, the InfoMessage is behind the preceding results from the select statement and not available until it is consumed. – Dan Guzman Jun 08 '15 at 11:23
  • @yazanpro, do I have to use DataTables to do that? Still pretty new to this. Thanks for any info you can give me – Jacob Stamm Jun 08 '15 at 12:52
  • 1
    Jacob: no, this has nothing to do with DataTables and/or DataAdapter. If anything, someone using a DataTable with the `Fill` method might appear to get the message sooner, but only due to the fill method moving through the reader entirely before proceeding to the next line of code. In the end, you don't need the messages as I showed in my answer. You only need `reader.HasRows()`. – Solomon Rutzky Jun 08 '15 at 13:05
  • @DanGuzman It is not necessarily true that the messages come at the end of the reader. That is the default behavior, but that can be altered via the `FireInfoMessageEventOnUserErrors` property. I explain this in my answer. – Solomon Rutzky Jun 08 '15 at 13:08
  • @srutzky, it is true that FireInfoMessageEventOnUserErrors causes the event to be fired immediately upon receipt by the client but my point is that it is not received immediately; the message is behind the result set data being sent over the wire so the resultset must first be consumed before the message is received. – Dan Guzman Jun 09 '15 at 03:06

2 Answers2

2

There are two way to do it based on selection of sql execution.

1) If you are using ExecuteNonQuery than use OUT param in sproc like below.

CREATE PROCEDURE GetEmployee
   @employeeID INT,
   @Message VarChar(100) OUTPUT
AS
BEGIN
   -- Here is your result set from select statement
   SELECT Emp_Name From Employee Where EmpId = @employeeID 

   -- Here is your message
   SELECT @Message = 'Your Message!'
END

Now to get it in your code pass @Message parameter with OUTlike below.

//Add the output parameter to the command object
SqlParameter outPutParameter = new SqlParameter();
outPutParameter.ParameterName = “@Message”;
outPutParameter.SqlDbType = System.Data.SqlDbType.Varchar;
outPutParameter.Direction = System.Data.ParameterDirection.Output;
cmd.Parameters.Add(outPutParameter);

cmd.ExecuteNonQuery();

//Retrieve the value of the output parameter
string Message = outPutParameter.Value.ToString();

// You can get remaining result set as always

2) If you are using Reader than....

Set the message and use Select statement instead of print.Like below'

 SELECT Emp_Name From Employee Where EmpId = @employeeID 
  -- It should be set previously
  SELECT @Message

To get in your code just Use

using (SqlDataReader reader = command.ExecuteReader())
        {
            while (reader.Read())
            {
                //Your result set or message
            }

            if(reader.NextResult())
            {
               while (reader.Read())
              {
                 //Your result set or message
              }
            }
        }
Harshad Vekariya
  • 972
  • 1
  • 7
  • 28
  • This is both convoluted and unnecessary. Please (everyone) read the code posted by the OP in the question and not just the title of the question. While the OP is asking about capturing messages, that isn't what the OP is, in the end, really trying to accomplish. And if capturing messages was truly the goal here, then this still doesn't help and the `OUTPUT` param is a single message, and additional result sets are clunky and can adversely affect the running of the proc in various situations, such as trying to capture the result set into a temp table, etc. – Solomon Rutzky Jun 08 '15 at 13:12
  • @srutzky: Please let us know what you understood... and provide better solution. – Harshad Vekariya Jun 08 '15 at 13:17
  • Also, the scenario of using `ExecuteNonQuery` was already properly handled by the OP and stated in the question as working as desired. – Solomon Rutzky Jun 08 '15 at 13:17
  • @srutzky: It was just code sample. i had never mentioned it to use using anywhere. – Harshad Vekariya Jun 08 '15 at 13:18
  • I have already done as you asked in my answer here on this question. To reiterate: the OP is merely looking for an indication of there being a result set in the first place and never even uses the text of the message. It is a poor means of testing as a SqlDataReader provides that answer via the boolean `HasRows` property. – Solomon Rutzky Jun 08 '15 at 13:20
  • Regarding "It was just code sample. i had never mentioned it to use using anywhere": that doesn't make sense. This is a question and answer site and you provided an answer. You can't say that you never intended for anyone to use it as that is the only purpose of posting it. If that were true, then it is irresponsible to post because people copy / paste stuff from here all of the time. – Solomon Rutzky Jun 08 '15 at 13:23
  • @srutzky, I don't see the need for such hostility - this isn't Reddit. Also, I _am_ using the message. See the first commented line of my `else` block. – Jacob Stamm Jun 08 '15 at 13:35
  • @JacobStamm I apologize if this is coming off as hostile, but it is bad advice. And you are only using the message in a debug, and there is only a single message, hence it can be inferred that if not empty, it is the one message that was sent. Regardless, from the code that you posted, I cannot see how trapping messages benefits the goal in any way. – Solomon Rutzky Jun 08 '15 at 13:50
1

So, I think we first need to rethink the operation here. You want to do a duplicate as a test to determine if the app code should proceed or not. The proc returns a result set and then prints a message if there are dupes, else returns neither. And you are inferring that there will be a result set based on the existence of a captured message. That type of indirect test is less than ideal and unnecessary.

A direct and simpler method is available to test for the existence of a result set. Forget the message (especially since you aren't even using it) and instead just check the data reader:

    if (reader.HasRows())
    {
        //System.Diagnostics.Debug.WriteLine(sqlPrintMessage);
        //List<DuplicatePartCount> duplicateParts = new List<DuplicatePartCount>();
        //while (reader.Read())
        //{
        //    DuplicatePartCount record = new DuplicatePartCount();
        //    record.MFG_PART_NUMBER = reader.GetString(0);
        //    record.count = reader.GetInt32(1);
        //    duplicateParts.Add(record);
        //}
    }
    else
    {
        //do some logic
    }

If you are still just curious about the PRINT message getting trapped, do these two tests:

  1. First, add a second System.Diagnostics.Debug.WriteLine(sqlPrintMessage); AFTER the while loop, and re-run the test
  2. Second, without removing the 2nd debug line from #1, add a second PRINT command between the BEGIN and SELECT, with different text so that you can distinguish between them if necessary, and re-run the test

Also, the following "Remarks" from the MSDN page for the SqlConnection.FireInfoMessageEventOnUserErrors property might shed some light on this behavior:

When you set FireInfoMessageEventOnUserErrors to true, errors that were previously treated as exceptions are now handled as InfoMessage events. All events fire immediately and are handled by the event handler. If is FireInfoMessageEventOnUserErrors is set to false, then InfoMessage events are handled at the end of the procedure.

So, if you wanted / needed the messages as they happen, then you can set FireInfoMessageEventOnUserErrors to true, but that might alter the behavior of catching exceptions that might be more undesirable than getting the messages immediately.

Solomon Rutzky
  • 46,688
  • 9
  • 128
  • 171