DEV Community

Steven Kamwaza
Steven Kamwaza

Posted on AI-assisted

Twinify 1.1.0: Map Without Profiles, Hide Secrets With Attributes, and Ship Native AOT

If you missed the earlier posts on Twinify: Twinify is a MIT-licensed object mapper for .NET 8, 9, and 10. You call mapper.Map<UserDto>(user) and it copies matching members. Profiles still exist when names are not enough.

1.0.2 was about speed. 1.1.0 is about less boilerplate, safer DTOs, and Native AOT.

dotnet add package Twinify
dotnet add package Twinify.DependencyInjection
Enter fullscreen mode Exit fullscreen mode

NuGet: Twinify

The idea

You still inject IMapper and you still call Map. Existing profile-only apps keep the same behaviour. Everything below is additive.

1. Mapping without a profile

Matching names used to still need CreateMap. Not anymore.

builder.Services.AddTwinify();
Enter fullscreen mode Exit fullscreen mode
public class UserService(IMapper mapper)
{
    public UserDto ToDto(User user) => mapper.Map<UserDto>(user);
}
Enter fullscreen mode Exit fullscreen mode

No CreateMap. Twinify copies same-name members, flattens Address.CityAddressCity, and walks nested objects and collections.

No DI? Same thing:

var mapper = new Mapper();
var dto = mapper.Map<UserDto>(user);
Enter fullscreen mode Exit fullscreen mode

2. Keep your profiles. Auto-map the rest.

Registering a profile is still classic Twinify: only declared top-level pairs map.

Want both? One flag:

builder.Services.AddTwinify(cfg => cfg.AddProfile<UserProfile>())
    .AllowMappingWithoutProfiles();
Enter fullscreen mode Exit fullscreen mode

CreateMap always wins for that pair. Nested types inside an explicit map still auto-map — you do not need CreateMap<Address, AddressDto>() just because User has an Address.

3. Attributes instead of another ForMember

Hide secrets

public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; } = "";

    [IgnoreMap]
    public string? Password { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Rename or flatten without a profile

public class UserDto
{
    [MapFrom("Address.City")]
    public string Town { get; set; } = "";
}
Enter fullscreen mode Exit fullscreen mode

Pick a constructor when the destination has more than one: [MapConstructor].

Need to turn attributes off? UseMappingAttributes(false).

4. Native AOT: a source generator in the same package

JIT apps still compile maps at runtime. Native AOT cannot do that.

The Twinify package now ships Twinify.SourceGenerator. Mark the pair; Twinify emits a static, reflection-free map at compile time.

[Twinify(typeof(User))]
public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
}
Enter fullscreen mode Exit fullscreen mode

Or at assembly scope:

[assembly: TwinifyMap(typeof(User), typeof(UserDto))]
Enter fullscreen mode Exit fullscreen mode

Uncustomized CreateMap in a profile constructor can be generated too. Nested convention-compatible members of a generated pair are included.

Honest limitation: fluent ForMember / ConvertUsing are not generated. Prefer [MapFrom] / [IgnoreMap] on AOT, or keep those pairs on JIT. A pair with no generated map throws MappingNotFoundException on Native AOT.

That is the 1.0.2 “what’s next” item, shipped.


5. Records, in-place map, fail at boot

Records bind constructor parameters by name. Extra settable properties still copy after construction.

public record Person(int Id, string Name);
public record PersonDto(int Id, string Name);

var dto = mapper.Map<PersonDto>(new Person(1, "Ada"));
Enter fullscreen mode Exit fullscreen mode

Update an existing instance:

mapper.Map(user, existingDto);
Enter fullscreen mode Exit fullscreen mode

Catch bad maps at startup instead of on the first request:

builder.Services.AddTwinify(cfg => cfg.AddProfile<UserProfile>())
    .ValidateMappings();
Enter fullscreen mode Exit fullscreen mode

Still stuck? mapper.Explain<User, UserDto>() shows where each destination member gets its value.


Upgrade path

You have You do
Profile-only 1.0.x app Update the package. Behaviour stays the same.
Tired of CreateMap for identical DTOs AllowMappingWithoutProfiles() or drop profiles for those pairs.
Native AOT / trimming Add [Twinify] or [TwinifyMap] and prefer attributes over fluent customizations.

IMapper and Map<TDestination>(source) did not change.

Try it

dotnet add package Twinify --version 1.1.0
dotnet add package Twinify.DependencyInjection --version 1.1.0
Enter fullscreen mode Exit fullscreen mode

If you map DTOs in ASP.NET Core, workers, or AOT apps, I would love a try, a star, or a comment about the pair that still feels too noisy.

Twinify — object mapping without the ceremony.

Top comments (0)