Windows 10 Update: Good News and Bad News
A few months 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.
Here, I’ll go over what Microsoft hath wrought with its most recent Windows update and how you can implement a workaround for crashes caused by the release and how to use Sentry to monitor for errors and iterate on your app without breaking a sweat.
Windows 10’s release was an in-place installation of .NET Framework 4, which means that any .NET application you built at any time 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 a decade 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 is that, 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 is that this snippet executed on .NET 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 which method that throws. Here, we see what the constructor and the setter are calling 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;
}
Compare this 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 the exception. Luckily, the error only happens if we try to reassign the ConnectionString
value.
The issue has been reported to Microsoft, and they’ve posted an update to say 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. Yikes 🙀
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, .NET Core is a very young technology. 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.
Fix .NET Errors Before Your Users Even Know There’s an Issue
Whether you use .NET Core, .NET Framework, or Mono, unexpected bugs can ruin anybody’s picnic.
To make sure you’re the first to see bugs and know the context so you can resolve the issue fast, you need an error tracking tool that is built for .NET developers and fits right into your existing workflow.
Sentry is an open-source workflow productivity platform that aggregates errors and crashes from any .NET or C# application and provides complete app logic, deep context, and visibility across the entire stack in real time.
Sentry’s core value includes exposing the complete stack trace for every error, providing detailed context for each exception, showing the events and preceding steps as a trail of breadcrumbs that led up to the error, and tying the error to a specific commit and author. Sentry has also recently added clear information about runtime and operating system versions of the device that raised the event, as well as espousing an owner to every issue so that nobody’s inbox gets overwhelmed.
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 pull down 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](https://paper.dropbox.com/?q=%2348-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.
Sentry Is Here to Help
As always, you can try Sentry for your .NET app. On average, it’ll cut time to resolution for app errors from five hours to five minutes. Save weeks per year fixing unseen bugs in your .NET app by resolving production issues right in your existing workflow. Sentry is 100% open source, takes two minutes to set up, and is loved by 500K developers.
That’s it. Happy error monitoring!