A compile-time source generator that automatically creates immutable DTO (Data Transfer Object) types from Entity Framework Core entities or any POCO classes, eliminating boilerplate code while maintaining type safety.
DKNet.EfCore.DtoGenerator is a Roslyn Incremental Source Generator that generates DTO classes at compile time. Instead of manually creating and maintaining DTO classes that mirror your entities, you simply apply the [GenerateDto] attribute to an empty partial record or class, and the generator creates all the properties and mapping methods automatically.
The generator intelligently synthesizes public init properties for every public instance readable property on the source entity, while excluding indexers and static properties. It also generates helpful mapping methods (FromEntity, ToEntity, and FromEntities) that leverage Mapster when available or fall back to property-by-property initialization.
Exclude parameterDKNet.EfCore.DtoGenerator primarily serves the Application Layer and Presentation Layer of the Onion Architecture by providing clean, immutable DTOs that decouple external representations from internal domain models:
┌─────────────────────────────────────────────────────────────────┐
│                    🌐 Presentation Layer                        │
│                   (Controllers, API Endpoints)                  │
│                                                                 │
│  Uses: Generated DTOs for request/response models              │
│  📄 CustomerDto, OrderDto, ProductDto                          │
│  ✅ Validation happens on DTOs, not domain entities            │
│  🔒 Domain entities never exposed directly to clients          │
└─────────────────────────┬───────────────────────────────────────┘
                          │
┌─────────────────────────┴───────────────────────────────────────┐
│                   🎯 Application Layer                          │
│              (Use Cases, Application Services)                  │
│                                                                 │
│  🎯 DKNet.EfCore.DtoGenerator - Generates DTOs at compile time │
│  📋 Maps between domain entities and DTOs                      │
│  🔄 Uses Mapster for efficient transformations                 │
│  ✅ Maintains separation between domain and presentation       │
└─────────────────────────┬───────────────────────────────────────┘
                          │
┌─────────────────────────┴───────────────────────────────────────┐
│                    💼 Domain Layer                             │
│           (Entities, Aggregates, Domain Services)              │
│                                                                 │
│  Domain entities remain pure and focused on business logic     │
│  No knowledge of DTOs or external representations              │
└─────────────────────────┬───────────────────────────────────────┘
                          │
┌─────────────────────────┴───────────────────────────────────────┐
│                 🗄️ Infrastructure Layer                        │
│                  (Data Access, Persistence)                    │
│                                                                 │
│  Domain entities persist without DTO concerns                  │
└─────────────────────────────────────────────────────────────────┘
DTOs serve as an anti-corruption layer, preventing external API concerns from leaking into the domain model:
// Domain Entity - Pure business logic
public class Customer : AggregateRoot
{
    public string Name { get; private set; }
    public Email Email { get; private set; }
    public CustomerStatus Status { get; private set; }
    
    public void ActivateAccount() 
    {
        // Business logic
    }
}
// Generated DTO - External representation
[GenerateDto(typeof(Customer))]
public partial record CustomerDto;
// Auto-generated properties: Name, Email, Status (as string/primitive types)
DTOs help maintain clear boundaries between bounded contexts by providing explicit translation points:
// Order Context
[GenerateDto(typeof(Order))]
public partial record OrderDto;
// Customer Context
[GenerateDto(typeof(Customer))]
public partial record CustomerSummaryDto;
// Integration between contexts uses DTOs, not domain entities
DTOs prevent clients from directly modifying aggregate internals:
// Domain aggregate with encapsulated behavior
public class Order : AggregateRoot
{
    private List<OrderItem> _items = new();
    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
    
    public void AddItem(Product product, int quantity)
    {
        // Business rules enforced here
    }
}
// DTO for reading - no behavior, just data
[GenerateDto(typeof(Order))]
public partial record OrderDto;
Generated DTOs enable complete decoupling between presentation and domain layers:
// Application Service - translates between layers
public class CustomerService
{
    private readonly ICustomerRepository _repository;
    
    public async Task<CustomerDto> GetCustomerAsync(Guid id)
    {
        var customer = await _repository.GetByIdAsync(id);
        return CustomerDto.FromEntity(customer); // Generated mapping
    }
    
