Skip to content

Mutations

Updating an entity from a DTO is surprisingly tricky. Consider a typical scenario:

// The naive approach — manual property mapping
public 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);
}
}

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);
}

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; }
}
StrategyWhat It DoesWhen to Use
ReplaceClear the existing collection, add all items from the DTOWhen the DTO represents the complete new state (“these are the new lines”)
MergeMatch existing items by ID, update them; add new items; remove items not in the DTOWhen the DTO represents incremental changes (“update line 1, add line 3, remove line 2”)
AppendAdd all items from the DTO, don’t touch existing itemsWhen you only want to add new items (“add these lines to the order”)

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 removed

Mutations can operate in different modes depending on the use case:

[Mutation]
[Mutation(Mode = MutationMode.Update)] // Default: load entity, apply changes, save
public partial class UpdateOrderDto { /* ... */ }
[Mutation]
[Mutation(Mode = MutationMode.Create)] // Create a new entity from the DTO
public partial class CreateOrderDto { /* ... */ }
[Mutation]
[Mutation(Mode = MutationMode.Delete)] // Delete (or soft-delete) the entity
public partial class DeleteOrderDto { /* ... */ }
[Mutation]
[Mutation(Mode = MutationMode.Restore)] // Restore a soft-deleted entity
public partial class RestoreOrderDto { /* ... */ }
ModeGenerated Behavior
UpdateLoads entity by ID, calls ApplyToEntity(), saves
CreateCreates a new entity via Create() factory, applies properties, saves
DeleteLoads entity by ID, marks as deleted (soft-delete if [SoftDelete]), saves
RestoreLoads 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.

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
}

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 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).