Avatar of rwheeler23
rwheeler23
Flag for United States of America asked on

Returning a single character value from a SQL stored procedure using C#.

I need to call an sp from C# that will return the next highest rank number. I want to do this the proper way. Using a dataset like my other calls is one way but is there a way to convert both of these so when I call the sp from C# it returns the value? The value in this case is a string with a value between '000' and '999'. I have both the C# and the sp listed below.
------------------------------------------------------------------------------------------------------------------
C# code
            try
            {
                NextRankDataSet = new System.Data.DataSet();          /* Define the data set for the list of assignments */
                NextRankDataSet.CaseSensitive = false;

                NextRankDataCommand = new System.Data.SqlClient.SqlCommand();
                NextRankDataCommand.Connection = GPDataConnection;

                NextRankDataCommand.CommandText =
                    "SELECT CONCAT(REPLICATE('0'," + ChrCount.ToString() + "-LEN(CONVERT(NVARCHAR(3),CONVERT(SMALLINT,MAX([RANKNMBR])+1)))),CONVERT(NVARCHAR(3),CONVERT(SMALLINT,MAX([RANKNMBR])+1))) AS NEXTRANK" +
                    "FROM [APDSDATA].[dbo].[PERSASGN] " +
                    "WHERE [GRUPFLAG] = '" + GroupFlag + "'";

                NextRankDataAdapter = new System.Data.SqlClient.SqlDataAdapter();
                NextRankDataAdapter.SelectCommand = NextRankDataCommand;
                _commandBuilder = new System.Data.SqlClient.SqlCommandBuilder(NextRankDataAdapter);

                NextRankDataAdapter.Fill(NextRankDataSet);

                txtRankNumber.Text = NextRankDataSet.Tables[0].Rows[0]["NEXTRANK"].ToString();
            }

Open in new window

--------------------------------------------------------------------------------------------------------------------------------------------
Stored procedure

/****** Object:  StoredProcedure [dbo].[GetNextRank]    Script Date: 1/29/2019 5:09:51 PM ******/
DROP PROCEDURE [dbo].[GetNextRank]
GO

/****** Object:  StoredProcedure [dbo].[GetNextRank]    Script Date: 1/29/2019 5:09:51 PM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

-- ==========================================================================
-- Author:		Richard E. Wheeler - Revered Business Solutions, Inc.
-- Create date: 02/10/2019
-- Description:	Determine the next highest ranking of personnel within each group
-- @I_CHRCOUNT 2 for 2 digit rank numbers and 3 for 3 digit rank numbers
-- ==========================================================================
CREATE PROCEDURE [dbo].[GetNextRank]
	@I_GRUPFLAG NVARCHAR(50),
	@I_CHRCOUNT INT,
	@I_NEXTRANK NVARCHAR(50)
		
AS
BEGIN

	SET NOCOUNT ON

DECLARE @Err_Msg NVARCHAR(4000)
DECLARE @ErrSeverity INT
DECLARE @ErrCode INT

/* If an active transaction cannot be committed, roll it back */
IF XACT_STATE() = -1
	ROLLBACK TRANSACTION
ELSE
/* If an active transaction(s) can be committed, commit it(them). */
IF XACT_STATE() = 1
	WHILE @@TRANCOUNT > 0
		COMMIT TRANSACTION

BEGIN TRANSACTION

BEGIN TRY

	SELECT @I_NEXTRANK = CONCAT(REPLICATE('0',@I_CHRCOUNT-LEN(CONVERT(NVARCHAR(3),CONVERT(SMALLINT,MAX([RANKNMBR])+1)))),CONVERT(NVARCHAR(3),CONVERT(SMALLINT,MAX([RANKNMBR])+1))) 
	FROM [APDSDATA].[dbo].[PERSASGN]
	WHERE [GRUPFLAG]=@I_GRUPFLAG

	SELECT @I_NEXTRANK

	SET @ErrCode = 0

END TRY
BEGIN CATCH
	SELECT @Err_Msg = ERROR_MESSAGE(),
					@ErrSeverity = ERROR_SEVERITY();
	RAISERROR(@Err_Msg,@ErrSeverity,1)

	SET @ErrCode = 1
END CATCH

-- Complete the transaction
IF XACT_STATE() = 1
	COMMIT TRANSACTION
ELSE
IF XACT_STATE() = -1
	ROLLBACK TRANSACTION

