C# 10 Falls Just a Bit Short
.NET’s keystone language plays it safe, and cuts back on a few new features

It’s a dangerous game, trying to anticipate what features will make the final cut of a highly anticipated software release. C# 10 is a case in point. They opened the doors for us to watch them debating the language in plain sight. They built a website that collects goals and future plans. They let Mads Torgersen — C#’s current lead designer and all-around brilliant mind — demonstrate prospective features in public. So perhaps it was inevitable that we’d feel the sting of disappointment when some of these features didn’t make it into this month’s release of .NET 6.
A few of cuts might come as a surprise. The C# team removed features that appeared tidy and mature, and didn’t seem to disrupt the rest of the C# ecosystem. But as the old adage goes, you can’t count your chicken array before it’s instantiated. C# 10 is officially settled, and all that’s left is for us to figure out what we’ve got, what we lost, and what’s still likely to appear at this time next year in C# 11.
New features are fun, but poorly thought-out features are forever. Once it’s in C#, it’s there for life.
File-scoped namespaces (It’s in!)
File-scoped namespaces is a syntax that lets you apply a namespace to an entire file, without the usual indenting. It’s a relatively small refinement, but if you’ve ever wasted ten minutes counting tabs or scrolling horizontally, you’ll appreciate the improvement.
Here’s some C# code in an ordinary namespace:
namespace LoggingTestApp
{
public class Startup
{
...
}
}And here’s the same code written with the file-scoped namespace syntax:
namespace LoggingTestApp;public class Startup
{
...
}Notice the semicolon after the namespace name, which is the key detail that switches on a file-scoped namespace.
If you add a namespace block to a file that uses a file-scoped namespace, it creates a nested namespace, as you would expect:
namespace Company.Product;// This block creates the namespace Company.Product.Component
namespace Component
{
}The C# designers describe this change as cleaning up horizontal waste. The overall goal is shorter, narrower, more concise code expression. But these changes are also part of the ongoing work to make C# look less intimidating to newcomers.
Automatic null parameter checking (Cut)
Like many dropped C# features, automatic null parameter checking has been proposed before, but its kinks have never been completely ironed out. The goal is to save your methods the tedium of checking parameters and raising an ArgumentNullException, with the help of a !! operator that developers either love or love to hate. But questions remain. Is it really worth giving C# an entirely new operator just for one small convenience?
The idea was first introduced in the C# 8 timeframe, and today the debate continues.
Global usings (It’s in!)
This is possibly C# 10’s most frequently demonstrated addition. Like file-scoped namespaces, global usings is all about tidier, less intimidating code. But this time we’re going after the vertical waste — in other words, the extra lines of boilerplate we scroll past every day.
The problem is namespace imports, which aren’t particularly compact. For example, a code file in a typical ASP.NET web application starts like this:
using LoggingTestApp.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;namespace LoggingTestApp
{
public class Startup
{
...
}
}C# 10 adds the global using directive, which lets you define namespace imports across an entire project. It’s recommended that you place your global imports in a separate file (one for each project), possibly named globalusings.cs. Here’s an example:
global using Microsoft.AspNetCore.Builder;
global using Microsoft.AspNetCore.Hosting;
global using Microsoft.AspNetCore.HttpsPolicy;
global using Microsoft.AspNetCore.Identity;
global using Microsoft.AspNetCore.Identity.UI;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;Now you can simplify the original file:
using LoggingTestApp.Data;
using Serilog;namespace LoggingTestApp
{
public class Startup
{
...
}
}Visual Studio highlights namespaces that are duplicated (imported globally and locally in a file). Although that’s not an error, removing duplicate namespaces allows you to reduce the amount of stock code and focus attention on the special namespaces that a particular file is using.
But wait… We also have implicit usings
This falls under the heading of “features we’ll view with suspicion today, but probably accept tomorrow.” In its drive for simplicity, new .NET 6 projects don’t contain a global usings file stuffed with a bunch of namespaces. Instead, they hide the namespaces altogether with a complimentary feature called implicit usings.
Implicit usings are simply using statements that the compiler emits automatically for certain project types. For example, a plain vanilla Console project will automatically have the implicit usings for the following namespaces, if you create it using the .NET 6 project template:
System;System.IO;System.Collections.Generic;System.Linq;System.Net.Http;System.Threading;System.Threading.Tasks;
You can disable implicit usings by editing the .csproj file for your project and changing ImplicitUsings from enable to disable. You can also explicitly remove individual implicit usings, or add new ones using the syntax described in the documentation, but that seems like a great way to confuse people.
The field keyword (Cut)
This was a surprise. There was a lot of work behind the field keyword, it ties into the popular autoimplemented properties feature, and it’s appeared in several Microsoft talks.
The idea is simple. Today, C#’s autoimplemented properties save a significant amount of work but aren’t convenient in all scenarios. And when they don’t work, you need to switch back to the standard property pattern, with all the get and set boilerplate we’ve used for generations.
The field keyword addresses some of these limitations by letting you access the backing field for an autoimplemeted property. Now you can keep using autoimplemented properties even if you have to deal with validation, notification events, and the MVVM frameworks that depend on that infrastructure. Or at least you could have, if fields hand’t been dropped from C# 10.
The field keyword is down but not out. There’s a surprising number of edge cases and a healthy debate about other syntactical approaches to semi-autoimplemented properties. It’s currently being considered for C# 11.
Bonus: some extra struct features
C# 10 did get the opportunity to slip in a few refinements with the way that structs work. You can now create your own parameterless constructor in a struct. (Before, you were stuck with the default implementation that initializes the struct’s default values.) As a happy side effect, you can finally use property initializers in a struct:
public struct Rectangle
{
public int Width { get; init; } = 10;
public int Height { get; init; } = 10;
}On another note, you can now create a record that’s also a struct:
public readonly record struct Employee
{
public string Name { get; init; }
public decimal YearlySalary { get; init; }
public DateTime HiredDate{ get; init; }
}But wait, why the readonly? It turns out that even though records are all about immutable data, a record struct breaks that rule — unless you add the familiar readonly modifier. Does that sit well with you? Me neither, but I’m still open to enlightenment if a genius-level language designer wants to make the case.
Record structs pick up the other frills that you get with ordinary records, like the ability to use with and automatic support for value-type equality with the == and != operators. And behind the scenes, record structs are — like all structs — value types that are allocated on the stack. This can be a significant consideration when optimizing certain code routines. To learn more, I recommend this low-level look at the way record structs are implemented.
Required properties (Cut)
It seems like a simple ask — a way to define a property so it needs to be set during initialization. In classic OOP programming, the constructor fills that role, but the price is a lot of boilerplate. And modern studies tell us that the source of a surprisingly large share of programming bugs is minor mistakes in exactly this kind of boilerplate.
In C# 9, we started using the record type as a lightweight way to deal with immutable structures data. Bulky constructors aren’t a great fit for modern records, and the required keyword stood out as a much cleaner solution. But as always, the devil is in the details. After all, we already have a lot of modifier keywords clogging up our properties. And should required really be called mustinit or something else? Does the modifier belong on the property itself or the setter?
As is often the case with code, these details seem trivial until you actual begin implementing a solution. The C# overlap between old-school OOP and newer, functional-influenced coding practices needs to be managed very carefully so it doesn’t overwhelm the language with extra baggage. The required keyword is just the latest casualty, and the discussion continues for C# 11. As the team says, “We know we want to do something here, but we need to find a good syntax.”
A hidden feature: Static abstracts in interfaces
Interestingly, there’s one new feature that disappeared from view but is still in the C# compiler. The feature is static abstract members in interfaces, and it does exactly what its long mouthful of a name says.
Here’s a quick review. C# 8 added the ability to put static members in an interface, but the interface needed to include the implementation code. In C#10, the static members can be abstract, so the class that implements them can provide its own implementation. This sounds neat, but a bit over-complicated, until you consider some of the potential uses. One of the most requested is generic math, which allows you to use operators (which are implemented as static methods) on generic types. You can dive into that deep subject over here.
The catch is that this feature is in preview, and you need to explicitly enable it using the EnablePreviewFeatures project setting. This raises an obvious question — why is this feature being shipped out to developers in preview mode rather than quietly deferred, like so many other missing C# features? The answer is that Microsoft wants to collect developer feedback before it formalizes static abstracts in interfaces. The feature isn’t production-ready (in fact, it’s almost certain to pick up some breaking changes before an official release), but it’s ready for experimentation. Fun!
Any language designer will tell you that what gets left out of a language is more important than what’s put in. Add a half-baked feature without proper consideration of the potential side effects, and it’s there forever — or at least as long as the language lives. With that in mind, it’s no surprise that the team managing C# is a conservative bunch, willing to wait on new ideas, even if they seem to be relatively small changes. As you’ve seen, many of the features that were dropped from C# 10 are still on the agenda for C# 11. And there’s only 12 more months of waiting to see if they make the next cut.
You can read a list of all the C# 10 tweaks on Microsoft’s developer blog, and follow planning for C# 11 on GitHub. And why not subscribe for the once-a-month Young Coder newsletter?
