File-Scoped Types and Namespaces Expansion in C# 14

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:

ScopeKeywordAccessible From
GlobalpublicEverywhere
Assembly-wideinternalWithin the same assembly
File-levelfileOnly within the declaring file
Class-levelprivateInside 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 LogFormatter is file-scoped, ensuring internal privacy.
  • The result: a minimalist, self-contained source file with no excess indentation or exposure.

6. Limitations and Considerations

  • file can only be applied to top-level type declarations (not nested types).
  • You cannot combine file with public or internal.
  • 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.