C#‘s Hidden Gems and Modern Features
C# has evolved significantly over the years, introducing many powerful features that can significantly improve your code quality and productivity. Let’s explore the most valuable but often overlooked features.
Modern Pattern Matching
public static string GetShapeDescription(object shape)
{
return shape switch
{
Circle c => $"Circle with radius {c.Radius}",
Rectangle r when r.Width == r.Height => $"Square with side {r.Width}",
Rectangle r => $"Rectangle {r.Width}x{r.Height}",
Line l => $"Line with length {l.Length}",
_ => "Unknown shape"
};
}
- Concise and expressive
- Type-safe pattern matching
- Supports complex conditions
- Better performance than if-else chains
- Requires C# 8.0 or later
- May be less familiar to junior developers
Pattern matching provides a powerful way to handle different types and conditions in a clean, type-safe manner. The switch
expression syntax makes the code more readable and maintainable than traditional if-else chains.
Traditional if-else Approach
public static string GetShapeDescription(object shape)
{
if (shape is Circle c)
{
return $"Circle with radius {c.Radius}";
}
else if (shape is Rectangle r && r.Width == r.Height)
{
return $"Square with side {r.Width}";
}
else if (shape is Rectangle r)
{
return $"Rectangle {r.Width}x{r.Height}";
}
else if (shape is Line l)
{
return $"Line with length {l.Length}";
}
return "Unknown shape";
}
- Familiar syntax
- Works in all C# versions
- Explicit control flow
- More verbose
- Nested conditions can become complex
- Less maintainable The traditional if-else approach works but becomes increasingly complex as conditions multiply. It’s more prone to errors and harder to maintain than pattern matching.## Records and With Expressions
Modern Records with With Expressions
public record Person(string FirstName, string LastName, int Age);
var person = new Person("John", "Doe", 30);
var updated = person with { LastName = "Smith" };
// Deconstruct and use
var (firstName, lastName, age) = updated;
- Immutable by default
- Built-in value-based equality
- Concise syntax for updates
- Automatic deconstruction
- Requires C# 9.0 or later
- May be confusing for OOP developers
Records provide a concise way to create immutable reference types with value-based equality. The with
expression makes updating immutable data straightforward and expressive.
Traditional Class Approach
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public Person(string firstName, string lastName, int age)
{
FirstName = firstName;
LastName = lastName;
Age = age;
}
public Person UpdateLastName(string newLastName)
{
return new Person(FirstName, newLastName, Age);
}
}
- Familiar OOP approach
- Works in all C# versions
- Full control over implementation
- More verbose
- Manual implementation required
- Reference-based equality
- More prone to errors
The traditional class approach requires more boilerplate code and manual implementation of immutability patterns. It’s more error-prone and harder to maintain than records.
Modern Top-Level Program
// File: program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
- Clean and concise
- No boilerplate code
- Easier to read and maintain
- Modern C# style
- Requires C# 9.0 or later
- May be unfamiliar to some developers
Top-level programs eliminate the need for explicit Main
methods and Program
classes, making the code more concise and easier to understand.
Traditional Program Structure
// File: Program.cs
namespace MyApplication
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
}
}
}
- Explicit structure
- Familiar to all C# developers
- Clear namespace organization
- More verbose
- Unnecessary boilerplate
- Less modern approach
The traditional approach requires more code structure, including explicit namespace, class, and Main
method declarations. While functional, it’s more verbose than necessary.## Global Using Directives
Modern Global Using Directives
// File: GlobalUsings.cs
global using Microsoft.AspNetCore.Builder;
global using Microsoft.Extensions.DependencyInjection;
// File: Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
- Cleaner file headers
- Reduced code duplication
- Better IDE performance
- More maintainable
- Requires C# 10.0 or later
- May hide dependencies Global using directives centralize namespace imports, making your code files cleaner and more maintainable. This feature is particularly useful in larger projects.
Traditional Using Directives
// File: Program.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
- Explicit dependencies
- Works in all C# versions
- Clear file-level scope
- Repetitive across files
- Clutters file headers
- Harder to maintain
Traditional using directives require repeating namespace imports in each file, leading to code duplication and maintenance challenges as projects grow.