C# 10 is a major language update designed to make code shorter, safer, and faster—especially for modern .NET apps. Many features improve code structure (like namespace changes), performance (like interpolated string handlers), and safety (like improved definite assignment).
If you build Web APIs, MVC apps, microservices, console tools, or libraries, C# 10 helps you write cleaner and more maintainable code with fewer lines—without losing readability.
Your Codebase
|
+--> Organization (global using, file-scoped namespace)
|
+--> Performance (interpolated string handlers)
|
+--> Safety (definite assignment, diagnostics)
|
+--> Expressiveness (patterns, lambdas, records)
Try It Yourself
This ToC is generated from the sidebar subtopics. Use it like W3Schools to jump to any section.
| Category | Sections |
|---|---|
| Type System | Record structs • Struct improvements • Seal ToString() |
| Performance | Interpolated string handlers • Const interpolated strings |
| Organization | global using directives • File-scoped namespace |
| Patterns & Lambdas | Extended property patterns • Lambda improvements |
| Safety & Diagnostics | Definite assignment • Deconstruction updates • AsyncMethodBuilder • CallerArgumentExpression • #line pragma • Warning wave 6 |
A struct is great for small data, but traditional structs take extra code for equality and printing. C# 10 introduces record structs, giving you record-style value equality and nice output while still being a value type. This is perfect for simple “data carriers” like coordinates, money amounts, or IDs.
You can create them as positional (short form) or full form. Record structs can also be readonly to reduce accidental mutation.
class (reference type) struct (value type) record struct (value type + value equality)
---------------------- -------------------- ------------------------------------------
Stored by reference Stored by value Stored by value
Equality: reference by default Equality: field-by-field? Equality: built-in value equality
ToString: type name ToString: type name ToString: useful generated output
// C# 10
public readonly record struct Money(decimal Amount, string Currency);
public class Program
{
public static void Main()
{
var m1 = new Money(100m, "INR");
var m2 = new Money(100m, "INR");
// Value equality (same data => equal)
Console.WriteLine(m1 == m2); // True
// Nice ToString output
Console.WriteLine(m1); // Money { Amount = 100, Currency = INR }
}
}
Try It Yourself
Real-world use case: In an e-commerce app, you can represent totals, discounts, and tax as small value objects (Money) to avoid passing raw decimals everywhere.
C# 10 improves how you write and initialize struct types. The goal is to make struct code feel less “ceremony-heavy” and more consistent with modern C#.
Important updates include better support for parameterless constructors (more realistic scenarios), better field initialization patterns, and safer compiler checks. While some behavior depends on runtime and language rules, the big picture is: cleaner struct creation with fewer surprises.
Old struct risk:
create struct -> forget to set fields -> accidental default values
C# 10 improvements:
create struct -> clearer initialization options -> compiler checks -> safer usage
public struct Point2D
{
public int X { get; init; }
public int Y { get; init; }
}
public class Program
{
public static void Main()
{
// Clear init-only property initialization (safe and readable)
Point2D p = new Point2D { X = 10, Y = 20 };
Console.WriteLine($"({p.X}, {p.Y})");
}
}
Try It Yourself
Real-world use case: In a gaming or mapping application, millions of small points/coordinates can be represented using structs to reduce GC pressure.
Normal string interpolation (like $"Hello {name}") often creates strings even when you may not use them (example: debug logs turned off).
C# 10 adds interpolated string handlers so APIs (like loggers) can build strings only when needed.
Think of it like “lazy string building”: the handler receives pieces and decides whether to format/append them. This reduces allocations and improves speed in heavy logging systems.
Eager:
build string -> pass to logger -> logger discards (level disabled) ❌ wasted work
Handler:
logger checks level -> if enabled then build string ✅ saves work
public static class SimpleLogger
{
public static bool IsEnabled = false;
// When disabled, we skip the expensive formatting work.
public static void Log(Func<string> messageFactory)
{
if (!IsEnabled) return;
Console.WriteLine(messageFactory());
}
}
public class Program
{
public static void Main()
{
SimpleLogger.IsEnabled = false;
// Similar idea: don't build string unless needed
SimpleLogger.Log(() => $"Heavy value: {Expensive()}");
}
static string Expensive()
{
Console.WriteLine("Expensive() executed!");
return new string('X', 1000);
}
}
Real-world use case: In microservices, request logging can be huge. Handlers reduce CPU + memory when debug logs are off in production.
Before C# 10, every file often repeated the same using lines (System, Collections, Linq, etc.).
With global using, you can declare common namespaces once and the whole project can use them.
This keeps files clean and helps beginners focus on the code logic rather than boilerplate imports.
Typically, you create a file like GlobalUsings.cs.
GlobalUsings.cs (once)
|
+--> File1.cs uses it automatically
+--> File2.cs uses it automatically
+--> File3.cs uses it automatically
// GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
// Now any .cs file can use Console, List<>, LINQ without repeating using lines.
Try It Yourself
Real-world use case: In a large enterprise solution, you may have 200+ files. Global usings reduce repetitive edits and keep diff/PR clean.
In older C#, namespaces used braces and added one extra indentation level to your whole file. C# 10 introduces file-scoped namespaces: write the namespace with a semicolon and remove the extra indentation.
This is especially helpful in teaching projects and real enterprise solutions where every file becomes easier to scan quickly.
Old:
namespace MyApp
{
class A { }
}
New (C# 10):
namespace MyApp;
class A { }
namespace ItTechGenie.CSharp10;
public class InvoiceService
{
public decimal CalculateTotal(decimal basePrice, decimal tax)
{
return basePrice + tax;
}
}
Try It Yourself
Real-world use case: In MVC or Web API projects, every controller/service file becomes shorter and easier to maintain.
Pattern matching lets you “check structure” instead of writing many if conditions.
C# 10 extends property patterns so you can match nested properties more naturally.
This makes validation and decision logic more readable, especially when dealing with nested DTOs (request objects) in APIs.
Order
├─ Customer
│ ├─ Address
│ │ └─ Country
└─ TotalAmount
Pattern matching checks: Order.Customer.Address.Country == "IN"
public class Address { public string Country { get; set; } = ""; }
public class Customer { public Address Address { get; set; } = new Address(); }
public class Order { public Customer Customer { get; set; } = new Customer(); public decimal Total { get; set; } }
public class Program
{
public static void Main()
{
var order = new Order { Total = 1200m, Customer = new Customer { Address = new Address { Country = "IN" } } };
// Extended property pattern style
if (order is { Customer: { Address: { Country: "IN" } }, Total: > 1000m })
{
Console.WriteLine("Apply India premium delivery rules");
}
}
}
Try It Yourself
Real-world use case: In a shipping microservice, decide shipping rules based on nested request data (country, state, membership type).
IsPremiumIndianOrder(order).
Lambdas are “small functions” written inline. C# 10 improves lambda typing and reduces friction when passing lambdas to methods. This helps when working with LINQ, event handlers, and modern minimal APIs.
Practically, you get better inference and more consistent behavior when converting lambdas to delegate types.
Data list
|
+--> LINQ Where(x => ...)
|
+--> Select(x => ...)
|
+--> Result list
using System;
using System.Linq;
public class Program
{
public static void Main()
{
int[] marks = { 35, 55, 72, 90 };
// Lambda expressions inside LINQ
var passed = marks.Where(m => m >= 40).ToArray();
Console.WriteLine(string.Join(", ", passed)); // 55, 72, 90
}
}
Try It Yourself
Real-world use case: Filtering incoming data (students, employees, orders) using LINQ expressions in service layer.
m => m >= 40 means “m is passed, returns true/false.”
Earlier, you could not write interpolated strings as const.
C# 10 allows const interpolated strings if every piece is also compile-time constant.
This is useful for constant messages, routes, and standard labels.
The compiler builds it once at compile time (not runtime), which makes it safe and fast.
const string OK:
const string x = $"Hello {"World"}"; // all parts are constant
NOT allowed:
string name = "Gopi";
const string x = $"Hello {name}"; // name is not constant
public class Program
{
public static void Main()
{
const string app = "ItTechGenie";
const string msg = $"{app} - Welcome"; // OK in C# 10 (all const)
Console.WriteLine(msg);
}
}
Try It Yourself
Real-world use case: Constant route prefixes, constant UI labels, constant telemetry event names.
const for values that never change. It prevents accidental modification.
static readonly when values need runtime computation.
ToString()
Records auto-generate a helpful ToString(). In inheritance scenarios, derived records could override it.
C# 10 allows record authors to seal ToString()
to keep output consistent.
This is valuable when logs, audit trails, or debugging tools depend on a stable string representation.
Base record ToString() -> used in logs
|
Derived record overrides ToString() -> logs change unexpectedly ❌
Seal ToString() -> derived cannot change it -> logs remain stable ✅
public record Employee(string Name, int Id)
{
public sealed override string ToString() => $"Emp({Id}) - {Name}";
}
public class Program
{
public static void Main()
{
var e = new Employee("Ananya", 101);
Console.WriteLine(e.ToString());
}
}
Try It Yourself
Real-world use case: Audit systems where consistent ToString() output is used in logs or compliance reports.
Definite assignment is the compiler’s way of protecting you from using variables before they have a value. C# 10 improves the compiler’s ability to “understand” more code paths, especially around pattern matching and conditions.
The result: fewer false warnings and safer code. It’s like having a smarter assistant that checks your variable usage.
Declare variable
|
Assign in all possible paths?
|-- Yes --> You can read it ✅
|-- No --> Compiler warns you ❌
public class Program
{
public static void Main()
{
int value;
if (DateTime.Now.DayOfWeek == DayOfWeek.Sunday)
{
value = 100;
}
else
{
value = 200;
}
// Safe: value is assigned in all paths
Console.WriteLine(value);
}
}
Try It Yourself
Real-world use case: Decision logic (discount rules, eligibility rules) where you must ensure values are set in every path.
Deconstruction is when you unpack a tuple into variables. C# 10 improves this by allowing you to declare a new variable and assign to an existing variable in the same deconstruction statement.
This is useful when you already have one variable (like a running total) but want a new value from a method at the same time.
Method returns: (newValue, updatedValue)
Deconstruction:
declare one new variable
assign one existing variable
public class Program
{
static (int current, int next) GetNumbers() => (10, 11);
public static void Main()
{
int next = 0;
// C# 10 allows: declare "current" and assign existing "next"
(int current, next) = GetNumbers();
Console.WriteLine($"Current: {current}, Next: {next}");
}
}
Try It Yourself
Real-world use case: Parsing pipelines where you keep one “state variable” but also get fresh computed values from each stage.
This feature is advanced and mostly used by library/framework authors. It allows applying an attribute to an async method to control which “builder” type is used behind the scenes.
In simple terms: it gives power users a way to customize how async state machines are created—helpful for performance or specialized async types.
async method
|
compiler creates state machine
|
builder coordinates:
- when to continue
- how to store state
- how to complete result
using System;
using System.Threading.Tasks;
public class Program
{
public static async Task Main()
{
Console.WriteLine("Start");
await Task.Delay(200);
Console.WriteLine("End");
}
}
Real-world use case: High-performance libraries (like custom task-like types) where you want fine control over async machinery.
async/await first. Treat this feature as “advanced internals.”
This feature improves error messages and validation.
When you write a guard method like NotNull(x), it can automatically capture the exact expression used by the caller (like user.Name).
Result: your exceptions become more informative without manually typing parameter names everywhere. This makes debugging faster in real projects.
Caller writes: Guard.NotNull(user.Name);
Guard receives:
value = user.Name
expressionText = "user.Name" (captured automatically)
using System;
using System.Runtime.CompilerServices;
public static class Guard
{
public static void NotNull(object? value,
[CallerArgumentExpression("value")] string? expr = null)
{
if (value is null)
throw new ArgumentNullException(expr, "Value cannot be null");
}
}
public class Program
{
public static void Main()
{
string? name = null;
// Throws: ArgumentNullException: name (Value cannot be null)
Guard.NotNull(name);
}
}
Try It Yourself
Real-world use case: Validation libraries, domain guard checks, preconditions in service methods.
"paramName" strings.
The #line directive tells the compiler what line number and file name to report in errors.
C# 10 enhances this for better tooling and generated code scenarios.
If you use source generators or code generation tools, this helps map errors back to the “original” source location.
GeneratedFile.g.cs (compiler sees)
|
#line "OriginalTemplate.tt"
|
Errors appear as if they happened in OriginalTemplate.tt ✅
public class Program
{
#line 100 "MyTemplateFile.txt"
public static void Main()
{
// If error happens here, compiler can report it as line 100 in MyTemplateFile.txt
int x = "not a number"; // Intentional error
}
}
Real-world use case: Source generators in .NET (like auto-generating DTOs or mapping code) with accurate error reporting.
#line directly in daily app development—know it mainly for tooling scenarios.
C# compiler teams release “warning waves” to introduce new warnings gradually. Warning wave 6 adds more diagnostics to help you catch mistakes earlier (nullability, pattern checks, correctness).
The idea is: your code quality improves over time without suddenly breaking everything in one upgrade.
Old version: fewer warnings
|
Upgrade: wave introduces new warnings
|
Fix warnings gradually -> cleaner, safer code ✅
/*
In Visual Studio / .NET, you can configure:
- Treat warnings as errors (strict)
- Or fix warnings in batches (practical)
*/
public class Program