The evolution of C# spans more than two decades, from a Java-like object-oriented language in 2002 to the multi-paradigm, performance-aware language that shipped as C# 14 with .NET 10 in November 2025. This guide is for developers who learned C# at some point along the way, maintain code from several eras, or want to understand why modern C# looks the way it does. You will get a version-by-version timeline, the major themes that drove each era, an explanation of how LangVersion relates to your target framework, and practical lessons for writing idiomatic C# today.

What Is the Evolution of C#?#

C# first shipped in 2002 alongside .NET Framework 1.0 and Visual Studio .NET 2002. Its original design goal was a simple, modern, general-purpose object-oriented language, and version 1.0 delivered classes, structs, interfaces, properties, events, delegates and attributes. Every release since then has added capabilities without abandoning that core, so most code written in 2002 still compiles today even though the idiomatic style has changed dramatically.

The release rhythm changed over time. Early versions arrived every two to three years with a new Visual Studio. The 7.x series in 2017 and 2018 introduced point releases. Since C# 9 and .NET 5 in 2020, a new major version ships every November with the annual .NET release, and C# 15 is on track to ship with .NET 11 in November 2026.

C# is also an international standard. Ecma publishes it as ECMA-334, and the compiler's ISO-1 and ISO-2 language versions refer to the ISO/IEC 23270 editions for C# 1.x and 2.0. The published standard trails the shipping compiler by several versions: the latest Ecma text covers C# 7, while the committee drafts later versions in the open on GitHub.

C# Version Timeline: From 1.0 to 14#

The table below maps every major version to its release year, Visual Studio version, the .NET platform of that era and its headline features.

VersionReleasedVisual Studio.NET platformHeadline features
C# 1.02002Visual Studio .NET 2002.NET Framework 1.0Classes, structs, interfaces, properties, events, delegates, attributes
C# 1.22003Visual Studio .NET 2003.NET Framework 1.1foreach disposes enumerators that implement IDisposable
C# 2.02005Visual Studio 2005.NET Framework 2.0Generics, iterators, nullable value types, anonymous methods, partial types
C# 3.02007Visual Studio 2008.NET Framework 3.5LINQ, lambdas, extension methods, var, anonymous types, expression trees
C# 4.02010Visual Studio 2010.NET Framework 4dynamic, named and optional arguments, generic variance
C# 5.02012Visual Studio 2012.NET Framework 4.5async and await, caller info attributes
C# 6.02015Visual Studio 2015.NET Framework 4.6Roslyn compiler, string interpolation, nameof, null-conditional operators
C# 7.02017Visual Studio 2017.NET Framework 4.7, .NET Core 1.xTuples, pattern matching, out variables, local functions, ref returns
C# 7.1 to 7.32017 to 2018Visual Studio 2017 15.3 to 15.7.NET Core 2.xasync Main, default literal, Span<T> and ref struct, in parameters
C# 8.02019Visual Studio 2019 16.3.NET Core 3.0Nullable reference types, switch expressions, async streams, ranges
C# 92020Visual Studio 2019 16.8.NET 5Records, init setters, top-level statements, relational patterns
C# 102021Visual Studio 2022 17.0.NET 6Record structs, global usings, file-scoped namespaces, lambda improvements
C# 112022Visual Studio 2022 17.4.NET 7Generic math, raw string literals, required members, list patterns
C# 122023Visual Studio 2022 17.8.NET 8Primary constructors, collection expressions, inline arrays
C# 132024Visual Studio 2022 17.12.NET 9params collections, the Lock type, partial properties
C# 142025Visual Studio 2026 18.0.NET 10Extension members, the field keyword, null-conditional assignment

C# 15 is not in the table because it has not shipped yet. As of September 2026 it is the default language version in the .NET 11 release candidate and adds union types, closed class hierarchies, collection expression arguments, extension indexers and labeled break and continue. The C# 14 features guide covers the current release in depth.

How C# Evolves: The Design Process#

Understanding how features are made explains why the language evolves the way it does. Language design happens in the open in the dotnet/csharplang repository, where proposals, champion issues and Language Design Meeting notes are published. The compiler lives in the dotnet/roslyn repository, which keeps a feature status page showing which features are merged into which Visual Studio preview.

Large features often ship as previews first. Static abstract interface members were a preview in C# 10 before becoming the foundation of generic math in C# 11, and the field keyword was a C# 13 preview before it became final in C# 14. Previews are only available when you set LangVersion to preview, which gives the team room to change the design based on feedback.

