Skip to content

Repository files navigation

HK.MapperGenerator

Roslyn source generator that emits MapTo{Target} extension methods — in both directions — for classes annotated with [GenerateMapper]. Properties are matched by exact name and type; unmatched properties are silently skipped.

Install

dotnet add package HK.MapperGenerator

Requires a Roslyn 4.14+ toolchain (Visual Studio 2022 17.14+ / .NET SDK 9.0.3xx+) to build the consuming project — the generator itself targets any project that can reference an analyzer.

Usage

public class Entidade
{
    public int Id { get; set; }
    public string Nome { get; set; }
}

[GenerateMapper(typeof(Entidade))]
public class EntidadeDto
{
    public int Id { get; set; }
    public string Nome { get; set; }
}
using HK.MapperGenerator;   // GenerateMapperAttribute
using GeneratedMappers;    // MapperExtensions

var entidade = new Entidade { Id = 1, Nome = "Teste" };
EntidadeDto dto = entidade.MapToEntidadeDto();

// Reverse direction, generated from the same annotation:
Entidade roundTrip = dto.MapToEntidade();

The generator adds [GenerateMapper] to your compilation automatically — you don't need to declare it yourself. It emits one static GeneratedMappers.MapperExtensions class per compilation with two MapTo{Target} extension methods per annotated class: one for each direction.

If the destination type has no public setters (e.g. Entidade above used constructor-only or init properties), the generator picks a public constructor whose parameters match source properties by name and type, and fills any remaining writable properties via an object initializer. If no constructor can be satisfied, that direction is skipped and reported as HKMG002.

Composition (nested objects and collections)

No extra annotation is needed beyond the ones you already have. If a property's type doesn't match exactly but a pair for it is also annotated somewhere in the compilation, the generator composes automatically:

public class Address { public string Street { get; set; } = ""; }

[GenerateMapper(typeof(Address))]
public class AddressDto { public string Street { get; set; } = ""; }

public class Person
{
    public Address Address { get; set; } = new();
    public List<Address> PreviousAddresses { get; set; } = new();
}

[GenerateMapper(typeof(Person))]
public class PersonDto
{
    public AddressDto Address { get; set; } = new();
    public List<AddressDto> PreviousAddresses { get; set; } = new();
}

Both Address and PreviousAddresses map deeply — a new AddressDto/Address instance per element, not a reference copy — because Address <-> AddressDto is already a known pair. Matching priority, first one that fits wins: (1) exact type (including reference copy when both sides use the identical type), (2) nested object 1:1, (3) collection element-by-element.

Collections: List<T>, arrays, and any of IEnumerable<T> / ICollection<T> / IList<T> / IReadOnlyCollection<T> / IReadOnlyList<T> as the destination type — all materialize via .ToList() (arrays via .ToArray()). Anything else on the destination side (HashSet<T>, Collection<T>, a custom collection type) isn't a supported materialization target and the property is skipped, same as before composition existed — this never turns into a compile error. string is never treated as a collection of char, despite implementing IEnumerable<char>.

Cyclic graphs. The mapper does no reference tracking (no PreserveReferences equivalent). A property whose type maps back to an ancestor in the composition graph — most commonly self-reference, e.g. Person.Manager of type Person — still gets a mapping method, but calling it on data that actually forms a cycle at runtime will stack-overflow, the same way any hand-written recursive mapper would. The generator reports HKMG005 on any pair that participates in such a cycle so you know the risk exists; it does not block emission, because acyclic uses (a finite management chain, a tree of categories) are legitimate.

Notes

  • Mapping is by exact name and type match; properties without a matching source property are left at their default value.
  • Nullability annotations are not part of the type match — string? matches string. This is deliberate: the generator only encodes what the CLR type is, not nullable-context metadata.
  • Only non-static, public target properties with a set or init accessor are considered (minus any consumed by the chosen constructor). Inherited properties (from any base class up to but not including object) count too, on both source and destination.
  • class, positional record, and struct can all be annotated. Generic types (open or annotated with a generic argument) are not supported — the pair is skipped and reported as HKMG001.
  • A class annotated with its own type, or a partial class whose declarations carry attributes on more than one part, still emits each direction once — duplicate (source, target) pairs are deduplicated before emission.
  • Any change to any annotated class regenerates the whole output file (the generator collects all candidates before emitting), though the underlying per-type model is cached incrementally.
  • The generated extension methods live in GeneratedMappers, a namespace deliberately separate from HK.MapperGenerator (where the attribute itself lives), so a using GeneratedMappers; is always needed alongside using HK.MapperGenerator;.

Diagnostics

All warnings, never errors — the generator never fails a build that already compiled.

ID Meaning
HKMG001 Source or target is a generic type; the pair was skipped.
HKMG002 No accessible constructor of the destination could be satisfied from the source; that direction was skipped.
HKMG003 Two destinations produced the same MapTo{Name} signature from the same source; the second was skipped.
HKMG004 The argument passed to [GenerateMapper] is not a named type.
HKMG005 The pair participates in a composition cycle (nested object or collection) between generated methods; risk of infinite recursion at runtime.

Known limitations

  • GenerateMapperAttribute is injected as internal sealed, so it does not cross an assembly boundary — you cannot reference it from a different assembly than the one the generator is applied to. This matches the analyzer-only distribution model (no runtime library is shipped).

Development

Build

dotnet build HK.MapperGenerator.slnx
dotnet build HK.MapperGenerator.slnx -c Release
dotnet test  HK.MapperGenerator.slnx

Roslyn caches analyzer DLLs in the IDE/build server. If a consumer project keeps seeing stale generator output after a rebuild:

dotnet build-server shutdown

Inspect the generated output in a consumer

Add to the consuming project's .csproj:

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

Generated files then appear under obj/<Configuration>/<TFM>/generated/.

Pack

dotnet build-server shutdown
dotnet pack src/HK.MapperGenerator/HK.MapperGenerator.csproj -c Release

Produces artifacts\HK.MapperGenerator.<version>.nupkg.

Test the package locally before publishing

Point a throwaway consumer project at the local artifacts folder instead of nuget.org:

<!-- nuget.config next to the consumer project -->
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="local" value="C:\path\to\HK.MapperGenerator\artifacts" />
    <add key="nuget" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>
dotnet new console -o ConsumerApp
cd ConsumerApp
dotnet add package HK.MapperGenerator --version <version>
dotnet run

Publish to nuget.org manually

Requires a nuget.org account and an API key (scope: Push new packages and package versions, glob HK.*).

dotnet build-server shutdown
dotnet pack src/HK.MapperGenerator/HK.MapperGenerator.csproj -c Release
dotnet nuget push artifacts\HK.MapperGenerator.<version>.nupkg `
  --api-key <YOUR_API_KEY> `
  --source https://api.nuget.org/v3/index.json

Publishing is irreversible — a pushed version cannot be re-uploaded, only unlisted.

Publish via GitHub Actions

.github/workflows/publish.yml pushes to nuget.org whenever a tag matching v* is pushed. Requires a NUGET_API_KEY secret in the repository (Settings → Secrets and variables → Actions).

git tag v0.1.0
git push origin v0.1.0

Bump the version

Edit <Version> in src/HK.MapperGenerator/HK.MapperGenerator.csproj, then repeat the pack/push steps above with a matching git tag.

About

Roslyn source generator that emits `MapTo{Target}` extension methods for classes annotated with `[GenerateMapper]`. Properties are matched by exact name and type; unmatched properties are silently skipped.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages