ICYMI C# 9 New Features: Adding foreach Support To Any Type

This is part of a series of articles on new features introduced in C# 9.

Prior to C# 9 you could add foreach support to a class by modifying the class and implementing the IEnumerable interface. With C# 9 you can add foreach support to an existing class without actually needing to modify that class.

As an example, suppose the following class exists in our codebase:

public class Choices
{
    public string FirstChoice { get; set; }
    public string SecondChoice { get; set; }
    public string ThirdChoice { get; set; }
    public string FourthChoice { get; set; }
    public string FifthChoice { get; set; }
}

Because the class is in our control we could add the ability to foreach through all the choices (FirstChoice, SecondChoice, etc.).

One way to do this would be to modify the Choices class and implement IEnumerable – we could also just create a method called GetEnumerator and then have it return a class which has a MoveNext and Current method:

public class ChoicesEnumerator
{
    public string[] _choices = new string[5];

    int position = -1;

    public ChoicesEnumerator(Choices choices)
    {
        // Additional null/validation/etc checking code omitted

        _choices[0] = choices.FirstChoice;
        _choices[1] = choices.SecondChoice;
        _choices[2] = choices.ThirdChoice;
        _choices[3] = choices.FourthChoice;
        _choices[4] = choices.FifthChoice;
    }

    public bool MoveNext()
    {
        position++;
        return position < _choices.Length;
    }

    public string Current
    {
        get
        {
            try
            {
                return _choices[position];
            }
            catch (IndexOutOfRangeException)
            {
                throw new InvalidOperationException();
            }
        }
    }
}

We could then modify the Choices class and add the GetEnumerator method:

public class Choices
{
    public string FirstChoice { get; set; }
    public string SecondChoice { get; set; }
    public string ThirdChoice { get; set; }
    public string FourthChoice { get; set; }
    public string FifthChoice { get; set; }
    
    public ChoicesEnumerator GetEnumerator()
    {
        return new ChoicesEnumerator(this);
    }
}

Now we can iterate through the choices in a foreach loop:

var choices = new Choices
{
    FirstChoice = "Orange",
    SecondChoice = "Blue",
    ThirdChoice = "Green",
    FourthChoice = "Cyan",
    FifthChoice = "Grey"
};

foreach (var choice in choices)
{
    WriteLine(choice);
}

But what if we were not able to modify the Choices class – for example if it is from an external library? From C# 9 you can follow a similar approach to the above but add the foreach support using an extension method. This way we don’t have to modify the original class.

If the original class that we couldn’t modify looked like the following:

public sealed class Choices
{
    public string FirstChoice { get; set; }
    public string SecondChoice { get; set; }
    public string ThirdChoice { get; set; }
    public string FourthChoice { get; set; }
    public string FifthChoice { get; set; }
}

We could once again create the ChoicesEnumerator class as above and then just create an extension method that adds the GetEnumerator method to the Choices class, without needing to modify the Choices class itself:

public static class ChoicesExtensions
{
    public static ChoicesEnumerator GetEnumerator(this Choices choices)
    {
        return new ChoicesEnumerator(choices);
    }
}

Now we can once again foreach through all the choices.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

ICYMI C# 9 New Features: Reducing Code with Target-typed New Expressions

This is part of a series of articles on new features introduced in C# 9.

C# 9 introduced some enhancements to reduce the amount of code you need when creating new instances of objects.These target-typed new expressions “Do not require type specification for constructors when the type is known.” [MS]

As an example in C# 8 with fields you need to explicitly create an instance:

class Preferences
{
    private List<string> _favoriteColors = new List<string>();
}

From C# 9 you can instead write:

class Preferences
{
    private List<string> _favoriteColors = new();
}

Notice in the preceding code you can simply write new() because the target type is known to be List<string>.

If you are calling a method (or constructor) that requires an instance of an object, you can also use new(). For example suppose you had the following method that requires a DisplayOptions instance:

public void DisplayColors(DisplayOptions options)
{
    Console.WriteLine(options.Title);
    foreach (var color in _favoriteColors)
    {
        Console.WriteLine(color);
    }
}

Prior to C# 9, if you wanted to just create a new instance of DisplayOptions and pass it in you would write:

var prefs = new Preferences();            
prefs.DisplayColors(new DisplayOptions());

With C# 9 you can simplify this to:

var prefs = new Preferences();
prefs.DisplayColors(new());