    public async Task<Guid> CreateCustomerAsync(CreateCustomerDto dto)
    {
        var customer = dto.ToEntity(); // Generated mapping
        await _repository.AddAsync(customer);
        return customer.Id;
    }
}
DTOs with generated mapping methods are easily testable:
[Fact]
public void CustomerDto_ShouldMapFromEntity()
{
    // Arrange
    var customer = new Customer("John Doe", "john@example.com");
    
    // Act
    var dto = CustomerDto.FromEntity(customer);
    
    // Assert
    dto.Name.ShouldBe("John Doe");
    dto.Email.ShouldBe("john@example.com");
}
Different DTO versions can be generated from the same entity:
// V1 API
[GenerateDto(typeof(Customer))]
public partial record CustomerDtoV1;
// V2 API - exclude sensitive fields
[GenerateDto(typeof(Customer), Exclude = new[] { "InternalNotes", "CreditScore" })]
public partial record CustomerDtoV2;
<ItemGroup>
  <PackageReference Include="DKNet.EfCore.DtoGenerator" Version="1.0.0" 
                    PrivateAssets="all" OutputItemType="Analyzer" />
  <!-- Optional but recommended for efficient mapping -->
  <PackageReference Include="Mapster" Version="7.4.0" />
</ItemGroup>
Add these properties to your .csproj file:
<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
  <!-- Force analyzer to reload on every build to avoid caching issues -->
  <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
Configure properties to exclude globally across all DTOs:
<!-- Define global exclusions (comma or semicolon separated) -->
<PropertyGroup>
  <DtoGenerator_GlobalExclusions>CreatedBy,UpdatedBy,CreatedAt,UpdatedAt</DtoGenerator_GlobalExclusions>
</PropertyGroup>
<!-- Make the property visible to the source generator -->
<ItemGroup>
  <CompilerVisibleProperty Include="DtoGenerator_GlobalExclusions" />
</ItemGroup>
This is particularly useful for:
For debugging and verification, copy generated DTOs to your project:
<!-- Custom target to copy generated DTOs to project/GeneratedDtos folder -->
<Target Name="CopyGeneratedDtosToOutputFolder" AfterTargets="CoreCompile" 
        Condition="Exists('$(CompilerGeneratedFilesOutputPath)')">
    <ItemGroup>
        <GeneratedDtoFiles Include="$(CompilerGeneratedFilesOutputPath)\**\*Dto.g.cs"/>
    </ItemGroup>
    <MakeDir Directories="$(ProjectDir)GeneratedDtos" Condition="'@(GeneratedDtoFiles)' != ''"/>
    <Copy SourceFiles="@(GeneratedDtoFiles)"
          DestinationFiles="$(ProjectDir)GeneratedDtos\%(Filename)%(Extension)"
          SkipUnchangedFiles="false"
          OverwriteReadOnlyFiles="true"
          Condition="'@(GeneratedDtoFiles)' != ''"/>
    <Message Text="Copied %(Filename)%(Extension) to $(ProjectDir)GeneratedDtos" 
             Importance="high" Condition="'@(GeneratedDtoFiles)' != ''"/>
</Target>
<!-- Exclude generated DTOs from compilation, but keep them visible in Solution Explorer -->
<ItemGroup>
    <Compile Remove="GeneratedDtos\**\*.cs"/>
    <None Include="GeneratedDtos\**\*.cs"/>
</ItemGroup>
// Entity
public class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
    public DateTime CreatedAt { get; set; }
}
// DTO Declaration
[GenerateDto(typeof(Product))]
public partial record ProductDto;
// Usage
var product = await repository.GetByIdAsync(productId);
var dto = ProductDto.FromEntity(product);
return Results.Ok(dto);
// Exclude internal/sensitive properties
[GenerateDto(typeof(Product), Exclude = new[] { "StockQuantity", "CreatedAt" })]
public partial record ProductSummaryDto;
// Generated DTO will only include: Id, Name, Description, Price
Configure global exclusions via MSBuild properties to exclude common audit or internal properties across all DTOs:
<!-- In your .csproj file -->
<PropertyGroup>
  <DtoGenerator_GlobalExclusions>CreatedBy,UpdatedBy,CreatedAt,UpdatedAt</DtoGenerator_GlobalExclusions>
</PropertyGroup>
<!-- Make the property visible to the source generator -->
<ItemGroup>
  <CompilerVisibleProperty Include="DtoGenerator_GlobalExclusions" />
</ItemGroup>
Global exclusions are applied to all DTOs by default:
// This DTO will automatically exclude CreatedBy, UpdatedBy, CreatedAt, UpdatedAt
[GenerateDto(typeof(Product))]
public partial record ProductDto;
// Local exclusions are combined with global exclusions
[GenerateDto(typeof(Product), Exclude = ["InternalNotes"])]
public partial record ProductSummaryDto; // Excludes global properties + InternalNotes
// Include parameter overrides global exclusions
[GenerateDto(typeof(Product), Include = ["Id", "Name", "CreatedAt"])]
public partial record ProductNameDto; // Only includes these 3 properties, ignoring global exclusions
Benefits of Global Exclusions:
[GenerateDto(typeof(Product))]
public partial record ProductDto
{
    // Add computed property
    public string DisplayPrice => $"${Price:N2}";
    
