Overview
C# 14 continues Microsoft’s steady refinement of code readability and project scalability by expanding file-scoped syntax beyond namespaces to include file-scoped types.
This evolution is designed to simplify source organisation, reduce boilerplate, and minimise name collisions in modern modular applications – particularly useful for SDK-style projects, source generators, and single-file utilities.
File-scoped constructs build on a concept first introduced with file-scoped namespaces in C# 10, which replaced the traditional block-scoped namespace braces with a colon syntax. C# 14 takes this a step further by letting developers declare types that exist only within the current file, tightening encapsulation and improving compile-time clarity.
1. Recap: File-Scoped Namespaces
Before diving into file-scoped types, it’s worth revisiting file-scoped namespaces, which remain a foundational convenience in modern C#.
Traditional block-scoped namespace:
namespace MyProject.Utilities
{
public class Logger
{
public void Write(string message) => Console.WriteLine(message);
}
}
File-scoped namespace (C# 10+):
namespace MyProject.Utilities;
public class Logger
{
public void Write(string message) => Console.WriteLine(message);
}
This syntax removes one level of indentation and improves readability – especially when many top-level declarations exist in the same file.
2. Introducing File-Scoped Types
C# 14 now introduces file-scoped types, declared using the file modifier before a class, struct, record, or interface declaration.
These types are visible only within the file where they are defined, preventing external access even within the same assembly.
Syntax:
file class LocalHelper
{
public static string Normalize(string value) =>
value.Trim().ToLowerInvariant();
}
Usage within the same file:
namespace MyProject.Data;
public class Repository
{
public string GetNormalized(string input) =>
LocalHelper.Normalize(input); // works fine
}
If another file tries to reference LocalHelper, the compiler raises:
CS0246: The type or namespace name 'LocalHelper' could not be found
This enforces strict encapsulation, limiting helper or temporary types to the file they belong to.
3. Why File-Scoped Types Matter
File-scoped types fill an important gap in the type visibility hierarchy:
| Scope | Keyword | Accessible From |
|---|---|---|
| Global | public | Everywhere |
| Assembly-wide | internal | Within the same assembly |
| File-level | file | Only within the declaring file |
| Class-level | private | Inside containing type only |
This new layer of visibility provides:
- Better encapsulation for internal logic and helper types.
- Cleaner assemblies, avoiding the clutter of one-off internal classes.
- Reduced naming conflicts, since file-scoped types don’t pollute the global namespace.
- Simpler source-generated code, where supporting types can now be hidden automatically.
4. Practical Example: EF Core Entity Configuration
Consider an Entity Framework model configuration file:
namespace MyApp.Data;
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("Users");
builder.Property(u => u.Email)
.HasMaxLength(120)
.IsRequired();
EmailValidator.ValidateDomain(builder); // internal helper
}
}
file static class EmailValidator
{
public static void ValidateDomain(EntityTypeBuilder<User> builder)
{
// Example validation or metadata logic here
}
}
Here, EmailValidator exists purely to support configuration logic for this file’s entity.
It’s hidden from the rest of the application, keeping the domain model namespace clean and intentional.
5. Combining File-Scoped Types with File-Scoped Namespaces
Developers can now combine both modern constructs for maximum clarity and brevity:
namespace MyApp.Logging;
file static class LogFormatter
{
public static string Format(string message) =>
$"[{DateTime.Now:HH:mm:ss}] {message}";
}
public class Logger
{
public void Write(string message) =>
Console.WriteLine(LogFormatter.Format(message));
}
namespace MyApp.Logging;is file-scoped (no braces).file static class LogFormatteris file-scoped, ensuring internal privacy.- The result: a minimalist, self-contained source file with no excess indentation or exposure.
6. Limitations and Considerations
filecan only be applied to top-level type declarations (not nested types).- You cannot combine
filewithpublicorinternal. - Reflection will not expose file-scoped types to external assemblies.
- This feature is purely compile-time — it doesn’t alter runtime metadata beyond visibility.
7. Best Practices
✅ Use file-scoped types for:
- Helper or extension classes used by one logical unit (e.g., EF model config files).
- Source generator scaffolding and temporary classes.
- Encapsulating small utilities or constants.
🚫 Avoid using file-scoped types for:
- Any reusable or shareable business logic.
- Classes intended for testing or public consumption.
🔍 Style Tip: Keep file-scoped types at the bottom of the file below public declarations – this mimics how private helpers appear after public members in standard class design.
8. Final Thoughts
The File-Scoped Types and Namespaces Expansion in C# 14 reflects Microsoft’s continued focus on streamlined, developer-friendly syntax.
By enabling both file-level visibility and namespace flattening, developers can now write cleaner, more maintainable source code that scales effortlessly across large solutions.
File-scoped types elegantly close the visibility gap between private and internal, empowering teams to build modular systems without clutter — a small feature, but one with big architectural benefits.