You could also write this using a target-typed new expression for the Preferences instance:

Preferences prefs = new();
prefs.DisplayColors(new());

If you have init only properties you can also use target-typed new expressions:

class DisplayOptions
{
    public string Title { get; init; }
}
DisplayOptions options = new() { Title = "Colors" };

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

ICYMI C# 9 New Features: More Pattern Matching Features

This is part of a series of articles on new features introduced in C# 9.

Pattern matching was introduced in earlier versions of C# and C# 9 continued to evolve this feature. Pattern matching in a more general sense allows you to write an expression and evaluate it to see whether or not it meets some specified characteristic or “pattern”. In some situations pattern matching can reduce the amount of code you need to write and improve readability.

Pattern matching is supported in is expressions, switch statements, and switch expressions.

C# 9 Type Pattern

The type pattern allows your code to perform a runtime check that an expression of a specified type. In C# 9, when using the type pattern you no longer need to assign to a variable or use a discard.

Prior to C# 9:

object o = "hello";
string message = o switch
{
    string _ => "it's a string",
    int _ => "it's an int",
    _ => "it's something else"
};

WriteLine(message);

ReadLine();

Notice in the preceding code the required discards (or variable assignments) _.

From C# 9 this can be simplified to:

object o = "hello";

string message = o switch
{
    string => "it's a string",
    int => "it's an int",
    _ => "it's something else"
};

WriteLine(message);

ReadLine();

Notice that now no discard is required.

Logical, Parenthesized, and Relational Patterns in C# 9

With C# 9 you can now create logical patterns using not, and, or.

Prior to C# 9 you had to write:

if (o != null)
{
    WriteLine(o);
}

From C# 9 you can write:

if (o is not null)
{
    WriteLine(o);
}

As another example, prior to C# 9 you could not write:

if (o is not string)
{
    WriteLine("not a string");
}

Examples of or, and:

string minuteHandDescription = minuteHandValue switch
{
    >=0 and <= 15 => "first quarter",
    >15 and <= 30 => "second quarter",
    >30 and <= 45 => "third quarter",
    (>45 and <=58) or 59 or 60 => "fourth quarter",
    _ => "unknown"
};

Also notice in the preceding code the use of the parenthesized pattern (>45 and <=58) or 59 or 60 to enforce precedence.

The code also makes use of the new C# 9 relational patterns  that include: <, >, <=, and >=.When using these “The right-hand part of a relational pattern must be a constant expression. The constant expression can be of an integer, floating-point, char, or enum type.” [Microsoft]

For more information on patterns check out the C# language reference.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

ICYMI C# 9 New Features: Top-level Statements

This is the first in a series of articles on new features introduced in C#9.

Top-level statements allow you to simplify and remove some of the “ceremony” in your code.

For example, take the following console application written in C#8:

using System.Linq;
using static System.Console;

namespace ConsoleAppCS8
{
    class Program
    {
        static int Main(string[] args)
        {
            string greeting = "";

            if (args.Any())
            {
                greeting = args[0];
            }

            WriteLine("Please enter your name");
            var name = ReadLine();

            var upperName = ConvertToUpper(name);

            WriteLine($"{greeting} {upperName}");

            return 42;
        }

        public static object ConvertToUpper(string name)
        {
            return name.ToUpperInvariant();
        }
    }
}

In the preceding code the “ceremony” consists of things such as the enclosing namespace, the Program class outline, and the Main method itself.

With top-level statements in C# 9 this code can be simplified to the following:

using System.Linq;
using static System.Console;


string greeting = "";

if (args.Any())
{
    greeting = args[0];
}

WriteLine("Please enter your name");
var name = ReadLine();

var upperName = ConvertToUpper(name);

WriteLine($"{greeting} {upperName}");

return 42;


static object ConvertToUpper(string name)
{
return name.ToUpperInvariant();
}

Notice in the C# 9 version that the structure is a lot “flatter” because there are no nested {} from the namespace, class, and Main method.

The application will still act in the same way, there is just less boilerplate code.

Notice that in the top-level version you can still have methods e.g. ConvertToUpper(), you can still return a value return 42; and you can still access any command line arguments greeting = args[0];

Some Considerations

Whilst top-level statement make the application seem more “script like” this may or may not be a good thing from someone learning C# for the very first time, depending on if they have any other prior programming experience.