    // Add custom property not in entity
    public bool IsAvailable => StockQuantity > 0;
}
// Map multiple entities to DTOs
var products = await repository.GetAllAsync();
var dtos = ProductDto.FromEntities(products);
return Results.Ok(dtos);
// Async with EF Core and Mapster
var dtos = await dbContext.Products
    .ProjectToType<ProductDto>() // Mapster extension
    .ToListAsync();
public class Order
{
    public Guid Id { get; set; }
    public string OrderNumber { get; set; } = string.Empty;
    public Customer Customer { get; set; } = null!;
    public List<OrderItem> Items { get; set; } = new();
    public OrderStatus Status { get; set; }
}
// Generate DTOs for related entities
[GenerateDto(typeof(Customer))]
public partial record CustomerDto;
[GenerateDto(typeof(OrderItem))]
public partial record OrderItemDto;
[GenerateDto(typeof(Order))]
public partial record OrderDto;
// Configure Mapster to map nested objects
TypeAdapterConfig<Order, OrderDto>
    .NewConfig()
    .Map(dest => dest.Customer, src => CustomerDto.FromEntity(src.Customer))
    .Map(dest => dest.Items, src => OrderItemDto.FromEntities(src.Items));
// Read DTO
[GenerateDto(typeof(Customer))]
public partial record CustomerDto;
// Create DTO - exclude Id and timestamps
[GenerateDto(typeof(Customer), Exclude = new[] { "Id", "CreatedAt", "UpdatedAt" })]
public partial record CreateCustomerDto;
// Update DTO - exclude Id and CreatedAt
[GenerateDto(typeof(Customer), Exclude = new[] { "Id", "CreatedAt" })]
public partial record UpdateCustomerDto;
// API Endpoint
app.MapPost("/customers", async (CreateCustomerDto dto, ICustomerRepository repo) =>
{
    var customer = dto.ToEntity();
    await repo.AddAsync(customer);
    return Results.Created($"/customers/{customer.Id}", CustomerDto.FromEntity(customer));
});
// Configure mappings at startup
public static class MappingConfiguration
{
    public static void Configure()
    {
        TypeAdapterConfig.GlobalSettings.Scan(typeof(Program).Assembly);
        
        // Custom mapping rules
        TypeAdapterConfig<Customer, CustomerDto>
            .NewConfig()
            .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}")
            .Ignore(dest => dest.InternalId);
    }
}
// Efficient database queries with projection
public async Task<List<ProductDto>> GetProductsAsync()
{
    return await _dbContext.Products
        .Where(p => p.IsActive)
        .ProjectToType<ProductDto>() // Mapster projects directly from DB
        .ToListAsync();
}
// Register custom adapter for value objects
TypeAdapterConfig<Email, string>
    .NewConfig()
    .MapWith(email => email.Value);
TypeAdapterConfig<string, Email>
    .NewConfig()
    .MapWith(str => new Email(str));
namespace MyApp.V1.Dtos
{
    [GenerateDto(typeof(Product))]
    public partial record ProductDto;
}
namespace MyApp.V2.Dtos
{
    [GenerateDto(typeof(Product), Exclude = new[] { "InternalCode" })]
    public partial record ProductDto
    {
        // V2 adds new computed field
        public string Category { get; init; } = string.Empty;
    }
}
// Read model - all properties
[GenerateDto(typeof(Order))]
public partial record OrderDto;
// Command model - only writable properties
[GenerateDto(typeof(Order), Exclude = new[] { "Id", "OrderNumber", "CreatedAt", "Status" })]
public partial record CreateOrderCommand;
// Update model - fewer properties
[GenerateDto(typeof(Order), Exclude = new[] { "Id", "OrderNumber", "CreatedAt" })]
public partial record UpdateOrderCommand;
public class Order
{
    public Guid Id { get; set; }
    public Address ShippingAddress { get; set; } = null!;
}
[GenerateDto(typeof(Order))]
public partial record OrderDto
{
    // Override to flatten
    public new string ShippingAddress { get; init; } = string.Empty;
}
// Configure flattening in Mapster
TypeAdapterConfig<Order, OrderDto>
    .NewConfig()
    .Map(dest => dest.ShippingAddress, 
         src => $"{src.ShippingAddress.Street}, {src.ShippingAddress.City}");
DtoGenerator works at compile time, not runtime:
With Mapster (Recommended):
Without Mapster (Fallback):
// Efficient collection mapping with Mapster
var dtos = await dbContext.Products
    .ProjectToType<ProductDto>() // Maps in database, not in memory
    .ToListAsync();
