Writing to the Backing Field
DShop.Common stamps a server-generated Guid into an immutable command by reflecting into its compiler-generated backing field - immutability the type system only pretends to enforce.
Commands in this estate are immutable. Open any of them and you find get-only auto-properties — public Guid Id { get; } — no setters, the C# idiom for “this value is fixed at construction.” It is the right instinct: a command is a request, and a request should not be mutable in flight. Except the gateway needs to do one thing to an incoming command before it dispatches it — stamp a freshly minted resource id onto it — and the command, being immutable, won't let it. DShop.Common's answer is two small extension methods that reach past the type system and write directly into the property's hidden backing field. It works, it is used on every write request, and it is immutability theatre.
The public face
Two clean-looking extensions live in the MVC helpers:
public static T Bind<T>(this T model, Expression<Func<T, object>> expression, object value)
=> model.Bind<T, object>(expression, value);
public static T BindId<T>(this T model, Expression<Func<T, Guid>> expression)
=> model.Bind<T, Guid>(expression, Guid.NewGuid());
At the call site this reads beautifully. The gateway receives a CreateProduct command with no id, and does command.BindId(c => c.Id) — “give this command a new id on its Id property.” A strongly-typed lambda names the property; a new Guid appears on it. You would never guess from this surface that Id has no setter and that the assignment is happening through reflection.
The reflection underneath
The private Bind is where the type system gets circumvented:
private static TModel Bind<TModel, TProperty>(this TModel model,
Expression<Func<TModel, TProperty>> expression, object value)
{
var memberExpression = expression.Body as MemberExpression
?? ((UnaryExpression) expression.Body).Operand as MemberExpression;
var propertyName = memberExpression.Member.Name.ToLowerInvariant();
var modelType = model.GetType();
var field = modelType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
.SingleOrDefault(x => x.Name.ToLowerInvariant().StartsWith($"<{propertyName}>"));
if (field == null)
return model;
field.SetValue(model, value);
return model;
}
Read it slowly, because every line is doing something the language is trying to prevent:
- It pulls the property name out of the lambda expression tree (
c => c.Idyields"id"), handling theUnaryExpressioncase for value types that get boxed intoobject. - It then asks the runtime for the type's non-public instance fields and looks for one whose name starts with
<id>. That is not a field anyone wrote. When you declarepublic Guid Id { get; }, the C# compiler generates a hidden field called<Id>k__BackingFieldto hold the value. TheStartsWith($"<{propertyName}>")match is deliberately reaching for that compiler-generated name. - Finding it,
field.SetValue(model, value)writes straight into the backing field, bypassing the missing setter entirely.
The command's immutability is a decoration; the backing field is writable to anyone willing to name it. Get-only auto-properties are not truly read-only — they are read-only through the public surface. Reflection sees the field the language hid, and this code walks right up to it.
Two costs and a design smell
The technique works, and on a demo shop it hurts nothing, but a source-reader should log three things.
It is uncached reflection on every call. GetFields runs, allocates, and linear-scans the type's fields every single time a command is bound — and every write request through the gateway binds at least one id. Reflection metadata like this is trivially cacheable (a static ConcurrentDictionary<(Type, string), FieldInfo> keyed by type and property), and none of that caching is here. It is the kind of per-request overhead that never shows up in a demo and lights up a profiler under load.
It fails silently. If the field isn't found — a property that isn't an auto-property, a name typo, a future C# that changes the backing-field naming scheme — Bind hits field == null and simply return model. No exception, no log. The command proceeds without the id it was supposed to receive, carrying Guid.Empty instead, and the failure surfaces far downstream as a mysterious all-zeros resource id. A method that quietly does nothing when it can't do its job is a debugging trap.
And the design itself is the smell. Reflecting into a backing field to mutate an “immutable” object is fighting your own design. If a command needs a server-assigned id, the honest options are a constructor parameter (new CreateProduct(id: Guid.NewGuid(), ...)), a factory, or a deliberate with-style copy — each of which keeps the type honest. <Id>k__BackingField write-through gets the same effect while preserving the appearance of immutability, which is the worst of both: readers trust the get-only property and the runtime ignores it.
To be fair, there is a real problem being solved here, and it is a genuinely awkward one: the id has to be minted at the edge (so the gateway can return a 202 Accepted naming the resource before the async pipeline runs — a trick the third series pulls apart) yet the command is shaped for immutability. The BindId helper threads that needle in two lines at the call site. It is clever, and I understand why it exists. But it is the kind of clever that quietly redefines what “immutable” means in the codebase, and the next reader deserves to know the word has an asterisk.
The lesson generalises past this estate: { get; } is a promise the type system keeps and reflection breaks. When you see a codebase reflecting into k__BackingField, an “immutable” type nearby is not as immutable as it looks — and it is usually a sign the real requirement wanted a constructor, not a decoration.
We have four parts left, and they turn to the cross-cutting middleware every service runs: error handling, auth, and composition. Next, the error middleware that turns every exception into a 400 — and hands the caller more than it should.