Skip to content
Ian Griffiths By Ian Griffiths Technical Fellow I
C# 8.0 nullable references: prepare today by not misusing 'as'

If you're not ready to enable nullable references in your C# projects today, you can still develop habits that will make adoption easier if you eventually decide to tackle it. In this post I'll look at one such technique: avoiding a popular misuse of C#'s as operator.

In fact, getting this right is a good habit in itself, because this widespread misuse causes problems even if you never step into the world of nullable references.

The as operator

The as operator is one of several mechanisms C# offers for when you have reason to believe that a reference refers to something more specialized than its static type. Take this example:

var o = JToken.Parse(text) as JObject;
bool hasItem = o.ContainsKey("item");

This uses the Json.NET library's JToken.Parse method, which has a return type of JToken. That is an abstract class, so in practice it must return some type derived from JToken. The exact type you get depends on the input. If you pass an argument of "null", "42", or "\"Hello, world\"", for example, JToken.Parse returns a JValue. If you pass "[]", "[1, 2, 3]", or "[1, "two", {"item":3}]", you'll get a JArray. And if you pass JSON representing an object, such as "{\"item\":3}", you'll get a JObject.

The Introduction to Rx.NET 2nd Edition (2024) Book, by Ian Griffiths & Lee Campbell, is now available to download for FREE.

The as operator is designed for when you don't know for certain what type to expect: if we write someExpression as JObject, we are asking C# to test whether someExpression refers to a JObject. If it does, we get a suitably-typed reference back, but we get null otherwise. So it is really meant to be used in conjunction with some sort of test for null, to ensure that you only attempt to use the result when the conversion succeeds, as in these examples:

var o = JToken.Parse(text) as JObject;
if (o != null)
{
    Console.WriteLine("item is " + (o.ContainsKey("item") ? "present" : "absent"));
}

bool hasItem = o != null ? o.ContainsKey("item") : false;

bool? itemPresence = o?.ContainsKey("item");

Since the introduction of pattern matching in C# 7, it rarely makes sense to use as at all, because you can now use is to combine the test with the conversion:

if (JToken.Parse(text) is JObject o)
{
    bool hasItem = o.ContainsKey("item");
    Console.WriteLine(hasItem);
}

What's wrong with not testing for null?

But why am I saying that as needs to be combined with a test? If you are absolutely certain that the conversion is going to succeed—maybe in this JSON case you happen to know that the string will invariably represent an object, and so JToken.Parse will always return a JObject—what's wrong with the initial example? After all, it's widely used, and appears to work.

Programming C# 10 Book, by Ian Griffiths, published by O'Reilly Media, is now available to buy.

I'll start with a philosophical objection, and then move onto practical ones.

A significant problem with the first example is that it doesn't state our intent clearly. The as operator is expressly designed for situations where the input expression might refer to an object that is not of the target type. If that's not your scenario, there's a better way: C# provides another mechanism intended for use when you expect the conversion always to succeed: a cast.

var o = (JObject)JToken.Parse(text);
bool hasItem = o.ContainsKey("item");

I believe that we should strive for our code to express our intents and expectations as clearly and precisely as possible. If we expect JToken.Parse to return a JObject we should use the language mechanism that says exactly that: a cast. Using as here is a less good fit for what we want the code to say.

The best hour you can spend to refine your own data strategy and leverage the latest capabilities on Azure to accelerate your road map.

But what about practical consequences? The obvious one is that if our expectation turns out to be incorrect, we get a less useful exception when we use as than we do if we use a cast. With a cast we've made it clear that we expect the result to be a JObject, and if it turns out not to be, we'll get an exception that makes the nature of our mistake plain. The cast will throw an InvalidCastException to let us know that the cast failed, and the exception's Message will contain the following text (or a localised equivalent):

Unable to cast object of type 'Newtonsoft.Json.Linq.JValue' to type 'Newtonsoft.Json.Linq.JObject'.

That makes it pretty clear what the mistake is. We tried to cast something to JObject, but a JValue is not a JObject. The presumption embodied in our cast turned out to be wrong.

Compare that with what happens if we're wrong in exactly the same way when relying on as—how does the first example fail? The as doesn't throw an exception, because by definition it doesn't treat a conversion failure as an error—it just produces null. So the error we actually see is a NullReferenceException when we first try to use the result of the as operator. So the exception won't even be thrown from the line that made the mistake—it will occur slightly later—and its message gives us no useful clues as to the nature of the problem:

Object reference not set to an instance of an object.

With the nullable reference types feature added in C# 8, there's an additional practical problem with abusing the as operator in this way.

Untested use of as, and nullability

In a project where you have enabled C# 8's nullable references feature, suppose you try to use the first snippet of code shown in this post, repeated here for convenience:

var o = JToken.Parse(text) as JObject;
bool hasItem = o.ContainsKey("item");

You will get a warning on the 2nd line of code:

warning CS8602: Dereference of a possibly null reference.

This is a direct result of this misuse of as. By using as, we've told the compiler quite clearly that we think the conversion to JObject might fail. (That might not have been what the developer meant, but it is what the code says.) The compiler's nullability analysis correctly concludes from this that by the time we reach the second line, o may well be null. (After all, if we were confident that it was definitely going to be a JObject, we could have said so by using a cast.) So the compiler warns us that we've got a potential error here—the attempt to use o might fail because it might be null.

If we use any of the other snippets shown above to test that we really do have a JObject before using it, that warning goes away. And if we don't want to write any such test because we're confident that we'll definitely have a JObject here, we can avoid this warning by using a cast instead of as.

Stop (mis)using as today

Even if you've got no plans to start using nullable references, you should not be using as in cases where a cast better expresses your intent and expectation. (In fact there's rarely any reason to use as at all—patterns now generally provide a more succinct alternative.) And if you ever do enable nullable references for your code, this will be one less class of warnings needing attention.

Ian Griffiths

Technical Fellow I

Ian Griffiths

Ian has worked in various aspects of computing, including computer networking, embedded real-time systems, broadcast television systems, medical imaging, and all forms of cloud computing. Ian is a Technical Fellow at endjin, and Microsoft MVP in Developer Technologies. He is the author of O'Reilly's Programming C# 10.0, and has written Pluralsight courses on WPF (and here) and the TPL. He's a maintainer of Reactive Extensions for .NET, Reaqtor, and endjin's 50+ open source projects. Technology brings him joy.