// vs. inefficient approach
var products = await dbContext.Products.ToListAsync(); // Load all entities
var dtos = products.Select(p => ProductDto.FromEntity(p)); // Map in memory
Ensure these properties are set:
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
Check obj/Generated folder for generated files.
EnforceExtendedAnalyzerRules is set to trueIf you get duplicate property errors:
Exclude parameter to skip duplicatesEnsure Mapster package reference is added:
<PackageReference Include="Mapster" Version="7.4.0" />
Generator checks for Mapster at compile time.
If global exclusions aren’t being applied:
.csproj:
    <PropertyGroup>
  <DtoGenerator_GlobalExclusions>Property1,Property2</DtoGenerator_GlobalExclusions>
</PropertyGroup>
CompilerVisibleProperty is configured:
    <ItemGroup>
  <CompilerVisibleProperty Include="DtoGenerator_GlobalExclusions" />
</ItemGroup>
Clean and rebuild to regenerate all DTOs
obj/Generated or your configured output folderThe generator reports several diagnostic codes:
Check build output for specific diagnostic messages.
Navigation and collection properties are included as shallow copies:
Customer, Order) are generated with the = null!; initializer to satisfy C# nullable reference type requirements= []; for non-nullable collection typesExclude parameter if they’re not needed// Entity with navigation property
public class Order
{
    public Guid Id { get; set; }
    public Customer Customer { get; set; } = null!; // Navigation property
    public List<OrderItem> Items { get; set; } = new();
}
// Generated DTO includes navigation property with null! initializer
[GenerateDto(typeof(Order))]
public partial record OrderDto;
// Generated: public Customer Customer { get; init; } = null!;
// Generated: public List<OrderItem> Items { get; init; } = [];
// Alternative: Exclude navigation properties if not needed
[GenerateDto(typeof(Order), Exclude = new[] { "Customer", "Items" })]
public partial record OrderSummaryDto;
required modifier= []= null!; initializer to satisfy compiler null-state analysisLimited support for generic entity types. DTO shells must be non-generic.
Entity inheritance is not automatically handled. Create separate DTOs for each entity type or use Mapster configuration.
// Preferred
[GenerateDto(typeof(Product))]
public partial record ProductDto;
// Instead of class
[GenerateDto(typeof(Product))]
public partial class ProductDto; // Works but records are more idiomatic for DTOs
[GenerateDto(typeof(Customer))]
public partial record CustomerDto; // Full read model
[GenerateDto(typeof(Customer), Exclude = new[] { "Id", "CreatedAt" })]
public partial record CreateCustomerDto; // Create command
[GenerateDto(typeof(Customer), Exclude = new[] { "Id", "Email" })]
public partial record CustomerSummaryDto; // List view
/Features
  /Customers
    /Entities
      Customer.cs
    /Dtos
      CustomerDto.cs
      CreateCustomerDto.cs
      UpdateCustomerDto.cs
  /Orders
    /Entities
      Order.cs
    /Dtos
      OrderDto.cs
// Startup.cs or Program.cs
var config = TypeAdapterConfig.GlobalSettings;
config.Scan(typeof(Program).Assembly);
config.RequireExplicitMapping = false;
config.RequireDestinationMemberSource = false;
// ✅ Good - DTOs at API boundary
public async Task<CustomerDto> GetCustomerAsync(Guid id)
{
    var customer = await _repository.GetByIdAsync(id); // Domain entity internally
    return CustomerDto.FromEntity(customer); // Convert to DTO for API
}
// ❌ Bad - DTOs in domain layer
public async Task ProcessOrderAsync(OrderDto dto) // DTOs shouldn't be in domain services
DtoGenerator works seamlessly with other DKNet components:
public class CustomerService
{
    private readonly IReadRepository<Customer> _repository;
    
    public async Task<PagedList<CustomerDto>> GetCustomersAsync(int page, int pageSize)
    {
        return await _repository.Gets()
            .ProjectToType<CustomerDto>() // DtoGenerator + Mapster
            .ToPagedListAsync(page, pageSize); // DKNet.EfCore.Repos
    }
}
public record GetCustomerQuery : IWitResponse<CustomerDto>
{
    public required Guid CustomerId { get; init; }
}
public class GetCustomerHandler : IHandler<GetCustomerQuery, CustomerDto>
{
    private readonly ICustomerRepository _repository;
    
    public async Task<CustomerDto?> OnHandle(GetCustomerQuery request, CancellationToken ct)
    {
        var customer = await _repository.FindAsync(request.CustomerId, ct);
        return customer != null ? CustomerDto.FromEntity(customer) : null;
    }
}
💡 Pro Tip: Use DtoGenerator for all API data transfer needs to maintain a clean separation between your domain model and external representations. Combined with Mapster, it provides a powerful, type-safe, and performant solution for object mapping in DDD applications.