Skip to main content

C# Versioning

Introduction

Understanding C# versioning is crucial for any developer working with the language. Each C# version brings new features, syntax improvements, and performance enhancements that can make your code more efficient and readable. Whether you're maintaining legacy code or starting a new project, knowing which C# version you're targeting and what features are available to you is an essential part of C# best practices.

In this guide, we'll explore how C# has evolved from version 1.0 to the latest release, how to specify which version you want to use in your projects, and how to adopt new language features responsibly.

C# Version History and Key Features

C# 1.0 (2002) - The Beginning

C# 1.0 was released alongside the .NET Framework 1.0 and Visual Studio .NET. It provided the core functionality of an object-oriented language.

Key Features:

  • Classes and objects
  • Properties and events
  • Delegates and events
  • Basic types and operators
  • Basic exception handling

C# 2.0 (2005) - Adding Productivity

C# 2.0 introduced several major features that significantly improved developer productivity.

Key Features:

  • Generics
  • Partial types
  • Nullable value types
  • Anonymous methods
  • Iterators
  • Static classes

Example: Generics

csharp
// C# 1.0 - No generics
ArrayList list = new ArrayList();
list.Add("Hello");
list.Add(42); // Can add any type
string item = (string)list[0]; // Requires explicit casting

// C# 2.0 - Using generics
List<string> typedList = new List<string>();
typedList.Add("Hello");
// typedList.Add(42); // Compile error - type safety!
string typedItem = typedList[0]; // No casting needed

C# 3.0 (2007) - LINQ Revolution

C# 3.0 brought a revolutionary approach to data querying with LINQ (Language Integrated Query).

Key Features:

  • Lambda expressions
  • Extension methods
  • Query expressions (LINQ)
  • Anonymous types
  • Object and collection initializers
  • Automatic properties
  • Implicitly typed local variables (var)

Example: LINQ

csharp
// Before LINQ
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
List<int> evenNumbers = new List<int>();
foreach (int number in numbers)
{
if (number % 2 == 0)
{
evenNumbers.Add(number);
}
}

// With LINQ (C# 3.0)
var evenNumbersLinq = numbers.Where(n => n % 2 == 0).ToList();

// Query syntax
var evenNumbersQuery = from n in numbers
where n % 2 == 0
select n;

C# 4.0 (2010) - Dynamic Programming

C# 4.0 added features for dynamic programming and improved COM interoperability.

Key Features:

  • Dynamic binding
  • Named and optional arguments
  • Generic covariance and contravariance
  • Embedded interop types ("NoPIA")

Example: Dynamic Type

csharp
// Using dynamic type
dynamic data = "Hello";
Console.WriteLine(data.ToUpper()); // Works fine
data = 42;
Console.WriteLine(data + 10); // Works fine, outputs 52

// Named and optional parameters
public void ProcessOrder(string customer, string product, int quantity = 1, bool express = false)
{
// Implementation
}

// Can be called in these ways:
ProcessOrder("John", "Laptop");
ProcessOrder("Mary", "Phone", 3);
ProcessOrder("Bob", "Tablet", express: true);

C# 5.0 (2012) - Asynchronous Programming

C# 5.0 was a smaller release but introduced a crucial feature for modern programming: async/await.

Key Features:

  • Async/await pattern for asynchronous programming
  • Caller information attributes

Example: Async/Await

csharp
// Pre C# 5.0 asynchronous code (complicated and error-prone)
void DownloadDataCallback()
{
WebClient client = new WebClient();
client.DownloadStringCompleted += (sender, e) =>
{
if (e.Error != null)
{
HandleError(e.Error);
return;
}

ProcessData(e.Result);
};
client.DownloadStringAsync(new Uri("https://example.com/data"));
}

// C# 5.0 with async/await
async Task DownloadDataAsync()
{
try
{
WebClient client = new WebClient();
string data = await client.DownloadStringTaskAsync(new Uri("https://example.com/data"));
ProcessData(data);
}
catch (Exception ex)
{
HandleError(ex);
}
}

C# 6.0 (2015) - Syntactic Sugar

C# 6.0 focused on reducing code verbosity with many small but useful features.

Key Features:

  • String interpolation
  • Expression-bodied members
  • Null-conditional operators
  • Auto-property initializers
  • nameof operator
  • Exception filters
  • await in catch/finally blocks

Example: String Interpolation and Null-conditional Operator

csharp
// Before C# 6.0
string name = "John";
int age = 30;
string message = string.Format("Hello, {0}! You are {1} years old.", name, age);

// C# 6.0 with string interpolation
string newMessage = $"Hello, {name}! You are {age} years old.";

