Skip to content

Revenge of the Extension Members in C# 14

Published: at 11:42 AM

Extension Members Revenge

In our earlier post JavaScript Envy in C# - Dynamic Properties, we talked about various different ways to emulate dynamic properties from JavaScript and landed on a pretty good solution (published on NuGet as MentalDesk.Fuse).

The only down side to that solution really is aesthetic. There’s no way to create extension properties in C# 13 (which shipped with .NET 9) or earlier versions of the C# language.

In this post we take a look at the new Extension Members syntax introduced in C# 14 and use this to improve upon our earlier solution - creating a truly stateful extension property that has all the goodness of JavaScript Dynamic properties, without leaving any of C#ā€˜s static type checking behind šŸŽ‰

Table of contents

Open Table of contents

Mock Scenario

Let’s start by creating an problem. Assume we have the following block of code:

// We start with some simple strings
string brendan = "Brendan Eich (/ˈaɪk/; born July 4, 1961)";
string roberto = "Roberto Ierusalimschy (Brazilian Portuguese: [ʁoˈbɛʁtu jeɾuzaˈlÄ©ski]; born May 21, 1960)";
string grace = "Grace Hopper (nƩe Murray; born December 9, 1906)";
Person[] people = [brendan, roberto, grace];

foreach (var person in people)
{
    Console.WriteLine($"{person.FirstName} {person.LastName}");
}

Console.WriteLine("Press any key to exit...");
Console.ReadKey();

sealed record Person(string FirstName, string LastName, string Description)
{
    private static readonly Regex PersonRegex = new(@"(?<first>\w+) (?<last>\w+) \((?<description>.*)\)");

    public static implicit operator Person(string input)
    {
        var match = PersonRegex.Match(input);
        return new Person(
            match.Groups["first"].Value,
            match.Groups["last"].Value,
            match.Groups["description"].Value
        );
    }
}

We’d like to extend this code so that we can easily display the date of birth as well… and whilst we could do that inline in the foreach loop, let’s also imagine this is part of a larger code base where we’re going to need to access the Date Of Birth of each of these legendary character’s frequently in various different situations.

The traditional approach

Using the traditional approach we could do this as an extension method. Maybe something like:

static class PersonExtensions
{
    private static readonly Regex DobRegex = new(".*; born (?<dob>.+)");
    public static DateTime DateOfBirth(this Person source)
    {
        var match = DobRegex.Match(source.Description);
        var dob = match.Groups["dob"].Value;
        return DateTime.ParseExact(dob, "MMMM d, yyyy", CultureInfo.InvariantCulture);
    }
}

And that would work… we could then rewrite our original code as:

foreach (var person in people)
{
    Console.WriteLine($"{person.FirstName} {person.LastName} - born {person.DateOfBirth()}");
}

There are a couple of issues with this.

  1. Aesthetically, I wish we didn’t need those parenthesis… we’re not passing any arguments so why does this have to be a method?
  2. From a performance point of view, we’re evaluating a RegEx every time we access this property… which is wasteful. Parsing the regex is an expensive operation - something we really only need to be performing once.

Problem 1: Fixing the visuals

First things first, the new preview release of .NET 10 ships with C# 14… so we can get rid of those pesky parenthesis by changing our extension method to an extension property:

static class PersonExtensions
{
    private static readonly Regex DobRegex = new(".*; born (?<dob>.+)");
    extension(Person source)
    {
        public DateTime DateOfBirth
        {
            get
            {
                var match = DobRegex.Match(source.Description);
                var dob = match.Groups["dob"].Value;
                return DateTime.ParseExact(dob, "MMMM d, yyyy", CultureInfo.InvariantCulture);
            }
        }
    }
}

And now our the code that makes use of this extension looks so much tidier (if you’re a bit on the spectrum):

foreach (var person in people)
{
-    Console.WriteLine($"{person.FirstName} {person.LastName} - born {person.DateOfBirth()}");
+    Console.WriteLine($"{person.FirstName} {person.LastName} - born {person.DateOfBirth}");
}

Problem 2: Fixing the Performance

With visuals out of the way, we may as well also address the performance issue. Our extension member evaluates the regex every time the property is referenced 😱 In our little console application that’s probably not a big deal. If you’re writing an app that handles thousands of requests per second, each referencing Person.DateOfBirth frequently, that’s no good.

Ideally we’d like to declare the property as a Lazy<DateTime>… Extension Members are static just like their traditional Extension Method brethren, so the new syntax can’t help us there. However we can cache the DateOfBirth (once resolved) as a Fused property on each Person instance (using our NuGet package from earlier) and then cover up all those details using the new Extension Method syntax.

The code for the Extension Property might look something like this:

static class PersonExtensions
{
    private static readonly Regex DobRegex = new(".*; born (?<dob>.+)");
    extension(Person source)
    {
        public DateTime DateOfBirth
        {
            get
            {
                if (source.GetFused<DateTime?>("dob") is { } cachedDob)
                {
                    return cachedDob;
                }

                var match = DobRegex.Match(source.Description);
                var dobString = match.Groups["dob"].Value;
                var dob = DateTime.ParseExact(dobString, "MMMM d, yyyy", CultureInfo.InvariantCulture);
                source.SetFused("dob", dob);
                return dob;
            }
        }
    }
}

And the code that uses our Extension Property doesn’t need to change - it’s just more performant. What we have now is something that looks and feels like a property, and even holds state - just like a regular property! 🄳

Wrapping Up

So there you have it. Thanks to C# 14’s shiny new Extension Members, we can finally give our code that JavaScript-like dynamic flair, but with all the safety and performance we love from C#. No more awkward parentheses, no more unnecessary regex runs, and no reason to envy dynamic properties in JavaScript anymore.

With a sprinkle of new syntax and a dash of clever caching, our legendary programmers can now have their birthdays remembered efficiently and elegantly.

Happy coding, and may your properties be only as dynamic or static as you need.