RETURN @ErrCode
END



GO

Open in new window

C#Microsoft SQL Server

Avatar of undefined
Last Comment
rwheeler23

8/22/2022 - Mon
Pavel Celba

ste5an

Just a comment: without synchronization, you can call that SP twice at the same time and it will return the same value.

Take a look at sp_getapplock.


btw, the explicit transaction handling is redundant. And the try..catch is also useless, cause it does nothing than catching the error and raising it again.
AndyAinscow

Just being pedantic:

Question title
Returning a single character value from a SQL stored procedure using C#.

Question body
I need to call an sp from C# that will return the next highest rank number. I want to do this the proper way....  The value in this case is a string with a value between '000' and '999'.


A single character is different from a string which itself is not a number.  You have to decide just what you want before asking a question.
Experts Exchange is like having an extremely knowledgeable team sitting and waiting for your call. Couldn't do my job half as well as I do without it!
James Murphy
rwheeler23

ASKER
I should have provided an example. My apologies.  The client has an nvarchar(50) column in their table. In this case it represents a rank value. So if the are are 49 records currently in the table I want to retrieve '050'. They want the leading zero which is why it is an nvarchar. I did not create the database so I need to work with what is there.
ste5an

Where do they want the leading zeros? Looks like formatting, which is normally better placed in the front-end.
Pavel Celba

I've been focusing on the output parameter w/o comments to the SP itself...

The SP contains several unnecessary or even erroneous parts. Why would you need to COMMIT or ROLLBACK the existing transaction? This should be solved outside the SP if necessary. The TRANSACTION inside the SP is useless because it does not solve any possible conflicts.

You are either executing the SP inside the existing transaction then you would need to ensure no other application instance returns the same MAX number or you don't care how many app instances can obtain the same MAX number when calling the SP simultaneously.

If you need to ensure just one exclusive MAX value inside a transaction then lock the table using the WITH hint as visible in the commented part of the SELECT command:
-- ==========================================================================
-- Author:            Richard E. Wheeler - Revered Business Solutions, Inc.
-- Create date: 02/10/2019
-- Description:      Determine the next highest ranking of personnel within each group
-- @I_CHRCOUNT 2 for 2 digit rank numbers and 3 for 3 digit rank numbers
-- ==========================================================================
CREATE PROCEDURE [dbo].[GetNextRank]
      @I_GRUPFLAG NVARCHAR(50),
      @I_CHRCOUNT INT,
      @I_NEXTRANK NVARCHAR(50) OUTPUT
            
AS
BEGIN

  SET NOCOUNT ON

  DECLARE @Err_Msg NVARCHAR(4000)
  DECLARE @ErrSeverity INT
  DECLARE @ErrCode INT
  
  BEGIN TRY

   SELECT @I_NEXTRANK = RIGHT('000' + CAST(MAX([RANKNMBR])+1 AS varchar(3)), @I_CHRCOUNT)
     FROM [APDSDATA].[dbo].[PERSASGN] -- WITH (UPDLOCK, HOLDLOCK)
    WHERE [GRUPFLAG]=@I_GRUPFLAG
   
   SELECT @I_NEXTRANK

   SET @ErrCode = 0
   
  END TRY
  BEGIN CATCH
    SELECT @Err_Msg = ERROR_MESSAGE(),
                              @ErrSeverity = ERROR_SEVERITY();
    RAISERROR(@Err_Msg,@ErrSeverity,1)

    SET @ErrCode = 1
  END CATCH

  RETURN @ErrCode
  
END

Open in new window

Additional notes: varchar data type is sufficient for digits and you may remove the error processing as it can be done as exception handling at C# side. Of course, some handling of overflow would also be useful.
⚡ FREE TRIAL OFFER
Try out a week of full access for free.
Find out why thousands trust the EE community with their toughest problems.
Pavel Celba

One more note: If you don't have database then install it on your development machine ASAP. It is free and it will allow better debugging of the C# code.
ASKER CERTIFIED SOLUTION
ste5an

THIS SOLUTION ONLY AVAILABLE TO MEMBERS.
View this solution by signing up for a free trial.
Members can start a 7-Day free trial and enjoy unlimited access to the platform.
See Pricing Options
Start Free Trial
GET A PERSONALIZED SOLUTION
Ask your own question & get feedback from real experts
Find out why thousands trust the EE community with their toughest problems.
Pavel Celba