The compiler also depends on the libraries and sometimes the runtime. The compiler ships in the .NET SDK and Visual Studio, but many features need types or runtime support to exist. C# 7.0 tuples rely on System.ValueTuple, and C# 8 ranges need System.Index and System.Range. Default interface members needed CLR changes that first shipped in .NET Core 3.0. That dependency is why Microsoft ties each language version to a .NET version, and why C# 8.0 was the first release designed specifically for .NET Core rather than .NET Framework.

Getting Started: Check Which C# Version You Use#

Before adopting a feature, confirm which language version your project compiles with. The quickest trick is a deliberate compiler error. The #error version directive makes the compiler report error CS8304 with both the compiler version and the selected language version:

C#
#error version

In most projects you should not set the language version at all. The SDK derives it from the target framework:

XML
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <!-- No LangVersion: net10.0 implies C# 14 -->
  </PropertyGroup>
</Project>

If you need to try preview features, set <LangVersion>preview</LangVersion>, ideally in a Directory.Build.props file scoped to an experimental folder rather than across the whole repository.

How LangVersion and the Target Framework Relate#

The C# compiler can compile any language version up to the newest one it knows, but the default depends on the target framework moniker (TFM). The rules below come from Microsoft's language versioning documentation:

Target frameworkDefault C# version
.NET 11 (preview)C# 15
.NET 10C# 14
.NET 9C# 13
.NET 8C# 12
.NET 7C# 11
.NET 6C# 10
.NET 5C# 9
.NET Core 3.x and .NET Standard 2.1C# 8.0
.NET Core 2.x, .NET Standard 2.0 and 1.xC# 7.3
.NET Framework (all versions)C# 7.3

Three consequences follow. First, a multi-targeted library gets a different default for each target, so a project targeting net10.0 and netstandard2.0 compiles one target with C# 14 and the other with C# 7.3. Second, the LangVersion values mean different things: preview enables the newest preview features, latest means the latest version the installed compiler supports, and default or latestMajor means the latest major version regardless of the target framework. Omitting the property is not the same as default. Third, Microsoft states that using a language version newer than your target framework's default is unsupported. Some syntax-only features work on older targets, but anything that needs runtime support or new library types can fail at compile time or behave differently, as the C# 14 span overload changes show.

Microsoft also explicitly advises against <LangVersion>latest</LangVersion>: the result depends on which SDK happens to be installed, so two machines can build the same commit with different language rules.

One Task Across Four Eras#

The clearest way to see the evolution is to solve the same problem in the style of different versions: find customers with a negative balance. In C# 1.0 you used non-generic collections and casts:

C#
// C# 1.0 (2002): non-generic collections and runtime casts
public static ArrayList FindOverdrawn(ArrayList customers)
{
    ArrayList result = new ArrayList();
    foreach (object item in customers)
    {
        Customer customer = (Customer)item;   // fails at run time if the list is wrong
        if (customer.Balance < 0)
        {
            result.Add(customer);
        }
    }
    return result;
}

C# 2.0 generics moved that check to compile time and removed boxing for value types, while iterators made lazy sequences trivial to write:

C#
// C# 2.0 (2005): generics and iterators
public static IEnumerable<Customer> FindOverdrawn(List<Customer> customers)
{
    foreach (Customer customer in customers)
    {
        if (customer.Balance < 0)
        {
            yield return customer;   // produced lazily, no temporary list
        }
    }
}

C# 3.0 then turned the loop into a declarative query. Query syntax and method syntax compile to the same calls, and lambdas, extension methods, var and anonymous types all exist largely to make this possible:

C#
// C# 3.0 (2007): LINQ with query syntax and the equivalent method syntax
var debtors =
    from c in customers
    where c.Balance < 0
    orderby c.Balance
    select new { c.Name, Debt = -c.Balance };

var sameDebtors = customers
    .Where(c => c.Balance < 0)
    .OrderBy(c => c.Balance)
    .Select(c => new { c.Name, Debt = -c.Balance });

A modern version combines records, primary constructors, params spans, collection expressions and C# 14 members. The code is shorter, but more importantly each feature encodes intent the compiler can check:

C#
// C# 12 to 14: primary constructors, collection expressions, params spans,
// the field keyword and extension members
public sealed class OverdraftMonitor(ILogger<OverdraftMonitor> logger)
{
    public decimal Threshold
    {
        get;
        set
        {
            ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 0m);
            field = value;
        }
    }

    public IReadOnlyList<Customer> Scan(params ReadOnlySpan<Customer> customers)
    {
        List<Customer> flagged = [];
        foreach (var customer in customers)
            if (customer.Balance < Threshold) flagged.Add(customer);

        logger.LogInformation("Flagged {Flagged} of {Total}", flagged.Count, customers.Length);
        return flagged;
    }
}

public sealed record Customer(string Name, decimal Balance);

