{"id":60647,"date":"2026-08-24T10:00:00","date_gmt":"2026-08-24T17:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=60647"},"modified":"2026-08-24T10:00:00","modified_gmt":"2026-08-24T17:00:00","slug":"explore-csharp-15","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/explore-csharp-15\/","title":{"rendered":"Explore new features available in C# 15 preview"},"content":{"rendered":"<p>C# 15 will ship with .NET 11 in November. All the new features are available in .NET 11 preview 7 for you to try now. C# 15 adds <em>union types<\/em>, <em>closed hierarchies<\/em>, the first preview of an updated <code>unsafe<\/code> model, collection expression arguments, extension indexers, and labeled <code>break<\/code> and <code>continue<\/code>. For more details on each of these features, see <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/whats-new\/csharp-15\">What&#8217;s new in C# 15<\/a> and the articles linked from that page.<\/p>\n<h2>Union types<\/h2>\n<p>A <em>union type<\/em> lets you say, in the type system, exactly which types a value may hold. With <code>object<\/code>, a marker interface, or an abstract base class, the runtime type can still be anything derived from <code>object<\/code> or anything that implements or derives from the shared type. A union constrains the value to one of the specified <em>case types<\/em>. The case types in a union aren&#8217;t necessarily related by inheritance. Use the <a href=\"#closed-hierarchies\">closed hierarchies<\/a> feature to restrict the set of types related by inheritance.<\/p>\n<pre><code class=\"language-csharp\">public record class Cat(string Name);\npublic record class Dog(string Name);\npublic record class Bird(string Name);\n\npublic union Pet(Cat, Dog, Bird);<\/code><\/pre>\n<p>A <code>Pet<\/code> holds a <code>Cat<\/code>, a <code>Dog<\/code>, or a <code>Bird<\/code>. Each case type converts implicitly to <code>Pet<\/code>. Because the compiler knows the complete set, a <code>switch<\/code> expression over a non-null <code>Pet<\/code> is exhaustive without a discard or default arm:<\/p>\n<pre><code class=\"language-csharp\">Pet pet = new Dog(\"Rex\");\n\nstring name = pet switch\n{\n    Dog d =&gt; d.Name,\n    Cat c =&gt; c.Name,\n    Bird b =&gt; b.Name,\n};<\/code><\/pre>\n<p>Union types are one expression of a broader design idea: make the allowed set of types explicit. <em>Closed hierarchies<\/em>, covered next, and the planned <em>closed enums<\/em> apply the same idea in other shapes.<\/p>\n<p>For more, read the <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/csharp-15-union-types\/\">C# 15 union types<\/a> post, try the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/whats-new\/tutorials\/unions\">Work with union types<\/a> tutorial, and see the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/builtin-types\/union\">union types<\/a> reference.<\/p>\n<h2>Closed hierarchies<\/h2>\n<p><em>Closed hierarchies<\/em> apply the same idea to class hierarchies you design. The <code>closed<\/code> modifier on a class makes it implicitly <code>abstract<\/code>, and restricts its direct derived types to the declaring assembly. You declare which subtypes are allowed instead of leaving derivation open-ended. For example, you can model the states of a background job as records that derive from a closed base:<\/p>\n<pre><code class=\"language-csharp\">public closed record class JobStatus;\npublic record class Queued : JobStatus;\npublic record class Running(int PercentComplete) : JobStatus;\npublic record class Completed(TimeSpan Elapsed) : JobStatus;\npublic record class Failed(string Error) : JobStatus;<\/code><\/pre>\n<p>Closed hierarchies, union types, and the planned <em>closed enums<\/em> all express the allowed set of shapes directly in the type system. For rules, including how <code>closed<\/code> composes with inheritance, see the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/keywords\/closed\">closed modifier<\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/operators\/patterns#closed-hierarchy-patterns\">closed hierarchy patterns<\/a> references.<\/p>\n<h2>Memory-safety redesign (preview)<\/h2>\n<p>C# 15 starts a redesign of <code>unsafe<\/code>: from a syntax marker\u2014&#8221;there are pointers here&#8221;\u2014to a contract the compiler can&#8217;t verify and a developer upholds. This is a <strong>preview<\/strong> feature in .NET 11 and C# 15. The model and syntax might change for .NET 12 and C# 16. The current preview includes two language changes you can try. You need to opt in explicitly: add <code>&lt;Features&gt;$(Features);updated-memory-safety-rules&lt;\/Features&gt;<\/code> and <code>&lt;LangVersion&gt;preview&lt;\/LangVersion&gt;<\/code> to your project file.<\/p>\n<p>In the new model, pointer types no longer need an <code>unsafe<\/code> context. You can declare a pointer type, take an address with <code>&amp;<\/code>, use the <code>fixed<\/code> statement, convert a <code>stackalloc<\/code> to a pointer, and use <code>sizeof<\/code> on an unmanaged type in a safe context.<\/p>\n<p>Operations that dereference pointers still require <code>unsafe<\/code>: pointer indirection (<code>*p<\/code>), member access through a pointer (<code>p-&gt;m<\/code>), element access through a pointer (<code>p[i]<\/code>), fixed-size-buffer element access, and function-pointer invocation.<\/p>\n<p>Finally, when a member adds the <code>unsafe<\/code> modifier to its signature, that member can be called only in an <code>unsafe<\/code> context. This is a breaking change from the current semantics of <code>unsafe<\/code> on a member. The member is declaring that its callers must ensure that the contract is followed, or propagate the unsafety by also including the <code>unsafe<\/code> modifier in its declaration.<\/p>\n<p>Read <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/improving-csharp-memory-safety\/\">Improving C# memory safety<\/a> for the full model, and see the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/unsafe-code\">unsafe code<\/a> reference for the rules implemented in preview in C# 15.<\/p>\n<p><div class=\"alert alert-info\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Important<\/strong><\/p>This is a preview feature in .NET 11 and C# 15. The design isn&#8217;t final and will continue to evolve before it ships in its final form. We want people to try the preview behavior and <a href=\"https:\/\/github.com\/dotnet\/csharplang\">give us feedback in csharplang<\/a> so we can shape the final experience.<\/div><\/p>\n<h2>Collection expression arguments<\/h2>\n<p>Collection expressions convert to many collection types, but until now you couldn&#8217;t pass arguments to the underlying constructor or <code>Create<\/code> method. C# 15 adds a <code>with(...)<\/code> element, written first, that forwards arguments to the constructor or factory method.<\/p>\n<p>This feature is necessary for the upcoming <em>dictionary expressions<\/em> syntax. You will often specify the comparer for a dictionary. You can use the feature now for sequence containers:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Before\nList&lt;string&gt; names = new(capacity: values.Length * 2);\nnames.AddRange(values);\n\nvar set = new HashSet&lt;string&gt;(StringComparer.OrdinalIgnoreCase) { \"Hello\", \"HELLO\" };<\/code><\/pre>\n<pre><code class=\"language-csharp\">\/\/ After (C# 15)\nList&lt;string&gt; names = [with(capacity: values.Length * 2), .. values];\n\nHashSet&lt;string&gt; set = [with(StringComparer.OrdinalIgnoreCase), \"Hello\", \"HELLO\"];<\/code><\/pre>\n<p>Learn more in <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/operators\/collection-expressions#collection-expression-arguments\">collection expression arguments<\/a>.<\/p>\n<h2>Extension indexers<\/h2>\n<p>C# 14 introduced extension members: properties and operators alongside methods. C# 15 adds extension <em>indexers<\/em>, so you can index into a receiver as if the indexer were declared on its type. Indexers can&#8217;t be static, so the extension container must include a named receiver.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Before: a helper method\npublic static class SequenceExtensions\n{\n    public static int ElementAtIndex(this IEnumerable&lt;int&gt; sequence, int index)\n        =&gt; sequence.ElementAt(index);\n}\n\nint third = numbers.ElementAtIndex(2);<\/code><\/pre>\n<pre><code class=\"language-csharp\">\/\/ After (C# 15): an extension indexer\npublic static class SequenceExtensions\n{\n    extension(IEnumerable&lt;int&gt; sequence)\n    {\n        public int this[int index] =&gt; sequence.ElementAt(index);\n    }\n}\n\nint third = numbers[2];<\/code><\/pre>\n<p>Learn more in the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/keywords\/extension#extension-indexers\">extension indexers<\/a> reference.<\/p>\n<h2>Labeled break and continue<\/h2>\n<p>Breaking out of a nested loop usually means a flag, a <code>goto<\/code>, or an extracted method. With C# 15, you can label a loop and target it directly with <code>break<\/code> or <code>continue<\/code>.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Before: a flag to unwind the outer loop\nbool found = false;\nforeach (Warehouse warehouse in warehouses)\n{\n    foreach (Bin bin in warehouse.Bins)\n    {\n        if (bin.Sku == requestedSku &amp;&amp; bin.Quantity &gt; 0)\n        {\n            reserved = Reserve(bin);\n            found = true;\n            break;\n        }\n    }\n    if (found)\n        break;\n}<\/code><\/pre>\n<pre><code class=\"language-csharp\">\/\/ After (C# 15): label the loop and break it directly\nscan: foreach (Warehouse warehouse in warehouses)\n{\n    foreach (Bin bin in warehouse.Bins)\n    {\n        if (bin.Sku == requestedSku &amp;&amp; bin.Quantity &gt; 0)\n        {\n            reserved = Reserve(bin);\n            break scan;\n        }\n    }\n}<\/code><\/pre>\n<p>The intent is in the code, with no flag to track. See the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/statements\/jump-statements\">jump statements<\/a> reference.<\/p>\n<h2>Try the preview<\/h2>\n<p>Download <a href=\"https:\/\/dotnet.microsoft.com\/download\/dotnet\">.NET 11<\/a> and try C# 15 on your apps. Read <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/whats-new\/csharp-15\">What&#8217;s new in C# 15<\/a> for the complete reference and participate in the ongoing discussions in <a href=\"https:\/\/github.com\/dotnet\/csharplang\">csharplang<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>C# 15 ships with .NET 11. It adds union types, closed hierarchies, a preview of the updated unsafe model, and a few smaller changes that remove everyday ceremony.<\/p>\n","protected":false},"author":56654,"featured_media":60648,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,756],"tags":[7893,8201,46,8200,63],"class_list":["post-60647","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-csharp","tag-dotnet-11","tag-net-11-preview-7","tag-c","tag-c-15","tag-dotnet"],"acf":[],"blog_post_summary":"<p>C# 15 ships with .NET 11. It adds union types, closed hierarchies, a preview of the updated unsafe model, and a few smaller changes that remove everyday ceremony.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/60647","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/56654"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=60647"}],"version-history":[{"count":1,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/60647\/revisions"}],"predecessor-version":[{"id":60649,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/60647\/revisions\/60649"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/60648"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=60647"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=60647"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=60647"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}