11 KiB
PATCH Delta Handling – Backend & Frontend Reference
This document is the single reference for how PATCH payloads (deltas) are structured and interpreted so backend (BE) and frontend (FE) stay consistent. It follows the MaksIT.Core patch contract (same rules as shared deepDelta helpers). MaksIT-CertsUI uses that model with a hostnames collection on account PATCH.
Audience: Backend (C# / ASP.NET) and Frontend (TypeScript / React) developers.
TL;DR (start here)
- PATCH sends only what changed, not the full resource. Each change is tagged with an operation (set, remove, add item, remove item).
- Root fields (e.g.
description,contact): send new value +operations["fieldName"] = SetFieldorRemoveField. - Collections (e.g.
hostnames): do not replace the whole array when the API is “patchable collection” semantics. Send per-item changes: each added item hasoperations.collectionItemOperation = AddToCollection, each removed item hasRemoveFromCollection, and changed items send identity and changed fields. - Frontend (Certs WebUI): For Edit Account, use
deepDelta(formState, backupState, { arrays: { hostnames: { identityKey: 'hostname', idFieldKey: 'hostname' } } })
so hostname rows are itemized (including “add first hostname”) and stay in sync with the backend. - Backend: Use
TryGetOperation(Constants.CollectionItemOperation, out var op)on each collection item; never treat rootOperations["hostnames"] = SetFieldas “replace all” if the API follows per-item patch semantics.
1. Core contract (MaksIT.Core)
The following come from MaksIT.Core and must be respected by all consumers.
1.1 PatchOperation enum
| Value | Integer | Meaning |
|---|---|---|
SetField |
0 | Set or replace a scalar or root-level value |
RemoveField |
1 | Set a field to null |
AddToCollection |
2 | Add an item to a collection (used on collection items, not root) |
RemoveFromCollection |
3 | Remove an item from a collection (used on collection items, not root) |
- Source:
MaksIT.Core.Webapi.Models.PatchOperation - FE mirror:
PatchOperationenum in WebUI (src/MaksIT.WebUI/src/models/PatchOperation.ts) must keep the same numeric values for JSON serialization.
1.2 PatchRequestModelBase
- Operations:
Dictionary<string, PatchOperation>?(C#) /{ [key: string]: PatchOperation }(TS). - Lookup: Case-insensitive by property name (e.g.
"hostnames","description"). - Usage:
- Root level:
Operations["propertyName"]describes the operation for that property (e.g.SetFieldfor a changed field,RemoveFieldfor null). - Collection items: Each element of a collection property is itself a patch model; it uses a reserved key (see below) to indicate add/remove/update for that item.
- Root level:
1.3 Collection item operation key
For elements inside a collection property (e.g. each item in hostnames), the operation is stored under a fixed key so the backend can distinguish “add/remove this item” from “update fields of this item”.
- Key name:
collectionItemOperation - BE:
Constants.CollectionItemOperation(same string across MaksIT services when aligned with Core). - FE:
COLLECTION_ITEM_OPERATIONinsrc/MaksIT.WebUI/src/models/PatchOperation.ts; same string in payloads and indeepDelta. Keep in sync with backend.
Allowed values for collection items: AddToCollection (2), RemoveFromCollection (3). For in-place updates (same item, changed fields), the item typically has an id and no collectionItemOperation, or field-level changes follow your API’s semantics.
2. Backend (BE) rules
2.1 Root-level properties
- Read
request.TryGetOperation(propertyName, out var op). - If
op == SetField: apply the new value from the request for that property. - If
op == RemoveField: set the property to null (or clear it) where applicable. - If the property is not in
Operationsbut is present with a value, treat as optional direct assignment (or ignore), depending on your API convention; for strict PATCH, prefer requiring operations for changed fields.
2.2 Collection properties (e.g. hostnames)
- Rule: Collection properties are patched only via per-item operations when the API follows patchable-collection semantics. The backend does not treat root
Operations["hostnames"] = SetFieldas “replace the entire collection” unless explicitly documented for that endpoint. - For each item in the collection payload:
- Call
item.TryGetOperation(Constants.CollectionItemOperation, out var collectionOp)(or the agreed constant). - If
collectionOp == AddToCollection: add the item to the collection (merge by id if present, or append). - If
collectionOp == RemoveFromCollection: remove the item (byitem.Idor by matching key fields such as hostname string). - If no
collectionItemOperationbut the item is identifiable: treat as in-place update. - If no
collectionItemOperationand the item cannot be identified: do not add ambiguous items; the FE must sendAddToCollectionfor new rows when required by your rules.
- Call
2.3 Consistency checklist (BE)
- Use the same
CollectionItemOperationkey as the FE (see Constants / Core). - Do not rely on root-level
SetFieldfor patchable collections to mean “replace all”; use per-item add/remove/update only (unless documented otherwise). - For add: require
TryGetOperation(CollectionItemOperation) == AddToCollection(or equivalent) for new items where applicable. - For remove: use
RemoveFromCollectionand/or identity fields agreed with the FE.
3. Frontend (FE) rules
3.1 Building the delta (deepDelta)
- Scalar / root fields: Emit the new value and set
operations[propertyName] = SetFieldorRemoveFieldas appropriate. - Primitive arrays: Emit the full array and
operations[propertyName] = SetField(full replace). - Object arrays that are “patchable collections”: Must always produce itemized deltas when configured with an array policy (identity key / id field):
- Each added item must have
operations.collectionItemOperation = AddToCollection. - Each removed item must have
operations.collectionItemOperation = RemoveFromCollection. - Updated items carry changed fields and identity; see
deepDeltaimplementation insrc/MaksIT.WebUI/src/functions/deep/deepDelta.ts.
- Each added item must have
3.2 Patchable collections – identity requirement
For the backend to interpret add/remove/update correctly, each collection item must be identifiable:
- Existing items: Use
idfrom the server when present. - New items: May have no server
id; the FE must pass an array policy withidentityKey/idFieldKeyso the delta stays itemized (e.g. hostname string as stable key for hostnames).
3.3 Shared array policies (this repository)
MaksIT-CertsUI does not ship a shared patchCollectionPolicies.ts; the Edit Account form passes an inline policy for the hostnames collection:
| Collection | Policy (inline) | Used in |
|---|---|---|
hostnames |
{ identityKey: 'hostname', idFieldKey: 'hostname' } |
EditAccount.tsx |
Example:
deepDelta(fromFormState, fromBackupState, {
arrays: {
hostnames: {
identityKey: 'hostname',
idFieldKey: 'hostname',
},
},
})
3.4 Consistency checklist (FE)
- Use the same
PatchOperationnumeric values as Core (SetField 0, RemoveField 1, AddToCollection 2, RemoveFromCollection 3). - Use
COLLECTION_ITEM_OPERATIONfromPatchOperation.ts(same string as backendConstants.CollectionItemOperation). - For account
hostnames, pass thehostnamesarray policy indeepDeltaso the delta is itemized, not a blind full-replace of the array. - New items must have
operations.collectionItemOperation = AddToCollectionwhen the backend expects it.
4. Payload examples
4.1 Root-level SetField (scalar)
{
"description": "Updated",
"operations": {
"description": 0
}
}
0 = SetField.
4.2 Root-level RemoveField (clear optional field)
{
"operations": { "someOptionalField": 1 }
}
1 = RemoveField.
4.3 Root-level SetField (primitive array – full replace)
{
"tags": ["a", "b", "c"],
"operations": {
"tags": 0
}
}
4.4 Collection property – itemized (add items)
Example shape for new hostname rows (numeric ops match PatchOperation enum):
{
"hostnames": [
{
"hostname": "api.example.com",
"isDisabled": false,
"operations": { "collectionItemOperation": 2 }
}
]
}
2 = AddToCollection.
4.5 Collection property – remove item
{
"hostnames": [
{
"hostname": "old.example.com",
"operations": { "collectionItemOperation": 3 }
}
]
}
3 = RemoveFromCollection (identity may be hostname or server id depending on API).
4.6 Collection property – in-place update
Item exists; fields change; no collectionItemOperation on the item (or only nested field operations per your model).
5. Quick reference
| Aspect | Backend | Frontend |
|---|---|---|
| Operation enum | PatchOperation (Core) |
PatchOperation (same values 0–3) |
| Root operations | TryGetOperation(propertyName, out op) |
operations[propertyName] = op |
| Collection item key | Constants.CollectionItemOperation ("collectionItemOperation") |
COLLECTION_ITEM_OPERATION in payload and deepDelta |
| New collection item | Require AddToCollection on item when applicable |
Send operations.collectionItemOperation: 2 for new items |
| Certs account hostnames | Per-item patch semantics | deepDelta + hostnames array policy in EditAccount.tsx |
6. Related docs
- MaksIT.Core:
PatchOperation,PatchRequestModelBase(README / XML).
7. Current implementation vs reference (MaksIT-CertsUI)
7.1 Frontend (FE)
Checked: deepDelta usage in src/MaksIT.WebUI/src/forms/EditAccount.tsx for account PATCH; inline hostnames array policy; COLLECTION_ITEM_OPERATION in PatchOperation.ts and usage in deepDelta.ts.
- Safe: Hostname rows use
identityKey/idFieldKeyso itemized deltas includeAddToCollection/RemoveFromCollectionwhere appropriate. - Consistent: Same
PatchOperationvalues and collection-item key string as Core.
FE summary: Follows the shared reference; account PATCH covers account fields and hostnames.
7.2 Backend (BE)
Maintainers: Confirm the account PATCH handler in MaksIT.CertsUI WebAPI applies the same per-item rules (TryGetOperation(CollectionItemOperation, ...)) for hostnames as described in sections 2.2 and 3.
7.3 Gaps and maintenance
| Topic | Status | Note |
|---|---|---|
| Shared policies file | N/A in Certs | Inline policy in EditAccount.tsx for hostnames. |
| New forms with patchable collections | Ongoing | When adding a form that patches a collection, pass the correct arrays: { key: policy } to deepDelta. |
Last updated: 2026-04-12