Behind the scenes, a synthesized entry point into the application is created for you. If you return a value, the synthesized entry point (a bit like the Main method in the C# 8 version) will have a return value and if you use any async code the synthesized entry point will return Task or Task<int> – essentially using top-level statements doesn’t restrict you from the kind of code you can write.

Notice in the C# 8 version the method public static object ConvertToUpper(string name) could be accessed by another class in the console app:

class Class1
{
    public void XYZ()
    {
        Program.ConvertToUpper("ddd");
    }
}

In the C# 9 top-level version the method becomes:

static object ConvertToUpper(string name)
{
  return name.ToUpperInvariant();
}

If we try to write the same Class1 in the C# 9 project we’ll get an error “Error    CS0103    The name 'Program' does not exist in the current context “. We could of course refactor the ConvertToUpper() method into an explicit static class to make it accessible

You can only have one top-level file in a project, and you can’t also have an explicit Main method entry point at the same time without getting a compiler warning.

A file with top level statements can also include namespaces/classes but they must be after the top-level statements.

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

Free .NET Testing Courses This Month

This month all my Pluralsight courses are available for free including a lot of .NET testing content that can help you either get started with .NET testing or level-up your tests to make them easier to read and more maintainable.

Suggested Courses

Step 1: Learn a base testing framework:

Step 2: Level-up the base testing framework:

Step 3: Power-ups to complete your test strategy

Bonus Step: Help convince your co-workers and managers to let you write tests with the Testing Automation: The Big Picture course.

SHARE:

ICYMI C# 8 New Features: Asynchronous Streams

This is part 8 in a series of articles.

In earlier versions of C# you could return an IEnumerable<T> from a method, for example to be consumed by a foreach loop.

The following example shows a method from a WPF app that returns 3 web pages as string content:

public static IEnumerable<string> LoadWebPages()
{
    using (var client = new HttpClient())
    {
        yield return client.GetStringAsync("http://dontcodetired.com/blog/post/ICYMI-C-8-New-Features-Switch-Expressions").Result;
        yield return client.GetStringAsync("http://dontcodetired.com/blog/post/ICYMI-C-8-New-Features-Write-Less-Code-with-Using-Declarations").Result;
        yield return client.GetStringAsync("http://dontcodetired.com/blog/post/ICYMI-C-8-New-Features-Write-Less-Code-with-Using-Declarations").Result;
    }
}

This method could be used in a click event handler in the WPF app (or via MVVM etc.):

private void NotAsync_Click(object sender, RoutedEventArgs e)
{
    foreach (var page in WebPageLoader.LoadWebPages())
    {
        var titleIndex = page.IndexOf("<title>");
        txt.Text += Environment.NewLine + $"Got page: {page.Substring(titleIndex, 110)}";
    }
}

When you run the app and click the button, the 3 web pages will be loaded and added to the content of the <TextBlock x:Name="txt"></TextBlock>

While loading the 3 web pages and looping through the foreach loop however, the app will be unresponsive until all 3 pages have been returned in the foreach loop.

C# 8 introduced the ability to use the IAsyncEnumerable<T> interface to iterate items asynchronously.

The “asynchronous streams” feature of C# 8 should not be confused with the streams in the System.IO namespace.

The LoadWebPages method can be re-written in C# 8 as:

public static async IAsyncEnumerable<string> LoadWebPagesAsync()
{
    using var client = new HttpClient();

    yield return await client.GetStringAsync("http://dontcodetired.com/blog/post/ICYMI-C-8-New-Features-Switch-Expressions");
    yield return await client.GetStringAsync("http://dontcodetired.com/blog/post/ICYMI-C-8-New-Features-Write-Less-Code-with-Using-Declarations");
    yield return await client.GetStringAsync("http://dontcodetired.com/blog/post/ICYMI-C-8-New-Features-Simplify-If-Statements-with-Property-Pattern-Matching");
}

And to consume this version:

private async void Async_Click(object sender, RoutedEventArgs e)
{
    await foreach (var page in WebPageLoader.LoadWebPagesAsync())
    {
        var titleIndex = page.IndexOf("<title>");
        txt.Text += Environment.NewLine + $"Got page: {page.Substring(titleIndex, 110)}";
    }
}

Now when the <Button Content="Async" x:Name="Async" Click="Async_Click"></Button> button is clicked, the 3 webpages will be enumerated in an async manner meaning that the UI will remain responsive.

The IAsyncEnumerable<T> interface also provides for cancellation: IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

ICYMI C# 8 New Features: Upgrade Interfaces Without Breaking Existing Code

This is part 7 in a series of articles.

Prior to C# 8, if you add members to an interface, exiting code breaks if you do not implement the new members in any class that implements the interface.

As an example, consider the following interface definition:

public interface ICustomer
{
    int Id { get; }
    string Name { get; set; }
    int MonthsAsACustomer { get; set; } 
    decimal TotalValueOfAllOrders { get; set; }
    // etc.
}

We could implement this interface:

class Customer : ICustomer
{
    public int Id { get; }
    public string Name { get; set; }
    public int MonthsAsACustomer { get; set; }
    public decimal TotalValueOfAllOrders { get; set; }
    public Customer(int id) => Id = id;
}

This will compile without error. We could have multiple classes implementing this interface in the same project or across multiple projects.

What happens now if we wanted to add a new interface method that represents the ability to calculate a discount based on the customer’s previous order value and how long they have been a customer?

We could make the following change:

public interface ICustomer
{
    int Id { get; }
    string Name { get; set; }
    int MonthsAsACustomer { get; set; } 
    decimal TotalValueOfAllOrders { get; set; }
    decimal CalculateLoyaltyDiscount();
    // etc.
}

Now if we try and build, we’ll get the error: 'Customer' does not implement interface member 'ICustomer.CalculateLoyaltyDiscount()'

If we have multiple implementations of ICustomer, they will all break.

Default Interface Methods in C# 8

From C# 8 we can fix this problem by providing a default implementation of an interface method.

If we only had one implementation of ICustomer then we could go and add the implementation of CalculateLoyaltyDiscount. But if we had multiple implementations or we didn’t want to force a breaking change on existing implementers then we can actually add the implementation of the the method in the interface itself.

public interface ICustomer
{
    int Id { get; }
    string Name { get; set; }
    int MonthsAsACustomer { get; set; } 
    decimal TotalValueOfAllOrders { get; set; }
    public decimal CalculateLoyaltyDiscount()
    {
        if (MonthsAsACustomer > 24 || TotalValueOfAllOrders > 10_000)
        {
            return 0.05M;
        }

        return 0;
    }
    // etc.
}

If we build now there will be no error, even though we haven’t implemented the method in Customer. Customer ‘inherits’ the default implementation.

Notice in the preceding code that from C# 8, access modifiers are now allowed on interface members.

We could make use of this new method:

var c = new Customer(42)
{
    MonthsAsACustomer = 100
};

decimal discount = ((ICustomer)c).CalculateLoyaltyDiscount();

Notice in the preceding code that we have to cast Customer to ICustomer to be able to call CalculateLoyaltyDiscount – that’s because the method implementation is in the interface, not the Customer class.

The Customer class can still implement it’s own version of the CalculateLoyaltyDiscount method if the default implementation is not acceptable:

class Customer : ICustomer
{
    public int Id { get; }
    public string Name { get; set; }
    public int MonthsAsACustomer { get; set; }
    public decimal TotalValueOfAllOrders { get; set; }
    public Customer(int id) => Id = id;
    public decimal CalculateLoyaltyDiscount()
    {
        if (TotalValueOfAllOrders > 1_000_000)
        {
            return 0.1M;
        }

        return 0;
    }
}

We could refactor the upgraded interface to allow implementers to still be able to access the default implementation:

public interface ICustomer
{
    public int Id { get; }
    string Name { get; set; }
    int MonthsAsACustomer { get; set; } 
    decimal TotalValueOfAllOrders { get; set; }
    public decimal CalculateLoyaltyDiscount() => CalculateDefaultLoyaltyDiscount(this);
    
    protected static decimal CalculateDefaultLoyaltyDiscount(ICustomer customer)
    {
        if (customer.MonthsAsACustomer > 24 || customer.TotalValueOfAllOrders > 10_000)
        {
            return 0.1M;
        }

        return 0;
    }
    // etc.
}

And then in Customer:

class Customer : ICustomer
{
    public int Id { get; }
    public string Name { get; set; }
    public int MonthsAsACustomer { get; set; }
    public decimal TotalValueOfAllOrders { get; set; }
    public Customer(int id) => Id = id;
    public decimal CalculateLoyaltyDiscount()
    {
        if (TotalValueOfAllOrders > 1_000_000)
        {
            return 0.2M;
        }

        return ICustomer.CalculateDefaultLoyaltyDiscount(this);
    }
}

From C# 8, interfaces can also now have static fields, methods, and properties, e.g.:

private static int MonthsAsACustomerThreshold = 24;
public static int MonthsThreshold
{
    get => MonthsAsACustomerThreshold;
    set => MonthsAsACustomerThreshold = value;
}

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

ICYMI C# 8 New Features: Prevent Bugs with Static Local Functions

This is part 6 in a series of articles.

C# 7 introduced local functions that allow you to create functions “local” to another function or method. These local functions are implicitly private to the enclosing function and cannot be accessed by other methods/properties/etc.  in the class. You can read more about local functions in this previous article.

C# 8 introduced the ability to make local functions static. This can help prevent bugs when using local functions.

Take the following code:

[Fact]
public void NestedCs7()
{
    int a = 10;

    int result1 = Add(10, 5);

    Assert.Equal(15, result1); // True, pass

    int result2 = Add(15, 2);

    Assert.Equal(17, result2); // False, fail

    int Add(int num1, int num2)
    {
        return a + num2;
    }
}

In the preceding code, the Add function is a local function. However there is a bug in this Add method, we are accidently referencing the variable a from the enclosing method NestedCs7. This means that instead of adding num1 and num2 and returning the result, we are adding a and num2. This will cause errors in the application.

Sometimes it may be convenient to reference (“capture”) the enclosing methods variables but to be more explicit and prevent unintended side-effects and bugs, from C# 8 you can make the method static.

In the following code we make the Add method static:

[Fact]
public void NestedCs8()
{
    int a = 10;

    int result1 = Add(10, 5);

    Assert.Equal(15, result1); 

    int result2 = Add(15, 2);

    Assert.Equal(17, result2); 

    static int Add(int num1, int num2)
    {
        return a + num2;
    }
}

If we try and build the preceding code, we’ll get a compilation error: Error CS8421    A static local function cannot contain a reference to 'a'.

This error tells us we’ve accidentally captured a and we can go and fix the bug:

[Fact]
public void NestedCs8()
{
    int a = 10;

    int result1 = Add(10, 5);

    Assert.Equal(15, result1); // True, pass

    int result2 = Add(15, 2);

    Assert.Equal(17, result2); // True, pass

    static int Add(int num1, int num2)
    {
        return num1 + num2;
    }
}

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

ICYMI C# 8 New Features: Simplify Array Access and Range Code

This is part 5 in a series of articles.

One of the new features that C# 8 introduced was the ability to work more simply with arrays and items within arrays.

Take the following code that uses various ways to manipulate an array of strings:

string[] letters = { "A", "B", "C", "D", "E", "F" };

string firstLetter = letters[0];
string secondLetter = letters[1];
string lastLetter = letters[letters.Length - 1];
string penultimateLetter = letters[letters.Length - 2];

string[] middleTwoLetters = letters.Skip(2)
                                   .Take(2)
                                   .ToArray();

string[] everythingExceptFirstAndLast = letters.Skip(1)
                                               .Take(letters.Length - 2)
                                               .ToArray();

It’s pretty easy to get the first element in an array [0] and the last item [letters.Length - 1] but when we get to getting ranges (like the middle 2 letters) or 2 from the end things get a little more complicated.

C# 8 introduced a new shorter syntax for dealing with indices and ranges. The preceding code could be re-rewritten in C# 8 as follows:

string[] letters = { "A", "B", "C", "D", "E", "F" };

string firstLetter = letters[0];
string secondLetter = letters[1];
string lastLetter = letters[^1];
string penultimateLetter = letters[^2];

string[] middleTwoLetters = letters[2..4];

string[] everythingExceptFirstAndLast = letters[1..^1];

There’s a few different things going on in the C#8 version.

First of all, C# 8 gives us the new index from end operator ^. This essentially gives us an element by starting at the end and counting back. One thing to note here is that the index ^0 is not the last element, rather the length of the array (remember in C# arrays are zero-based). The last element is actually at ^1. If you try and access an element using [^0] you’ll get an IndexOutOfRangeException just as you would if you wrote [letters.length].

In additional to the index from end operator, C# 8 also introduced the range operator .. – this allows you to specify a range of elements. For example in the code, the range [2..4] gives us the middle 2 letters C & D – or more specifically, it gives us the range of elements starting at 2 and ending at 4. Why 4 though? When using the range operator, the first index is inclusive but the last index is exclusive. So [2..4] really means “2 to 3 inclusive”.

You can also use open ended ranges where you omit the start or end range, for example string[] lastThreeLetters = letters[^3..];

To make the new indexing feature work, there is a new struct called Index. And to make ranges work there is a new struct called Range.

For example, you can create and pass Range instances around:

Range middleTwo = new Range(2, 4);
string[] middleTwoLetters = letters[middleTwo];

You can also use the index from end operator in a range, for example to get the last 3 letters:

string[] lastThreeLetters = letters[^3..^0]; // D E F

Notice in this code we are using ^0 (and get no exception) to specify the end because the end of a range is exclusive.

As well as arrays you can also use these techniques with other types which you can read more about  in the Microsoft documentation e.g. the MS docs state: “For example, the following .NET types support both indices and ranges: String, Span<T>, and ReadOnlySpan<T>. The List<T> supports indices but doesn't support ranges.” And also from the docs: “Any type that provides an indexer with an Index or Range parameter explicitly supports indices or ranges respectively. An indexer that takes a single Range parameter may return a different sequence type, such as System.Span<T>.”…and… “A type is countable if it has a property named Length or Count with an accessible getter and a return type of int. A countable type that doesn't explicitly support indices or ranges may provide an implicit support for them. For more information, see the Implicit Index support and Implicit Range support sections of the feature proposal note. Ranges using implicit range support return the same sequence type as the source sequence.”

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE:

ICYMI C# 8 New Features: Nested Switch Expressions

This is part 4 in a series of articles.

In this series we’ve already covered switch expressions and one little-known feature is the ability to nest switch expressions.

Suppose we have the following 3 classes:

class SavingsAccount
{
    public decimal Balance { get; set; }
}

class HomeLoanAccount
{
    public int MonthsRemaining { get; set; }
}

class ChequingAccount
{
    public decimal NumberOfTimesOverdrawn { get; set; }
    public int NumberOfAccountHolders { get; set; }
}

(Notice that none of the preceding classes are linked by inheritance.)

Suppose we wanted to run a gift card promotional mailing depending on what accounts customers had. We can use pattern matching on the type of object in a switch expression:

decimal CalculatePromotionalGiftCardValue(object account) => account switch
{
    SavingsAccount sa when (sa.Balance > 10_000) => 100,
    SavingsAccount _ => 0, // all other savings accounts

    HomeLoanAccount hla when (hla.MonthsRemaining < 12) => 20,
    HomeLoanAccount _ => 0, // all other home loan accounts

    ChequingAccount ca when (ca.NumberOfTimesOverdrawn == 0 && ca.NumberOfAccountHolders == 1) => 20,
    ChequingAccount ca when (ca.NumberOfTimesOverdrawn == 0 && ca.NumberOfAccountHolders == 2) => 40,
    ChequingAccount ca when (ca.NumberOfTimesOverdrawn == 0 && ca.NumberOfAccountHolders == 3) => 50,
    ChequingAccount _ => 0, // all other chequing accounts

    { } => throw new ArgumentException("Unknown account type", paramName: nameof(account)), // all other non-null object types
    null => throw new ArgumentNullException(nameof(account))
};

Notice in the preceding expression-bodied method containing a switch expression (that’s a mouthful!), that there is a bit of repetition in the ChequingAccount section with the ca.NumberOfTimesOverdrawn == 0 code being repeated. We can replace this section with a nested switch expression:

decimal CalculatePromotionalGiftCardValueNested(object account) => account switch
{
    SavingsAccount sa when (sa.Balance > 10_000) => 100,
    SavingsAccount _ => 0,

    HomeLoanAccount hla when (hla.MonthsRemaining < 12) => 20,
    HomeLoanAccount _ => 0,

    ChequingAccount ca when (ca.NumberOfTimesOverdrawn == 0) => ca.NumberOfAccountHolders switch
    {
        1 => 20,
        2 => 40,
        3 => 50,
        _ => 0
    },
    ChequingAccount _ => 0,

    { } => throw new ArgumentException("Unknown account type", paramName: nameof(account)),
    null => throw new ArgumentNullException(nameof(account))
};

If you want to fill in the gaps in your C# knowledge be sure to check out my C# Tips and Traps training course from Pluralsight – get started with a free trial.

SHARE: