DTOs und Mapping¶
DTOs lassen sich von Hand schreiben, und viele sind es auch: Requests, Dashboards und andere Formen, die keine Entity abbilden. Für DTOs, die eine Entity abbilden, erzeugt das optionale Paket Coworkee.CodeGen sie aus der Entity, zusammen mit dem Mapping in beide Richtungen. Es bringt den Source-Generator Nextended.CodeGen mit; gemappt wird entweder mit einfachen Extension-Methoden oder mit einem Mapperly-Mapper, pro Entity wählbar.
Einschalten¶
Das Paket gehört in das Projekt mit den Entities, meist das Modulprojekt:
<PackageReference Include="Coworkee.CodeGen" />
<!-- nur für Entities, die mit Mapperly mappen -->
<PackageReference Include="Riok.Mapperly" PrivateAssets="all" ExcludeAssets="runtime" />
Daneben liegt eine codegen.json. Das Paket findet sie über den Namen; ohne sie wird nichts erzeugt.
{
"DtoGeneration": {
"Namespace": "MyApp.Catalog",
"OutputPath": "../MyApp.Contracts/Catalog/Generated/",
"MappingOutputPath": "./Generated/",
"DeepProperties": true,
"PropertiesToIgnore": [ "TenantId", "CreatedAt", "CreatedBy", "ModifiedAt", "ModifiedBy" ],
"CreateFileHeaders": false,
"CreateRegions": false,
"DefaultMappingSettings": { "ToSourceMethodName": "ToEntity" }
}
}
| Einstellung | Wozu |
|---|---|
OutputPath |
Die DTOs landen im Contracts-Projekt, so teilen API, Blazor-Client und SDKs sie. Das Contracts-Projekt braucht dafür nichts. |
MappingOutputPath |
Die Mappings bleiben im Modulprojekt, dem einzigen, das Entities und DTOs kennt. |
Namespace |
Namespace der Mappings. Mit dem Root-Namespace des Moduls steht brand.ToDto() in jedem Handler ohne using bereit. |
DeepProperties |
Übernimmt die Id aus Entity ins DTO. |
PropertiesToIgnore |
Spalten, die kein DTO tragen soll. Ein Attribut kann eine eigene Liste setzen. |
CreateFileHeaders: false |
Hält den Zeitstempel aus den Dateien heraus; ein Build ändert sie nur, wenn sich die Entity geändert hat. |
DefaultMappingSettings |
Methodennamen; ToDtoMethodName ist standardmäßig ToDto. |
Die erzeugten Dateien sind normale Quelldateien im Repository und werden im Pull Request geprüft wie jede andere Änderung. Neu geschrieben werden sie nur, wenn der Build eine geänderte Entity sieht; eine Änderung ist nach dem nächsten Build sichtbar. Wer eine Datei löscht, bekommt sie beim nächsten Build neu.
Entities markieren¶
[AutoGenerateDto(Namespace = "MyApp.Contracts.Catalog")]
public sealed class Brand : AuditedEntity, IMultiTenant
{
public required string Name { get; set; }
public string? Description { get; set; }
public decimal Tax { get; set; }
public Guid TenantId { get; set; }
}
public partial class BrandDto : IBrandDto
{
public required string Name { get; set; }
public string? Description { get; set; }
public decimal Tax { get; set; }
public System.Guid Id { get; set; }
}
required-Member bleiben required, nullbare bleiben nullbar, und die Klasse ist partial, das Contracts-Projekt kann sie also ergänzen. [IgnoreOnGeneration] an einer Property oder PropertiesToIgnore am Attribut lässt Member weg, [GenerationPropertySetting(PropertyName = ...)] benennt sie um. Alle Optionen stehen in der Nextended.CodeGen-Referenz.
Mapping mit Extension-Methoden¶
Das ist der Standard. Brand bekommt Extension-Methoden in einer partiellen Klasse MappingExtensions:
Der Code besteht aus einfachen Zuweisungen und steht lesbar in MyApp.Catalog/Generated/MappingExtensions.g.Brand.cs. Braucht ein Feld mehr als eine Zuweisung, ergänzt GenerateBeforeAndAfterAssignPartialsInMapping = true partielle Hooks vor und nach den Zuweisungen.
Mapping mit Mapperly¶
[AutoGenerateDto(Namespace = "MyApp.Contracts.Catalog", MappingStrategy = DtoMappingStrategy.Mapperly)]
public sealed class Product : AuditedEntity, IMultiTenant { … }
Product bekommt stattdessen einen partiellen Mapperly-Mapper. Die Aufrufe sehen gleich aus (product.ToDto()); Mapperly schreibt die Implementierung beim Kompilieren und meldet Member, die es nicht abbilden kann.
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
[UseStaticMapper(typeof(MyApp.Catalog.MappingExtensions))]
public static partial class ProductMapper
{
public static partial ProductDto ToDto(this Product src);
[MapperIgnoreTarget("TenantId")] /* … die übrigen ignorierten Spalten */
public static partial Product ToEntity(this ProductDto src);
public static partial void AssignTo(this Product src, [MappingTarget] ProductDto dest);
[MapperIgnoreTarget("TenantId")] /* … */
public static partial void AssignTo(this ProductDto src, [MappingTarget] Product dest);
}
Über UseStaticMapper nutzt die verschachtelte Brand das Mapping von Brand, beide Strategien lassen sich also beliebig mischen.
Eine erzeugte Methode überschreiben¶
Der Mapper ist partial. Eine Methode mit demselben Namen im eigenen Teil der Klasse ersetzt die erzeugte, der erzeugte Teil lässt sie weg. Dasselbe gilt für das [Mapper]-Attribut, etwa um Einstellungen von Mapperly zu ändern.
public static partial class ProductMapper
{
[MapProperty(nameof(Product.Rate), nameof(ProductDto.Rate), Use = nameof(RoundRate))]
public static partial ProductDto ToDto(this Product product);
[UserMapping(Default = false)]
private static decimal RoundRate(decimal rate) => Math.Round(rate, 2);
}
Hier geht der Preis des Produkts auf Cent gerundet hinaus; alles andere mappt weiterhin Mapperly. Methoden werden über den Namen zugeordnet, ein eigenes AssignTo übernimmt also beide Richtungen.
Warum die Dateien auf der Platte liegen
Source-Generatoren sehen die Ausgabe anderer Generatoren nicht. Mapperly kann nur einen Mapper implementieren, der als echte Datei existiert; deshalb braucht die Mapperly-Strategie OutputPath und MappingOutputPath. Fehlen sie, enthält der erzeugte Mapper einen Kompilierfehler, der genau das sagt.
Welche Variante¶
| Extension-Methoden | Mapperly | |
|---|---|---|
| Zusätzliches Paket | keins | Riok.Mapperly |
| Eigene Logik | partielle Hooks vor und nach den Zuweisungen | Mapperly-Attribute an der eigenen Deklaration, jede Methode ersetzbar |
| Prüfungen | keine, nicht abbildbare Member werden übersprungen | Mapperly meldet DTO-Member ohne Quelle |
| Geeignet für | einfache DTOs | DTOs mit Umbenennungen, Konvertierungen oder verschachtelten Objekten |
Enums sowie generische und abstrakte Entities mappen immer mit Extension-Methoden.
Ohne das Paket¶
Ohne Coworkee.CodeGen ändert sich nichts: DTOs und Mappings von Hand schreiben oder den Mapper der Wahl verwenden. Wird das Paket später entfernt, bleiben die erzeugten Dateien erhalten und sind ab dann normaler Quellcode.