Refactoring with C# 10

December 22nd, 2021

For the C# Advent this year, we'll refactor a sample app using the latest C# 10 language improvements

This post was written for the Fifth Annual C# Advent Calendar (#csadvent). A TON of thanks to Matt Groves (@mgroves) for putting this together again! Awesome job, Matt!

With C# 10 recently released, there are a number of features we can immediately start using in our own projects.

However, it's the same with sprinkling JavaScript throughout an application: use them where it makes sense and not just to include the latest shiny new thing.

After hearing about all of the new language features, I decided to grab a code snippet from an older application I wrote and refactor it using the C# 10 improvements.

For those new to coding, refactoring is the process of restructuring existing code to make it easier to read and more maintainable.

While this post doesn't cover every C# 10 feature, I feel it covers the primary features you'll use the most throughout the day. 

What We Start With

When I was younger, I created an amusement park guide to show you where attractions where and had a grand vision of showing how much time people would have to stand in a queue before they were able to hop on the ride.

Of course, similar to buying domains, this project didn't see the light of day.

This is the snippet we'll be working with throughout the post.

namespace WebApplication3.Models
{
    public class ParkSection
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public ParkSection(string name)
        {
            Name = name;
            Id = default(Guid);
        }
    }

   public class Attraction
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public ParkSection Park { get; set; }
        public Attraction(string name, string type, ParkSection park)
        {
            Name = name;
            Type = type;
            Park = park;
        }
    }

   public class AttractionQueue
    {
        public TimeSpan CalculateWaitTime(Attraction attraction)
        {
            var result = TimeSpan.Zero;

           switch (attraction.Park.Name)
            {
                case "Western":
                    result = TimeSpan.FromMinutes(10);
                    break;

               case "Space":
                    result = TimeSpan.FromHours(1);
                    break;

               case "American":
                    result = TimeSpan.FromMinutes(20);
                    break;

               case "High Seas":
                    result = TimeSpan.FromMinutes(45);
                    break;
            }

           return result;
        }
    }
}

Mind the last method with the switch statement. This was before I started getting a switch twitch.

1. Record Structs

Record Structs are considered Value types and used for specifically holding data.

Since it's a Value type, it's saved on the Stack which is for static memory allocation. This is preferred since it takes up less space than classes. With classes, they're placed on the Heap for the dynamic memory allocation.

One place I see this used a lot would be for DTOs. This would make applications a little less memory-hungry.

In our case, since our ParkSection doesn't have too much functionality, we can change this from a class into a record struct (change in bold)

public record struct ParkSection
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public ParkSection(string name)
    
{
        
Name = name;
        
Id = default(Guid);
    
}
}

We create a constructor and assign a default GUID to the Id.

We've now created a ParkSection value type using record struct.

2. Parameterless Struct Constructor

Another great feature to avoid carpal tunnel is the ability to create a record struct without a constructor.

With our example above, we can now turn those 10 lines of code into 3.

public record struct ParkSection(string Name)
{
    public Guid Id { get; init; } = default(Guid);
}

This will automatically create a getter and setter for the Name parameter passed in. However, if you want to explicitly add the getter and setter under the Id member, you can.

It makes the syntax a little less verbose.

To make things even easier, we can refactor and covert the Attraction class into a record struct as well along with our two techniques.

This will cut the Attraction class of 11 lines down to an Attraction record struct of 1.

Yes, 1 line!

public record struct Attraction(string Name, string Type, ParkSection Park);

Done!

3. File-Scoped Namespaces

This is another feature confirming my belief that C# is starting to look like JavaScript and vice-versa (with TypeScript): the removal of the namespace brackets.

The brackets at the namespace level seems to be more noise than anything.

So instead of 

namespace WebApplication3.Models
{
    .
    .
    .
}

we can now simply have one line at the top of our file.

namespace WebApplication3.Models;

This makes your code a little easier to read since we won't have a "bracket racket" and go insane knowing if we're inside a bracket or not.

4. Extended Property Patterns

Finally, we'll show an example of the new Extended Property Patterns in C# 10.

