MergePatchDto 0.2.0
dotnet add package MergePatchDto --version 0.2.0
NuGet\Install-Package MergePatchDto -Version 0.2.0
<PackageReference Include="MergePatchDto" Version="0.2.0" />
<PackageVersion Include="MergePatchDto" Version="0.2.0" />
<PackageReference Include="MergePatchDto" />
paket add MergePatchDto --version 0.2.0
#r "nuget: MergePatchDto, 0.2.0"
#:package MergePatchDto@0.2.0
#addin nuget:?package=MergePatchDto&version=0.2.0
#tool nuget:?package=MergePatchDto&version=0.2.0
MergePatchDto
MergePatchDto is a NuGet package for .NET APIs that want DTO-shaped PATCH
request bodies while still distinguishing omitted properties, explicit null,
and explicit values. It keeps PATCH bodies as ordinary JSON objects instead of
JSON Patch operation arrays.
It gives PATCH endpoints:
- a typed allowlist of patchable fields
- explicit null vs missing tracking
- optional generated
ApplyTomapping
That lets a PATCH endpoint distinguish:
- missing property: leave the target unchanged
- explicit
null: assign or clear the target value - explicit value: assign the target value
Install
dotnet add package MergePatchDto
Quick Start
Define a partial patch DTO and point it at the type it updates:
using MergePatch;
using YourApi.Models;
[MergePatch(typeof(Person))]
public partial class UpdatePersonPatch
{
public string Name { get; set; } = "";
public string? Email { get; set; }
public string? Bio { get; set; }
}
The patch DTO is the allowlist: only properties declared on
UpdatePersonPatch can be updated through this endpoint. The target type
enables generated ApplyTo; it does not make every Person property
patchable.
Use it in a normal PATCH endpoint after loading the entity being updated:
[HttpPatch("{id:guid}")]
public async Task<IActionResult> PatchPerson(
Guid id,
UpdatePersonPatch patch,
CancellationToken cancellationToken)
{
var person = await db.People.FindAsync([id], cancellationToken);
if (person is null)
{
return NotFound();
}
patch.ApplyTo(person);
await db.SaveChangesAsync(cancellationToken);
return NoContent();
}
ApplyTo only applies fields that were present in the JSON body. For example:
{
"bio": null
}
This clears Person.Bio and leaves every omitted property unchanged.
The same presence information is available directly when an endpoint needs custom logic:
if (patch.Has.Bio)
{
person.Bio = patch.Bio;
}
Use C# nullability to model whether a field may be set to null; presence is
tracked separately through the generated Has API. A property does not need to
be nullable just to be optional in the request body. Omitted fields are skipped
because Has is false. This patch DTO uses nullable properties where the API
accepts explicit clearing.
For renamed target properties, accepted client metadata, or domain-specific update methods, use the mapping attributes described below.
Manual Updates
Generated ApplyTo is useful when patch properties map cleanly to a target type.
When validation or domain logic needs to act only on fields sent by the client,
use the generated Has API directly.
Targetless [MergePatch] DTOs are for endpoints that want presence tracking
but own the update logic. Omit the target type:
using MergePatch;
[MergePatch]
public partial class ManualPersonPatch
{
public bool? IsActive { get; set; }
public string? AdminNote { get; set; }
}
MergePatchDto still generates the JSON converter and Has API, but no
ApplyTo method:
var has = patch.Has;
if (has.IsActive)
{
person.IsActive = patch.IsActive.GetValueOrDefault();
person.DeactivatedAt = person.IsActive ? null : DateTimeOffset.UtcNow;
}
if (has.AdminNote)
{
person.AdminNote = patch.AdminNote;
}
Mapping Attributes
By default, patch properties map to target properties with the same CLR name. If a target property should not be patchable, leave it off the DTO.
[PatchTo(nameof(Target.OtherName))]maps a patch property to a differently named target property.[PatchIgnore]is for accepted DTO properties that should still be deserialized and presence-tracked, but excluded from generatedApplyTo.[PatchUsing(nameof(Method))]calls custom domain logic when the patch property was provided.- Targets can be interfaces when the same patch shape should apply to multiple concrete types.
[MergePatch(typeof(Person))]
public partial class UpdatePersonPatch
{
[JsonPropertyName("phone")]
[PatchTo(nameof(Person.PhoneNumber))]
public string? Phone { get; set; }
[PatchIgnore]
public string? RequestId { get; set; }
[PatchUsing(nameof(ApplyAge))]
public int? Age { get; set; }
private static void ApplyAge(Person target, int? value)
{
target.SetAge(value);
}
}
Supported custom apply signatures are:
private static void ApplyAge(Person target, int? value)
private static void ApplyAge(UpdatePersonPatch patch, Person target, int? value)
Target-specific mapping attributes are invalid on targetless patch types.
Invalid patch shapes and mappings fail at build time with MPDxxx diagnostics.
JSON Behavior
MergePatchDto uses System.Text.Json for patch DTOs. It records top-level
property presence, including properties sent as null.
Nested object properties are replacement values, not recursive document merges. Arrays are replacement values, not element-level merges.
Unknown JSON properties are ignored by default. To reject them:
[MergePatch(UnknownPropertyHandling = UnknownPropertyHandling.Reject)]
public partial class StrictPatch
{
public string? Name { get; set; }
}
Compatibility
MergePatchDto supports SDK-style projects built with the .NET 8 SDK or newer.
The runtime assembly targets netstandard2.0, but the source generator runs
inside the consumer's C# compiler, so the supported compiler floor is the .NET
8 SDK / Roslyn 4.8.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.