codelessgenie guide

Exploring the Latest C# Features: What's New in C# 10

C# has long been a cornerstone of modern software development, powering everything from desktop apps to cloud services. With each iteration, the language evolves to enhance developer productivity, improve code readability, and unlock new capabilities. C# 10, released in November 2021 alongside .NET 6, is no exception. This version introduces a host of features designed to reduce boilerplate, streamline common patterns, and boost performance. Whether you’re building a microservice, a web app, or a desktop tool, C# 10 has something to make your code cleaner, safer, and more efficient. In this blog, we’ll dive deep into the most impactful features of C# 10, with practical examples to help you understand how to leverage them in your projects.

Table of Contents

  1. Global Using Directives
  2. File-Scoped Namespace Declarations
  3. Record Structs
  4. Improved Lambda Expressions
  5. Interpolated String Handlers
  6. Constant Interpolated Strings
  7. Sealed Record ToString()
  8. Implicit Global Usings
  9. Required Properties
  10. Conclusion
  11. 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>  

References