Getting Started: Add PATCH Support to a Mutation
This guide walks through adding HTTP PATCH support to an entity using Pragmatic.Patch and the Pragmatic source generator.
Prerequisites
Section titled “Prerequisites”Pragmatic.PatchNuGet package referencedPragmatic.SourceGeneratorconfigured as analyzer- An entity with settable properties
Step 1: Define Your Entity
Section titled “Step 1: Define Your Entity”Start with an entity that has internal setters (the Pragmatic convention) or public setters:
public class Guest{ public Guid Id { get; set; } public string FirstName { get; set; } = ""; public string LastName { get; set; } = ""; public string? Email { get; set; } public string? Phone { get; set; } public int LoyaltyPoints { get; set; }
// Pragmatic setter convention (generated by Persistence SG) internal void SetFirstName(string value) => FirstName = value; internal void SetLastName(string value) => LastName = value; internal void SetEmail(string? value) => Email = value; internal void SetPhone(string? value) => Phone = value; internal void SetLoyaltyPoints(int value) => LoyaltyPoints = value;}Step 2: Declare the Patch DTO
Section titled “Step 2: Declare the Patch DTO”Create a partial record annotated with [GeneratePatch<TEntity>]:
using Pragmatic.Patch.Attributes;
[GeneratePatch<Guest>]public partial record UpdateGuestPatch;The source generator will populate this with:
Optional<string> FirstNameOptional<string> LastNameOptional<string?> EmailOptional<string?> PhoneOptional<int> LoyaltyPointsvoid ApplyTo(Guest entity)IReadOnlySet<string> ModifiedProperties
Properties like Id are automatically excluded.
Step 3: Wire Up the Endpoint
Section titled “Step 3: Wire Up the Endpoint”Minimal API
Section titled “Minimal API”app.MapPatch("/api/guests/{id}", async (Guid id, UpdateGuestPatch patch, AppDbContext db) =>{ var guest = await db.Guests.FindAsync(id); if (guest is null) return Results.NotFound();
patch.ApplyTo(guest); await db.SaveChangesAsync(); return Results.Ok(guest);});With Pragmatic.Actions
Section titled “With Pragmatic.Actions”If you use domain actions, inject the patch as an input:
public class UpdateGuest : DomainAction<GuestDto>{ public required Guid Id { get; init; } public required UpdateGuestPatch Patch { get; init; }
private IRepository<Guest, Guid> _repository = null!;
public override async Task<Result<GuestDto, IError>> Execute(CancellationToken ct = default) { var guest = await _repository.GetByIdAsync(Id, ct); if (guest is null) return new NotFoundError("Guest", Id);
Patch.ApplyTo(guest); await _repository.SaveAsync(ct); return guest.ToDto(); }}Step 4: Send a PATCH Request
Section titled “Step 4: Send a PATCH Request”The generated JSON converter handles all three states automatically.
Update only the email
Section titled “Update only the email”PATCH /api/guests/abc123Content-Type: application/json
{ "email": "newemail@example.com"}Result: only Email is updated. FirstName, LastName, Phone, LoyaltyPoints are untouched.
Clear the phone number
Section titled “Clear the phone number”PATCH /api/guests/abc123Content-Type: application/json
{ "phone": null}Result: Phone is set to null. All other fields are untouched.
Update multiple fields
Section titled “Update multiple fields”PATCH /api/guests/abc123Content-Type: application/json
{ "firstName": "Alice", "loyaltyPoints": 150}Result: FirstName and LoyaltyPoints are updated. Other fields untouched.
Empty body
Section titled “Empty body”PATCH /api/guests/abc123Content-Type: application/json
{}Result: nothing changes. All properties remain Undefined.
Step 5: Inspect Modified Properties
Section titled “Step 5: Inspect Modified Properties”You can check which fields were actually sent before applying:
var modified = patch.ModifiedProperties;// e.g., {"email", "loyaltyPoints"}
if (modified.Contains("Email")){ // The client explicitly sent an email value (or null)}This is useful for audit logging or conditional business logic.
Step 6: Conditional Logic with Optional<T>
Section titled “Step 6: Conditional Logic with Optional<T>”Use the Optional<T> API for fine-grained control:
// Execute only if the field was sentpatch.Email.IfPresent(email =>{ if (email is null) logger.LogInformation("Guest {Id} cleared their email", id); else logger.LogInformation("Guest {Id} changed email to {Email}", id, email);});
// Transform the valuevar upperName = patch.FirstName.Map(n => n?.ToUpperInvariant());Common Patterns
Section titled “Common Patterns”Validation Before Apply
Section titled “Validation Before Apply”if (patch.Email.HasValue && patch.Email.Value is not null){ if (!IsValidEmail(patch.Email.Value)) return new ValidationError("Invalid email format");}
patch.ApplyTo(guest);Selective Apply
Section titled “Selective Apply”If you need to apply only certain properties:
// Apply email onlyif (patch.Email.HasValue) guest.SetEmail(patch.Email.Value);
// Or check ModifiedPropertiesif (patch.ModifiedProperties.Contains("LoyaltyPoints")) guest.SetLoyaltyPoints(patch.LoyaltyPoints.Value);Combine with PUT
Section titled “Combine with PUT”Use the same entity for full replacement (PUT) and partial updates (PATCH):
// PUT: full replacement DTOpublic record UpdateGuestRequest{ public required string FirstName { get; init; } public required string LastName { get; init; } public string? Email { get; init; } public string? Phone { get; init; } public int LoyaltyPoints { get; init; }}
// PATCH: partial update[GeneratePatch<Guest>]public partial record UpdateGuestPatch;Troubleshooting
Section titled “Troubleshooting”PRAG1900: Type must be partial
Section titled “PRAG1900: Type must be partial”// Wrong[GeneratePatch<Guest>]public record UpdateGuestPatch; // Missing 'partial'
// Correct[GeneratePatch<Guest>]public partial record UpdateGuestPatch;PRAG1902: No patchable properties
Section titled “PRAG1902: No patchable properties”The entity has no settable properties. Check that at least one property has either a public setter or a Set* method:
// No settable properties -- PRAG1902public class Immutable{ public Guid Id { get; } public string Name { get; }}
// Has settable properties -- workspublic class Mutable{ public Guid Id { get; } public string Name { get; set; }}Properties not appearing in generated code
Section titled “Properties not appearing in generated code”Check if the property falls into one of the excluded categories (identity, auditing, soft delete, navigation). See the README for the full list.