Back to Blog
C#

C# File Local Type: Scoped Type Declarations

c# file local type: Learn what C# file local types are, how they restrict type visibility to a single file, and when they solve real maintainability problems.

C# 11file-local typestype scopingC# accessibilitysource generators
Illustration of a C# source file containing a type that is enclosed within a file boundary, representing file-local type scoping.

The c# file local type feature, introduced in C# 11, lets you declare a type whose visibility is restricted to the file in which it is declared. The file modifier can be applied to classes, structs, interfaces, enums, and delegates. This is a compile-time accessibility feature, not a runtime construct. The generated IL for a file type may include a generated unique name to prevent collisions, but the language guarantees that the type is only accessible within the same source file.

Consider a common problem: a large file contains several helper types that are implementation details. Without file, these types are either public (polluting the public API) or internal (visible to the entire assembly). With file, you can make the helper type visible only to the code in that specific source file. This is particularly useful when you want to avoid cluttering the assembly's internal namespace, and when you want to prevent other files in the same assembly from accidentally depending on a type that is only meant for one file.

How to Declare a File-Local Type

Applying the file modifier is straightforward. Place it before the type declaration, and combine it with other modifiers only when allowed. For example, a file class cannot have a higher accessibility than file itself, so you cannot use public or internal together with file.

file class Helper { public static int Square(int x) => x * x; } file struct Coordinate { public int X { get; set; } public int Y { get; set; } } file interface ICalculator { int Add(int a, int b); }

You can also apply file to a nested type, though the nested type will still be visible only within the same file.

public class DataProcessor { file struct ProcessingContext { public int Offset; public int Count; } }

In this example, ProcessingContext is only accessible within the file that contains the DataProcessor class. Even code inside DataProcessor in a different file cannot reference it.

Why the File Modifier Is Useful

Many large C# projects face the problem of internal types that are only used in a single file. Previously, you had to mark them internal, which makes them visible to everything in the same assembly. That visibility sometimes encouraged coupling: other files might start using a type that was never intended for them, making future refactoring harder. The file modifier gives you a more precise scope. It also helps source generators and code-generation scenarios, where you want to generate helper types that must not collide with user types or other generated types.

Another real-world benefit appears in unit testing. Test projects sometimes have helper classes that are only needed in one test file. Marking them file prevents accidental reuse across test files, enforcing that each test file is self-contained. This reduces hidden dependencies between test fixtures.

Rules and Constraints

The file modifier is only valid on type declarations, not on members such as methods, properties, or events. You cannot apply file to a top-level statement's local function, but you can apply it to a type inside a namespace or at the top level of a compilation unit. A file type cannot be used as a base type for a non-file type, and a non-file type cannot derive from a file type because that would widen the visibility of the file type.

Consider the following illegal code:

file class Base { } public class Derived : Base { } // Error: 'Base' is not accessible here

The compiler reports an error because Derived is not in the same file and cannot see Base. Even within the same file, an interface can only inherit from another file interface if both are in the same file.

Here is a summary of key constraints:

ConstraintExplanation
ScopeAccessible only within the same source file.
Modifier compatibilityCannot be combined with public, internal, protected, or private.
Nested typesAllowed, but the nested type's scope is still the file.
Base typesCannot be used as a base type for a non-file type.
Default visibilityA file type is effectively file-scoped; no other modifier is needed.

Runtime Behavior and Type Identity

At the runtime level, the compiler generates a unique name for each file type, often appending a hash or a unique identifier to the type name. This ensures that two files in the same assembly can declare a file type with the same simple name without conflicting. For example:

// File1.cs file class Cache { } // File2.cs file class Cache { }

Both files can define a Cache class, and the compiler will create distinct types in the assembly. However, because each type is scoped to its own file, you never directly reference the other file's version. From a reflection perspective, you can see these types, but their names are not the simple Cache; they are compiler-generated names. That means you cannot easily use reflection to find a file type by its simple name, which is a natural consequence of the feature.

Maintaining Type Safety With File-Local Types

One subtle advantage of file types is that they participate fully in type inference and generic type inference within the file. You can use them in method signatures, local functions, and lambda expressions without any runtime boxing or casting penalties. For example:

public class Processor { file record ParsedData(string Name, int Value); public object Process(string input) { ParsedData data = Parse(input); return data; } private ParsedData Parse(string input) { // ... parsing logic return new ParsedData(input, input.Length); } }

Here, ParsedData is only used within the same file, but it still provides strong compile-time typing. You avoid having to return object or a tuple, making the code clearer and safer. This pattern is especially useful when a method needs to return a complex object that is only relevant to a single file's algorithms.

Edge Cases and Common Mistakes

One common mistake is trying to use a file type in a partial type across multiple files. file types cannot be declared with the partial modifier across multiple files, because the scope is per-file, and there is no way to merge declarations from different files. Attempting to do so causes compiler error CS0246 or a similar accessibility error.

Another mistake is attempting to expose a file type as a return type of a public method. The compiler disallows that because the caller cannot see the type. The error message typically states that the return type is less accessible than the method. For instance:

file class Hidden { } public class Exposer { public Hidden GetHidden() => new Hidden(); // Error }

Even if the method is public and the type is file, the method's visibility is effectively restricted, which contradicts the public modifier. In such cases, you must either make the method file as well or change the return type to a type that is accessible.

Compatibility Considerations

The file keyword is only available in C# 11 and later. If you are targeting a C# version earlier than 11, the compiler will treat file as an identifier, causing errors if you have a type or variable named file. However, in C# 11 and later, file is a contextual keyword, so you can still name a variable file in most contexts, but not a type without a preceding modifier.

Because this feature is a language feature, it works with all .NET runtime versions as long as the compiler supports C# 11. There is no runtime dependency; the generated IL uses standard metadata. That means you can use file types in libraries that target .NET Framework as long as you use a modern compiler, but you must ensure your build environment is configured for C# 11.

When to Use File-Local Types

Use a file type when you have a type that is an implementation detail of a single file, and you want to prevent other developers from accidentally using it. If the type is genuinely an internal helper that could be reused across files, internal is still the right choice. The file modifier is not a replacement for internal; it is a stricter alternative for a narrow scenario.

Consider a large data transformation file that has a private helper class for parsing a temporary format. If you internal that helper, it becomes accessible to all other files in the same assembly, which may be unnecessary. Using file keeps the parsing logic truly private to that file. If you later need to share that helper with another file, you must remove the file modifier and adjust visibility—an explicit action that forces you to consider the design.

For source generators, file types are beneficial because they let a generator emit a type with the same name across multiple generated files without worrying about collisions. Since each generated file is a separate compilation unit, each gets its own unique type, and no naming conflict occurs.

c# file local type: Practical Usage and Code Examples | RYUSLOG DEV