How a Windows 10 Update Might Have Broken Your .NET App
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;“)) { }
Avoid automatic framework updates with .NET Core
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:
Of course, there’s a wealth of information and context on this page, but the most important information is visible at first glance. Notice the icons at the top: runtime and operating system.
First, we see the .NET Framework icon followed by version 4.7.2, which makes sense considering this error was introduced with that version. We also see the operating system: Windows, Version 10.0.17134. As Microsoft words it, this number denotes: "Windows 10 build 17134 (also known as the April Update or version 1803)."
To be able to display this information, Sentry relies on its .NET SDK to provide data, including the .NET Framework release number. In case of this Windows Update, Sentry knows that the release id 461808 means .NET Framework 4.7.2.
In addition to reporting .NET Framework, the event details include .NET Core and Mono, which can be reported from Windows, macOS and Linux:
An application built for .NET Framework running on Linux under Mono will report the OS as Unix. In the example above, I'm using Ubuntu on the Windows Subsystem for Linux (WSL) so the build number is the same as Windows:
Linux Tampere 4.4.0-17134-Microsoft #48-Microsoft Fri Apr 27 18:06:00 PST 2018 x86_64 x86_64 x86_64 GNU/Linux
Another interesting case is that of an app targeting .NET Core and running on macOS. The event details would report simply as Darwin, which could be a macOS or iOS. If there’s enough demand, we can improve this further by looking up certain files in the system that can tell us which distribution is running. Does this sound interesting to you? Make your demands… I mean, er, please let us know your interest via the forum.
Have questions about Sentry’s new context interface? Reach out to our support engineers. They’re here to help. And also to code. But mostly to help.