// Null-conditional operator
// Before
string value = null;
int length = 0;
if (value != null)
{
length = value.Length;
}

// With null-conditional operator
string value = null;
int? length = value?.Length; // This is null, not a NullReferenceException

C# 7.0-7.3 (2017-2018) - Performance and Code Simplification

C# 7.0 through 7.3 brought features focused on performance and further code simplification.

Key Features:

  • Tuples and deconstruction
  • Pattern matching
  • Local functions
  • Out variables
  • Expression-bodied everything
  • Ref locals and returns
  • Readonly ref structs (like Span<T>)
  • In parameters
  • Non-trailing named arguments

Example: Tuples and Pattern Matching

csharp
// Tuples
(string name, int age) person = ("John", 30);
Console.WriteLine($"{person.name} is {person.age} years old");

// Deconstruction
var (name, age) = person;
Console.WriteLine($"{name} is {age} years old");

// Pattern matching
object data = "Hello";

// Type pattern
if (data is string message)
{
Console.WriteLine($"String length: {message.Length}");
}

// Switch pattern matching
switch (data)
{
case string s when s.Length > 0:
Console.WriteLine($"Non-empty string: {s}");
break;
case int i:
Console.WriteLine($"Integer: {i}");
break;
case null:
Console.WriteLine("Null value");
break;
default:
Console.WriteLine("Unknown type");
break;
}

C# 8.0 (2019) - .NET Core Focus

C# 8.0 was the first version with features that required a new runtime (.NET Core 3.0), marking a shift from .NET Framework.

Key Features:

  • Nullable reference types
  • Default interface methods
  • Pattern matching enhancements
  • Using declarations
  • Static local functions
  • Async streams (IAsyncEnumerable)
  • Indices and ranges
  • Null-coalescing assignment

Example: Nullable Reference Types and Indices

csharp
// Nullable reference types
#nullable enable
string nonNullableString = null; // Warning: Cannot be null
string? nullableString = null; // OK, explicitly marked as nullable

// Indices and ranges
string[] names = { "Ana", "Bob", "Charlie", "David", "Eva" };
string last = names[^1]; // "Eva" (from the end)
string secondLast = names[^2]; // "David"

string[] firstThree = names[..3]; // "Ana", "Bob", "Charlie"
string[] lastTwo = names[^2..]; // "David", "Eva"
string[] middle = names[1..4]; // "Bob", "Charlie", "David"

C# 9.0 (2020) - .NET 5 Features

C# 9.0 was released with .NET 5 and focused on reducing ceremony in common code patterns.

Key Features:

  • Records
  • Init-only properties
  • Top-level statements
  • Pattern matching improvements
  • Target-typed new expressions
  • Covariant return types

Example: Records and Top-level Statements

csharp
// Records - immutable data classes
public record Person(string FirstName, string LastName, int Age);

// Usage
var john = new Person("John", "Doe", 30);
var jane = john with { FirstName = "Jane" }; // Non-destructive mutation

// Top-level statements (Program.cs)
// No need for Program class and Main method
Console.WriteLine("Hello, World!");
var sum = AddNumbers(5, 10);
Console.WriteLine($"Sum: {sum}");

int AddNumbers(int a, int b) => a + b;

C# 10 (2021) - .NET 6 Features

C# 10 was released with .NET 6, continuing to streamline common patterns.

Key Features:

  • Global using directives
  • File-scoped namespaces
  • Extended property patterns
  • Constant interpolated strings
  • Record structs
  • Improved lambda expressions
  • Parameter null checking

Example: Global Using Directives and File-scoped Namespaces

csharp
// Global using directives (in a separate file)
global using System;
global using System.Collections.Generic;
global using System.Linq;

// File-scoped namespace
namespace MyApp; // Instead of namespace MyApp { ... }

public class Calculator
{
public int Add(int a, int b) => a + b;
}

C# 11 (2022) - .NET 7 Features

C# 11 came with .NET 7 and introduced features for more expressive code.

Key Features:

  • Raw string literals
  • List patterns
  • Required members
  • Auto-default structs
  • Pattern matching with spans
  • Generic attributes

Example: Raw String Literals and List Patterns

csharp
// Raw string literals
var json = """
{
"name": "John",
"age": 30,
"isActive": true
}
""";

// List patterns
int[] numbers = { 1, 2, 3, 4, 5 };
if (numbers is [1, 2, 3, 4, 5])
{
Console.WriteLine("Exact match!");
}

if (numbers is [1, 2, ..])
{
Console.WriteLine("Starts with 1, 2");
}

if (numbers is [.., 4, 5])
{
Console.WriteLine("Ends with 4, 5");
}

How to Specify C# Version in Your Project

Using Visual Studio

