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.
- Aesthetically, I wish we didnāt need those parenthesis⦠weāre not passing any arguments so why does this have to be a method?
- 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.