Table of Contents
- Global Using Directives
- File-Scoped Namespace Declarations
- Record Structs
- Improved Lambda Expressions
- Interpolated String Handlers
- Constant Interpolated Strings
- Sealed Record
ToString() - Implicit Global Usings
- Required Properties
- Conclusion
- References
1. Global Using Directives
One of the most tedious parts of C# development is repeating using directives at the top of every file (e.g., using System;, using System.Collections.Generic;). C# 10 introduces global using directives to eliminate this redundancy: a single global using statement makes a namespace available to all files in a project.
How It Works
Prefix a using directive with global to apply it project-wide. Typically, you’ll place these in a dedicated file (e.g., GlobalUsings.cs) to keep your code organized.
Example
// GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
Now, any file in the project can use List<T>, LINQ methods, or System types without re-declaring the using directives.
Benefit
Reduces boilerplate and keeps file headers clean, especially in large projects with hundreds of files.
2. File-Scoped Namespace Declarations
Traditionally, C# files wrap all code in a namespace block, leading to unnecessary indentation. C# 10 simplifies this with file-scoped namespace declarations, allowing you to declare a namespace for the entire file in one line.
How It Works
Replace the nested namespace block with namespace <Namespace>; at the top of the file. All code in the file automatically belongs to this namespace.
Before (Traditional Namespace)
namespace MyApp.Services
{
public class UserService
{
// ...
}
}
After (File-Scoped Namespace)
namespace MyApp.Services;
public class UserService
{
// ... (no nested indentation!)
}
Benefit
Reduces nesting and visual clutter, making code easier to read. This is especially helpful in files with a single class or record.
3. Record Structs
C# 9 introduced record types (reference types) for immutable data models with value-based equality. C# 10 extends this with record structs—value-type records that combine the benefits of struct (value semantics) and record (immutability, built-in equality).
How It Works
Declare a record struct with record struct (for nominal structs) or record struct <Name>(<Parameters>); (for positional structs, similar to positional records).
Example: Positional Record Struct
// Positional record struct (immutable by default)
public record struct Point(int X, int Y);
// Usage
var point1 = new Point(10, 20);
var point2 = new Point(10, 20);
Console.WriteLine(point1 == point2); // Output: True (value-based equality)
Example: Nominal Record Struct
For mutable or complex structs, use a nominal declaration:
public record struct Person
{
public string Name { get; set; }
public int Age { get; init; } // Init-only (immutable after initialization)
}
Key Difference from record class
record class: Reference type (equality checks reference by default, but records override this for value equality).record struct: Value type (equality is always value-based, like regular structs).
Benefit
Ideal for small data containers (e.g., coordinates, DTOs) where value semantics and immutability are critical.
4. Improved Lambda Expressions
C# 10 enhances lambda expressions with better type inference, support for attributes, and explicit return types.
Better Type Inference
The compiler now infers delegate types for lambdas more reliably, even when no target type is specified.
Example
// Before C# 10: Requires explicit delegate type (e.g., Func<int, int>)
Func<int, int> square = x => x * x;
// C# 10: Compiler infers the type
var square = (int x) => x * x; // Inferred as Func<int, int>
Attributes on Lambdas
You can now apply attributes to lambda parameters or the lambda itself (useful for code analysis or serialization).
Example
var logMessage = ([CallerMemberName] string memberName = "") =>
$"Log from {memberName}";
Explicit Return Types
For complex lambdas, explicitly specify the return type to improve readability.
Example
var calculateTotal = (decimal price, int quantity) => decimal:
{
if (quantity == 0) return 0m;
return price * quantity;
};
Benefit
Lambdas feel more expressive and flexible, reducing the need for verbose delegate type declarations.
5. Interpolated String Handlers
Interpolated strings (e.g., $"User: {user.Name}") are convenient but can create unnecessary string allocations. C# 10 introduces interpolated string handlers to optimize performance by deferring string construction until needed.
How It Works
Handlers (like DefaultInterpolatedStringHandler) build strings incrementally, avoiding intermediate allocations. The compiler automatically uses handlers when an interpolated string is passed to a method expecting a handler.
Example: Logging with Reduced Allocations
using System.Runtime.CompilerServices;
// Method accepting an interpolated string handler
void LogInfo([InterpolatedStringHandlerArgument("")] ref DefaultInterpolatedStringHandler handler)
{
if (!IsInfoEnabled) return; // Skip if logging is disabled
string message = handler.ToStringAndClear(); // Only build string if needed
Console.WriteLine(message);
}
// Usage
LogInfo($"User {user.Name} logged in at {DateTime.Now}");
Benefit
Critical for performance-sensitive scenarios (e.g., high-throughput logging), as it avoids allocating strings when logging is disabled.
6. Constant Interpolated Strings
C# 10 allows interpolating constants into other constants, as long as all placeholders are constants themselves.
Example
const string AppName = "MyApp";
const string WelcomeMessage = $"Welcome to {AppName}!"; // Valid in C# 10
Before C# 10
This would throw a compiler error, forcing you to concatenate constants manually:
const string WelcomeMessage = "Welcome to " + AppName + "!"; // Old approach
Benefit
Simplifies constant definitions, making them more readable and maintainable.
7. Sealed Record ToString()
Records automatically generate a ToString() method that includes all properties. C# 10 lets you seal this method to prevent derived records from overriding it.
Example
public record Person(string Name)
{
public sealed override string ToString() => $"Person: {Name}";
}
public record Employee(string Name, string Department) : Person(Name)
{
// Error: Cannot override sealed method 'Person.ToString()'
public override string ToString() => $"Employee: {Name} ({Department})";
}
Benefit
Ensures consistent string representation across derived records, preventing accidental changes to ToString() output.
8. Implicit Global Usings
.NET 6+ projects (e.g., console, web apps) automatically include implicit global usings for common namespaces based on the project type. For example:
- Console apps:
System,System.Linq,System.Collections.Generic - ASP.NET Core apps:
Microsoft.AspNetCore.Mvc,Microsoft.Extensions.DependencyInjection
How to Control Implicit Usings
Enable/disable or customize implicit usings via the project file (*.csproj):
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <!-- Enable/disable -->
</PropertyGroup>
</Project>
Benefit
Eliminates even more boilerplate, especially for standard project types.
9. Required Properties
C# 10 introduces the required keyword to enforce that certain properties must be set when initializing an object (via object initializers or constructors).
How It Works
Mark a property as required to force callers to initialize it. The compiler throws an error if the property is missing.
Example
public class User
{
public required string Email { get; set; } // Must be initialized
public string? Name { get; set; } // Optional
}
// Valid: Email is set
var user1 = new User { Email = "[email protected]" };
// Error: Email is required but not set
var user2 = new User { Name = "Alice" }; // Compiler error: "Required property 'Email' not initialized"
Required vs. Constructors
required properties offer flexibility over constructors: you can initialize objects with object initializers (e.g., new User { Email = "..." }) instead of defining multiple constructor overloads.
Benefit
Ensures objects are always initialized with critical data, reducing runtime errors from missing properties.
Conclusion
C# 10 is a landmark release that prioritizes developer happiness and code quality. From reducing boilerplate with global usings and file-scoped namespaces to improving performance with interpolated string handlers, each feature feels tailored to real-world pain points. Whether you’re writing a small script or a large enterprise app, these features will make your code cleaner, safer, and more maintainable.
To start using C# 10, upgrade to .NET 6 or later and set your project’s language version to 10.0 (or higher) in your .csproj file:
<PropertyGroup>
<LangVersion>10.0</LangVersion>
</PropertyGroup>