In Visual Studio, you can specify the C# language version for your project:

  1. Right-click on your project in Solution Explorer
  2. Select "Properties"
  3. Go to the "Build" tab
  4. Click "Advanced" button
  5. Set "Language version" dropdown to your desired version

You can also specify the C# version directly in your .csproj file:

xml
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>11.0</LangVersion> <!-- Specifies C# 11 -->
</PropertyGroup>

Common language version values:

  • default - The latest major version
  • latest - The latest version (including preview)
  • preview - The preview version
  • 10 - C# 10.0
  • 9.0 - C# 9.0
  • 8.0 - C# 8.0
  • etc.

Real-World Application: Migrating a Project to a Newer C# Version

Let's look at a real-world scenario where you might need to migrate an older project to use newer C# features:

csharp
// Original C# 7 code
public class UserService
{
private readonly ILogger _logger;
private readonly IUserRepository _repository;

public UserService(ILogger logger, IUserRepository repository)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
}

public async Task<User> GetUserByIdAsync(int id)
{
_logger.Log($"Getting user with id: {id}");
var user = await _repository.GetByIdAsync(id);
if (user == null)
{
throw new UserNotFoundException($"User with id {id} not found");
}
return user;
}

public IEnumerable<User> GetActiveUsers()
{
var allUsers = _repository.GetAll();
var activeUsers = new List<User>();
foreach (var user in allUsers)
{
if (user.IsActive)
{
activeUsers.Add(user);
}
}
return activeUsers;
}
}

Migrating to C# 10

csharp
// Updated to C# 10
namespace UserManagement;

public class UserService
{
private readonly ILogger _logger;
private readonly IUserRepository _repository;

public UserService(ILogger logger, IUserRepository repository)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
}

public async Task<User> GetUserByIdAsync(int id)
{
_logger.Log($"Getting user with id: {id}");
var user = await _repository.GetByIdAsync(id);
return user ?? throw new UserNotFoundException($"User with id {id} not found");
}

public IEnumerable<User> GetActiveUsers() =>
_repository.GetAll().Where(user => user.IsActive);
}

Migrating Further to C# 11

csharp
// Updated to C# 11
global using System;
global using System.Threading.Tasks;
global using System.Collections.Generic;
global using System.Linq;

namespace UserManagement;

public class UserService
{
private readonly ILogger _logger;
private readonly IUserRepository _repository;

public UserService(ILogger logger, IUserRepository repository)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(repository);

_logger = logger;
_repository = repository;
}

public async Task<User> GetUserByIdAsync(int id)
{
_logger.Log($"""
Getting user with the following details:
- ID: {id}
- Timestamp: {DateTime.UtcNow}
""");

var user = await _repository.GetByIdAsync(id);
return user ?? throw new UserNotFoundException($"User with id {id} not found");
}

public IEnumerable<User> GetActiveUsers() =>
_repository.GetAll().Where(user => user.IsActive);
}

Best Practices for C# Versioning

  1. Document your minimum C# version requirements: Make it clear what C# version your project requires.

  2. Phase adoption of new features: Don't rush to rewrite everything using the latest features. Adopt gradually.

  3. Consider backward compatibility: If your code needs to be used by others who might be on older versions, be conservative with adopting newer features.

  4. Use conditional compilation when needed:

csharp
#if CSHARP10
// C# 10 specific code
#else
// Fallback for older versions
#endif
  1. Update team knowledge: When adopting newer language features, ensure your team understands the new syntax.

  2. Leverage IDE suggestions: Modern IDEs like Visual Studio will suggest newer C# patterns and features.

  3. Keep dependencies in sync: Make sure your project dependencies (libraries, frameworks) are compatible with your chosen C# version.

Summary

C# has evolved significantly since its introduction in 2002, with each version bringing new features that make code more concise, safer, and more expressive. Understanding C# versioning helps you:

  • Make informed decisions about which language features to use
  • Maintain compatibility across projects and teams
  • Gradually modernize your codebase as new versions are released
  • Improve code quality by using the most appropriate language features

As a beginner, it's important to know what C# version you're working with, as tutorials and examples you find online might use features from different versions. By understanding the evolution of C#, you can better navigate the language ecosystem and choose the right features for your projects.

Additional Resources

Exercises

  1. Identify the C# version of your current project and list three features from that version you're actively using.

  2. Take a code sample written in C# 7 and modernize it using C# 10 features.

  3. Create a small application that demonstrates at least one feature from each C# version 8, 9, and 10.

  4. Research and identify which C# version introduced a feature you frequently use but weren't aware of its origin.

  5. Set up a project that targets different C# versions for different build configurations (e.g., Debug uses the latest, Release uses C# 9).



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)