Let's look at the CalculateWaitTime method in the AttractionQueue class.

public class AttractionQueue
{
    public TimeSpan CalculateWaitTime(Attraction attraction)
    {
        var result = TimeSpan.Zero;

       switch (attraction.Park.Name)
        {
            case "Western":
                result = TimeSpan.FromMinutes(10);
                break;

           case "Space":
                result = TimeSpan.FromHours(1);
                break;

           case "American":
                result = TimeSpan.FromMinutes(20);
                break;

           case "High Seas":
                result = TimeSpan.FromMinutes(45);
                break;
        }

       return result;
    }
}

We have a simple switch statement to identify how long you'll stand in line based on your location in the park.

Each one of these sections have popular attractions and can result in high-wait times.

In C# 9, the pattern matching enhancements converted this switch statement into something a little more compact.

public TimeSpan CalculateImprovedWaitTime_CSharp9(Attraction attraction) =>
    attraction switch
    {
        { Park.Name: "Western" } => TimeSpan.FromMinutes(10),
        { Park.Name: "Space" } => TimeSpan.FromHours(1),
        { Park.Name: "American" } => TimeSpan.FromMinutes(20),
        { Park.Name: "High Seas" } => TimeSpan.FromMinutes(45),
        _ => TimeSpan.Zero,
    };

As you can see, we can simplify this as an expression body.

In C# 10, you can implement Extended Property Patterns by breaking down the properties into a more granular approach.

if you wanted to pattern match on a property even further, you could implement the following:

public TimeSpan CalculateImprovedWaitTime_CSharp10(Attraction attraction) =>
    attraction switch
    {
        { Park: { Name: "Western" } }   => TimeSpan.FromMinutes(10),
        { Park: { Name: "Space" } }     => TimeSpan.FromHours(1),
        { Park: { Name: "American" } }  => TimeSpan.FromMinutes(20),
        { Park: { Name: "High Seas" } } => TimeSpan.FromMinutes(45),
        _ => TimeSpan.Zero,
    };

While this may look the same as C# 9 example, the power in this approach comes from the following example:

public TimeSpan CalculateImprovedWaitTime_CSharp10(Attraction attraction) =>
    attraction switch
    {
        { Park: { Name: "Western" } } => TimeSpan.FromMinutes(10),
        { Name: "Coaster", Park: { Name: "Space" } } => TimeSpan.FromHours(1),
        { Park: { Name: "American" } } => TimeSpan.FromMinutes(20),
        { Park: { Name: "High Seas" } } => TimeSpan.FromMinutes(45),
        _ => TimeSpan.Zero,
    };

The bold indicates the flexibility of extended property pattern matching.

This looks to me like an 'if..then..else' in disguise?

What We End With

With the little amount of refactoring, we took a 56-line snippet and trimmed it down to 20 lines of code.

namespace WebApplication3.Models;

public record struct ParkSection(string Name, string Type)
{
    public Guid Id { get; init; } = default(Guid);
}

public
 record struct Attraction(string Name, string Type, ParkSection Park);

public
 class AttractionQueue
{
    public TimeSpan CalculateImprovedWaitTime(Attraction attraction) =>
        attraction switch
        {
            { Park: { Name: "Western" } } => TimeSpan.FromMinutes(10),
            { Name: "Coaster", Park: { Name: "Space" } } => TimeSpan.FromHours(1),
            { Park: { Name: "American" } } => TimeSpan.FromMinutes(20),
            { Park: { Name: "High Seas" } } => TimeSpan.FromMinutes(45),
            _ => TimeSpan.Zero,
        };
}

Definitely more compact and it does the same amount of work.

Conclusion

Refactoring code should always be on your mind and, as you can see, when you use some of C# 10's language features, you will gain a number of benefits from using them.

As I mentioned above, there are way too many language features to cover in just one post.

If you are interested in learning even more, check out the C# 10 language features starting with Record Structs.

Did you see one of your favorite features in this post? What is your favorite C# 10 feature? Post your comments below and let's dig into it!