C# 13: Explore the latest preview features

Kathleen Dollard

C# 13 is starting to take shape with features that focus on flexibility, performance, and making your favorite features even better for everyday use. We build C# in the open and at this year’s Microsoft Build we gave you a peek into what was coming in C# 13. Today, we want to share the current status of what you can try today in C# 13 and provide updates on features planned for this release and beyond. Let’s take a look at these new features in more detail.

Try C# 13 today

Before we dive into each new features of C# 13 you may be wondering how do you try it out.

You can find the latest previews of C# 13 in latest .NET 9 preview (Preview 6 at the time of writing) and in the latest preview of Visual Studio 2022-17.11. To access preview features, set your language version to preview in your project file:

<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
      <!--other settings-->
      <LangVersion>preview</LangVersion>
      <!--other settings-->
   </PropertyGroup>
</Project>

params collections

C# 13 extends params to work with any type that can be constructed via a collection expression. This adds flexibility whether you are writing a method or calling it.

When the params keyword appears before a parameter, calls to the method can provide a comma delimited list of zero or more values. The following works in all versions of C#:

public void WriteNames(params string[] names)
   => Console.WriteLine(String.Join(", ", names));

WriteNames("Mads", "Dustin", "Kathleen");
WriteNames(new string[] {"Mads", "Dustin", "Kathleen"});
// Both of these Would output: Mads, Dustin, Kathleen

Note that you can call the method with either a comma delimited list of values, or an object of the underlying type.

Starting in C# 13 params parameters can be of any of the types supported for collection expressions. For example:

public void WriteNames(params List<string> names)
   => Console.WriteLine(String.Join(", ", names));

Whenever you call a method that has a parameter that is an IEnumerable<T>, you can pass the results of a LINQ expression. If the IEnumerable<T> parameter has the params modifier, you can also pass a comma delimited list. You can use a comma delimited list when you have constants and a LINQ expression when you need it:

public void WriteNames(params IEnumerable<string> names)
   => Console.WriteLine(String.Join(", ", names));

var persons = new List<Person>
{
   new Person("Mads", "Torgersen"),
   new Person("Dustin", "Campbell"),
   new Person("Kathleen", "Dollard")
};

// All of the following output: Mads, Dustin, Kathleen
WriteNames("Mads", "Dustin", "Kathleen");
WriteNames(persons.Select(person => person.FirstName));
WriteNames(from p in persons select p.FirstName);

Overload resolution

When authoring a method, you can supply multiple params overloads. For example, adding an IEnumerable<T> overload supports LINQ and adding a ReadOnlySpan<T> or Span<T> overload reduces allocations, which can improve performance.

public void WriteNames(params string[] names)
   => Console.WriteLine(String.Join(", ", names));

public void WriteNames(params ReadOnlySpan<string> names)
   => Console.WriteLine(String.Join(", ", names));

public void WriteNames(params IEnumerable<string> names)
   => Console.WriteLine(String.Join(", ", names));

When one of the specified types is passed, that overload is used. When comma delimited values or no values, are passed, the best overload is selected. Using the overloads above:

// IEnumerable overload is used
WriteNames(persons.Select(person => person.FirstName)); 

// array overload is used
WriteNames(new string[] {"Mads", "Dustin", "Kathleen"}); 

// most efficient overload is used: currently ReadOnlySpan
WriteNames("Mads", "Dustin", "Kathleen");                

Multiple overloads can add convenience and improve performance. Library authors should give all overloads the same semantics so that callers don’t need to be concerned about which overload is used.

lock object

.NET 9 includes a new System.Threading.Lock type for mutual exclusion that can be more efficient than locking on an arbitrary System.Object instance. The System.Threading.Lock type proposal has more about this type and why it was created. Over time, this type is expected to become the primary mechanism used for most locking in C# code.

C# 13 makes it easy to use this type. When the compiler recognizes that the target of the lock statement is a System.Threading.Lock object, C# now generates calls to the System.Threading.Lock API and provides warnings for cases where an instance of a Lock might be incorrectly treated as a normal object.

This update means the familiar syntax for the lock statement leverages new features in the runtime. Familiar code gets better with minimal change. Just change your project’s TargetFramework to .NET 9 and change the type of the lock from object to System.Threading.Lock:

public class MyClass 
{
    private object myLock = new object();

    public void MyMethod() 
    {
        lock (myLock)
        {
           // Your code
        }          
    }
}

public class MyClass 
{
    // The following line is the only change
    private System.Threading.Lock myLock = new System.Threading.Lock();

    public void MyMethod() 
    {
        lock (myLock)
        {
            // Your code
        }     
    }
}

Index from the end in initializers

The index operator ^ allows you to indicate a position in a countable collection relative to the end of the collection. This now works in initializers:

class Program
{ 
    static void Main()
    {
        var x = new Numbers
        {
            Values = 
            {
                [1] = 111,
                [^1] = 999  // Works starting in C# 13
            }
            // x.Values[1] is 111
            // x.Values[9] is 999, since it is the last element
        };
    }
}

class Numbers
{
    public int[] Values { get; set; } = new int[10];
} 

Escape sequence \e

C# 13 introduces a new escape sequence for the character you know as ESCAPE or ESC. You previously had to type this as a variation of \u001b. This new sequence is especially convenient when interacting with terminals with the VT100/ANSI escape codes to System.Console. For example:

// Prior to C# 13
Console.WriteLine("\u001b[1mThis is a bold text\u001b[0m");

// With C# 13
Console.WriteLine("\e[1mThis is a bold text\e[0m");

This makes creating fancy terminal output easier and less prone to errors.

Partial properties

C# 13 adds partial properties. Like partial methods their primary purpose is to support source generators. Partial methods have been available for many releases with additional improvements in C# 9. Partial properties are much like their partial method counterparts.

