19

I understand that around 2019 Microsoft created Microsoft.Data.SqlClient as their replacement for System.Data.SqlClient. System.Data.SqlClient will be supported ongoing but new dev & features will all be in Microsoft.Data.SqlClient. Both of these libraries have a class named 'SqlException'.

Assuming I'm using Microsoft.Data.SqlClient everywhere then presumably relevant exceptions will be of type Microsoft.Data.SqlClient.SqlException, BUT I use a bunch of 3rd-party libraries, how can I be sure whether they'll raise a a Microsoft.Data.SqlClient.SqlException or a System.Data.SqlClient.SqlException? Does this mean in a few cases where I have catch handlers for Microsoft.Data.SqlClient.SqlException I should also check for System.Data.SqlClient.SqlException? Or is there something clever that happens which means I only need to think about Microsoft.Data.SqlClient.SqlException?

e.g. I have some old code a bit like I've shown below, written before we started using Microsoft.Data.SqlClient. I fear if I simply change it to use Microsoft.Data.SqlClient then there will be some exceptions that are System.Data.SqlClient.SqlException and my code will no longer notice them.

    private static bool HandleSqlExceptionInSomeWay(Exception ex)
    {
        var se = ex as System.Data.SqlClient.SqlException;

        if (se != null)
        {
            // ... do something with the SqlException
            return true;
        }

        return false;
    }

So should I change it to be something like this, i.e. check separately for the different types?

    private static bool HandleSqlExceptionInSomeWay(Exception ex)
    {
        // handle old SqlExceptions (e.g. from any old 
        // libraries not using MS package yet)
        var se1 = ex as System.Data.SqlClient.SqlException;

        if (se1 != null)
        {
            // ... do something with the SqlException ...

            return true;
        }

        // handle shiny new SqlExceptions 
        var se2 = ex as Microsoft.Data.SqlClient.SqlException;

        if (se2 != null)
        {
            // ... do something with the SqlException ... 

            return true;
        }

        return false;
    }
Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
Rory
  • 40,559
  • 52
  • 175
  • 261
  • Always use the new library when possible. Most NuGet packages already use `Microsoft.Data.SqlClient.SqlException` already. – Panagiotis Kanavos Nov 12 '21 at 10:35
  • `how can I be sure whether` which package do they use? If they use the old library, upgrade to a version that uses the new one. – Panagiotis Kanavos Nov 12 '21 at 10:36
  • I suppose if you just catch `Microsoft.Data.SqlClient.SqlException` your "catch all global logging handler" will start seeing `System.Data.SqlClient.SqlException` if dependencies are throwing it? – Caius Jard Nov 12 '21 at 10:37
  • If it helps, they both inherit from `System.Data.Common.DbException` – DavidG Nov 12 '21 at 10:42
  • 1
    **In theory**, the library should document the type of exceptions which can be thrown by its methods (for example, using `` xmldoc). This would be part of the method contract, and, thus, changing that would be a breaking change, requiring a new major version number in semantic versioning as well as being mentioned in the release notes. However, **in practice**, theory and practice don't always align... – Heinzi Nov 12 '21 at 10:43
  • 2
    As an aside, your `var se1 = ex as System.Data.SqlClient.SqlException; if (se1 != null)` might be more readable as `if(ex is System.Data.SqlClient.SqlException sex) ...` – Caius Jard Nov 12 '21 at 10:45

3 Answers3

12

The two classes are different but they do inherit from the same base class, DbException. That's the common class for all database exceptions though and won't have all the properties in the two derived classes

You should inspect the libraries/NuGet packages you use and ensure you use versions that support the new Microsoft.Data.SqlClient library. Mixing up data providers isn't fun and should be avoided when possible. Most popular NuGet packages use Microsoft.Data.SqlClient already.

If you can't do that, the options depend on how you actually handle database exceptions. Do you inspect the SQL Server-specific properties or not?

Another option is to postpone upgrading until all NuGet packages have upgraded too. Both libraries include native DLLs that need to be included during deployment. If you mix libraries, you'll have to include all native DLLs.

This can be painful.

Handling the exceptions

If both libraries need to be used, each exception type needs to be handled separately. Pattern matching makes this a bit easier :

switch (ex) 
{
    case  System.Data.SqlClient.SqlException exc:
        HandleOldException(exc);
        return true;
    case Microsoft.Data.SqlClient.SqlException exc:
        HandleNewException(exc);
        return true;
    case DbException exc:
        HandleDbException(exc);
        return true;
    default:
        return false;
}

Mapping Exceptions

Another option could be to map the two exception types to a new custom type that contains the interesting properties. You'd have to map both the SqlException and SqlError classes. Using AutoMapper would make this easier:

