Mutations
The Problem
Section titled “The Problem”Updating an entity from a DTO is surprisingly tricky. Consider a typical scenario:
// The naive approach — manual property mappingpublic async Task UpdateOrder(Guid id, UpdateOrderRequest request, CancellationToken ct){ var order = await repo.GetByIdAsync(id, ct); if (order is null) return NotFound();
// You must manually map every nullable property if (request.Total.HasValue) order.SetTotal(request.Total.Value); if (request.Status.HasValue) order.SetStatus(request.Status.Value); if (request.ShippingAddress is not null) request.ShippingAddress.ApplyToEntity(order.ShippingAddress);
// What about collections? Replace? Merge? Append? if (request.Items is not null) { // This is where it gets really messy... foreach (var item in request.Items) { /* merge logic */ } }
await uow.SaveChangesAsync(ct);}For every DTO, you write the same if (property.HasValue) pattern. For collections, you implement merge/replace/append logic. With 30 update DTOs, this becomes thousands of lines of error-prone mapping code.
The Solution: Mutation<TEntity> + [Mutation]
Section titled “The Solution: Mutation<TEntity> + [Mutation]”A mutation is a class that inherits from Mutation<TEntity> and is marked with [Mutation]. You declare the properties you want to update, and the source generator produces the ApplyToEntity() method.
// ═══ What YOU write ═══[Mutation(Mode = MutationMode.Update)]public partial class UpdateOrderDto : Mutation<Order>{ public decimal? Total { get; init; } public OrderStatus? Status { get; init; }}// ═══ What the SG generates ═══public partial class UpdateOrderDto{ public override void ApplyToEntity(Order entity) { if (Total.HasValue) entity.SetTotal(Total.Value); if (Status.HasValue) entity.SetStatus(Status.Value); }}Why properties are nullable
Section titled “Why properties are nullable”Each property on the mutation DTO is nullable (decimal?, OrderStatus?). This means:
null= “don’t change this property” (skip it)- A value = “update this property to this value”
This gives you partial updates — you only send the fields you want to change. This is the same concept as PATCH in REST APIs.
public async Task UpdateOrder(Guid id, UpdateOrderDto dto, CancellationToken ct){ var order = await repo.GetByIdAsync(id, ct); if (order is null) return NotFound();
dto.ApplyToEntity(order); // One line — all mapping is generated await uow.SaveChangesAsync(ct);}Collection Strategies
Section titled “Collection Strategies”When your mutation includes a collection property, you need to decide how to update the collection. There are three strategies:
[Mutation]public partial class UpdateOrderDto{ [CollectionStrategy(CollectionMutationStrategy.Merge)] public List<UpdateLineDto>? Lines { get; init; }}| Strategy | What It Does | When to Use |
|---|---|---|
Replace | Clear the existing collection, add all items from the DTO | When the DTO represents the complete new state (“these are the new lines”) |
Merge | Match existing items by ID, update them; add new items; remove items not in the DTO | When the DTO represents incremental changes (“update line 1, add line 3, remove line 2”) |
Append | Add all items from the DTO, don’t touch existing items | When you only want to add new items (“add these lines to the order”) |
How Merge works
Section titled “How Merge works”The Merge strategy matches items by their PersistenceId:
// ═══ Generated merge logic (simplified) ═══foreach (var dtoItem in dto.Lines){ var existing = target.Items.FirstOrDefault(e => e.PersistenceId == dtoItem.Id); if (existing is not null) dtoItem.ApplyToEntity(existing); // Update existing item else target.Items.Add(LineItem.Create(dtoItem)); // Add new item}// Items in target.Items but NOT in dto.Lines are removedMutation Modes
Section titled “Mutation Modes”Mutations can operate in different modes depending on the use case:
[Mutation][Mutation(Mode = MutationMode.Update)] // Default: load entity, apply changes, savepublic partial class UpdateOrderDto { /* ... */ }
[Mutation][Mutation(Mode = MutationMode.Create)] // Create a new entity from the DTOpublic partial class CreateOrderDto { /* ... */ }
[Mutation][Mutation(Mode = MutationMode.Delete)] // Delete (or soft-delete) the entitypublic partial class DeleteOrderDto { /* ... */ }
[Mutation][Mutation(Mode = MutationMode.Restore)] // Restore a soft-deleted entitypublic partial class RestoreOrderDto { /* ... */ }| Mode | Generated Behavior |
|---|---|
Update | Loads entity by ID, calls ApplyToEntity(), saves |
Create | Creates a new entity via Create() factory, applies properties, saves |
Delete | Loads entity by ID, marks as deleted (soft-delete if [SoftDelete]), saves |
Restore | Loads entity by ID (bypassing soft-delete filter), resets IsDeleted / DeletedAt / DeletedBy, saves |
For [SoftDelete] entities, MutationMode.Delete automatically performs a soft-delete — you don’t need to specify this explicitly.
Nested Mutations
Section titled “Nested Mutations”Mutations can reference other mutation DTOs for nested entity updates:
[Mutation]public partial class UpdateOrderDto{ public decimal? Total { get; init; } public UpdateAddressDto? ShippingAddress { get; init; }}
[Mutation]public partial class UpdateAddressDto{ public string? Street { get; init; } public string? City { get; init; }}// ═══ Generated ApplyTo ═══public void ApplyToEntity(Order target){ if (Total.HasValue) target.SetTotal(Total.Value); ShippingAddress?.ApplyToEntity(target.ShippingAddress); // Nested apply}Generated MutationInvoker
Section titled “Generated MutationInvoker”When a mutation also has an [Endpoint] attribute, the SG generates a complete MutationInvoker that handles the full pipeline:
// ═══ Generated: UpdateOrderDtoMutationInvoker.g.cs ═══// This class handles:// 1. Load entity from repository (Update/Delete/Restore modes)// 2. Or create new entity (Create mode)// 3. Apply the mutation via ApplyToEntity()// 4. Save changes via IUnitOfWork// 5. Return Result<T, IError>This means you don’t need to write the load-apply-save boilerplate shown in the “Usage” section above — the invoker does it for you when combined with the endpoint pipeline.
Properties with required
Section titled “Properties with required”Properties marked required are mandatory in the mutation:
[Mutation][Mutation(Mode = MutationMode.Create)]public partial class CreateOrderDto{ public required string OrderNumber { get; init; } // Must be provided public required decimal Total { get; init; } // Must be provided public string? SpecialNotes { get; init; } // Optional}In MutationMode.Create, you typically use required for properties that the Create() factory needs. In MutationMode.Update, all properties are usually nullable (for partial updates).