0

Occasionally I am getting a SqlException exception on the line marked below. It occurs when we have network problems and the server cannot be found.

public void markComplete(int aTKey)
{
  SqlCommand mySqlCommand = null;
  SqlConnection myConnect = null;
  mySqlCommand = new SqlCommand();
  try
  {
    myConnect = new SqlConnection(ConfigurationManager.ConnectionStrings["foo"].ConnectionString);
    mySqlCommand.Connection = myConnect;
    mySqlCommand.Connection.Open();        //<<<<<<<< EXCEPTION HERE <<<<<<<<<<<<<<<<<<
    mySqlCommand.CommandType = CommandType.Text;
    mySqlCommand.CommandText =
       " UPDATE dbo.tb_bar " +
       " SET    LastUpdateTime    = CONVERT(Time,GETDATE()) " +
       " WHERE  AKey = " + aTKey;
    mySqlCommand.ExecuteNonQuery();
    mySqlCommand.Connection.Close();
  }
  finally
  {
    if(mySqlCommand != null)
    {
      mySqlCommand.Dispose();
    }
  }
}

I have two questions so maybe should split into 2 SO questions:

  1. Is my finally statement sufficiently defensive ?
  2. Rather than just failing how involved would it be to amend the method so instead of crashing it waits for say 10 minutes and then tries again to open the connection - tries a maximum of 3 times and if still no valid connection it moves on?
whytheq
  • 31,528
  • 57
  • 155
  • 255
  • 4
    If you use `using` for your connection, that `try`-`finally` wouldn't even be necessary. Also note this does not actually catch or process the error. – Nyerguds Mar 07 '18 at 11:50
  • By the way, there is no use in first assigning `null` to `mySqlCommand` and then filling it with a new object. That's just code pollution. Fill it with the new object right away. – Nyerguds Mar 07 '18 at 11:52
  • In addition to @Nyerguds comment, you can set the connection timeout in the connection string – Miguel A. Arilla Mar 07 '18 at 11:52
  • 1
    I just noticed, the connection itself is never even disposed in this code. Only the command. That's a major flaw. Which, as I said, can be fixed with `using`. – Nyerguds Mar 07 '18 at 11:57
  • I would note that your `finally` block is disposing the `SqlCommand`, but not actually closing the `SqlConnection`. https://stackoverflow.com/q/60919/8915494 – Trevor Mar 07 '18 at 11:57
  • 1
    Possible duplicate of [What are the uses of "using" in C#](https://stackoverflow.com/questions/75401/what-are-the-uses-of-using-in-c-sharp) – mjwills Mar 07 '18 at 11:57
  • @MiguelA.Arilla All the connection timeout will do is force this code to wait longer before it fails - if the network connection to the DB is dead, then increasing the timeout won't really help. – Zhaph - Ben Duguid Mar 07 '18 at 11:57
  • @mjwills I think we need to bring in the use of a Catch - whereas `using` is try/finally so I disagree that this is a duplicate of the question you cited – whytheq Mar 07 '18 at 12:00
  • @Nyerguds - if I need to use a catch clause then is `using` still a good option? I thought it was syntactic sugar for try/finally ? – whytheq Mar 07 '18 at 12:01
  • @Nyerguds do you have time to post an answer - I think your suggestion concerning disposing of the connection is very valid – whytheq Mar 07 '18 at 12:02
  • 1
    @whytheq `using` automates cleanup, so it is always advised over manually disposing for locally-used disposables. And, if you knew you'd need `catch`, why didn't you add it from the start? An app that doesn't handle its errors _at all_ is _never_ 'defensive'. – Nyerguds Mar 07 '18 at 12:03
  • @Nyerguds I agree - if I knew how to defend correctly, which I obviously don't, then I wouldn't have asked the question – whytheq Mar 07 '18 at 12:04

1 Answers1

4
  1. Use parameters. Do not concatenate strings to create SQL statements. Read about SQL Injection.

  2. Instead of using try...finally you can simplify your code with the using statement. You need to dispose all the instances that implements the IDisposable interface, so you also need to use the using statement with the SqlConnection. In fact, it's even more important then disposing the SqlCommand, since it allows ADO.Net to use connection pooling.

  3. You don't need to do the entire procedure again and again, just the connection.Open and the ExecuteNonQuery.

  4. Using the constructor that accepts a string an SqlConnection saves you the need to set them via properties.

  5. You don't need to specify CommandType.Text - it's the default value.

Here is a basic implementation of retry logic with some improvements to your code:

public void markComplete(int aTKey)
{
    var sql = " UPDATE dbo.tb_bar " +
               " SET    LastUpdateTime    = CONVERT(Time,GETDATE()) " +
               " WHERE  AKey = @aTKey";

    using(var myConnect = new SqlConnection(ConfigurationManager.ConnectionStrings["foo"].ConnectionString))
    {
        using(var mySqlCommand = new SqlCommand(sql, myConnect))
        {
            mySqlCommand.Parameters.Add("@aTKey", SqlDbType.Int).Value = aTKey;

            var success = false;
            var attempts = 0;
            do
            {
                attempts++;
                try
                {
                    mySqlCommand.Connection.Open();  
                    mySqlCommand.ExecuteNonQuery();
                    success = true;
                }
                catch(Exception ex)
                {
                    // Log exception here
                    Threading.Thread.Sleep(1000);
                }
            }while(attempts < 3 || !success);
        }   
    }   
}

Update:

Well, I've had some free time and I remember writing a general retry method a few years back. Couldn't find it but here is the general idea:

static void RetryOnException(Action action, int numberOfRetries, int timeoutBetweenRetries)
{
    var success = false;
    var exceptions = new List<Exception>();
    var currentAttempt = 0;
    do
    {
        currentAttempt++;

        try
        {
            action();
            success = true;
        }
        catch(Exception ex)
        {
            exceptions.Add(ex);
            Threading.Thread.Sleep(timeoutBetweenRetries);
        }
    } while(!success || currentAttempt < numberOfRetries);

    // Note: The Exception will only be thrown in case all retries fails.
    // If the action completes without throwing an exception at any point, all exceptions before will be swallowed by this method. You might want to log them for future analysis.
    if(!success && exceptions.Count > 0)
    {
        throw new AggregateException("Failed all {numberOfRetries} retries.", exceptions);
    }
}

Using this method you can retry all sort of things, while keeping your methods simpler and cleaner.
So here is how it should be used:

public void markComplete(int aTKey)
{
    var sql = " UPDATE dbo.tb_bar " +
               " SET    LastUpdateTime    = CONVERT(Time,GETDATE()) " +
               " WHERE  AKey = @aTKey";

    using(var myConnect = new SqlConnection(ConfigurationManager.ConnectionStrings["foo"].ConnectionString))
    {
        using(var mySqlCommand = new SqlCommand(sql, myConnect))
        {
            mySqlCommand.Parameters.Add("@aTKey", SqlDbType.Int).Value = aTKey;
            // You can do this inside a `try...catch` block or let the AggregateException propagate to the calling method
            RetryOnException(
                () => {
                    mySqlCommand.Connection.Open();  
                    mySqlCommand.ExecuteNonQuery();
                }, 3, 1000);
        }   
    }   
}
Zohar Peled
  • 73,407
  • 8
  • 53
  • 101