@AndyAinscow: SQL Server does not recognize string data type just character values of the defined length. "Single character value" could either mean 1 character or 1 value of character data type which is then clarified in the question itself. From this point of the view is the question fully understandable.
AndyAinscow

@pcelba.  It doesn't actually specify SQL Server in the question but anyway a number is not a string   ...return the next highest rank number.... Stefan is also pointing out that formatting of numbers is really a job for the FE
Experts Exchange has (a) saved my job multiple times, (b) saved me hours, days, and even weeks of work, and often (c) makes me look like a superhero! This place is MAGIC!
Walt Forbes
Pavel Celba

I can see the MS SQL Server zone in the question. The formatting place is almost irrelevant in this task. And that's probably the reason Stefan used the formatting at the BE side in his answer...
Snarf0001

Just weighing in on the DataSet portion (the sql portion is more than covered) - if you don't end up going the output parameter route, and returning the value in a select statement, then .ExecuteScalar() will be much more streamlined to get your value back than filling a dataset.
ExecuteScalar will return the first row / first column from the first returned result set, without needing to create the other objects.

If you called @ste5an's first example proc:

        try
        {
            NextRankDataCommand = new System.Data.SqlClient.SqlCommand();
            NextRankDataCommand.Connection = GPDataConnection;

            NextRankDataCommand.CommandText = "dbo.GetNextRank";
            NextRankDataCommand.CommandType = System.Data.CommandType.StoredProcedure;
            NextRankDataCommand.Parameters.AddWithValue("@I_GROUPFLAG", ChrCount);
            NextRankDataCommand.Parameters.AddWithValue("@I_CHRCOUNT", GroupFlag);

            NextRankDataCommand.Connection.Open();
            txtRankNumber.Text = NextRankDataSet.ExecuteScalar().ToString();
            NextRankDataCommand.Connection.Close();
        }

Open in new window

AndyAinscow

@pcelba.  I can see the MS SQL Server zone in the question  So do I, but at times I see things like javascript in a C# question so I always take that with a bucketful of salt.  ;-)
What is important is that a number is not a string and vice versa.  I've seen numbers of questions with the asker swearing blind he uses a number (or date is another favourite) but is actually using a string which is why nothing is working correctly.
⚡ FREE TRIAL OFFER
Try out a week of full access for free.
Find out why thousands trust the EE community with their toughest problems.
rwheeler23

ASKER
Thanks for all your tips. In an unrelated note I have another project where the varchar variable will begin with text. In this case the seed number will look like 'ORD01001'. So my thinking is that it would be cleaner to call a stored procedure that again will return a single character string that represents the next order number. From a performance standpoint which is more efficient, .ExecuteScalar() or using an output parameter?

I am going to submit another question on this as it is a bit more involved plus to offer the opportunity for additional points.
rwheeler23

ASKER
Why is this an invalid statement?
                      txtRankNumber.Text = NextRankDataSet.ExecuteScalar().ToString();
'DataSet' does not contain a definition for 'Execute Scalar'  is the message I am getting.


                    try
                    {
                        NextRankDataSet = new System.Data.DataSet();          /* Define the data set for the next highest rank */
                        NextRankDataSet.CaseSensitive = false;

                        NextRankDataCommand = new System.Data.SqlClient.SqlCommand();
                        NextRankDataCommand.Connection = GPDataConnection;

                        NextRankDataCommand.CommandText = "dbo.GetNextRank";
                        NextRankDataCommand.CommandType = System.Data.CommandType.StoredProcedure;
                        NextRankDataCommand.Parameters.AddWithValue("@I_GROUPFLAG", ChrCount);
                        NextRankDataCommand.Parameters.AddWithValue("@I_CHRCOUNT", GroupFlag);

                        NextRankDataCommand.Connection.Open();
                        txtRankNumber.Text = NextRankDataSet.ExecuteScalar().ToString();
                        NextRankDataCommand.Connection.Close();
                    }
                    catch (Exception ex)
                    {
                        string eMsg = "ASGNCHFLET 0002: ERROR: " + ex.Message;
                        if (Model.StackTraceWanted) eMsg += "\n" + ex.StackTrace;
                        if (Model.StackTraceWanted) MessageBox.Show(eMsg);
                    }
                }
Snarf0001

Sorry, mistype on my part.

