Response DTOs, mapping, ApplyWithChanges
ApplyWithChanges() (Update DTOs only)
Section titled “ApplyWithChanges() (Update DTOs only)”Like ApplyTo() but returns a tuple with the list of actually changed field names. Available on Update, Combined, and UpdateDtoFor requests:
var (changedFields, entity) = request.ApplyWithChanges(existingProduct);// changedFields: ["price", "stock"]// Useful for audit logs, webhooks, selective cache invalidationResponse DTO ([ResponseDto])
Section titled “Response DTO ([ResponseDto])”Generates a read-only record for GET responses with FromEntity() and IQueryable ProjectFrom():
[CreateDto][UpdateDto][ResponseDto]public class Player{ public int Id { get; set; } public required string Name { get; set; }
[DtoIgnore(DtoTarget.Response)] public required string Password { get; set; }}// Generated — plain properties, no PatchFieldpublic record PlayerResponse{ public int Id { get; init; } // DtoIgnore(DtoTarget.Create|Update|Query) doesn't affect Response public string Name { get; init; } // Password excluded by [DtoIgnore(DtoTarget.Response)]
public static PlayerResponse FromEntity(Player entity) => ...; public static IQueryable<PlayerResponse> ProjectFrom(IQueryable<Player> query) => ...;}
// Usage[HttpGet("{id}")]public IActionResult Get(int id){ var player = _db.Players.Find(id); return Ok(PlayerResponse.FromEntity(player));}
// EF Core projection — only fetches needed columns[HttpGet]public IActionResult List(){ var responses = PlayerResponse.ProjectFrom(_db.Players).ToList(); return Ok(responses);}Auto-recursive nested DTOs
Section titled “Auto-recursive nested DTOs”When a model has [CreateDto] or [UpdateDto], nested complex type properties automatically get their own DTOs generated — no need to annotate nested types. This works recursively to any depth, with deduplication (if a nested type already has an explicit [UpdateDto], its DTO is reused).
3-level example — Employee → Company → ContactInfo
Section titled “3-level example — Employee → Company → ContactInfo”// Your models — only the top level has attributes:[CreateDto][UpdateDto]public class Employee{ [DtoIgnore(DtoTarget.Create | DtoTarget.Update | DtoTarget.Query)] public int Id { get; set; } public required string Name { get; set; } public Company? Company { get; set; } // Level 2 — auto-generated}
public class Company{ public required string Name { get; set; } public ContactInfo? Contact { get; set; } // Level 3 — auto-generated}
public class ContactInfo{ public required string Phone { get; set; } public string? Fax { get; set; }}The generator produces three Update request records from a single [UpdateDto]:
// Generated — Level 1public record UpdateEmployeeRequest : ICanApply<Employee>, ICanValidate{ public PatchField<string> Name { get; init; } public PatchField<UpdateCompanyRequest?> Company { get; init; } // nested DTO, not Company
public void ApplyTo(Employee target) { if (Name.HasValue) target.Name = Name.Value!; if (Company.HasValue) { if (Company.Value is null) target.Company = null; // explicit clear else if (target.Company is not null) Company.Value.ApplyTo(target.Company); // recursive partial update } }}
// Generated — Level 2 (auto, no attribute on Company)public record UpdateCompanyRequest : ICanApply<Company>, ICanValidate{ public PatchField<string> Name { get; init; } public PatchField<UpdateContactInfoRequest?> Contact { get; init; }
public void ApplyTo(Company target) { if (Name.HasValue) target.Name = Name.Value!; if (Contact.HasValue) { if (Contact.Value is null) target.Contact = null; else if (target.Contact is not null) Contact.Value.ApplyTo(target.Contact); // chain continues } }}
// Generated — Level 3 (auto, leaf)public record UpdateContactInfoRequest : ICanApply<ContactInfo>, ICanValidate{ public PatchField<string> Phone { get; init; } public PatchField<string?> Fax { get; init; }
public void ApplyTo(ContactInfo target) { if (Phone.HasValue) target.Phone = Phone.Value!; if (Fax.HasValue) target.Fax = Fax.Value; }}Now a 3-level-deep partial update is a single PATCH:
PATCH /api/employees/1{ "company": { "contact": { "fax": null } }}Only employee.Company.Contact.Fax is cleared. Company.Name, Contact.Phone, Employee.Name — all untouched. Each level’s ApplyTo checks HasValue independently, so the partial-update semantics compose naturally without any manual wiring.
Create DTOs — same recursive pattern
Section titled “Create DTOs — same recursive pattern”[CreateDto] follows the same structure, but with ToEntity() that chains construction:
// Generatedpublic record CreateEmployeeRequest : ICanCreate<Employee>, ICanValidate{ public PatchField<string> Name { get; init; } public PatchField<CreateCompanyRequest?> Company { get; init; }
public Employee ToEntity() { return new Employee { Name = Name.HasValue ? Name.Value! : default!, Company = Company.HasValue && Company.Value is not null ? Company.Value.ToEntity() // recursive construction : default, }; }}Key patterns in the generated code
Section titled “Key patterns in the generated code”-
PatchField<UpdateXxxRequest?>wrapping — the nested type in the parent DTO is the generated request, not the original entity. This is what makes tri-state tracking recursive:Company.HasValue == falsemeans “don’t touch Company at all”,Company.Value == nullmeans “clear Company”,Company.Value != nullmeans “apply partial changes to Company’s fields”. -
Null-safe
ApplyTochaining — the generator emitsif (target.Company is not null)before calling the nestedApplyTo. If the parent’s navigation is null and the client sends a partial update to it, the update is silently skipped (you can’tApplyToa null target). To create a new nested object from a PATCH, the client should use a full object value, not a partial one. -
Deduplication — if
ContactInfois used in multiple parent types (Employee.Company.ContactandProject.Lead), the generator emitsUpdateContactInfoRequestonce and reuses it everywhere. -
ProjectFrom()skips nested properties — in the Response DTO,ProjectFrom()(LINQ-to-SQL projection) does not project nested objects because EF Core requires.Include()for navigation properties. UseFromEntity()with.Include()for nested responses.
Nested type mapping in Response
Section titled “Nested type mapping in Response”When a property’s type also has [ResponseDto], the generator uses the nested response DTO and maps via FromEntity() with null checks:
// Generatedpublic record OrderResponse{ public int Id { get; init; } public string Title { get; init; } public OrderLineResponse? Line { get; init; }
public static OrderResponse FromEntity(Order entity) { return new OrderResponse { Id = entity.Id, Title = entity.Title, Line = entity.Line is not null ? OrderLineResponse.FromEntity(entity.Line) // null-safe nested mapping : null, }; }
public static IQueryable<OrderResponse> ProjectFrom(IQueryable<Order> query) { return query.Select(x => new OrderResponse { Id = x.Id, Title = x.Title, // Line is NOT projected — use FromEntity() with .Include(x => x.Line) instead }); }}ProjectFrom() is EF Core-safe (no navigation property access in the LINQ expression). For nested data, load via Include and map with FromEntity:
var order = await db.Orders.Include(o => o.Line).FirstAsync(o => o.Id == id);return OrderResponse.FromEntity(order); // nested Line is mapped via OrderLineResponse.FromEntityFlatten nested properties ([Flatten])
Section titled “Flatten nested properties ([Flatten])”Collapses nested object properties into the parent DTO:
[ResponseDto]public class Store{ public string Name { get; set; }
[Flatten] public Address? Location { get; set; }}// Generated StoreResponse has: LocationStreet, LocationCity, LocationZipCode// FromEntity maps: entity.Location?.Street → LocationStreetValidation attribute propagation
Section titled “Validation attribute propagation”Attributes from System.ComponentModel.DataAnnotations are automatically copied to generated DTOs:
public class User{ [ZMaxLength(100)] [ZEmail] public required string Email { get; set; }
[ZRange(1, 999)] public int Quantity { get; set; }}// Generated CreateUserRequest.Email has [ZMaxLength(100)] and [ZEmail]Set-once (immutable) fields
Section titled “Set-once (immutable) fields”Migration note: The
[Immutable]attribute has been removed. Use[DtoIgnore(DtoTarget.Update)]instead — the property won’t appear in the PATCH DTO at all, which is cleaner than silently ignoring changes.
[CreateDto][UpdateDto]public class Article{ public required string Title { get; set; }
[DtoIgnore(DtoTarget.Update)] public required string Slug { get; set; } // set at creation, never changed}Diff(T entity) method
Section titled “Diff(T entity) method”Update DTOs include Diff() — compares request with an entity and returns changed field names:
var changes = request.Diff(existingProduct);// ["price", "stock"] — useful for audit logs
if (changes.Count == 0) return NoContent(); // nothing actually changedDtoMapper
Section titled “DtoMapper”Generic runtime mapper for copying properties between objects by matching names:
var copy = DtoMapper.Map<Product, ProductDto>(product);DtoMapper.MapTo(source, target);Swagger / OpenAPI support
Section titled “Swagger / OpenAPI support”When Swashbuckle.AspNetCore is detected at compile time, the generator automatically emits a PatchFieldSchemaFilter that unwraps PatchField<T> to its inner type in the Swagger schema — no manual registration needed. Just install the package:
dotnet add package Swashbuckle.AspNetCoreWithout Swashbuckle, PatchField<T> shows as { "hasValue": true, "value": "Bob" } in the OpenAPI schema. With it, the schema filter collapses it to just "Bob" (or null | "Bob" for nullable types).
Both Swashbuckle legacy (pre-v10) and v10+ with
IOpenApiSchemaare supported — the generator detects the API surface at compile time and emits the correct filter variant.