An Update That Blanks a Field the Form Never Showed
Server-owned fields, protected in one line
A user opens a record, changes one field and saves. Afterwards a field nobody touched is empty: the code the system minted when the record was created, a close date, an order total. No client sent a bad request, and every line involved passed review.
The previous post laid out the write path: preppers before EF Core sees the entity, primers and normalizers inside SaveChanges. This one is about a bug on that path that every DTO-based API meets sooner or later, and the declaration that prevents it — in the same sample as before (dotnet run, port 5310, $URL as defined there).
Universal or Regira? The bug belongs to every API that maps an input DTO onto a fresh entity, and so does the cure. The one-line declaration that applies it to every marked field on every entity is Regira Entities 6.2.
The field the client never sent
Good API hygiene says an input DTO carries only what the client may set. The sample's Ticket has a Code, minted when the ticket is created and quoted in every mail about it afterwards, and a ClosedOn, stamped on the first transition to Closed. Neither belongs on TicketInputDto, so neither is there.
Now follow a PUT. The controller maps the DTO to a fresh entity. Code and ClosedOn are not on the DTO, so on the entity they are null. The write service attaches that entity as Modified — and UPDATE Tickets SET Code = NULL, ClosedOn = NULL, ... goes to the database. The ticket has lost the code its mails refer to, and the date it was closed.
The cure is to put the stored value back before EF Core builds the UPDATE.
Declare the field the server's
The restore is an attribute on the entity, enforced by a prepper UseDefaults() registers for every entity:
public class Ticket : IEntityWithSerial, IHasTimestamps, IHasNormalizedContent, IArchivable
{
// the ONLY writer is TicketClosedOnPrimer; a PUT that omits it can no longer null it
[ServerOwned]
public DateTime? ClosedOn { get; set; }
// ...
}
The AutoServerOwnedPrepper receives (modified, original); when there is an original — an update — it copies every marked property from the stored row onto the incoming entity, before EF Core is involved. On create it does nothing: the attribute is protect-only, so a seeder can insert a back-dated value and it is written as-is.
The mint is the second half, and an attribute cannot carry a delegate. So the fluent form does both:
services.For<Ticket>(e =>
{
e.ServerOwned(x => x.Code, _ => $"TCK-{Guid.NewGuid().ToString("N")[..6].ToUpperInvariant()}");
});
Minted when the property is still unset on create (an imported ticket arriving with a legacy code keeps it), restored from the stored row on update. The sample shows both guards in two calls. Its TicketInputDto has neither code nor closedOn, and every field it does have is replaced by what the PUT sends, hence the repeated description:
curl -X PUT "$URL/tickets/1" -H "Content-Type: application/json" \
-d '{"id":1,"subject":"Printer on floor 2 jams","description":"Every third page, since Monday.","status":"Closed"}'
# -> "code":"TCK-067C9E","closedOn":"2026-09-27T19:05:42.5797371Z" (code as seeded, closedOn stamped by the primer)
curl -X PUT "$URL/tickets/1" -H "Content-Type: application/json" \
-d '{"id":1,"subject":"Printer on floor 2 jams (paper tray)","description":"Every third page, since Monday.","status":"Closed"}'
# -> "code":"TCK-067C9E","closedOn":"2026-09-27T19:05:42.5797371Z" (both kept)
With both guards taken out, the first call answers without a code and the second without a closedOn.
Three details decide whether this is the right tool:
- It is a prepper. It guards the
IEntityServicewrite path and leaves a domain service's rawDbContextwrite alone — which is also whyArchivablePrimer's delete-to-update is not reverted, and why a primer that stamps (ClosedOnabove) still has a job: the attribute protects, the primer writes. - Scalars and foreign keys only. A navigation, a property without both accessors, and
IArchivable.IsArchived(a restore has to be able to clear it) are refused — at registration for the fluent form, by startup validation for the attribute. - Registration order counts. Preppers run in the order they were registered, so an entity's own prepper, registered after
UseDefaults(), can still take a marked field over deliberately.
The fields this is for
They are the ones the client must be able to see but never set: on the read DTO, off the input DTO, and each a NULL or a 0 waiting to happen on the next PUT. The previous post's table places the rule that writes each; this one says what protects it.
| The field | Examples | Shortest form |
|---|---|---|
| Minted once, immutable after | Code — the sample's TCK-… |
e.ServerOwned(x => x.Code, mint): mints on create, restores on update |
| Written by another rule | ClosedOn, PublishedOn (a transition); Slug (from the title); OwnerId (the current user); an order's Total |
[ServerOwned] on the property; the primer or prepper that writes it stays |
| Gap-free sequential number | INV-2026-0042 |
[ServerOwned] plus a prepper with the DbContext; the mint delegate is synchronous and has no context |
What sending a child collection in that same PUT means — and why an omitted list is not an empty one — is the next post.
Why not restore the field by hand? A prepper per entity that copies original.Code back does the same job — the built-in one is that prepper, written once. Written per entity, it is one class per entity re-deriving the same two moves, restore on update, mint on create, and every one a place to forget a field when the entity grows one.
Docs: Regira Entities — see the packed guides' recipe "Server-owned / immutable fields on update" and the 6.2.0 entries in the changelog. The sample runs within the free tier, without a license key.
Versions: [ServerOwned] and e.ServerOwned(...) are Regira Entities 6.2; the sample pins 6.4.0.