SmartDto.NET 1.0.2

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SmartDto.NET" Version="1.0.2">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SmartDto.NET" Version="1.0.2" />
                    
Directory.Packages.props
<PackageReference Include="SmartDto.NET">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SmartDto.NET --version 1.0.2
                    
#r "nuget: SmartDto.NET, 1.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SmartDto.NET@1.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SmartDto.NET&version=1.0.2
                    
Install as a Cake Addin
#tool nuget:?package=SmartDto.NET&version=1.0.2
                    
Install as a Cake Tool

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 with IDtoValidator<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)
  • required keyword needs C# 11 / .NET 7+ (optional -- without it all properties are optional in Create)

License

MIT

There are no supported framework assets in this package.

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.

Version Downloads Last Updated
1.2.7 124 4/4/2026 1.2.7 is deprecated because it is no longer maintained.
1.2.6 95 4/4/2026
1.2.5 112 4/3/2026
1.2.1 103 4/3/2026
1.2.0 107 4/3/2026
1.1.2 105 4/3/2026
1.1.1 112 4/2/2026
1.1.0 100 4/2/2026
1.0.4 99 4/2/2026
1.0.3 102 4/2/2026
1.0.2 102 4/2/2026
1.0.1 97 4/2/2026
1.0.0 99 4/1/2026