Newer C♯ Features you’ll actually use

Blogpost for anic Homepage | Daniel Fahsig | Microsoft 365 Certified: Teams Application Developer Associate/MCP

Most new language features are not for everyday uses but cover very specific problems and situations. But some of the new features found in version 8 through 10 can be very helpful in our everyday work. In this post we’d like to share some of the new features that we found helpful in our work.

Use of the switch Expression (8.0)

When a switch statement is used to assign a value to a single variable based on the different cases or when it is used to just return a value, then the new switch-expression can be used.

So instead of writing code like this:

public static RGBColor FromRainbowClassic(Rainbow colorBand)
{
    switch (colorBand)
    {
        case Rainbow.Red:
            return new RGBColor(0xFF, 0x00, 0x00);
        case Rainbow.Orange:
            return new RGBColor(0xFF, 0x7F, 0x00);
        case Rainbow.Yellow:
            return new RGBColor(0xFF, 0xFF, 0x00);
        case Rainbow.Green:
            return new RGBColor(0x00, 0xFF, 0x00);
        case Rainbow.Blue:
            return new RGBColor(0x00, 0x00, 0xFF);
        case Rainbow.Indigo:
            return new RGBColor(0x4B, 0x00, 0x82);
        case Rainbow.Violet:
            return new RGBColor(0x94, 0x00, 0xD3);
        default:
            throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand));
    };
}

You can now write it that way:

public static RGBColor FromRainbow(Rainbow colorBand)
    => colorBand switch
    {
        Rainbow.Red     => new RGBColor(0xFF, 0x00, 0x00),
        Rainbow.Orange  => new RGBColor(0xFF, 0x7F, 0x00),
        Rainbow.Yellow  => new RGBColor(0xFF, 0xFF, 0x00),
        Rainbow.Green   => new RGBColor(0x00, 0xFF, 0x00),
        Rainbow.Blue    => new RGBColor(0x00, 0x00, 0xFF),
        Rainbow.Indigo  => new RGBColor(0x4B, 0x00, 0x82),
        Rainbow.Violet  => new RGBColor(0x94, 0x00, 0xD3),
        _               => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)),
    };

Some changes can be seen when compared with the switch-statement:

  • The variable that is used for switching comes before the switch keyword
  • case is replaced by =>
  • default is replaced by _ (discard)
  • Only expressions can be used in the cases

Enhancements to pattern matching

In C♯ 7 pattern matching was introduced. Since then we could do things like the following:

if (arg1 is ComplexObject complex)
{
    internal = complex.SophisticatedAction();
}

instead of:

var complex = arg1 as ComplexObject;
if (complex != null) {
    internal = complex.SophisticatedAction();
}

Property patterns (8.0)

Property pattern allow you to access properties of an object that you want to base a decision on, e.g. while using a switch statement.

Instead of doing this:

var state =  location.State;
state switch
{
    "WA" => salePrice * 0.08M;
    "MN" => salePrice * 0.06M;
    "MI" => salePrice * 0.09M;
    _ => 0M;
}

With property patterns now this is possible:

location switch
{
    { State: "WA" } => salePrice * 0.08M;
    { State: "MN" } => salePrice * 0.06M;
    { State: "WI" } => salePrice * 0.09M;
}

Even something like this is possible with property patterns:

public static bool IsReleaseDate(DateTime date)  => date is { Year: 2022, Month: 10, Day: 1 };

Extended with C♯ 10

With C♯ 10 the usage was improved further. Until then you had to write the following to include nested properties in your pattern:

if (e is MethodCallExpression { Method: { Name: "MethodName" } })

Now this is a bit shorter:

if (e is MethodCallExpression { Method.Name: "MethodName" })

Tuple patterns (8.0)

Sometimes you need to check the state of two variables at once for a decision. Maybe you would have used a nested if-statement to cover all cases. But with tuple patterns this is not needed anymore:

public static bool CanEditDocument(DocumentState state, UserRole role)
    => (state, role) switch
    {
        (DocumentState.Approval, UserRole.Approver) => true,
        (DocumentState.Open, UserRole.Editor) => true,
        (DocumentState.Open, UserRole.Manager) => true,
        (DocumentState.Closed, UserRole.Reviewer) => true,
        (_,_) => false,
    }

The roles that may edit the document can clearly be seen here and the discard pattern covers all other cases where editing is not allowed.

Type patterns (9.0)

Type-checking can now also be done within a switch-expression using type patterns:

public abstract class Vehicle {}
public class Car : Vehicle {}
public class Truck : Vehicle {}

public static class TollCalculator
{
    public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch
    {
        Car _ => 2.00m,
        Truck _ => 7.50m,
        null => throw new ArgumentNullException(nameof(vehicle)),
        _ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
    };
}

Logical patterns and relational patterns (9.0)

Instead of long if-statements for each test you can now do such checks within a switch-expression as follows:

Console.WriteLine(GetCalendarSeason(new DateTime(2021, 3, 14)));  // output: spring
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 7, 19)));  // output: summer
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 2, 17)));  // output: winter

static string GetCalendarSeason(DateTime date) => date.Month switch
{
    >= 3 and < 6 => "spring",
    >= 6 and < 9 => "summer",
    >= 9 and < 12 => "autumn",
    12 or (>= 1 and < 3) => "winter",
    _ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};

Here you can see the usage of relational patterns to check whether the month is in a specified ragne and the logical and and or is used to link the cases.

With the use of the logical not it is possible to write a more readable null-check like this:

if (input is not null)
{
    // ...
}

Using declarations (8.0)

Often you start the body of a method or of an if-branch with an using-statement, thus adding another level of braces, like this:

static int WriteLinesToFile(IEnumerable<string> lines)
{
    using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
    {
        int skippedLines = 0;
        foreach (string line in lines)
        {
            if (!line.Contains("Second"))
            {
                file.WriteLine(line);
            }
            else
            {
                skippedLines++;
            }
        }
        return skippedLines;
    } // file is disposed here
}

This can now be written as a using declaration:

static int WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines2.txt");
    int skippedLines = 0;
    foreach (string line in lines)
    {
        if (!line.Contains("Second"))
        {
            file.WriteLine(line);
        }
        else
        {
            skippedLines++;
        }
    }
    return skippedLines;
    // file is disposed here
}

This change reduces the amount of braces, the needed indentation and the number of lines within a method and file.

Indices and ranges (8.0)

To access specific elements within a sequence you can now use an improved syntax. Instead of writing

var wordBeforeLast = words[words.Length - 2];

you can now write:

var wordBeforeLast = words[^2];

The index from end operator acts the same as above, so ^0 would throw an exception (just as word[words.Length] would have).

And if you want to cut a specific subrange from a sequence you can now use the range operator:

var relevantPart = phoneNumber[4..9];

This would include the following elements: phoneNumber[4] to phonenumber[8] – so the beginning is included and the end is excluded. Both numbers are optional and if they are missing beginning or end of the sequence is assumed as the value. The index from end operator can also be used in ranges.

Null-coalescing assignment (8.0)

In methods where you have a parameter that defaults to null you may often have used the following construct to ensure a valid value for the parameter:

void Method(ComplexObject complex = null)
{
    if (complex is null)
    {
        complex = new ComplexObject(...);
    }

    ...
}

This can now be written a lot shorter:

void Method(ComplexObject complex = null)
{
    complex ??= new ComplexObject(...);

    ...
}

init-only setters (9.0)

If you wanted to enable the setting of properties of an object using the property initializers you had to make the setter for each property public. This also enabled the setting of the value later on, which might have been undesirable in many places.

With the new init only setters this can be prevented. A property that is specified as such can only be initialized with a property initializer and not be set afterwards.

public class ComplexObject
{
    public int ChildrenCount { get; init; }

    public ComplexObject()
    {
        ...
    }
}

var complex = new ComplexObject { ChildrenCount = 20 };
complex.ChildrenCount = 25; // Error! CS8852 - this is not allowed

top level statements (9.0)

Typically a program has one main file, e.g. the Program.cs in ASP.Net Core applications. And this file used to have a lot of boilerplate code. Now one file in the project may omit all the boilerplate and have just the code that needs to be executed (plus the using statements).

So this:

namespace Company.Application
{
    using System;
    ...

    public static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
        ...
    }
}

Would become this:

using System;
...

Console.WriteLine("Hello World!");
...

Omit the type when using new (9.0)

It is now possible to omit the type in a new expression when the created object’s type is already known:

private List<Chemical> chemicals = new();

This is also possible for arguments when calling a method:

public TimeSpan GetReactionTime(Chemical secondChemical, ReactionOptions options) {...}

var duration = chemical.GetReactionTime(catalysator, new());

And you can combine this with init only properties as follows:

ReactionOptions options = new() { Technician = "Heinz Müller" };

File-scoped namespace declaration (10)

To further reduce the indentations within source files you can now use the file-scoped namespace declaration. This reduces code as:

namespace Company.Application
{
    using System;

    using Company.Business;

    public class SpecialHandler
    {
        ...
    }
}

to this:

namespace Company.Application;
using System;

using Company.Business;

public class SpecialHandler
{
    ...
}

By using this you save space in both dimensions: indentation and count of lines.

Assignment and declaration in same deconstruction (10)

Since C♯ 7 you can deconstruct a user-defined object using a Deconstruct method. You could deconstruct and assign all values to new variables or to existing values. Now you can both things in one statement:

public class Point
{
    ...
    public void Deconstruct(out int x, out int y) {...}
}

var point = new Point(...);

int x = 0;
(x, int y) = point;

Related articles

Let’s Work together!

Let us help you envision the future of your company.

Please call me back.