Thanks to @toupswork for starting a funny discussion about extension properties on a recent post I made: "Adding static getter thanks to extensions". Therefore let's discuss two ridiculous ideas to add properties to existing types using extension methods in C# 14.
ConditionalWeakTable-based extension properties
Check the comments here from @toupswork
sealed record Person(string Name);
static class PersonExtensions
{
static readonly ConditionalWeakTable<Person, StrongBox<string?>> Nicknames = new();
extension(Person person)
{
public string? Nickname
{
get => Nicknames.TryGetValue(person, out var box) ? box.Value : null;
set => Nicknames.GetOrCreateValue(person).Value = value;
}
}
}
var person = new Person("Robert");
person.Nickname = "Bob";
This alone allows you to add properties to existing types without modifying their source code. Now why using ConditionalWeakTable over a Dictionary?
Using Dictionary would couple lifetimes of that dictionary with all associated Person instances, which is not what we want. So if we have mulitple entries in the dictionary, we keep all instances alive, even if they are no longer referenced anywhere else. ConditionalWeakTable allows us to associate data with an object without preventing that object from being garbage collected.
The second maybe "odd" part: StrongBox<T>. The defintion is very very simple:
public class StrongBox<T>
{
public T Value;
public StrongBox(T value) => Value = value;
}
Why would you need a wrapper like that? Generally, you would like to distinguish between a property that has not been set and a property that has been set to null. If you just store the value directly in the ConditionalWeakTable, you cannot distinguish between these two cases.
For me that is such an elegant solution, that I properly never ever use in production code, but it is a fun idea to play with.
Let's get complicated: Wrap it
Of course we can just have a wrapper class that contains the property we want to add. Something like
public class PersonWrapper
{
public Person Person { get; }
public string? Nickname { get; set; }
public PersonWrapper(Person person)
{
Person = person;
}
}
Yeah - no. That is super boring and basically everywhere I have to use PersonWrapper instead of Person. So let's misuse Unsafe and extensions:
// Same field layout as Person (one reference field first)
// So if Person looks different, we have a problem!
sealed class PersonPlus
{
public string Name = "";
public string? Nickname;
public override string ToString() => $"Person {{ Name = {Name} }}";
}
public static class PersonExtensions
{
public static Person Create(string name, string? nickname = null)
=> Unsafe.As<Person>(new PersonPlus { Name = name, Nickname = nickname });
extension(Person person)
{
public string? Nickname
{
get => person is PersonPlus
? Unsafe.As<PersonPlus>(person).Nickname
: null;
set
{
if (person.GetType() != typeof(PersonPlus))
throw new InvalidOperationException(
"This Person was not allocated by PersonExtensions.Create.");
Unsafe.As<PersonPlus>(person).Nickname = value;
}
}
}
}
Person person = PersonExtensions.Create("Steven Giesel");
person.Nickname = "Stevie";
Console.WriteLine(person.Nickname); // "Stevie"
Console.WriteLine(person.Name); // "Steven Giesel"
The idea is that we an object mirroring the original one plus more. But for sure here we have to check the field layout has to be the same! Renames, typechanges etc will fuck up the solution.
Also your compiler will for sure flag: person is PersonPlus as it sees that can never be true - but it is!
Let's make it safer!
To add insult to injury, we misuse static ctors to check the field layout:
public static class PersonExtensions
{
static PersonExtensions()
{
// Check the layout
var probe = Create("steven");
if (!ReferenceEquals(probe.Name, "steven"))
throw new InvalidOperationException("Person/PersonPlus layout mismatch.");
}
...