C# 1.0 was released in January 2002. The language existed before that product release: Ecma published the first C# language specification in December 2001. Separating language design, standardization, and product release gives a more useful answer than treating them as one event.
These dates come from Microsoft’s C# release history and the ECMA-334 archive. C# has continued to evolve; this guide follows representative features through C# 14 and connects them to a practical upgrade process.
Technical verification: 4 October 2026. Examples target .NET 10 and were checked with SDK 10.0.401 on Linux x64. Platform-specific deployment and package compatibility must be checked for your target environment.

Language, SDK, and runtime versions are different
C# defines source syntax and semantics. The SDK supplies compilers and build tools. A target framework such as net10.0 selects the APIs your application can compile against, while the deployment determines which runtime executes it.
For example, changing LangVersion can expose new syntax without supplying missing framework APIs. Conversely, installing a new runtime does not rewrite your source or rebuild your application. A migration should record all these settings separately. Microsoft’s language-version configuration guide documents the defaults and warns against machine-dependent latest selection.
Representative milestones
| C# release | Year | Feature to recognize in code |
|---|---|---|
| 1.0 | 2002 | Classes, interfaces, properties, delegates |
| 2.0 | 2005 | Generics and iterators |
| 3.0 | 2007 | LINQ, lambdas, extension methods |
| 4.0 | 2010 | Dynamic binding |
| 5.0 | 2012 | Async and await |
| 6.0 | 2015 | String interpolation and null-conditional access |
| 7.0 | 2017 | Tuples and pattern matching |
| 8.0 | 2019 | Nullable reference analysis |
| 9.0 | 2020 | Records |
| 10.0 | 2021 | Global using directives |
| 11.0 | 2022 | Required members and raw string literals |
| 12.0 | 2023 | Collection expressions |
| 13.0 | 2024 | Params collections |
| 14.0 | 2025 | Extension members and field-backed properties |
This is a selection, not the complete feature list. It follows the dated Microsoft release history; a release number is not a promise that every feature works on every older target.
Read the history in one runnable program
Create a .NET 10 console project with dotnet new console --framework net10.0, replace Program.cs with this example, and run dotnet run:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
record Score(string Name, int Value);
class Program
{
static async Task Main()
{
List<int> scores = new List<int> { 10, 20, 30 }; // Generics: C# 2.
var passing = scores.Where(score => score >= 20); // var/lambda/LINQ: C# 3.
int total = passing.Sum();
await Task.CompletedTask; // async/await: C# 5.
string? label = null; // Nullable reference analysis: C# 8.
Score result = new Score(label ?? "total", total); // Record: C# 9.
int[] extra = [1, 2, 3]; // Collection expression: C# 12.
Console.WriteLine($"{result.Name}={result.Value}");
Console.WriteLine($"extra={extra.Sum()}");
}
}
total=50
extra=6
List<int> uses generics to state the element type. The lambda supplies a filtering rule to LINQ. var asks the compiler to infer a static type; it does not make the value dynamically typed. The await line is included to identify the syntax introduced in C# 5; awaiting this already-completed task does not create a thread or simulate actual I/O.
The nullable annotation lets the compiler analyze possible null references. It does not wrap strings in a different runtime type, and it does not make a program automatically null-safe. Microsoft’s nullable reference documentation explains the warning-based analysis.
The record gives Score generated value-oriented behavior, including equality and a useful representation. A record containing mutable referenced objects is not automatically deeply immutable. The collection expression supplies array elements using the C# 12 syntax. See the record reference and LINQ guide to explore those mechanisms.
Use the timeline to plan a migration
The first question for an older project is which behavior must survive the upgrade. Do not begin by replacing every older syntax form. Working explicit types, ordinary classes, and loops remain valid tools. The sample’s generics and newer expressions coexist in one project.
- Inventory the application. Record project type, target framework, SDK/compiler settings, package versions, native libraries, deployment OS, configuration, serialization formats, and external contracts.
- Capture a baseline. Build the current version and retain fixtures for success, invalid input, errors, and persisted data. Keep a deployable rollback artifact and the configuration needed to use it.
- Choose the destination. Select a supported target and check that essential dependencies have compatible versions. Microsoft provides an upgrade overview; tool recommendations should be rechecked when the migration begins.
- Move in a bounded step. Update project/dependency/runtime settings separately from broad syntax cleanup when practical. Resolve compiler errors and warnings, then test the real deployment and external integrations.
- Modernize with a reason. Add nullable analysis to a manageable area, choose records for suitable data models, or introduce asynchronous I/O where the call chain can support it. Verify the changed behavior rather than only a clean build.
- Release with rollback. Compare traffic or job results, failures, and operational signals against the baseline. Define when to revert and who owns the decision.
Changing a target framework can introduce behavioral and API differences independently of source syntax. Review the relevant version’s compatibility notes as well as Microsoft’s library compatibility guidance. A compiler accepting old source is only one part of compatibility.
Practice without turning history into a rewrite checklist
Change the sample’s filter to include only scores above 20 and confirm that the total becomes 30. Then replace the collection expression with new int[] { 1, 2, 3 } and confirm that its sum remains 6. These small edits show how different language generations express behavior, while tests protect the result.
For the foundations, start with the first C# lesson, then generic lists and null-coalescing expressions. Consult the current Microsoft sources for feature availability instead of assuming that the publication year of an older tutorial is the language’s present limit.
Follow the C# learning roadmap to choose your next topic. For a guided explanation with an original practice task, explore the C# video tutorial collection.