C# 10.0 was released in November 2021 alongside .NET 6. It introduced several features to reduce boilerplate code, improve readability, support value-type records, and simplify lambda expressions.
We will cover four important features with example programs, explanations, comparisons, real-world scenarios, and interview key points.
1. Global Using Directives
What are Global Using Directives?
Before C# 10.0, developers typically needed to declare using directives in every C# file that required a particular namespace.
C# 10.0 introduced Global Using Directives, allowing a namespace to be imported once and made available across all C# files in the same project (compilation).
This reduces repetitive using statements.
Before C# 10.0
Suppose an application has three classes.
EmployeeService.cs
using System;
using System.Collections.Generic;
public class EmployeeService
{
public List<string> GetEmployees()
{
return new List<string>();
}
}
CustomerService.cs
using System;
using System.Collections.Generic;
public class CustomerService
{
public List<string> GetCustomers()
{
return new List<string>();
}
}
Both files declare the same namespaces.
C# 10.0 – Global Using
Create a file named GlobalUsings.cs.
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
Now other classes can use these namespaces without declaring them again.
Example Program
GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
EmployeeService.cs
public class EmployeeService
{
public List<string> GetEmployees()
{
return new List<string>
{
"John",
"David",
"Robert"
};
}
}
Program.cs
EmployeeService service = new();
List<string> employees = service.GetEmployees();
foreach (string employee in employees)
{
Console.WriteLine(employee);
}
Output:
John
David
Robert
Program Explanation
-
GlobalUsings.csdefines namespaces globally. -
EmployeeService.csusesList<string>without importingSystem.Collections.Generic. -
Program.csusesConsole.WriteLine()without importingSystem. -
The compiler makes global imports available throughout the project.
-
This reduces repeated namespace declarations.
How It Works
GlobalUsings.cs
|
+----------+-----------+
| | |
v v v
System Collections Linq
| | |
+----------+-----------+
|
v
Available throughout
the same project
|
+----------+-----------+
| | |
v v v
Program.cs Service.cs Repository.cs
Global Using vs Normal Using
|
Feature |
Normal Using |
Global Using |
|---|---|---|
|
Scope |
File |
Entire project |
|
Syntax |
|
|
|
Repetition |
May be required in multiple files |
Declared once |
|
Best for |
File-specific namespaces |
Common namespaces |
|
Introduced |
Earlier C# versions |
C# 10 |
Real-Time Example – ASP.NET Core Web API
In a Clean Architecture solution, the Application project might have:
global using Microsoft.Extensions.DependencyInjection;
global using System.Threading;
global using System.Threading.Tasks;
global using System.Collections.Generic;
The API project might have:
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore.Authorization;
global using Microsoft.Extensions.Logging;
Each project has its own global using scope.
Implicit Usings in .NET 6
.NET 6 introduced SDK support for implicit global usings.
In .csproj:
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
The SDK automatically generates global using directives for common namespaces.
Key Points
-
Introduced in C# 10.0.
-
Uses the
global usingkeyword. -
Reduces repeated namespace imports.
-
Applies to all source files in the same compilation.
-
Does not automatically apply to other projects in a solution.
-
Can be combined with
ImplicitUsings. -
Excessive global imports may create ambiguous type-name conflicts.
Interview Answer: Global Using Directives allow namespaces to be imported once and used throughout an entire C# project, reducing repetitive using statements and improving code maintainability.
2. File-Scoped Namespaces
What are File-Scoped Namespaces?
Before C# 10.0, developers typically declared namespaces using curly braces { }.
C# 10.0 introduced File-Scoped Namespaces, allowing developers to declare a namespace using a semicolon ; instead.
This removes an unnecessary level of indentation.
Before C# 10.0
namespace MyApplication.Services
{
public class EmployeeService
{
public void Display()
{
Console.WriteLine("Employee Service");
}
}
}
C# 10.0
namespace MyApplication.Services;
public class EmployeeService
{
public void Display()
{
Console.WriteLine("Employee Service");
}
}
Both versions define the same class in the same namespace.
Example Program
EmployeeService.cs
using System;
namespace MyApplication.Services;
public class EmployeeService
{
public void DisplayEmployee()
{
Console.WriteLine("Employee: John");
}
}
Program.cs
using MyApplication.Services;
EmployeeService service = new();
service.DisplayEmployee();
Output:
Employee: John
Program Explanation
-
namespace MyApplication.Services;declares a file-scoped namespace. -
No namespace braces are required.
-
EmployeeServicebelongs toMyApplication.Services. -
Program.csimports the namespace. -
The service method is called normally.
How It Works
EmployeeService.cs
|
v
namespace MyApplication.Services;
|
v
EmployeeService
|
v
DisplayEmployee()
|
v
Program.cs
|
v
Employee: John
Traditional vs File-Scoped Namespace
|
Traditional Namespace |
File-Scoped Namespace |
|---|---|
|
Uses |
Uses |
|
Additional indentation |
Less indentation |
|
Can declare multiple namespace blocks |
One file-scoped namespace per file |
|
Supported in older C# |
Introduced in C# 10 |
|
Same namespace behavior |
Same namespace behavior |
Real-Time Example – Clean Architecture
Consider an ASP.NET Core Web API project.
EmployeeController.cs
using Microsoft.AspNetCore.Mvc;
namespace MyApplication.API.Controllers;
[ApiController]
[Route("api/employees")]
public class EmployeeController : ControllerBase
{
[HttpGet]
public IActionResult GetEmployees()
{
return Ok(new[] { "John", "David" });
}
}
This is cleaner than wrapping the entire controller inside namespace braces.
Important Restrictions
A file can contain only one file-scoped namespace declaration.
Valid:
namespace MyApplication.Services;
public class EmployeeService
{
}
public class CustomerService
{
}
Invalid:
namespace MyApplication.Services;
public class EmployeeService
{
}
namespace MyApplication.Repositories; // Error
You also cannot mix file-scoped and block-scoped namespace declarations in the same file.
Key Points
-
Introduced in C# 10.0.
-
Uses a semicolon instead of braces.
-
Reduces indentation.
-
Does not change runtime behavior or performance.
-
Applies to the namespace's types throughout the file.
-
Only one file-scoped namespace is allowed per file.
-
Commonly used in modern .NET applications.
Interview Answer: File-Scoped Namespaces simplify namespace declarations by replacing curly braces with a semicolon, reducing indentation and improving code readability.
3. Record Structs
What are Record Structs?
C# 9.0 introduced records as reference types.
C# 10.0 introduced Record Structs, which are value types that provide record-style functionality.
A record struct combines features of structures with value-based equality and concise data-model syntax.
Record vs Record Struct
public record Employee(int Id, string Name);
This creates a reference-type record.
public record struct Employee(int Id, string Name);
This creates a value-type record.
Example Program
using System;
public readonly record struct Employee(int Id, string Name);
public class Program
{
public static void Main()
{
Employee emp1 = new(101, "John");
Employee emp2 = new(101, "John");
Console.WriteLine(emp1 == emp2);
Employee emp3 = emp1 with
{
Name = "David"
};
Console.WriteLine(emp3);
}
}
Output:
True
Employee { Id = 101, Name = David }
Program Explanation
-
readonly record struct Employeedefines a read-only record struct. -
emp1andemp2contain identical property values. -
emp1 == emp2returnstruebecause record structs support value-based equality. -
The
withexpression creates a copy with a changed name. -
The original
emp1remains unchanged.
How It Works
Record Struct
|
v
Value Type
|
+----------+----------+
| |
v v
emp1 emp2
Id = 101 Id = 101
Name = John Name = John
| |
+----------+----------+
|
v
Compare Values
|
v
True
Record vs Record Struct vs Struct
|
Feature |
Record Class |
Record Struct |
Normal Struct |
|---|---|---|---|
|
Type |
Reference |
Value |
Value |
|
Equality |
Value-based |
Value-based |
Value-based by default |
|
Generated |
Yes |
Yes |
No, unless implemented |
|
|
Yes |
Yes |
Yes in C# 10 |
|
Positional properties |
Init-only |
Mutable by default |
Manually defined |
|
Supports inheritance |
Record inheritance |
No struct inheritance |
No struct inheritance |
|
Typical use |
Data models |
Small value objects |
Value types |
record struct vs readonly record struct
This distinction is important.
Mutable record struct:
public record struct Product(int Id, decimal Price);
Usage:
Product product = new(101, 500);
product.Price = 600; // Allowed
Positional properties in a normal record struct are mutable by default.
Readonly record struct:
public readonly record struct Product(int Id, decimal Price);
Usage:
Product product = new(101, 500);
// product.Price = 600; // Compile-time error
Real-Time Example – Banking Application
A banking application might use a small value object to represent a monetary amount and currency.
public readonly record struct Money(
decimal Amount,
string Currency
);
Usage:
Money payment1 = new(1000, "INR");
Money payment2 = new(1000, "INR");
Console.WriteLine(payment1 == payment2);
Output:
True
Both values are equal because their components match.
This is useful for value objects where identity is determined by data rather than a unique entity identifier.
Memory Considerations
Record Class
|
v
Reference Type
|
v
Object generally allocated
on managed heap
Record Struct
|
v
Value Type
|
v
Stored inline within its
containing storage location
A record struct is not guaranteed to be stored on the stack. It can be stored inside heap-allocated objects, arrays, or other structures.
Key Points
-
Introduced in C# 10.0.
-
record structis a value type. -
Supports generated value-based equality.
-
Supports
withexpressions. -
Normal positional record structs are mutable.
-
readonly record structprovides stronger immutability guarantees. -
Useful for small value objects such as coordinates, amounts, and measurements.
-
Large structs can be expensive to copy, so choose value types carefully.
Interview Answer: Record Structs are value types introduced in C# 10 that combine struct semantics with record features such as generated value-based equality, concise syntax, and support for with expressions.
4. Improved Lambda Expressions
What are Improved Lambda Expressions?
Lambda expressions existed before C# 10.0.
However, C# 10.0 introduced improvements that make lambda expressions more flexible and easier to use.
Important improvements include:
-
Natural type inference for lambda expressions.
-
Explicit return types.
-
Attributes on lambda expressions and their parameters.
4.1 Natural Type Inference
Before C# 10.0, developers often needed to specify a delegate type explicitly.
Before C# 10:
Func<int, int> square = x => x * x;
C# 10:
var square = (int x) => x * x;
The compiler can infer the natural delegate type from the lambda expression.
Example Program
using System;
public class Program
{
public static void Main()
{
var square = (int x) => x * x;
int result = square(5);
Console.WriteLine(result);
}
}
Output:
25
Program Explanation
-
(int x) => x * xdefines a lambda expression. -
The parameter type is explicitly specified as
int. -
The compiler infers a suitable delegate type, effectively
Func<int, int>. -
The lambda calculates the square of the supplied number.
-
square(5)returns 25.
How It Works
Lambda Expression
(int x) => x * x
|
v
Compiler determines
delegate type
|
v
Func<int, int>
|
v
square(5)
|
v
25
4.2 Explicit Return Types
C# 10 allows an explicit return type before the lambda parameter list.
Example:
var calculate = int (int x, int y) => x + y;
Usage:
Console.WriteLine(calculate(10, 20));
Output:
30
Here:
-
The first
intspecifies the return type. -
(int x, int y)defines the parameters. -
x + yis the expression body.
Example with Conditional Return
var getResult = string (int marks) =>
marks >= 50 ? "Pass" : "Fail";
Console.WriteLine(getResult(75));
Output:
Pass
4.3 Attributes on Lambda Expressions
C# 10 also allows attributes on lambda expressions.
Example:
var message = [Obsolete] () => "Hello";
Attributes can also be applied to lambda parameters:
var process = ([System.Diagnostics.CodeAnalysis.NotNull] string name) =>
{
Console.WriteLine(name);
};
Attributes provide metadata that can be used by compilers, analyzers, reflection, or frameworks. Their effects depend on the particular attribute and how it is consumed.
Lambda Expressions Before and After C# 10
|
Feature |
Before C# 10 |
C# 10 |
|---|---|---|
|
Lambda expressions |
Supported |
Supported |
|
Natural delegate type inference |
Limited |
Improved |
|
Explicit lambda return type |
Not supported |
Supported |
|
Lambda attributes |
Not supported |
Supported |
|
|
Generally not supported directly |
Supported |
Real-Time Example – LINQ
Lambda expressions are commonly used with LINQ.
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
List<int> numbers = new()
{
10, 20, 30, 40, 50
};
var filter = (int number) => number > 25;
var result = numbers.Where(filter);
foreach (int number in result)
{
Console.WriteLine(number);
}
}
}
Output:
30
40
50
Program Explanation
-
numberscontains five integers. -
filterdefines a lambda that checks whether a number is greater than 25. -
C# 10 infers the lambda's natural delegate type.
-
Where(filter)filters the collection. -
Only matching numbers are displayed.
Important Limitation
Natural type inference does not mean every lambda can be used with var.
For example:
var square = x => x * x; // Error
The compiler cannot infer the parameter type of x from this expression alone.
You must provide enough type information:
var square = (int x) => x * x;
Key Points
-
C# 10 improved lambda type inference.
-
Supports
varwith lambdas when a natural type can be determined. -
Supports explicit lambda return types.
-
Supports attributes on lambdas and lambda parameters.
-
Reduces the need to write explicit
Func<>andAction<>declarations in suitable cases. -
Existing lambda expressions and delegate syntax continue to work.
-
Particularly useful in LINQ, callbacks, and functional-style code.
Interview Answer: C# 10 improved lambda expressions by introducing natural delegate type inference, explicit return types, and attribute support, making lambda expressions more flexible and concise.
C# 10.0 – Quick Interview Revision
|
Feature |
Main Purpose |
Important Syntax |
|---|---|---|
|
Global Using Directives |
Import namespaces across a project |
|
|
File-Scoped Namespaces |
Reduce namespace indentation |
|
|
Record Structs |
Create value-type records |
|
|
Improved Lambda Expressions |
Improve lambda type inference and flexibility |
|
Most Important C# 10 Interview Questions
|
Priority |
Interview Question |
|---|---|
|
High |
What is the difference between a record class and a record struct? |
|
High |
What is the difference between |
|
High |
What is the difference between global using directives and normal using directives? |
|
High |
What are file-scoped namespaces? |
|
Medium |
What are the improvements to lambda expressions in C# 10? |
|
Medium |
What is natural type inference in lambda expressions? |
|
Medium |
What is the difference between implicit usings and global usings? |
|
Medium |
Are record structs always allocated on the stack? |
Important takeaway: For modern ASP.NET Core development, Global Using Directives and File-Scoped Namespaces reduce boilerplate, while Record Structs provide a convenient way to model small value types. Improved Lambda Expressions make delegate-based code more concise.