ZibStack.NET.Dto
A C# source generator that produces strongly-typed Create, Update, Response, and Query DTOs from your domain models, and optionally generates full CRUD API endpoints (Minimal API + MVC Controllers). No reflection, no runtime overhead. Supports generics, inheritance, nested types, flattening, validation propagation, and more.
Two configuration styles, mixable per-class:
- Attribute markers —
[CreateDto]/[UpdateDto]/[CrudApi]/etc. for locality. - Fluent
IDtoConfigurator— central project-wide config with a typed builder. Useful for centralizing settings, configuring third-party types you can’t annotate, or overriding settings without editing the model. See Fluent configuration below.
See the working sample: SampleApi on GitHub
Install
Section titled “Install”dotnet add package ZibStack.NET.Dtodotnet add package ZibStack.NET.CoreZibStack.NET.Core provides TypeScript-style utility types ([PartialFrom], [IntersectFrom], [PickFrom], [OmitFrom]).
Quick start
Section titled “Quick start”Mark your model with [CreateDto] and/or [UpdateDto]:
using ZibStack.NET.Dto;
[CreateDto][UpdateDto]public class Player{ [DtoIgnore(DtoTarget.Create | DtoTarget.Update | DtoTarget.Query)] public int Id { get; set; }
public required string Name { get; set; } public int Level { get; set; } public string? Email { get; set; }}Register the JSON converter:
// Minimal API:builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Converters.Add(new PatchFieldJsonConverterFactory()));
// Or MVC Controllers:builder.Services.AddControllers() .AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add( new PatchFieldJsonConverterFactory()));Separate ([CreateDto] + [UpdateDto])
Section titled “Separate ([CreateDto] + [UpdateDto])”Generates two records — CreatePlayerRequest and UpdatePlayerRequest:
[HttpPost]public IActionResult Create([FromBody] CreatePlayerRequest request){ var validation = request.Validate(); if (!validation.IsValid) return BadRequest(new { validation.Errors });
Player player = request.ToEntity(); return Ok(player);}
[HttpPatch("{id}")]public IActionResult Update(int id, [FromBody] UpdatePlayerRequest request){ var validation = request.Validate(); if (!validation.IsValid) return BadRequest(new { validation.Errors });
request.ApplyTo(existingPlayer); return Ok(existingPlayer);}Combined ([CreateOrUpdateDto])
Section titled “Combined ([CreateOrUpdateDto])”Generates a single TeamRequest with ValidateForCreate() and ValidateForUpdate():
[CreateOrUpdateDto]public class Team{ [DtoIgnore(DtoTarget.Create | DtoTarget.Update | DtoTarget.Query)] public int Id { get; set; }
public required string Name { get; set; } public string? Description { get; set; } public int MaxMembers { get; set; }}[HttpPost]public IActionResult Create([FromBody] TeamRequest request){ var validation = request.ValidateForCreate(); if (!validation.IsValid) return BadRequest(new { validation.Errors });
Team team = request.ToEntity(); return Ok(team);}
[HttpPatch("{id}")]public IActionResult Update(int id, [FromBody] TeamRequest request){ var validation = request.ValidateForUpdate(); if (!validation.IsValid) return BadRequest(new { validation.Errors });
request.ApplyTo(existingTeam); return Ok(existingTeam);}What gets generated
Section titled “What gets generated”Separate
Section titled “Separate”public record CreatePlayerRequest{ public PatchField<string> Name { get; init; } // required in Validate() public PatchField<int> Level { get; init; } // optional public PatchField<string?> Email { get; init; } // optional, nullable
public DtoValidationResult Validate() { ... } public Player ToEntity() { ... }}
public record UpdatePlayerRequest{ public PatchField<string> Name { get; init; } // optional, but non-null if sent public PatchField<int> Level { get; init; } public PatchField<string?> Email { get; init; }
public DtoValidationResult Validate() { ... } public void ApplyTo(Player target) { ... }}Combined
Section titled “Combined”public record TeamRequest{ public PatchField<string> Name { get; init; } public PatchField<string?> Description { get; init; } public PatchField<int> MaxMembers { get; init; }
public DtoValidationResult ValidateForCreate() { ... } public DtoValidationResult ValidateForUpdate() { ... } public Team ToEntity() { ... } public void ApplyTo(Team target) { ... }}PatchField<T> distinguishes three states: not sent (HasValue = false), sent with value, and sent as null. It has implicit operators so you can assign and read values directly:
// Assignment -- no need for new PatchField<string>("Bob")var request = new CreatePlayerRequest { Name = "Bob", Level = 5 };
// Reading -- no need for .Valuestring name = request.Name;For custom logic (audit logging, conditional business rules), PatchField<T> works naturally with C# pattern matching:
switch (request.Email){ case { HasValue: false }: break; // not sent — leave unchanged case { HasValue: true, Value: null }: player.Email = null; break; // explicit clear case { HasValue: true, Value: var v }: player.Email = v; break; // new value}See the PatchField Tri-State guide for the full walkthrough — why nullable DTOs can’t distinguish “not sent” from “set to null”, and how PatchField<T> solves it.
Interfaces
Section titled “Interfaces”Generated types implement generic interfaces for type-safe generic handlers:
| Interface | Implemented by | Method |
|---|---|---|
ICanCreate<T> | Create requests, Combined | T ToEntity() |
ICanApply<T> | Update requests, Combined | void ApplyTo(T target) |
ICanValidate | Create/Update requests (not Combined) | DtoValidationResult Validate() |
Combined requests implement ICanCreate<T> and ICanApply<T> but not ICanValidate (they have ValidateForCreate()/ValidateForUpdate() instead).
// Generic handler using interfacespublic IActionResult HandleCreate<T>(ICanCreate<T> request) where T : class{ if (request is ICanValidate validatable) { var validation = validatable.Validate(); if (!validation.IsValid) return BadRequest(validation.Errors); } var entity = request.ToEntity(); return Ok(entity);}Read more
Section titled “Read more”This page covers the core mode + generated shape. Detailed reference and feature pages:
- Attributes reference — every class- and property-level attribute, what it does, when to use it.
- Fluent IDtoConfigurator — central project-wide config with a typed builder.
- External types (
[CreateDtoFor]/[UpdateDtoFor]) — DTOs for types you don’t own. - Utility types — Pick / Omit / Partial / Intersect from
ZibStack.NET.Core. - Query DTO (
[QueryDto]) — filter + sort + pagination DTOs. PaginatedResponse<T>— the standard list wrapper.- CRUD API (
[CrudApi]) — full endpoint generation. - Response DTOs, mapping,
ApplyWithChanges—FromEntity/ProjectFrom, nested + flatten, Diff, DtoMapper, Swagger. - JSON serializer & custom validation —
PatchFieldJSON registration + FluentValidation.
Related guides
Section titled “Related guides”Requirements
Section titled “Requirements”- .NET Standard 2.0+ (generator runs in any C# 12+ project)
- C# 12+ (
requiredkeyword, primary constructors)
License
Section titled “License”MIT.