SmartDto.NET
1.0.2
See the version list below for details.
dotnet add package SmartDto.NET --version 1.0.2
NuGet\Install-Package SmartDto.NET -Version 1.0.2
<PackageReference Include="SmartDto.NET" Version="1.0.2"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="SmartDto.NET" Version="1.0.2" />
<PackageReference Include="SmartDto.NET"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add SmartDto.NET --version 1.0.2
#r "nuget: SmartDto.NET, 1.0.2"
#:package SmartDto.NET@1.0.2
#addin nuget:?package=SmartDto.NET&version=1.0.2
#tool nuget:?package=SmartDto.NET&version=1.0.2
SmartDto.NET
A C# source generator that produces strongly-typed Create and Update request DTOs from your domain models. No reflection, no runtime overhead.
Install
dotnet add package SmartDto.NET
Quick start
Mark your model with [Dto]:
using SmartDto.NET;
[Dto]
public class Player
{
[DtoIgnore]
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:
builder.Services.AddControllers()
.AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(
new PatchFieldJsonConverterFactory()));
Modes
Separate mode (default)
Generates two records -- CreatePlayerRequest and UpdatePlayerRequest:
[HttpPost]
public IActionResult Create([FromBody] CreatePlayerRequest request)
{
var errors = request.Validate();
if (errors.Count > 0)
return BadRequest(new { errors });
Player player = request.ToEntity();
return Ok(player);
}
[HttpPatch("{id}")]
public IActionResult Update(int id, [FromBody] UpdatePlayerRequest request)
{
var errors = request.Validate();
if (errors.Count > 0)
return BadRequest(new { errors });
request.ApplyTo(existingPlayer);
return Ok(existingPlayer);
}
Combined mode
Generates a single PlayerRequest with ValidateForCreate() and ValidateForUpdate():
[Dto(Mode = DtoMode.Combined)]
public class Team
{
[DtoIgnore]
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 errors = request.ValidateForCreate();
if (errors.Count > 0)
return BadRequest(new { errors });
Team team = request.ToEntity();
return Ok(team);
}
[HttpPatch("{id}")]
public IActionResult Update(int id, [FromBody] TeamRequest request)
{
var errors = request.ValidateForUpdate();
if (errors.Count > 0)
return BadRequest(new { errors });
request.ApplyTo(existingTeam);
return Ok(existingTeam);
}
What gets generated
Separate mode
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 List<string> 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 List<string> Validate() { ... }
public void ApplyTo(Player target) { ... }
}
Combined mode
public record TeamRequest
{
public PatchField<string> Name { get; init; }
public PatchField<string?> Description { get; init; }
public PatchField<int> MaxMembers { get; init; }
public List<string> ValidateForCreate() { ... }
public List<string> 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 .Value
string name = request.Name;
Attributes
| Attribute | Target | Description |
|---|---|---|
[Dto] |
Class | Enables generation (Separate mode) |
[Dto(Mode = DtoMode.Combined)] |
Class | Generates a single DTO |
[DtoFor(typeof(T))] |
Class | Generates DTOs for an external type T using this class as mapping |
[PartialFrom(typeof(T))] |
Record (partial) | Generates PatchField properties + ApplyTo() for all properties of T |
[IntersectFrom(typeof(T))] |
Record (partial) | Combine multiple types into one (like TS &). Apply multiple times. |
[DtoIgnore] |
Property | Excludes from generated DTOs |
[DtoName("json_name")] |
Property | Overrides the JSON property name |
[CreateOnly] |
Property | Only included in Create (or ValidateForCreate/ToEntity in Combined) |
[UpdateOnly] |
Property | Only included in Update (or ValidateForUpdate/ApplyTo in Combined) |
required keyword
Properties marked required are validated as mandatory in create validation. In update validation, all properties are optional.
Custom names
// Separate mode
[Dto(CreateName = "NewPlayerDto", UpdateName = "EditPlayerDto")]
public class Player { ... }
// Combined mode
[Dto(Mode = DtoMode.Combined, RequestName = "PlayerDto")]
public class Player { ... }
Create/Update-only properties
[Dto]
public class Player
{
public required string Name { get; set; }
[CreateOnly]
public required string Password { get; set; } // only in CreatePlayerRequest
[UpdateOnly]
public string? DeactivationReason { get; set; } // only in UpdatePlayerRequest
}
In Combined mode, [CreateOnly] properties are included in the record but excluded from ValidateForUpdate() and ApplyTo(). [UpdateOnly] properties are excluded from ValidateForCreate() and ToEntity().
External types ([DtoFor])
For classes you don't control (e.g. from a NuGet package), create a mapping class:
// External class you can't modify
public class ExternalOrder
{
public int Id { get; set; }
public string ProductName { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
public string? Notes { get; set; }
}
// Your mapping — defines which properties appear in the DTO
[DtoFor(typeof(ExternalOrder))]
public class OrderMapping
{
public required string ProductName { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
public string? Notes { get; set; }
// Id is not here — excluded from DTO
}
This generates CreateExternalOrderRequest and UpdateExternalOrderRequest with ToEntity() returning ExternalOrder and ApplyTo(ExternalOrder target).
[DtoFor] supports all the same options: Mode, CreateName, UpdateName, RequestName, CreateValidator, UpdateValidator, [CreateOnly], [UpdateOnly], [DtoIgnore], [DtoName].
Partial types ([PartialFrom])
Like TypeScript's Partial<T> -- generates a class where every property is optional:
[PartialFrom(typeof(Player))]
public partial record PartialPlayer;
Generates:
public partial record PartialPlayer
{
public PatchField<string> Name { get; init; }
public PatchField<int> Level { get; init; }
public PatchField<string?> Email { get; init; }
// ... all public properties from Player
public void ApplyTo(Player target) { ... }
}
All properties from the target type are included (no [DtoIgnore] filtering). The record is partial so you can add your own methods and properties. No Validate() or ToEntity() -- just ApplyTo().
Intersection types ([IntersectFrom])
Like TypeScript's & operator -- combine properties from multiple types into one:
[IntersectFrom(typeof(Player))]
[IntersectFrom(typeof(Address))]
public partial record PlayerWithAddress;
Generates a single record with all properties from both types (deduplicated by name), and a separate ApplyTo() overload for each source type:
public partial record PlayerWithAddress
{
public PatchField<string> Name { get; init; } // from Player
public PatchField<int> Level { get; init; } // from Player
public PatchField<string> Street { get; init; } // from Address
public PatchField<string> City { get; init; } // from Address
// ...
public void ApplyTo(Player target) { ... }
public void ApplyTo(Address target) { ... }
}
JSON serializer support
The generator detects which serializers are available and produces the corresponding converters:
| Serializer | Generated converter | Registration |
|---|---|---|
| System.Text.Json | PatchFieldJsonConverterFactory |
options.Converters.Add(new PatchFieldJsonConverterFactory()) |
| Newtonsoft.Json | PatchFieldNewtonsoftConverter |
settings.Converters.Add(new PatchFieldNewtonsoftConverter()) |
Both are generated if both packages are referenced. No converter is auto-registered -- you choose which one to use, or write your own.
Custom validation
Simple validator
Implement IDtoValidator<T> and point to it from the attribute:
public class MyCreateValidator : IDtoValidator<CreatePlayerRequest>
{
public List<string> Validate(CreatePlayerRequest instance)
{
var errors = new List<string>();
if (instance.Name.HasValue && instance.Name.Value.Length < 3)
errors.Add("Name must be at least 3 characters.");
return errors;
}
}
[Dto(CreateValidator = typeof(MyCreateValidator))]
public class Player { ... }
When a validator is set, Validate() delegates entirely to it -- the default generated rules are replaced.
FluentValidation
When FluentValidation is installed, the generator additionally produces:
FluentDtoValidator<T>-- base class bridging FluentValidation withIDtoValidator<T>{RequestName}CreateBaseValidator-- contains the generated required/null rules{RequestName}UpdateBaseValidator-- contains the generated null rules
Inherit to extend:
public class MyCreateValidator : CreatePlayerRequestCreateBaseValidator
{
public MyCreateValidator()
{
RuleFor(x => x.Name)
.Must(f => !f.HasValue || f.Value.Length >= 3)
.WithMessage("Name must be at least 3 characters.");
}
}
[Dto(CreateValidator = typeof(MyCreateValidator))]
public class Player { ... }
Or start from scratch:
public class MyCreateValidator : FluentDtoValidator<CreatePlayerRequest>
{
public MyCreateValidator()
{
// your rules only
}
}
Requirements
- .NET 6+ (or .NET Framework with SDK-style projects and System.Text.Json NuGet)
requiredkeyword needs C# 11 / .NET 7+ (optional -- without it all properties are optional in Create)
License
MIT
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.