.NET Object Mapper Guide: Transitio.Mapper with Expression Trees

If you've worked with .NET for any length of time, you've reached for an object mapper — probably AutoMapper. It works, but as projects scale, reflection-heavy mapping starts to show its cost: slower runtime performance and configuration that's hard to reason about at scale.

Transitio.Mapper expression tree mapping diagram for .NET

Transitio.Mapper is a lightweight, high-performance object mapping framework for .NET, built around expression-based mapping with support for profiles, nested mapping, and first-class dependency injection.

Installing Transitio.Mapper

dotnet add package Transitio.Mapper

# optional: Microsoft.Extensions.DependencyInjection integration
dotnet add package Transitio.Dependency

Quick Start

using Transitio.Mapper;

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

var config = new TransitioMapperConfiguration(cfg =>
{
    cfg.CreateMap<User, UserDto>();
});

IMapper mapper = config.BuildMapper();

var dto = mapper.Map<UserDto>(new User { Name = "Hitesh", Age = 30 });
// dto.Name == "Hitesh", dto.Age == 30

Notice the single generic argument on Map<UserDto>(user) — Transitio infers the source type, so you only specify the destination.

Core Features

Custom member mapping

cfg.CreateMap<User, UserViewDto>()
    .ForMember(dest => dest.DisplayName, opt => opt.MapFrom(src => src.Name.ToUpper()));

Ignoring a property

cfg.CreateMap<User, UserDto>()
    .ForMember(dest => dest.Age, opt => opt.Ignore()); // Age keeps its default

Conditional mapping

cfg.CreateMap<User, UserViewDto>()
    .ForMember(dest => dest.Age, opt => opt.Condition(src => src.Age >= 18));

Reverse mapping

cfg.CreateMap<User, UserDto>().ReverseMap();

var dto = mapper.Map<UserDto>(user);   // User -> UserDto
var back = mapper.Map<User>(dto);      // UserDto -> User

Nested and collection mapping — resolved automatically once both types are registered:

cfg.CreateMap<User, UserDto>();
cfg.CreateMap<Order, OrderDto>();  // Order.Customer (User) mapped via the User -> UserDto map
cfg.CreateMap<Cart, CartDto>();    // Cart.Orders (List<Order>) mapped element-by-element

var cartDto = mapper.Map<CartDto>(cart);
var array   = mapper.Map<OrderDto[]>(orders);      // IEnumerable -> array
var asList  = mapper.Map<IList<OrderDto>>(orders);  // IEnumerable -> interface

Configuration validation — catch mapping mistakes early instead of at runtime:

config.AssertConfigurationIsValid(); // throws if a destination property has no valid source

Organizing Maps with Profiles

For anything beyond a handful of maps, group them into a MappingProfile:

public class UserProfile : MappingProfile
{
    public override void Configure(TransitioConfigBuilder cfg)
    {
        cfg.CreateMap<User, UserDto>().ReverseMap();
    }
}

var config = new TransitioMapperConfiguration(cfg =>
{
    cfg.AddProfile<UserProfile>();
});

Registering with Dependency Injection

With Transitio.Dependency installed, registration is a single call:

services.AddTransitio(cfg =>
{
    cfg.CreateMap<User, UserDto>();
    cfg.CreateMap<Order, OrderDto>();
});

Then inject IMapper directly wherever you need it:

public class UserService
{
    private readonly IMapper _mapper;
    public UserService(IMapper mapper) => _mapper = mapper;

    public UserDto ToDto(User user) => _mapper.Map<UserDto>(user);
}

AddTransitio registers the mapper as a singleton by default, so the compiled-mapping cache is shared across the whole app. If a ConvertUsing<TConverter>() converter depends on a scoped service like a DbContext, you can register with a scoped or transient lifetime instead — see the Dependency Injection guide for the full pattern, including assembly scanning for profiles and keyed mappers.

When You'd Reach for This

  • API-heavy services mapping entities to DTOs on every request
  • .NET-to-cloud migration projects where you're reshaping legacy models into new service contracts — a scenario I run into constantly in AWS migration work
  • Any codebase where you want mapping mistakes caught by AssertConfigurationIsValid() at startup, rather than discovered in production

What's Next

Transitio.Mapper is one piece of a small, focused open-source ecosystem I'm building under the Transitio name, alongside Transitio.Dependency and the upcoming Transitio.Validator.

The next post in this series covers pairing Transitio.Mapper with Transitio.Dependency in a real migration pipeline.


Have questions or found an edge case? Open an issue on GitHub or reach out on X @hstarkar87.

Comments

Popular posts from this blog

Diversity and networking: why they matter?

Kanban vs. Scrum: Choosing the Right Approach for Your Team

What is agile methodology and how it change the software development process?