C# 10 and the dangers of file-scoped namespaces

Oliver Oliver • Published 2 years ago Updated 2 years ago


Today I'll be stepping away from a Unity environment and focusing on one of the new features available in C#, and a very important note to keep in mind when using it. That is, the dangers of file-scoped namespaces.

Starting with C# 10, namespace declarations can now be in the form of file-wide statements rather than a custom-defined scope. Consider a simple Hello World from older C#:

using System;

namespace MyApplication
{
    internal static class Program
    {
        private static void Main(string[] args)
        {
            Console.WriteLine("Hello World");
        }
    }
}

Previously, we had to indent for Main method, which is normal and expected. We also had to indent the class itself, which is mildly annoying but still perfectly normal.

But we also had to indent the namespace. And when you think about it, this is ridiculous. Every class should live within a namespace, and so every method body (including the Console.WriteLine call here) never actually started until character 13. Meanwhile in a language like C/C++, you can begin function bodies at character 5 - the single indentation of the function body is all you need. Damn, even Java with its package syntax means you only have to indent to character 9.

Well, C# 10 changes this. Namespaces can now be declared via a statement instead:

using System;
<mark>namespace MyApplication;</mark>

internal static class Program
{
    private static void Main(string[] args)
    {
        Console.WriteLine("Hello World");
    }
}

This frees up one indentation level, which is something we've needed for a very long time. Something that Java has had for an eternity with its packages.

Breaking expectations, one line at a time

Running this application, we get our expected output. “Hello World” is printed to the console, and everything is fine.

Yes, it's 4am. Don't judge me.

Now I'll change one thing. I will move my using statement so that it appears below the namespace declaration.

<mark>namespace MyApplication;</mark>
<mark>using System;</mark>

internal static class Program
{
    private static void Main(string[] args)
    {
        Console.WriteLine("Hello World");
    }
}

This is the only thing I'm changing. I still call Console.WriteLine("Hello World"), but look what happens:

⸮tahw taiW

The output somehow got reversed, and all I did was move my using System; import down one line. What on earth is happening here?

The secret revealed

The answer as to why this happens is that I actually have my own nefarious Console class with a static WriteLine method, which lives inside a child namespace MyApplication.System.

using System.Linq;
namespace MyApplication.System;

public static class Console
{
    public static void WriteLine(string value)
    {
        global::System.Console.WriteLine(string.Concat(value.Reverse()));
    }
}

In the body, I'm calling the actual Console.WriteLine method provided by .NET, but I'm feeding it a reversed version of the string. To accomplish this, I had to use the global:: prefix so that there's no ambiguity with .NET's Console and my Console.

But, wait. Back in the Program class I'm not importing MyApplication.System right? I'm importing System. So why is it choosing to use my custom Console class?

How namespaces resolve

When you import a namespace with a using directive, the C# compiler actually does something rather interesting - it will actually attempt to resolve the namespace starting at the current namespace, not from the global namespace. This is critically important!

Let's take a simple example to explain what I mean.

Assume I have a class named MyClass in namespace Foo, and another class MyOtherClass in a child namespace Foo.Child. As you can see, my naming skills are paramount. Each of these will live in their own files.

// MyClass.cs
namespace Foo
{
    class MyClass
    {
    }
}
// MyOtherClass.cs
namespace Foo.Child
{
    class MyOtherClass
    {
    }
}

Now suppose we wish to store an instance of MyOtherClass in MyClass. In MyClass.cs, we would have to add an import to Foo.Child:

<mark>using Foo.Child;</mark>

namespace Foo
{
    class MyClass
    {
        <mark>private MyOtherClass _someInstance;</mark>
    }
}

However, using directives can also be placed within a namespace. We could instead format it as such:

namespace Foo
{
    <mark>using Foo.Child;</mark>

    class MyClass
    {
        private MyOtherClass _someInstance;
    }
}

Except now, something interesting happens. Because we are already in the Foo namespace, the fully-qualified name becomes redundant. A decent IDE will actually point this out for you:

… and by IDE, I mean anything that isn't VS Code.

This means we can simply change our code to import Child. The Foo. parent will be inferred since that is where this using directve lives.

namespace Foo
{
    <mark>using Child;</mark>

    class MyClass
    {
        private MyOtherClass _someInstance;
    }
}

Just to really hammer this point home: Namespace imports resolve from the current namespace. When the using directive lived within the global namespace, the name resolved from global::, which meant we had fully qualify Foo.Child. However, when the directive lived within Foo, the name resolved from Foo, which meant we could omit it.

You might already be starting to see the problem

Now, let's take advantage of the new C# 10 feature where namespaces can be file-scoped.

<mark>namespace Foo;</mark>
using Child;

class MyClass
{
    private MyOtherClass _someInstance;
}

The same trick works here too. When the using appears below the namespace declaration - i.e. when it appears within the namespace - we can omit the fully qualified name. When it appears above / outside, we must fully qualify Foo.Child.

using Foo.Child;
namespace Foo;

// vs

namespace Foo;
using Child;

The caveat with this is that the current namespace takes precedence over the global namespace. If we call back to my earlier example where I wrote a Console class which reverses the output, it lived within MyApplication.System. Ultimately…

... the compiler programmer made an incorrect assumption

In the Program class when I wrote using System; outside of the namespace, the compiler began searching from the global namespace. It found that global::System.Console existed, and resolved to that.

However, when I moved the using directive down one, so that it lives inside the MyApplication namespace, the compiler actually went “Okay, so I see that you are importing System. Since we're currently in MyApplication, I will check if there is a MyApplication.System - oh look, there is! And it has a Console class too, so I will use that!”

This is by design. Namespaces are - and have always been - resolved this way. Parent namespaces can be omitted from the import; after all, it's not as if you have to write using global::System.Collections.Generic; every time, is it? It's valid, we just don't do that because it's wasteful to write global:: if we're already in global.

Why this is dangerous

If you are just starting out in C#, this is not something you should generally have to worry about. However, not being aware of this very important rule about namespace resolution could lead to bugs down the road. Since using X; and namespace X; can now exist as statements together, one might come to the seemingly-reasonable conclusion that “the compiler didn't complain at me. I guess it doesn't matter which way 'round they go”. Indentation used to make you think twice about it, not anymore.

The next thing you know, you are working with a new team on a new project and have been tasked to solve the most subtle issue in existence leading to a lot of wasted time and resources, stress-related hairloss, an insatiable caffeine addiction, and HR has engulfed the office in flames while suffering with amnesia as they misinterpreted the meaning of “fire-and-forget”.

How to solve this problem

This is really a non-issue. The way you solve this problem is by preventing it in the first place. using directives should exist outside namespaces except in edge-cases where it actually makes sense to do so. Yes I'm aware this is in direct violation of SA1200, but considering that rule was set in place long before C# 10, I think it's time to redraft the spec and revisit that rule. Or you could simply suppress the rule, that is an option too.

Now if you'll excuse me, I need to find the fire extinguisher and have strong words with human resources.