For example, starting with .NET 7 (C# 12), the regular expression source generator creates efficient code for methods:

[GeneratedRegex("abc|def")]
private static partial Regex AbcOrDefMethod();

if (AbcOrDefMethod().IsMatch(text))
{
   // Take action with matching text
}

In .NET 9 (C# 13), the Regex source generator has been updated and if you prefer to use a property, you can also use:

[GeneratedRegex("abc|def")]
private static partial Regex AbcOrDefProperty { get; };

if (AbcOrDefProperty.IsMatch(text))
{
   // Take action with matching text
}

Partial properties will make it easier for source generator designers to create natural feeling APIs.

Method group natural type improvements

The natural type of an expression is the type determined by the compiler, such as when the type is assigned to var or Delegate. That’s straightforward when it’s a simple type. In C# 10 we added support for method groups. Method groups are used when you include the name of a method without parentheses as a delegate:

Todo GetTodo() => new(Id: 0, Name: "Name");
var f = GetTodo; // the type of f is Func<ToDo>

C# 13 refines the rules for determining the natural type to consider candidates by scope and to prune candidates that have no chance of succeeding. Updating these rules will mean less compiler errors when working with method groups.

allows ref struct

C# 13 adds a new way to specify capabilities for generic type parameters. By default, type parameters cannot be ref struct. C# 13 lets you specify that a type parameter can be a ref struct, and applies the appropriate rules. While other generic constraints limit the set of types that can be used as the type parameter, this new specification expands the allowed types. We think of this as an anti-constraint since it removes rather than adds a restriction. The syntax allows ref struct in the where clause, where allows indicates this expansion in usage:

T Identity<T>(T p)
    where T : allows ref struct
    => p;

// Okay
Span<int> local = Identity(new Span<int>(new int[10]));

A type parameter specified with allows ref struct has all of the behaviors and restrictions of a ref struct type.

ref and unsafe in async methods and iterators

Prior to C# 13, iterator methods (methods that use yield return) and async methods couldn’t declare local ref variables, nor could they have an unsafe context.

In C# 13, async methods can declare ref local variables, or local variables of a ref struct type. These variables can’t be preserved across an await boundary or a yield return boundary.

In the same fashion, C# 13 allows unsafe contexts in iterator methods. However, all yield return and await statements must be in safe contexts. These relaxed restrictions let you use ref local variables and ref struct types in more places.

Update on Extension Types

We are very excited about the Extension Types feature that Mads and Dustin showed at Build. We also described Extension Types in the blog post .NET announcements at Build. At the time, we were aiming for key parts of the feature to be in C# 13, but the design and implementation are going to take more time. Look for Extension Types in early C# 14 (NET 10) previews.

Summary

You can find more on these features at What’s new in C# 13. We’re still working on features for C# 13, and you can check out what we’re doing at the Roslyn Feature Status page. Also, be sure to follow the .NET 9 preview release notes where you can now find C# release notes for each release.

Download the latest preview of Visual Studio 2022-17.11 with .NET 9, check out these new features, and let us know what you think!

43 comments

Leave a comment

  • Vin 7

    Idk, most of these features look…redundant. For example params keyword can easily be replaced with: Method( [1, 2, 3] ), where it also indicates that parameter should be collection.
    Index from end of initializer ([^] =) is … whoa, I can not even think of any use-case for that.
    Only one feature I was looking forward … extensions … dissapointment
    If “field” keyword will not make it even in this release (which I am skeptical from github progress), I will probably start question my decision to use C# for my projects 😅
    DU is typical complain that can be found under every thread, so no need to remind it (couldn’t resist 😆)
    And one more complain from unsatisfied childish dev about “Anonymous Interface implementation” feature, where there was massive request for that feature for long time. Dev team kind of unpleasantly put people off with explanation “this is never going to be implemented” without any serious reasoning. One explanation from language dev was “It is absolutely feasible but it is not our priority”. And here we are, new features that many devs have no idea why would they ever use it.

    • Kathleen DollardMicrosoft employee 0

      We are not yet done with C# 13. We thought this preview was a good time to update you on what you can experiment with in this preview.

      We’re still working on features for C# 13, and you can check out what we’re doing at the Roslyn Feature Status page.

    • Huo Yaoyuan 0

      The main reason for params span is to improve existing code without any change from user, because

      params Span

      will win over

      params T[]

      in overload resolution.
      The language team discussed around

      Method( [1, 2, 3] )

      , and decides it will benefit much more if no action required for callers.

  • Karen Payne 1

    Going to give them a spin.

  • Anthony Steiner 3

    Another C# update, another disappointment. What happened after .NET 7? Literally implementing things that nobody asked for, and refusing to do what people actually want. 2 years without a decent update to the language. That’s crazy.

  • JinShil 3

    Extension Types will be great and Partial Properties will be useful for source generators and other tooling features. The rest seem to be addressing arbitrary limitations in the language; that’s also quite welcome; please do more of that.

    C# 12 was a disappointment; please do less of that.

  • David Taylor 1

    Thanks for the update @Kathleen

    These are all nice refinements and remove past annoying limitations.

    We are all looking forward to Extension Everything, but none of us are surprised it will take another year as we all need this one done right. Hopefully it makes it into .NET 10 given that is the long term support release.

  • Renee GA 9

    Extensions failed to make it into C# 13, a huge disappointment.

    Also, the ‘field’ keyword – I’d been hearing about it for years, but it’s still not there. It’s awkward watching Mads present the ‘new’ field keyword every year at .NET Conf.

  • Steve 4

    Looking forward to the extensions feature in .NET 10 (or even .NET 11).
    And I’m glad to see that the C# team decides to give it more time to bake the feature, instead of rushing out a half-baked one.

    However, what about the `field` keyword enabling semi-auto properties? I don’t think such feature need to wait yet another year to be done.

    • Kathleen DollardMicrosoft employee 2

      We still working on field access in auto-properties. Since there is a breaking change involved, we are proceeding cautiously, but we are proceeding.

      • Rand Random 4

        So, the proposal was posted Feb 2017 and 7+ years later it is still not implemented because of a name conflict? And the poor developer running into the breaking change would need to rename its field named variable or add a @? sorry, but who actually cares?

  • Kenneth Hoff 4

    Extensions were clearly not finished – they fundamentally changed how they were to be implemented like a couple a weeks ago – so it’s a good thing they delayed it. Hopefully they’ll come up with a more cohesive implementation strategy (referring to how they intend to implement interfaces are fundamentally different to how they intend to implement everything else).

    That said, compared to even C# 12 this release seems to have even fewer relevant features. Partial properties seems to be the only feature I’ll ever write myself here, and that’s just for Regex Source Generator, which is a niche scenario in the first place.

    What is happening here? C# is dying at this rate. You cannot let backwards compatibility keep hindering the language at this point. Just rip off the bandaid and embrace breaking changes. This cannot continue like this.

  • Ben 1

    Most of these seem to be a net positive. But like others, I’m concentrating on what seems like a negative.

    I’m not convinced about the “Index from the end in initializers”
    – Is the sample calling the setter of the property? The “=” implies that it is to me, but if it is how does it know what the “^1” index is?
    – Will this work with a getter-only property?
    The syntax seems to conflict with how the code would act if the indexer wasn’t used. So, it makes the code confusing to me.

      • Ken Hornibrook 1

        I have to admit I’m also super confused.
        My mental model of the class initialiser syntax has always been “it’s as if you call the constructor and then call each of the property setters manually after that”.

        In the example code, I see “Value = (stuff)”. So I think this should be setting the Value property to a new array instance. But if I understand the explanation, it seems like it is not actually assigning to the Value property, it is really getting the Value property, and then assigning to one of the array elements, as if you had done “x.Values[^1] = 999”.

        I feel a lot of cognitive dissonance when the first line of the code has “Values = ” but this does not result in an assignment to the values property.

          • Ben 0

            Kathleen,
            Your provided link does not document the use of the indexer in an initializer.

            While I can confirm your statement with a code experiment, I have seen no documentation stating that this is valid syntax.

            Could you provide a link that documents a simple indexer used in initialization like that?

          • Kathleen DollardMicrosoft employee 0

            Other links in this thread cover indexing from the end. This link covers indexers in initialization Object and Collection Initializers (C# Programming Guide)

            Here is some code from that link:

            var thing = new IndexersExample
            {
                name = "object one",
                [1] = '1',
                [2] = '4',
                [3] = '9',
                Size = Math.PI,
                ['C',4] = "Middle C"
            }

            where IndexerExample is:

            public string name;
            public double Size { set { ... }; }
            public char this[int i] { set { ... }; }
            public string this[char c, int i] { set { ... }; }

            One nuance is missing in the example: the array needs to be initialized such that the positions being assigned are valid.

            This is not a particularly common construct, and you can write great C# code and not use it. However, for code that initializes classes with indexers, it can be extremely convenient, so we added it in C# 8. In C# 13 we filled the gap so that you index from the end.

            (Apparently comments here do not allow deeper nesting, so I could not comment at the end of the thread.)

          • Ben 0

            Are we looking at the same web page?
            When I follow the link, it gives me a page last updated on 11/14/2023.
            It contains no text for: object one
            Maybe Microsoft has an unpublished version of the page cached internally?

          • Kathleen DollardMicrosoft employee 0

            I copied the wrong link. I have updated the earlier comment. On that page, if your scroll down to just above “Object Initializers with anonymous types” you’ll find the code I quoted.

            We have not yet documented index from end in object initializers, but that link now takes you to a discussion of indexers in object initializers in general.

          • Ben 0

            OK, the updated link is better. Thanks.

            The page has two close but not quite examples.
            The one you mention is creating a new object and accessing the indexer of it. This is different than accessing an indexer of a property of the new object.

            The other example is:

            CatOwner owner = new CatOwner
            {
                Cats =
                {
                    new Cat{ Name = "Sylvester", Age=8 },
                    new Cat{ Name = "Whiskers", Age=2 },
                    new Cat{ Name = "Sasha", Age=14 }
                }
            };

            Here, it is mentioned in the web page that the property ‘Cats’ is having ‘Add’ called on it. This is different than accessing by index.

            Though, this is close enough for an answer to my initial post. Thanks.

  • Mark Adamson 9

    Two leftovers from .net 8 features that seem to have gone quiet:

    1. readonly parameters in primary constructors
    2. natural collection types in collection expressions

    Should we hold out hope for either of these?

    • Kenneth Hoff 3

      Natural collection types – especially in the context of inline expressions – is my most wanted feature currently (behind Unions at a solid top spot and extensions – especially interfaces – as number two).

      The fact that the following isn’t allowed – because they don’t know the type – is silly. Just use ReadOnlySpan at all times.


      bool success = ...;
      List<int> a = [
      1,
      2,
      3,
      .. success ? [4] : []
      ];

      • Mark Adamson 0

        that’s a good one, I was focused on using it with var, but there are other times that it is annoying like inside a SelectMany expression where you are returning a collection that you then later do .ToList or .ToImmutableArray and I want to minimise noise in the lambda

Feedback usabilla icon