Partial Resource Updates in ASP.NET Core: JSON Patch vs Nullable DTO vs Custom Update Strategy โ Enterprise Decision Guide

Partial resource updates are one of the most quietly contentious design decisions in ASP.NET Core API development. Every team hits the question eventually: a client sends a PATCH request to update two fields on a record that has twenty. What is the right server-side strategy? The answer shapes your API contract, your validation pipeline, your EF Core mapping, and how your consumers reason about state.
Three strategies dominate in practice: JSON Patch (RFC 6902), nullable DTO partial updates, and custom explicit update commands. Each solves the problem differently, and each carries trade-offs that only become visible under production load, evolving clients, and enterprise compliance requirements.
๐ Want implementation-ready .NET source code you can drop straight into your project? Join Coding Droplets on Patreon for exclusive tutorials, premium code samples, and early access to new content. ๐ https://www.patreon.com/CodingDroplets
What Makes Partial Updates Hard
Full replacement with PUT is simple: the client sends the entire resource, the server replaces the entity. No ambiguity. The complexity arrives the moment you allow clients to send only part of a resource.
The core problem is intent disambiguation. When a field is absent from a PATCH payload, it could mean two things:
- The client did not intend to change that field โ leave it alone.
- The client explicitly wants to set that field to null.
These two interpretations require fundamentally different mechanics. The asp.net core partial update strategy you choose determines how you resolve this ambiguity at every layer of your stack โ from the HTTP contract down to the database write.
Strategy 1: JSON Patch (RFC 6902)
JSON Patch is an RFC-standardised format for describing a sequence of operations to apply to a JSON document. Instead of sending a modified resource, the client sends a list of operations: add, remove, replace, move, copy, and test.
ASP.NET Core supports JSON Patch through the Microsoft.AspNetCore.JsonPatch package (which still depends on Newtonsoft.Json as of .NET 10). An endpoint accepts a JsonPatchDocument<TModel> parameter and calls .ApplyTo() to mutate the target object before persisting.
When JSON Patch Fits
JSON Patch is the right choice when your API surfaces a resource that clients need to manipulate with granular, operation-level control. It is particularly strong when:
- You expose a public or third-party API where the contract must be self-documenting and machine-readable.
- Clients need to atomically test-then-replace a value (the
testoperation enables optimistic concurrency at the payload level). - You are building a collaborative editing surface where individual field operations need to be logged, replayed, or audited per-operation.
- Your consumers are sophisticated and can generate RFC-compliant patch documents.
The test operation deserves special attention in enterprise contexts. It allows the client to assert the current value of a field before the replace operation is applied โ if the assertion fails, the entire patch document is rejected. This is a lightweight form of client-side optimistic concurrency that does not require ETags or version columns.
When JSON Patch Does Not Fit
JSON Patch carries real costs that are easy to underestimate.
The Newtonsoft.Json dependency is the most immediate friction point. In a modern ASP.NET Core 10 application standardised on System.Text.Json, adding JSON Patch pulls in the older serialisation stack, increases package surface, and creates a dual-serializer problem in projects that rely on custom System.Text.Json converters. A community SystemTextJson-based JSON Patch implementation exists but is not officially supported.
Validation is the second pressure point. When you apply a patch document to a model and then validate the result, you are validating after the fact rather than at the boundary. A missing required field introduced by a remove operation may only surface as a model state error after the mutation has already happened in memory, making the code harder to reason about.
Performance profiling on high-frequency PATCH endpoints also consistently shows that the JSON Patch operation pipeline introduces non-trivial per-request overhead at scale compared to direct DTO binding.
Anti-Patterns to Avoid
The most common mistake teams make with JSON Patch is skipping the DTO mapping layer. Applying the patch document directly to a domain entity, persisted object, or EF Core-tracked entity conflates the API contract with the data model. Any change to the domain entity immediately bleeds into the patch path contract. Always apply the patch to a dedicated API DTO, then map from DTO to entity.
A second anti-pattern is over-trusting the patch document path. Without a whitelist of allowed paths, a client can attempt to patch computed, internal, or security-sensitive fields. The ApplyTo method does not enforce path restrictions by default. Implement explicit path validation before applying.
Strategy 2: Nullable DTO Partial Updates
The nullable DTO strategy replaces the RFC-based approach with a simpler convention: every field in the update DTO is nullable. A null value means "do not change this field." A non-null value means "replace this field with the provided value."
This is the most widely adopted pattern in internal enterprise APIs because it maps naturally to how EF Core change tracking works and how developers already think about updates.
How the Semantics Work
The convention is intuitive: the client omits or sends null for fields it does not wish to update. The server checks each field: if non-null, apply the change; if null, skip. For fields where null is a valid business value โ such as optional profile fields โ you need a Nullable<T> wrapper (Optional<T> or a custom Patchable<T> type) that distinguishes between "not present" and "set to null."
When Nullable DTO Fits
This strategy is the natural fit for the majority of enterprise internal APIs where:
- Client teams are internal and can be coordinated.
- The partial update surface is stable โ fields change infrequently.
- You need seamless FluentValidation integration (nullable DTOs work naturally with standard pipeline behaviours).
- EF Core is your persistence layer and you want to generate targeted SQL
UPDATEstatements rather than full-entity updates.
The nullable DTO approach also integrates cleanly with MediatR commands and CQRS-style architectures. The update command becomes a direct, typed representation of intent โ far easier to unit-test than a JSON Patch pipeline.
If your team uses Chapter 11 of the ASP.NET Core Web API: Zero to Production course (Clean Architecture, CQRS, MediatR), nullable update commands fit naturally into that pattern โ each partial update maps to an explicit command with only the fields that matter for that operation.
The Null Disambiguation Problem
The limitation surfaces when a field has a valid null business state. Consider a notes field that a user explicitly wants to clear. If null means "skip," how does the client express "set to null"?
The idiomatic .NET solution is Optional<T> โ a value wrapper that distinguishes between three states: not provided, provided-as-null, and provided-with-value. Libraries like StronglyTypedId or a simple custom generic type provide this. The cost is a more complex DTO structure and custom serialisation handling. For most enterprise APIs where truly nullable fields are rare, this overhead is not worth the complexity.
Anti-Patterns to Avoid
Mixing nullable-means-skip with nullable-means-null in the same DTO is the most dangerous anti-pattern. Once the convention breaks down for a single field, the contract becomes ambiguous, and downstream consumers lose the ability to rely on the pattern.
Performing updates without targeting is the second common mistake. If your service layer simply maps the entire DTO onto the entity โ including all null fields โ you will accidentally overwrite values instead of skipping them. Always iterate over non-null properties explicitly, or use a mapping convention that respects the null-means-skip contract.
Strategy 3: Custom Explicit Update Commands
The third approach abandons the generic partial update concept entirely. Instead of one broad PATCH endpoint that handles any subset of fields, you model each update operation as a discrete, purpose-built endpoint or command.
Want to change a user's email? That is a PATCH /users/{id}/email endpoint accepting only a verified email payload. Want to change their role? That is PATCH /users/{id}/role. The partial update problem disappears because each endpoint is scoped to exactly the fields it modifies.
When Custom Commands Fit
This strategy is the correct choice when:
- Each update has distinct business rules, validation, side effects, or authorisation requirements.
- You are in a regulated domain (finance, healthcare, identity management) where each field change must be individually auditable.
- The update surface is stable and bounded โ you will not add dozens of new fields frequently.
- You are building a domain-rich API where operations carry semantic meaning beyond simple field replacement.
An UpdateUserEmailCommand is far more expressive than a PatchUserRequest with email set. It allows you to enforce email verification before the change, fire an EmailChangedDomainEvent, and write a typed audit log entry โ none of which are expressible through a generic patch mechanism without external logic.
The Cost: API Proliferation
Custom command endpoints scale poorly across large, flat resources with many independently updatable fields. A resource with twenty independent fields โ common in configuration-heavy or CMS-style APIs โ would require twenty PATCH endpoints. The developer experience degrades, and API documentation becomes repetitive.
The correct mitigation is to group semantically related fields into named operations rather than treating each field atomically. "Update contact details" groups name, phone, and address. "Update security settings" groups MFA flags, login timeout, and IP whitelist. If no natural grouping emerges, the resource itself is probably poorly modelled.
Is There a Hybrid Approach?
Yes, and it is worth naming. Many well-run enterprise teams use a hybrid: nullable DTO for standard read/update resources with low domain complexity, custom commands for operations that carry significant business rules or audit requirements, and JSON Patch only on purpose-built collaboration or document-style endpoints where the RFC format genuinely adds value.
The pattern is not a compromise. It is a deliberate mapping of mechanism to use case.
Decision Matrix
| Factor | JSON Patch | Nullable DTO | Custom Commands |
|---|---|---|---|
| Client sophistication | Requires RFC-compliant clients | Simple convention | Simple, purpose-built |
| Audit / compliance | Operation-level audit possible | Field-level audit requires extra logic | Best โ each command is typed and auditable |
| Validation | Post-apply, harder to integrate | Pre-apply, works with standard pipeline | Full pre-validation per command |
| Null disambiguation | Built-in (absence vs. remove op) | Requires Optional<T> for nullable fields |
N/A โ each command is explicit |
| EF Core integration | Complex โ apply then map | Natural โ map non-nulls | Natural โ targeted updates |
| System.Text.Json compatibility | Friction (.NET 10) | Full compatibility | Full compatibility |
| API surface growth | Single endpoint | Single endpoint | Grows with operations |
| Test complexity | High | Medium | Low |
How Should Your Team Decide?
Start by answering one question: does this update carry domain semantics, or is it a mechanical field replacement?
If mechanical โ a user updating their own profile fields with no business rules โ nullable DTO is the pragmatic choice. If the update has rules, events, or compliance requirements, custom commands are the defensible choice. If your API is document-like and your consumers are sophisticated external integrators, JSON Patch earns its complexity budget.
A practical tiebreaker: audit trail requirements. If your organisation requires logging which fields changed, when, by whom, and to what value, both nullable DTO and JSON Patch require additional instrumentation after the fact. Custom commands log intent at the call site โ the code is the audit trail.
Common Questions Teams Ask
Does JSON Patch work with System.Text.Json in .NET 10?
Not officially. The Microsoft.AspNetCore.JsonPatch package still depends on Newtonsoft.Json. Community alternatives exist but are not first-party supported. If your project is committed to System.Text.Json, nullable DTO or custom commands avoid the dependency entirely.
Can I use EF Core's ExecuteUpdate with nullable DTOs?
Yes. ExecuteUpdate with nullable DTO partial updates is one of the most efficient combinations available. Build the update call by checking each nullable field and chaining property setters conditionally. This generates a single targeted UPDATE statement without loading the entity first.
Is JSON Patch appropriate for aggregate roots in a DDD model?
Generally no. Aggregate roots should be mutated through domain methods that enforce invariants, not through generic operation sequences. Use custom commands that call aggregate methods directly.
What about OData PATCH?
OData provides its own partial update mechanism via Delta<T>, which tracks which properties have been explicitly set. It is a strong option if you are already committed to the OData protocol. If you are not, it adds significant framework overhead for a single use case.
โ Prefer a one-time tip? Buy us a coffee โ every bit helps keep the content coming!
FAQ
What is the difference between PUT and PATCH in ASP.NET Core? PUT replaces the entire resource โ the client sends all fields, and the server overwrites the entity completely. PATCH sends a partial update โ only the fields the client wants to change. The risk with PUT is that a client that sends only some fields may accidentally blank the others.
When should I use JSON Patch instead of a nullable DTO in ASP.NET Core?
Use JSON Patch when your clients are sophisticated external consumers who need RFC-compliant operation sequences, or when your resource behaves like a document that requires fine-grained operation semantics like test, move, or copy. For internal APIs with internal clients, nullable DTO is simpler and easier to validate.
How do I handle the case where null means "clear this field" in a partial update DTO?
Use a wrapper type like Optional<T> that has three states: not provided, provided-as-null, and provided-with-value. This requires a custom JSON converter but solves the null disambiguation problem cleanly without switching to JSON Patch.
Does ASP.NET Core JSON Patch support System.Text.Json in .NET 10?
Not officially. The Microsoft.AspNetCore.JsonPatch package still requires Newtonsoft.Json. If your application standardises on System.Text.Json, consider nullable DTO or custom update commands to avoid the dual-serializer overhead.
What is the most enterprise-safe strategy for partial updates in regulated industries? Custom explicit update commands. Each command is a typed, purpose-built operation that can carry its own validation, authorisation checks, audit events, and domain logic. In regulated environments like finance or healthcare, the ability to individually log, test, and authorise each distinct update operation is non-negotiable.
Can I mix strategies across endpoints in the same ASP.NET Core API? Yes, and it is often the right call. Use nullable DTOs for low-complexity profile and configuration updates, custom commands for operations with business rules or compliance requirements, and JSON Patch only where the RFC format genuinely adds client value. The key is to be deliberate and document the pattern per-endpoint rather than defaulting to one approach across the board.
How does FluentValidation integrate with nullable DTO partial updates?
Naturally. Define validators for the update DTO with null-conditional rules: RuleFor(x => x.Email).NotEmpty().EmailAddress().When(x => x.Email != null). FluentValidation applies rules only when the field is present, which matches the nullable-means-skip contract exactly.





