← Back to Articles         
C# 10.0 – Important Features Explained

C# 10.0 – Important Features Explained

Published on 12 Oct 2026     12 min read C#
C# 10 - Features

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

  1. GlobalUsings.cs defines namespaces globally.

  2. EmployeeService.cs uses List<string> without importing System.Collections.Generic.

  3. Program.cs uses Console.WriteLine() without importing System.

  4. The compiler makes global imports available throughout the project.

  5. 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

using System;

global using System;

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 using keyword.

  • 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

  1. namespace MyApplication.Services; declares a file-scoped namespace.

  2. No namespace braces are required.

  3. EmployeeService belongs to MyApplication.Services.

  4. Program.cs imports the namespace.

  5. 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

  1. readonly record struct Employee defines a read-only record struct.

  2. emp1 and emp2 contain identical property values.

  3. emp1 == emp2 returns true because record structs support value-based equality.

  4. The with expression creates a copy with a changed name.

  5. The original emp1 remains 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

with support

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 struct is a value type.

  • Supports generated value-based equality.

  • Supports with expressions.

  • Normal positional record structs are mutable.

  • readonly record struct provides 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:

  1. Natural type inference for lambda expressions.

  2. Explicit return types.

  3. 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

  1. (int x) => x * x defines a lambda expression.

  2. The parameter type is explicitly specified as int.

  3. The compiler infers a suitable delegate type, effectively Func<int, int>.

  4. The lambda calculates the square of the supplied number.

  5. 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 int specifies the return type.

  • (int x, int y) defines the parameters.

  • x + y is 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

var with suitable lambda

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

  1. numbers contains five integers.

  2. filter defines a lambda that checks whether a number is greater than 25.

  3. C# 10 infers the lambda's natural delegate type.

  4. Where(filter) filters the collection.

  5. 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 var with 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<> and Action<> 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

global using System;

File-Scoped Namespaces

Reduce namespace indentation

namespace MyApp.Services;

Record Structs

Create value-type records

record struct Employee(int Id, string Name);

Improved Lambda Expressions

Improve lambda type inference and flexibility

var square = (int x) => x * x;

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 record struct and readonly record struct?

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.

← Back to Articles