Several days ago, Microsoft released the April 2018 Update (1803) of Windows 10. Included in this update is an upgrade to .NET Framework, bringing it up to version 4.7.2.

This release was an in-place installation of .NET Framework 4, which means that any .NET application you built in nearly the last 10 years will run on it. (That is, if your application was compiled to target the following versions of the .NET Framework: 4.0, 4.5, 4.5.1, 4.5.2, 4.6, 4.6.1, 4.6.2, 4.7, 4.7.1, 4.7.2.)

Surprise! An app you created yesterday, or even years ago for that matter, could suddenly start crashing because of an operating system update.

Take for example this code snippet, which can target .NET Framework 4.0 (released in April 2010):

class Program { static void Main() { new System.Data.SqlClient.SqlConnection("Data Source=;") { ConnectionString = null }; } }

The good news: despite targeting an ancient version of .NET Framework, this code would have run successfully on subsequent .NET updates for the next eight years.

The bad news: this snippet executed on 4.7.2 throws the following exception:

Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object. at System.Data.SqlClient.SqlConnection.CacheConnectionStringProperties()

The Culprit

So, what is actually causing the issue?

While investigating, I noticed that if SqlConnection is instantiated while providing a connection string and the ConnectionString setter is subsequently called, the exception will be thrown. The exception will also be thrown by simply invoking the setter twice.

Using a reflector, we can observe the method that throws. Here, we see what is called by the constructor and the setter in ConnectionString :

private void CacheConnectionStringProperties() { SqlConnectionString connectionOptions = this.ConnectionOptions as SqlConnectionString; if (connectionOptions != null) this._connectRetryCount = connectionOptions.ConnectRetryCount; if (this._connectRetryCount != 1 || !ADP.IsAzureSqlServerEndpoint(connectionOptions.DataSource)) return; this._connectRetryCount = 2; }

Compared with the reference source, which does not include 4.7.2 yet:

private void CacheConnectionStringProperties() { SqlConnectionString connString = ConnectionOptions as SqlConnectionString; if (connString != null) { _connectRetryCount = connString.ConnectRetryCount; } }

Clearly, the call to ADP.IsAzureSqlServerEndpoint is newly introduced. There’s a null check before accessing ConnectionRetryCount but no guard before DataSource , which throws. Luckily, the error only happens if we try to reassign the ConnectionString value.

The issue has been reported to Microsoft, and they’ve updated to say that they’re working on a fix.

The Workaround

Don’t worry! You can easily work around the issue by using a new instance of SqlConnection instead of trying to reuse an existing one. Just make sure not to set the connection string more than once.

using (var first = new SqlConnection(“Data Source=first;“)) { } using (var second = new SqlConnection(“Data Source=second;“)) { }

Hopefully this (very real) example illustrates that even stable code that has been running bug-free for years can still error when run on new versions of .NET Framework, including those that are introduced as part of automatic operating system updates.

One way to protect against automatic framework updates is by using .NET Core.

Microsoft released .NET Core less than two years ago. Compared to the full .NET Framework it’s a very young tech, but one of the advantages of .NET Core is that installations are side-by-side. In other words, new installations do not affect older ones, and you can specify the exact version you want your code to run. Unsurprisingly, version selection allows you to avoid issues like this one.

Check out .NET Core and learn more with some of Microsoft’s comprehensive tutorials.

Stay informed with Sentry

Whether you use .NET Core, .NET Framework, or Mono, unexpected bugs can ruin anybody’s picnic.

To make sure you’re informed of such errors, you need to be closely monitoring your logs, or better yet, using a dedicated exception monitoring tool. For the latter, we at Sentry recommend Sentry.

On top of all the great stuff Sentry does (like display the stack trace, provide detailed context for each exception, show the details that lead up to the error, and tie the error to a specific commit and author), Sentry has recently added clear information about runtime and operating system versions of the device which raised the event.

Getting started with Sentry takes only a few minutes. Take a look at how to install our .NET SDK, SharpRaven.

Let’s update our previous sample to capture the error with Sentry:

using System; using System.Data.SqlClient; using SharpRaven; using SharpRaven.Data; class Program { static void Main() { var client = new RavenClient("https://your-dsn@sentry.io/project-id"); try { var connection = new SqlConnection("Data Source=;"); connection.ConnectionString = null; } catch (Exception ex) { client.Capture(new SentryEvent(ex)); } } }

Running this code (which you can take from GitHub) will cause the exception to be logged to Sentry.

A captured exception in Sentry looks like this: