C#11 added the file scoped types feature: a new file
modifier that can be applied to any type definition to restrict its usage to the current file. This way we can have several classes with the same name (namespace.name
) in a single project. This is shown through the project below that contains two classes named ConsoleApp1.Answer
:
Here are a few remarks:
- The types with the
file
modifier can be indirectly accessed outside of their source file. In the program above we rely on theinternal
classesInternalClassFromFile1
andInternalClassFromFile2
to run bothAnswer
classes fromProgram
. - Such
file
class can also be indirectly used outside its source file through an interface this way:
1 2 3 4 5 6 7 8 9 10 11 |
namespace ConsoleApp1 { file class Answer : IAnswer { public string GetFileScopeSecret() => "Answer from File1.cs"; } internal interface IAnswer { string GetFileScopeSecret(); } static class InternalClassFromFile1 { internal static IAnswer GetAnswer() => new Answer(); } } |
- Any kind of type can be marked with the
file
modifier:class
, interface, record,struct
,enum
,delegate
. file
cannot be used with another modifier likeinternal
orpublic
.- file partial can be used as long as all the type definitions belong to the same file:
1 2 3 4 5 6 7 8 9 10 |
namespace ConsoleApp1 { file static partial class Answer { internal static string GetFileScopeSecret() => "Answer from File1.cs"; } file static partial class Answer { internal static string AnotherGetFileScopeSecret() => "Another Answer from File1.cs"; } } |
- The
file
modifier doesn’t apply to types nested in a parent type. Also it doesn’t apply to methods properties, events and fields but the language design note explains: “Leaves the design space open for non-type file-scoped members to come along later.” - As the screenshot below shows, it is possible to have in a project, a single
internal
orpublic
class with full nameConsoleApp1.Answer
, and one or severalfile ConsoleApp1.Answer
. The only drawback is that the public class cannot be used within source files containing a file scoped class.
Why this feature?
Let’s underline that namespace
remains the preferred way to avoid type name collision. However we can imagine several use cases for this feature:
- Generated code: Typically generated templates often use the same class name again and again, like
Item
,Info
orDataSet
. To prevent collision we had to either nest anItem
class in a parent class or maintain an index in name for each generated version, likeItem1
,Item2
… Thus the keywordfile
can significantly ease code generation. - Extension method: Extension method is nowadays a popular language construct for C# developers. However there can be extension method naming collision. With this
file
visibility restriction it is now easy to have extension methods restricted to a single file. Another common issue with extension method -solved by thisfile
visibility restriction- is that they pollute intellisense dropdowns. - Nested class: To solve the naming collision within a project, a common solution mentioned above is that we used to declare private nested classes. However this is not clean code because an extra indentation level is added, and too much indentation clutters code. Thus the
file
keyword will help with this concern. - Module and Encapsulation: Typically what you consider as a module or a component is not a single class but a few highly cohesive types. For encapsulation purposes a pattern was to nest private implementation details within private nested classes and types. This
file
scope makes such encapsulation pattern cleaner. - Test: Often tests structure is standardized and each test class has its own
DataSet
class for example. Here also instead of declaring classes nested in test classes, thefile
restriction will help.
However keep in mind that a file
restricted class won’t be visible from tests. Those must be considered as private implementation (black box) and must be tested through internal
or public
classes that access them.
Quick tip: You can test your internal
implementations by tagging your projects with the System.Runtime.CompilerServices.InternalsVisibleToAttribute
. This way you don’t have to declare these classes as public just for testing purposes.
1 |
[assembly: InternalsVisibleTo("NDepend.Test.Unit, PublicKey=002400000480...")] |
What the compiler generates?
The C# compiler renames types with the file restriction this way: <SourceFileNameWithoutExtension>F$identfier$_TypeName. identfier is sometime an index incremented, or a random identifier, a bit like an UUID. This is just an implementation detail that is here to avoid type name collision in the context of the compiled assembly.
Thanks to the <> characters there is no risk for such class to be consumed from elsewhere than its file. This common compiler renaming pattern is called special naming and is used in a number of scenario including async/await , lambda, anonymous method, anonymous type.
Interestingly enough, when analyzing a bunch of projects within a solution, NDepend doesn’t try to change special names. Thus the NDepend dependency graph of the small program shown in the first screenshot above looks like this.
[ Edit 10th November 2022: Now NDepend v2022.2.1 and onward show the C# type name -here it is Answer– and provide the new property IType.IsVisibleOnlyInFile to detect this file situation in code queries and code rules ]
Again another great C# featurette that will help us write even cleaner code :))
“Your scientists were so preoccupied with whether they could, they didn’t stop to think if they should.”
Worst feature ever.. Who the hell came up with the idea that it would be nice to be able to create multiple classes with the same name and same namespaces. Just terrible.. Why add stuff like this, nobody wants it
And those edge cases explained above explaining why you would need them can and should be fixed in another way..
To expand/extend a programming language it is essential to know it very well… Not only knowing “what can be done”, but **why the language was made the way it was made**: what is the purpose of each feature or each restriction.
Allowing classes with the same “namespace.classname” in different files has created a problem where there was none and this does not solve any existing problem.
Without a doubt, of the various nonsense that Microsoft is committing when evolving C#, this has been the worst!