I came across a recent thing, where I hit the limit with the "normal" approach of modeling my entity and had to resort to extension methods.
The problem#
I have a frontend part that allows partial updates of an entity. Imagine your favourite tool: JIRA 😄 - you can update title, reporter, and so on in a single UI. For that I didn't want a DTO for each use case, but a single DTO that allows partial updates of an entity.
In the frontend (TypeScript / Angular) I have:
export type Optional<T> = {
hasValue: boolean;
value: T;
};
Which is used by a DTO like:
export type UpdateModel = {
propOne: Optional<number | null>;
propTwo: Optional<string | null>;
propThree: Optional<string>;
};
That allows me in the backend to do this:
/// <summary>
/// A struct representing an optional value.
/// </summary>
public readonly record struct Optional<T>(bool HasValue, T Value)
{
/// <summary>
/// Returns the value if present; otherwise, returns the provided default value.
/// </summary>
public T GetValueOrDefault(T current) => HasValue ? Value : current;
}
And then in my update method:
public void UpdateEntity(UpdateModel model)
{
PropOne = model.PropOne.GetValueOrDefault(entity.PropOne);
PropTwo = model.PropTwo.GetValueOrDefault(entity.PropTwo);
PropThree = model.PropThree.GetValueOrDefault(entity.PropThree);
}
That works fine, but we hit a wall if we add a new propFour that has different nullability:
public int? PropFour { get; set; }
But in the UpdateModel we want to have (because in an update case that is mandatory, whereas initially it isn't):
public Optional<int> PropFour { get; set; }
Now if we try:
PropFour = model.PropFour.GetValueOrDefault(entity.PropFour);
We get:
Argument type 'int?' is not assignable to parameter type 'int'
The solution#
The problem is that we can't easily override nullability in the GetValueOrDefault method. Here extension methods come into play:
public static class OptionalExtensions
{
/// <summary>
/// Returns the value if present; otherwise, returns the provided default value.
/// </summary>
public static T? GetValueOrDefault<T>(this Optional<T> optional, T? value)
where T : struct
=> optional.HasValue ? optional.Value : value;
}
Now: PropFour = model.PropFour.GetValueOrDefault(entity.PropFour); works as expected.