public static class CustomerExtensions
{
    extension(Customer customer)
    {
        public bool IsOverdrawn => customer.Balance < 0;
    }
}

Themes That Shaped Each Era of C#

Generics and Type Safety (C# 2.0)#

Generics are the most consequential addition in C# history. Unlike Java's erasure-based generics, .NET generics are reified by the runtime, so List<int> stores integers without boxing and type information survives to run time. Nullable value types, iterators and anonymous methods arrived in the same release and laid the groundwork for everything that followed. The generics and variance guide explains how the CLR implements them.

LINQ and the Functional Turn (C# 3.0)#

C# 3.0 is where C# became a hybrid object-oriented and functional language. Lambdas, extension methods, implicitly typed locals, anonymous types and expression trees all serve Language-Integrated Query. Expression trees made it possible to translate the same query into SQL, which is still how Entity Framework Core works. See LINQ in depth for execution semantics.

Dynamic Interop and Asynchrony (C# 4.0 and 5.0)#

C# 4.0 added dynamic and named and optional arguments, which made COM and dynamic-language interop far less painful, plus generic variance for interfaces and delegates. It remains useful at interop boundaries, but runtime binding requires runtime code generation that is incompatible with Native AOT. C# 5.0 was a focused release built around async and await, which replaced callback-based asynchronous patterns with sequential-looking code. It also fixed a notorious closure bug: each foreach iteration now gets a fresh loop variable.

C#
// C# 5.0 (2012): asynchronous code that reads like synchronous code
public async Task<int> CountWordsAsync(HttpClient http, string url)
{
    string html = await http.GetStringAsync(url);
    return html.Split(new[] { ' ', '\n' }, StringSplitOptions.RemoveEmptyEntries).Length;
}

The async/await deep dive covers what the compiler generates for this method.

Roslyn and Everyday Productivity (C# 6.0)#

C# 6.0 shipped with Roslyn, the open-source compiler written in C# that exposes syntax trees and semantic models as APIs. Roslyn made analyzers, code fixes and later source generators possible, and it let the team ship language features faster. The language changes themselves were small conveniences that removed boilerplate:

C#
// C# 6 (2015): getter-only auto-properties, expression-bodied members,
// string interpolation, nameof and null-conditional access
public class Account
{
    public Account(string owner)
    {
        if (owner == null) throw new ArgumentNullException(nameof(owner));
        Owner = owner;
    }

    public string Owner { get; }
    public decimal Balance { get; private set; }
    public List<string> Tags { get; } = new List<string>();
    public bool IsOverdrawn => Balance < 0;

    public override string ToString() => $"{Owner}: {Balance:C}";
}

Pattern Matching and Data-Oriented Code (C# 7 to 11)#

Pattern matching grew over five releases, from type patterns in C# 7.0 to switch expressions and property patterns in C# 8, relational and logical patterns in C# 9, extended property patterns in C# 10 and list patterns in C# 11:

C#
// C# 7.0: type pattern with a declaration
if (payment is CardPayment card && card.Amount > 1_000m)
    RequireStrongAuthentication(card);

// C# 8.0: switch expression with property patterns
decimal fee = payment switch
{
    CardPayment { IsInternational: true } => 2.5m,
    CardPayment _ => 1.2m,
    BankTransfer _ => 0m,
    _ => throw new NotSupportedException(payment.GetType().Name),
};

// C# 9.0: relational and logical patterns
string risk = score switch
{
    < 300 => "high",
    >= 300 and < 700 => "medium",
    _ => "low",
};

// C# 11: list patterns
if (args is ["--port", var portText, ..] && int.TryParse(portText, out var port))
    Console.WriteLine($"Listening on {port}");

Records in C# 9, record structs in C# 10 and required members in C# 11 complete the picture: data can be declared concisely, compared by value and copied with with expressions. The records and pattern matching guide goes further.

Safety by Default (C# 8 onward)#

Nullable reference types in C# 8 were arguably the biggest change to everyday C# since generics. Rather than changing the runtime, the compiler tracks null state and warns when you might dereference null. New project templates enable it by default, and the .NET libraries are annotated, so modern code treats null warnings as real defects.

Performance Without Unsafe Code (C# 7.2 to 14)#

A steady thread of features lets safe code reach speeds that once required pointers: ref returns in C# 7.0, Span<T>, ref struct and in parameters in C# 7.2, ref fields and scoped in C# 11, inline arrays in C# 12, allows ref struct in C# 13, and first-class span conversions plus user-defined compound assignment in C# 14. Most application developers consume these indirectly, through faster libraries such as System.Text.Json and ASP.NET Core.

Less Ceremony and Better Metaprogramming (C# 9 to 14)#

Top-level statements, global usings, file-scoped namespaces, primary constructors, collection expressions and the field keyword steadily removed boilerplate. At the same time, source generators (introduced with C# 9 and made incremental with C# 10), partial members and C# 14 extension members moved code generation and API extension from runtime reflection to compile time, which also suits trimming and Native AOT.

Best Practices: Writing Idiomatic Modern C#

  • Let the target framework choose the language version. Upgrade the TFM to get new features instead of overriding LangVersion.
  • Enable nullable reference types and treat warnings as defects. The annotations are part of your API contract.
  • Prefer expressions to statements when intent stays clear. Switch expressions, pattern matching and LINQ state what you want rather than how to loop.
  • Model data with records and required members. Value equality and with expressions make immutable data easy; keep classes for entities with identity and behavior.
  • Use async and await all the way down. Blocking on tasks with .Result is an artifact of older code.
  • Reach for spans in hot paths, not everywhere. They pay off in parsers, serializers and tight loops.
  • Prefer compile-time techniques over reflection. Source generators and extension members are faster and work with Native AOT.
  • Adopt new syntax deliberately. Apply it where it reads better, and use code-style rules in .editorconfig to keep the codebase consistent.

Common Pitfalls#

  • Setting LangVersion to latest. Builds then depend on whichever SDK is installed, so CI and developer machines can disagree.
  • Forcing new language versions onto .NET Framework. It is unsupported, and features that need runtime support, such as default interface members, cannot work there.
  • Mixing eras without a plan. A codebase that combines ArrayList, manual null checks and records confuses readers; modernize module by module.
  • Assuming primary constructor parameters are read-only fields. They are ordinary parameters captured by the class and can be reassigned; declare a readonly field if immutability matters.
  • Using records for everything. Value equality is wrong for entities whose identity is a database key.
  • Reaching for dynamic in new code. It moves errors to run time and does not work with Native AOT.

Legacy C# vs Modern C#: Idiom Comparison#

TaskLegacy idiom (version)Modern idiom (version)
CollectionsArrayList with casts (1.0)List<T> and collection expressions (2.0, 12)
Filtering dataHand-written loops (1.0)LINQ queries (3.0)
Asynchronous I/OCallbacks and IAsyncResult (1.0)async and await, async streams (5.0, 8.0)
Null handlingManual == null checksNullable reference types, ?. and ??= (6.0, 8.0)
Type checksis followed by a castPattern matching and switch expressions (7.0 to 11)
Data classesFields, constructors, Equals overridesRecords and required members (9, 11)
Program entrystatic void Main in a classTop-level statements (9)
Code generationRuntime reflectionSource generators and extension members (9 to 14)

Frequently Asked Questions#

Which C# version should I use in 2026?#

Use the version that matches your target framework. For most production code that means C# 14 on .NET 10, the current LTS release supported until November 2028. .NET 8 and .NET 9 reach end of support on November 10, 2026, so projects still on C# 12 or C# 13 should plan their upgrade now.

Can I use the latest C# version with .NET Framework?#

.NET Framework projects default to C# 7.3. You can set a higher LangVersion, and some syntax-only features compile, but Microsoft does not support the combination. Features that need runtime support, such as default interface members, cannot work on .NET Framework, and others require library types it lacks.

How often is a new version of C# released?#

Since C# 9 in 2020, a new major version has shipped every November together with the annual .NET release. Even-numbered .NET releases are LTS and odd-numbered releases are STS, but the language version cadence is yearly regardless.

Do new C# versions break existing code?#

Rarely. The team treats source compatibility as a priority, and breaking changes are documented for each release. Examples include the C# 5 change to foreach variable capture and the C# 14 field keyword, which can change the meaning of an existing identifier named field inside property accessors.

Is C# still similar to Java?#

The C# 1.0 syntax was deliberately familiar to Java developers, but the languages diverged quickly. Reified generics, LINQ, async and await, value types with ref semantics, records, pattern matching and extension members give modern C# a distinct character.

Summary#

  • C# evolved from a Java-like object-oriented language (2002) into a multi-paradigm language with functional, asynchronous and low-level performance features.
  • The big turning points were generics (2.0), LINQ (3.0), async and await (5.0), Roslyn (6.0), pattern matching (7 onward), nullable reference types (8.0), records (9) and extension members (14).
  • Since 2020, a new C# version ships every November with .NET; C# 14 ships with .NET 10 LTS, and C# 15 arrives with .NET 11.
  • The target framework determines the default language version; overriding it with a newer version is unsupported.
  • Idiomatic modern C# favors expressions, immutability, null safety, async all the way and compile-time metaprogramming.

Further Reading#