C# 10.0 implicit global using directives
One of the main goals of C# 10.0 and .NET 6.0 is to reduce clutter, enabling code to get straight to the point. In this blog I'll show one of the new features supporting this: implicit global using directives (aka 'global imports'). The one problem with .NET 6.0's new minimalism is that when you find you need to take control, it's often not obvious how, so I'll also show what to do if you find that the defaults aren't quite right for you. As always, the solution is to understand how it works.
Cut to the chase
The classic 'hello world' program offers a stark example of how some of C# 10's new features enable your code to get straight to the point.
Here's how it looked in C# 9:
using System;
namespace HelloWorldCs8
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
And here's the C# 10 version:
Console.WriteLine("Hello, World!");
That's quite a big difference.
Several features come together here to make this possible, and in this article I'm looking at just one: implicit global using directives. It's the feature making the smallest difference in this particular case, but along with file-scoped namespace declarations, it's typically the one that will have the broadest impact on your code, because it is likely to affect almost every source file in your project.
Strictly speaking, getting all the way down to the single line of code shown above relies not just on the C# 10 language changes, but also an associated .NET SDK 6 feature. So let's break it down.
Using directives
First of all, let's just recap what using directives are. A using directive enables us to avoid referring to things by their full name. Most .NET types have fairly long names. Our 'hello world' example uses just one API, the WriteLine
method, but since in C#, all methods belong to a class, we've written Console.WriteLine
. Even that's not the whole name: the defining class is really called System.Console
. That's a fairly short one, so writing System.Console.WriteLine
wouldn't be the end of the world, but nobody wants to have to write System.Collections.Generic.List<System.Threading.Tasks.Task<System.IO.Stream>>
. You'd expect to see just List<Task<Stream>>
.
Using directives enable us to write these shorter forms: they let us tell the compiler that we want to refer to members of certain namespaces without spelling out those namespaces in full every single time. The first line of the C# 9 example above is an example: that using System;
tells the C# compiler that if our code refers to some type (e.g., Console
) that is not defined in our code's own namespace, and which is not defined in the global namespace (the "no namespace" namespace; rarely used in practice), then it should try looking in the System
namespace. That first example wouldn't compile without this. The compiler understands that the reference to Console
really means System.Console
thanks to this using directive.
Using directives of this kind have been available since C# 1.0, and they are important because always using everything's full name would lead to intolerable clutter. But a downside is that C# files often start with a load of using directives. Some people consider this to be mere noise, meaningless 'ceremony' to be trudged through before you can get to the real content. That's debatable: there's a counterargument that the list of using directives give you a decent idea of what kinds of things the source file does. However, when certain common namespaces appear in almost every file (e.g., System
crops up a lot, as does System.Collections.Generic
), then you can't glean much useful information from their presence.
New in C# 10: the global modifier
C# 10 introduces the new global using directive language feature so that you don't have to write the same using directives again and again in hundreds of files. You turn an ordinary using directive into a global one by adding the global
modifier. E.g.:
global using System;
With that at the top of just a single source file, the effect is as though you had put an ordinary using System;
directive at the top of every file in your project. (Pedants will note that this is an oversimplification, because the way the compiler processes using directives within a file can be affected by other using directives in that same file. This doesn't happen with global using directives, so there are situations where this simple description isn't quite right. The language feature specification ties it down more precisely, but in the majority of cases, this informal description is good enough.)
As long as we've got this global using directive importing the System
namespace somewhere in our project, we can write Console
to refer to System.Console
in any file, without needing a using System;
in that file. You can see how the compiler interprets it by hovering the pointer over the Console
symbol (or invoking the Edit > IntelliSense > Quick Info menu action) as the screenshot below shows. Visual Studio's IntelliSense features are driven by the compiler, so this provides an accurate view of how the compiler understands the symbol:
Note that the global
modifier also works in conjunction with the other types of using directive. So it is possible to apply global
to a using directive that defines an alias. Likewise, you can combine the global
and static
modifiers. For example, global using static System.Console;
would enable any file to use just WriteLine
without even needing to say Console
. But as you'd expect, the global
modifier doesn't work with a using statement, which is an entirely different kind of thing from a using directive, united only by a common keyword.
There is a downside here: when the compiler relied on a global using directive to resolve a symbol, you can't tell how it did that just by looking at the containing source file. If you see code that uses a type you don't recognize, you can hover the mouse over it to find out its full name, but there's no direct way to discover where the using directive that enabled the compiler to understand it came from. When the using directive is at the top of the same file, that's straightforward, but if it came from a global using directive, then the directive could in theory live in literally any file in the project. So global using directives can reduce transparency. This implies that undisciplined use of global using directives might be a bad idea. Hold that thought.
Implicit global using directives
Returning to our 'hello world' example, you might now be thinking that maybe we didn't really save ourselves any effort at all: if all we did was move that using System;
to a different file and add the global
keyword, did we really simplify anything? It sounds like my 'hello world' now needs twice as many source files, and the using directive got longer.
Not so.
The one-line 'hello world' you see above really is the only source file you need to write. It relies not just on the new C# 10 language feature, global using directives, but also a related new feature added in the .NET 6 SDK: implicit global using directives. In essence, the .NET SDK build system can generate these global using directives for you.
You can enable generation of implicit global using directives by adding a <ImplicitUsings>enable</ImplicitUsings>
setting inside a <PropertyGroup>
in your .csproj
file. (They are disabled by default because enabling them on an existing project has the potential to change the way in which the compiler interprets some code, which would increase the chances that upgrading from .NET 5 to .NET 6 would break existing code.) The project templates in the .NET 6 SDK include this setting—all newly-created projects will have the relevant setting in their project file, so implicit global using directives are enabled by default in new projects, but not for projects you are upgrading from earlier versions of .NET.
If you create a brand new console application with the .NET 6 SDK, you will find that a whole bunch of .NET class library types can now be used without the qualification you would have needed on older versions of .NET—no need for either explicit using
directives or fully-qualified names.
How does that work?
I need to clarify something at this point: although global using directives are a C# 10 language feature, implicit global using directives are not a language feature: they are a feature of the .NET SDK. If you're like me, you'll have some questions at this point:
- how does that work?
- which namespaces do I get?
- can I change which namespaces I get?
When I first started looking at this feature, I was a bit alarmed that it wasn't at all obvious how it worked. I don't like things that work as if by magic, because that tends to make life hard if you need to change the behaviour for any reason. However, I've come to quite like the implicit form of global using directives now that I know how it works, not least because it actually imposes more consistency and discipline than the unconstrained use of global using directives. So how does this work?
You can answer the first two questions in that list by looking inside the obj
folder that gets created when you build a project. In here you'll find a subfolder named for your build configuration (usually Debug
or Release
), containing a net6.0
folder. And inside there you'll find a file called something like MyProject.GlobalUsings.g.cs
, where MyProject
is whatever your project file is called (but without the .csproj
extension). If you open it up you'll find something like this:
// <autogenerated />
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;
The set of namespaces changes according to the project type. If you create an ASP.NET Core application it looks like this:
// <autogenerated />
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;
global using global::System.Net.Http.Json;
global using global::Microsoft.AspNetCore.Builder;
global using global::Microsoft.AspNetCore.Hosting;
global using global::Microsoft.AspNetCore.Http;
global using global::Microsoft.AspNetCore.Routing;
global using global::Microsoft.Extensions.Configuration;
global using global::Microsoft.Extensions.DependencyInjection;
global using global::Microsoft.Extensions.Hosting;
global using global::Microsoft.Extensions.Logging;
This reveals that implicit global using directives are not a language feature. From the compiler's perspective, this is just a source file with a bunch of global using directives. The "implicit" part comes from the fact that the .NET SDK generates this file for us and hides it in the obj
folder.
In .NET SDK 6.0.100-rc.1, this file is generated by a build target in the Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.GenerateGlobalUsings.targets
file. (On my system, the RC1 SDK is installed at C:\Program Files\dotnet\sdk\6.0.100-rc.1.21458.32\
but it may be elsewhere on yours.) It uses an MSBuild Item Group to manage the generation of global using directives
. This tells us how to answer the third question above: we can control which namespaces are included here by modifying that item group. For example, we could add this to a csproj
file:
<ItemGroup>
<Using Include="System.Text.Json" />
<Using Include="System.Text.Json.Serialization" />
</ItemGroup>
The items shown here will cause the generated list of global using directives to include these two additional entries. Whether that's any better than just adding equivalent directives explicitly to one of your C# source files is debatable, although it does have the advantage that you know exactly which file to use. (So in this respect, it's arguably better than unconstrained use of global using directives.) But one thing that can only be achieved by controlling the item group is to switch off the generation for some of the default namespaces. For example, if you're writing code that uses WPF's 2D path handling APIs, you probably don't want the implicit global using directive for System.IO
, because it introduces ambiguity: both System.IO
and System.Windows.Shapes
define a class called Path
. (As it happens, WPF projects in .NET 6 don't use implicit global using directives by default, but this issue could arise if you write a class library.) You can disable it with the Remove
attribute:
<ItemGroup>
<Using Remove="System.IO" />
</ItemGroup>
Conclusion
Implicit global using directives are one of the many new features in C# 10 and .NET 6.0 that enable us to reduce the repetitive chatter in our source files. For most projects, there's no corresponding loss of expressiveness: it enables us to omit using directives that would have appeared in most of our files, and which therefore tell us very little of value. This comes at the cost of a slight decrease in transparency, but once you've understood the underlying mechanism, you have complete control of how the feature operates. The result is that we will only need to add ordinary (non-global) using directives because of a particular source file's distinctive requirements. This makes our code more communicative, which, in my view, is a good thing.