var configuration = new MapperConfiguration(cfg => {
    cfg.CreateMap<System.Data.SqlClient.SqlException, MySqlException>();
    cfg.CreateMap<System.Data.SqlClient.SqlError, MySqlError>();
    cfg.CreateMap<Microsoft.Data.SqlClient.SqlException, MySqlException>();
    cfg.CreateMap<Microsoft.Data.SqlClient.SqlError, MySqlError>();
});

This would allow mapping either exception to the common MySqlException type :

var commonExp=mapper.Map<MySqlException>(ex);
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • This - if you have a library that hasn't been updated in the past 2 years, consider upgrading to the latest version if available or consider getting rid of it. – Camilo Terevinto Nov 12 '21 at 10:44
  • Thanks, but is there any way of knowing whether a package uses the new library, without it being open source and me checking the code? – Rory Nov 12 '21 at 10:45
  • 2
    @Rory NuGet packages give you the dependency list. If you're not using NuGet packages, open the dll files through ILSpy which will give you the dependencies – Camilo Terevinto Nov 12 '21 at 10:46
  • @CamiloTerevinto - for many internal applications a 2yr old library is far from uncommon and upgrading or changing libraries is unfortunately too expensive. – Rory Nov 12 '21 at 10:51
  • @Rory I'm well aware of that, but it's also usually much cheaper than the security risks associated with outdated libraries :) It's all about trade-offs, that's why I said "consider" – Camilo Terevinto Nov 12 '21 at 10:53
  • @Rory you could use AutoMapper to map either exception to your own custom exception. You'd have to consider how much handling multiple exceptions would cost vs upgrading. Consider your salary and multiply it by 2-3 - it's not what your (gross) salary is, but what billable job you could be doing if you didn't have to handle multiple exception types. Maintaining that code costs too. – Panagiotis Kanavos Nov 12 '21 at 11:10
  • Surely `catch (System.Data.SqlClient.SqlException ex) {...} catch (Microsoft.Data.SqlClient.SqlException ex) {...}` makes more sense than pattern matching? – Charlieface Nov 12 '21 at 12:58
  • @Charlieface not in `private static bool HandleSqlExceptionInSomeWay(Exception ex)`. Besides, adding such code all over the place would be hard to maintain. This method would allow writing eg `catch(Exception ex) when HandleSqlExceptionInSomeWay(ex)` or `catch(DbException ex) when HandleSqlExceptionInSomeWay(ex)` without specifying the concrete type – Panagiotis Kanavos Nov 12 '21 at 13:54
4

To expand on Richard's answer, it means your try/catch end up looking like:

try{
  //boom
} catch(Microsoft.Data.SqlClient.SqlException ex){
  Handler(ex);
} catch (System.Data.SqlClient.SqlException ex) {
  Handler(ex);
}

If you want to deal with both..

Handler is some accessible overloaded (or parent typed parameter) method that deals with each kind.. Not the prettiest, but alas, there isn't a way for one catch to catch N different types, unless the types all have the same usable parent that can be caught instead (and then the type of exception inspected if required)..

Edit; completely forgot about catch when which is useful for this last case, thanks @Heinzi

try{
  //boom
} catch (DbException ex) 
  when (ex is Microsoft.Data.SqlClient.SqlException || ex is System.Data.SqlClient.SqlException)
{
  //handle
}
Caius Jard
  • 72,509
  • 5
  • 49
  • 80
  • 1
    Alternatively, if you want only one catch block and don't need specific exception properties: `catch (Exception ex) when (ex is Microsoft.Data.SqlClient.SqlException || ex is System.Data.SqlClient.SqlException)`. – Heinzi Nov 12 '21 at 10:46
  • 2
    @Heinzi At that point I'd rename them to `using OldSqlException = System.Data.SqlClient.SqlException; using NewSqlException = Microsoft.Data.SqlClient.SqlException;` :D – Camilo Terevinto Nov 12 '21 at 10:49
  • I don't think the last works because you get a compiler error that it might not be initialized. Tbh i like the first version most, one exception handler for the concrete type. The Handler method could use a wrapper class which derives from `System.Data.Common.DbException`(has already `ErrorCode`) to map properties that are in both `SqlException` types. – Tim Schmelter Nov 12 '21 at 11:18
  • @TimSchmelter I didn't test it, but I've used the same syntax to differentiate DTOs from different packages in the past. I also prefer `Handler` handling this internally though – Camilo Terevinto Nov 12 '21 at 11:20
  • You do realize formatting your C# code using K&R brace style which is completely nonstandard in the .NET world makes it harder for everybody in the community to read your code samples and work with them, right? – julealgon Feb 11 '23 at 17:03
1

If you can't be sure whether the third-party libraries are using the Microsoft or the System library, you'll need to handle both. There's no "magic sauce" that converts a SqlException from one library into a SqlException from the other.

Richard Deeming
  • 29,830
  • 10
  • 79
  • 151