NextRankDataCommand.ExecuteScalar(), not NextRankDataSet
I started with Experts Exchange in 2004 and it's been a mainstay of my professional computing life since. It helped me launch a career as a programmer / Oracle data analyst
William Peck
rwheeler23

ASKER
Thank you. Please excuse my ignorance but can you please clarify the use of the Open() and Close() directives.  My GPDataConnection defines an open connection to the database in another class.(SEE BELOW). Do the open and close directives leave that dataconnection untouched and only open and close the connection for NextRankDataCommand? I do not want to be leaving dataconnections open unnecessarily.

                    try
                    {
                        NextRankDataSet = new System.Data.DataSet();          /* Define the data set for the next highest rank */
                        NextRankDataSet.CaseSensitive = false;

                        NextRankDataCommand = new System.Data.SqlClient.SqlCommand();
                        NextRankDataCommand.Connection = GPDataConnection;

                        NextRankDataCommand.CommandText = "dbo.GetNextRank";
                        NextRankDataCommand.CommandType = System.Data.CommandType.StoredProcedure;
                        NextRankDataCommand.Parameters.AddWithValue("@I_GRUPFLAG", GroupFlag);
                        NextRankDataCommand.Parameters.AddWithValue("@I_CHRCOUNT", ChrCount);

                        /* NextRankDataCommand.Connection.Open(); */
                        txtRankNumber.Text = NextRankDataCommand.ExecuteScalar().ToString();    
                        /* NextRankDataCommand.Connection.Close(); */
                    }
                    catch (Exception ex)
                    {
                        string eMsg = "ASGNCHFLET 0002: ERROR: " + ex.Message;
                        if (Model.StackTraceWanted) eMsg += "\n" + ex.StackTrace;
                        if (Model.StackTraceWanted) MessageBox.Show(eMsg);
                    }

--------------------------------------------------------------------
OPEN DATA CONNECTION
--------------------------------------------------------------------
        public static SqlConnection GetConnection()
        {

            /* Create reader and open file containing connection string */
            TextReader tr = new StreamReader(@"CONNECTION.INI");

            try
            {
                DataConnection = new System.Data.SqlClient.SqlConnection(tr.ReadLine());    /* Define the connection specified in the CONNECTION.INI file */
                DataConnection.Open();                                                      /* Open the connection */

                /* Close the stream */
                tr.Close();
            }
            catch (Exception ex)
            {
                string eMsg = "Error-Opening SQL connection: " + ex.Message;
                if (Model.StackTraceWanted) eMsg += "\n" + ex.StackTrace;
                MessageBox.Show(eMsg);
            }

            return DataConnection;
        }
Snarf0001

Tough to say without seeing the rest of your code.
"Typically", the .Open() and .Close() methods should be called around the sqlcommands.  Database connections are expensive, you should try to open as late as possible, and close as soon as possible.

But if your entire underlying structure is already based around having it open it could be a daunting rewrite.

But normally you should wrap both the connection and the command objects in using statements, and open / close around the actual execution.  If you need to open and close multiple times, still better overall than leaving a connection open until it "might" be needed again.
Pavel Celba

If the GPDataConnection contains reference to open connection then you don't need to call Open() and Close() methods. Or call just Close() to close the unnecessary connection.

OTOH, to leave one connection open throughout the application run is a good idea as the connection opening is expensive due to the all necessary negotiations etc.
⚡ FREE TRIAL OFFER
Try out a week of full access for free.
Find out why thousands trust the EE community with their toughest problems.
rwheeler23

ASKER
In this application just about every click hits the database. There are many forms which display datagridviews and on forms all the buttons, with the exception of the Exit button, hit the database. I understand what you are saying but if I were to change things to opening and closing I would have hundreds of these.
Pavel Celba

You should leave one connection open all the time. Just be aware of possible timeouts or killing the session by admins...

Just remember to make possible SQL transactions as short as possible. It means when user enters data and then clicks on Save, the transaction should begin, and data verified and saved w/o any dialogs to user. Then all the changes are committed or rolled back and user is informed about the result.
rwheeler23

ASKER
I will keep an eye on this as this gets deployed. I am sure users will go to lunch and leave the application open despite being told not to do so Otherwise, the users are picking a group and are presented with a list, they highlight a record click modify, make a few changes and then click save and move onto the next record.
All of life is about relationships, and EE has made a viirtual community a real community. It lifts everyone's boat
William Peck