TitanMapper 1.1.2

dotnet add package TitanMapper --version 1.1.2
                    
NuGet\Install-Package TitanMapper -Version 1.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TitanMapper" Version="1.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TitanMapper" Version="1.1.2" />
                    
Directory.Packages.props
<PackageReference Include="TitanMapper" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TitanMapper --version 1.1.2
                    
#r "nuget: TitanMapper, 1.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TitanMapper@1.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TitanMapper&version=1.1.2
                    
Install as a Cake Addin
#tool nuget:?package=TitanMapper&version=1.1.2
                    
Install as a Cake Tool

TitanMapper

TitanMapper is a lightweight object-mapping library for .NET 8, 9 and 10.

It maps objects by convention and lets you configure reverse mapping, custom members, ignored members, lists, arrays, nested objects, and type converters.

Migrating from FlexMapper

TitanMapper is the continuation of FlexMapper / FlexMapper.DependencyInjection under a new name — same author, same codebase, same behavior. Versioning picks up where FlexMapper left off (last FlexMapper release was 1.1.1; TitanMapper continues from 1.1.2).

If you're on FlexMapper:

  1. Uninstall FlexMapper / FlexMapper.DependencyInjection, install TitanMapper / TitanMapper.DependencyInjection.

  2. Update your usings: using FlexMapper; → using TitanMapper;.

  3. Rename any of these types you reference directly:

    FlexMapper TitanMapper
    IFlexMapper ITitanMapper
    FlexMapperProfile TitanMapperProfile
    FlexValidationMode TitanValidationMode
    AddFlexMapper(...) AddTitanMapper(...)

Nothing else changes — it's a rename, not a rewrite.

Structure

src/
  TitanMapper/                    Core library
  TitanMapper.DependencyInjection/ ASP.NET Core DI integration
samples/
  ConsoleSample/                 Simple console example
benchmarks/
  TitanMapper.Benchmarks/         BenchmarkDotNet: TitanMapper vs AutoMapper vs manual
tests/
  TitanMapper.Tests/              Automated xUnit tests
  TitanMapper.Real/               Real projects that validate NuGet usage
docs/
  getting-started.md             Usage guide
  performance.md                 Benchmark results and regression limits
  security.md                    Safe mapping of sensitive DTO fields
  packaging.md                   Building and consuming local packages
  architecture.md                Architecture overview
  roadmap.md                     Next steps
release/                         Output of the generated NuGet packages

Install

Core package:

dotnet add package TitanMapper

ASP.NET Core integration:

dotnet add package TitanMapper.DependencyInjection

Basic usage

var config = new MapperConfiguration();

config.Map<Produto, ProdutoDto>()
      .Reverse();

var mapper = config.Build();

var dto = mapper.To<ProdutoDto>(produto);
var domain = mapper.To<Produto>(dto);

ASP.NET Core

builder.Services.AddTitanMapper(cfg =>
{
    cfg.Map<Produto, ProdutoDto>()
       .Reverse();
});

Profiles (organize mappings in classes)

To avoid centralizing everything in the builder, group your mappings in classes that inherit from TitanMapperProfile and register them by scanning assemblies (the same workflow as AddAutoMapper).

public sealed class ProdutoMapping : TitanMapperProfile
{
    public override void Configure(MapperConfiguration cfg)
    {
        cfg.Map<Produto, ProdutoDto>()
           .For(dest => dest.CategoriaNome, src => src.Categoria!.Descricao)
           .Reverse();

        cfg.AddConverter<bool, string>(value => value ? "YES" : "NO");
    }
}

Registration in ASP.NET Core:

// Scans the assembly and applies every TitanMapperProfile found
builder.Services.AddTitanMapper(typeof(ProdutoMapping).Assembly);

// Multiple assemblies
builder.Services.AddTitanMapper(AppDomain.CurrentDomain.GetAssemblies());

// Profiles plus additional inline configuration
builder.Services.AddTitanMapper(cfg =>
{
    cfg.AddConverter<bool, string>(value => value ? "YES" : "NO");
}, typeof(ProdutoMapping).Assembly);

The scan skips abstract types and requires a parameterless constructor. All profiles share the same configuration, resulting in a single ITitanMapper singleton.

Custom member

cfg.Map<Pessoa, PessoaDto>()
   .For(dest => dest.Nome, src => src.NomeCompleto)
   .Reverse();

Ignore a member

cfg.Map<Usuario, UsuarioDto>()
   .Ignore(dest => dest.SenhaHash);

Security

Mapping happens by convention, so sensitive members flow automatically unless you stop them. Ignore password hashes, tokens, roles and audit fields on the maps that touch them, and be careful mapping client input onto entities (Reverse() on an input DTO enables over-posting). Full guidance is in docs/security.md.

// Input DTO -> entity: only allow safe fields, ignore the rest
cfg.Map<UsuarioEntrada, Usuario>()
   .Ignore(dest => dest.Id)
   .Ignore(dest => dest.Role)
   .Ignore(dest => dest.SenhaHash);

Custom converter

cfg.AddConverter<string, bool>(value =>
{
    if (string.IsNullOrWhiteSpace(value))
        return false;

    return value.Equals("YES", StringComparison.OrdinalIgnoreCase)
        || value.Equals("S", StringComparison.OrdinalIgnoreCase)
        || value.Equals("TRUE", StringComparison.OrdinalIgnoreCase)
        || value.Equals("1");
});

Validate configuration

Call ValidateConfiguration() to fail fast on broken maps instead of discovering the problem when a request hits the mapping at runtime. It inspects every registered map and throws a single MapException listing every issue it finds (source, destination and member).

var config = new MapperConfiguration();

config.Map<Produto, ProdutoDto>()
      .For(dest => dest.CategoriaNome, src => src.Categoria!.Descricao);

config.ValidateConfiguration();      // throws MapException if anything is wrong

Two modes are available through TitanValidationMode:

Mode Checks
Lenient (default) Destination has a public parameterless constructor; every For/Ignore rule still points to a member that exists and is writable; the source path of each For resolves; known-incompatible conversions (e.g. a collection mapped onto a scalar member).
Strict Everything in Lenient plus every writable destination member must have a source — filled by convention, a For rule, or explicitly ignored.
// Opt into the stricter AutoMapper-style check
config.ValidateConfiguration(TitanValidationMode.Strict);

Calculated For expressions (resolvers) are not reversible and their source type cannot be inspected, so only the destination member is validated for those rules. A registered converter (AddConverter) suppresses the incompatible-conversion check for the pair it covers.

At startup (Program.cs)

Profiles are applied before the inline configuration callback runs, so validating inside the callback covers both the scanned profiles and any inline maps. A failure throws during AddTitanMapper, so the application never starts with a broken configuration.

builder.Services.AddTitanMapper(cfg =>
{
    cfg.ValidateConfiguration();
}, typeof(ProdutoMapping).Assembly);

When you configure everything inline (no profiles), validate on the same MapperConfiguration you build:

var config = new MapperConfiguration();
config.Map<Produto, ProdutoDto>().Reverse();

config.ValidateConfiguration();

var mapper = config.Build();

In a unit test

Running the check in the test suite keeps validation in CI without paying any cost in production runtime.

[Fact]
public void Mapping_configuration_is_valid()
{
    var config = new MapperConfiguration();
    config.Map<Produto, ProdutoDto>()
          .For(dest => dest.CategoriaNome, src => src.Categoria!.Descricao);

    config.ValidateConfiguration();   // no exception = valid
}

Require registered maps

TitanMapper maps by convention even for pairs you never registered. That is convenient, but it means a forgotten Map<Domain, Dto>() produces a DTO with unfilled members instead of an error — and ValidateConfiguration cannot help, because it only inspects maps that are registered.

Set RequireConfiguredMaps to make an unregistered pair fail loudly (the AutoMapper behaviour):

var config = new MapperConfiguration { RequireConfiguredMaps = true };

config.Map<Produto, ProdutoDto>();

var mapper = config.Build();

mapper.To<CategoriaDto>(categoria);
// 💥 MapException: Mapeamento nao configurado: Categoria -> CategoriaDto.
//                  Registre o mapa com Map<Categoria, CategoriaDto>() ...

It applies to nested members and collection elements too, so a missing nested map is caught as well. It defaults to false to keep convention mapping available without registration, and is read once by Build() — set it before building.

In ASP.NET Core, set it inside the configuration callback:

builder.Services.AddTitanMapper(cfg =>
{
    cfg.RequireConfiguredMaps = true;

    cfg.Map<Produto, ProdutoDto>().Reverse();
    cfg.Map<Categoria, CategoriaDto>().Reverse();
});

With profiles, use the overload that takes a callback plus assemblies. Profiles are applied before the callback, but that does not matter here: the flag is only read by Build(), which runs after both.

builder.Services.AddTitanMapper(cfg =>
{
    cfg.RequireConfiguredMaps = true;
}, typeof(ProdutoMapping).Assembly);

The assemblies-only overload — AddTitanMapper(typeof(ProdutoMapping).Assembly) — gives you nowhere to set the flag. Either use the overload above, or set it inside a profile's Configure. Prefer the callback: the intent stays visible in Program.cs instead of buried in a profile.

The two safety nets cover different problems, so use both:

builder.Services.AddTitanMapper(cfg =>
{
    cfg.RequireConfiguredMaps = true;                       // a forgotten map fails at map time
    cfg.ValidateConfiguration(TitanValidationMode.Strict);   // a member without a source fails at startup
}, typeof(ProdutoMapping).Assembly);
Check Fails when Sees
ValidateConfiguration(Strict) at startup only maps that are registered
RequireConfiguredMaps on the first map of an unregistered pair pairs that were never registered

Turning the flag on means every pair must be registered — including nested types (Produto.Categoria needs Map<Categoria, CategoriaDto>()) and collection element types. The exception names the missing map, so you can follow the messages until the app starts clean. On a large codebase, enable it first in a unit test that exercises the main paths.

Dates

TitanMapper converts strings to DateTime and DateOnly using InvariantCulture, the current culture, and pt-BR.

Accepted formats include 20/06/2026, 20/06/2026 14:30 and 2026-06-20, for DateTime, DateOnly and TimeOnly (and their nullable variants). An invalid string throws a MapException.

Cadastro = "20/06/2026 14:20:38"

Nullable and null-safety

  • Nullable source values are preserved. A null on int?, decimal?, bool?, DateTime? (etc.) maps to null on a nullable destination.
  • Null to a non-nullable destination uses default(T). Mapping a null int? onto an int yields 0; a null DateTime? onto DateTime yields DateTime.MinValue. This is intentional — mapping never throws just because a source was null.
  • Path-based For is null-safe. For(dest => dest.MarcaNome, src => src.Marca!.Nome) returns null when Marca (or Marca.Nome) is null, instead of throwing.
  • Calculated expressions are not null-safe. A resolver such as src => src.Marca.Nome.ToUpper() throws when Marca is null. Add a manual null check, and TitanMapper's message points you to it:
cfg.Map<Produto, ProdutoDto>()
   .For(dest => dest.MarcaNome,
        src => src.Marca == null ? null : src.Marca.Nome.ToUpper());

Thread-safety

The ITitanMapper built by MapperConfiguration.Build() is immutable and safe to share across threads. All internal caches (reflection, mappings and converters) use ConcurrentDictionary, so a single instance can map objects concurrently — which is why the ASP.NET Core integration registers it as a singleton. Configuration (Map, AddConverter) is expected to happen once, up front, before Build().

var mapper = config.Build();

Parallel.For(0, 10_000, i =>
{
    var dto = mapper.To<ProdutoDto>(produtos[i]);   // safe on the same instance
});

Limitations

  • No IQueryable projection (ProjectTo). TitanMapper maps in-memory objects; it does not build SQL-translatable projections for Entity Framework. For query scenarios, project directly with LINQ (query.Select(x => new ProdutoDto { ... })) or use Dapper/SQL returning the DTO. This may be revisited in a future version — see docs/adoption-gaps.md.

Build and test

build.cmd

Or manually:

dotnet restore TitanMapper.slnx
dotnet build TitanMapper.slnx --configuration Release
dotnet test tests/TitanMapper.Tests/TitanMapper.Tests.csproj --configuration Release

Generate packages

pack.cmd

The packages are generated in:

release/

The real project in tests/TitanMapper.Real has a nuget.config pointing to that folder.

Features

  • Mapping by convention
  • Reverse mapping
  • Profiles (mappings organized in classes)
  • Public properties and fields
  • Custom member mapping
  • Ignored members
  • Lists and arrays
  • Nullable
  • Enum
  • Guid
  • DateTime
  • DateOnly
  • TimeOnly
  • Nested objects
  • Custom converters
  • Configuration validation (ValidateConfiguration) and optional required maps (RequireConfiguredMaps)
  • Compiled mapping engine (faster than AutoMapper on simple objects; ≤ AutoMapper allocation)
  • Thread-safe mapper instance
  • ASP.NET Core dependency injection integration

Version history

Versions up to 1.1.1 were released under the name FlexMapper. TitanMapper is the same project, same author, same behavior, continuing from 1.1.2 — see Migrating from FlexMapper.

Version Highlights
1.1.2 Project renamed from FlexMapper to TitanMapper (package IDs, namespace, IFlexMapper → ITitanMapper, FlexMapperProfile → TitanMapperProfile, FlexValidationMode → TitanValidationMode, AddFlexMapper → AddTitanMapper). No functional changes.
1.1.1 (as FlexMapper) Compiled mapping engine (up to ~50x faster; beats AutoMapper on simple objects and large lists, ties elsewhere, allocates like manual mapping); configuration validation (ValidateConfiguration) and optional required maps (RequireConfiguredMaps); clearer MapException errors; security and performance docs; BenchmarkDotNet project.
1.0.4 (as FlexMapper) Packaging fix to keep FlexMapper.DependencyInjection aligned with the matching FlexMapper package.
1.0.3 (as FlexMapper) Profiles (FlexMapperProfile) with assembly scanning via AddFlexMapper(assemblies); mapping of public fields in addition to properties.
1.0.2 (as FlexMapper) Multi-targeting for net8.0, net9.0 and net10.0.
1.0.1 (as FlexMapper) Fixed Brazilian date-string parsing (e.g. 20/06/2026 14:20:38); version centralized in Directory.Build.props; local NuGet packaging via nuget.config.
1.0.0 (as FlexMapper) Initial release: convention and reverse mapping, custom members (For), ignored members (Ignore), custom converters, lists/arrays, nested objects, nullable, enum, Guid, DateTime, DateOnly, TimeOnly, and ASP.NET Core DI integration.

1.1.2

  • Changed project renamed from FlexMapper to TitanMapper: package IDs (FlexMapper → TitanMapper, FlexMapper.DependencyInjection → TitanMapper.DependencyInjection), root namespace, and public types (IFlexMapper → ITitanMapper, FlexMapperProfile → TitanMapperProfile, FlexValidationMode → TitanValidationMode, AddFlexMapper(...) → AddTitanMapper(...)). No functional changes.

1.1.1 (as FlexMapper)

  • Added ValidateConfiguration() and the FlexValidationMode enum (Lenient/Strict) to validate every registered map at startup or in tests, throwing a single MapException that lists each problem with its source, destination and member.
  • Added MapperConfiguration.RequireConfiguredMaps (default false): when enabled, mapping a type pair never registered with Map<,>() throws a MapException naming the missing map instead of returning a DTO with unfilled members. Applies to nested members and collection elements, covering the blind spot of ValidateConfiguration, which only sees registered maps.
  • Added security guidance (docs/security.md) and performance documentation (docs/performance.md) with benchmarks, regression limits and roadmap.
  • Added a BenchmarkDotNet project comparing FlexMapper, AutoMapper and manual mapping.
  • Added a dedicated NuGet package readme (PackageReadme.md), separate from the repository readme.
  • Changed the mapping engine: the whole per-type mapping compiles into a typed delegate (destination creation plus member copies, no boxing), backed by a per-type-pair cached mapping plan, a memoized conversion strategy with the plan baked in, a cached collection plan, and a JIT-specialized typed collection loop (no boxed enumerator, no IList.Add dispatch). Mapping is up to ~50x faster than before, faster than AutoMapper on simple objects (~2.9x) and large lists (~1.5x), ties elsewhere, and allocates exactly what manual mapping allocates.
  • Changed mapping error messages to report source, destination and member, including the underlying cause.
  • Fixed invalid date, number, Guid, enum and bool conversions to throw a clear MapException instead of raw internal exceptions.
  • Fixed destinations without a public parameterless constructor to throw a guiding MapException instead of MissingMethodException.
  • Fixed For member paths whose intermediate is null and whose target is a non-nullable value type: they now yield default(T) instead of throwing NullReferenceException.
  • Fixed calculated resolvers that touch a null nested object: they now throw a MapException telling you to add a null check.

1.0.4 (as FlexMapper)

  • Fixed package dependency resolution so FlexMapper.DependencyInjection is installed with the matching FlexMapper package version.

1.0.3 (as FlexMapper)

  • Added FlexMapperProfile base class to group mappings in dedicated classes.
  • Added AddFlexMapper(params Assembly[]) and AddFlexMapper(Action<MapperConfiguration>, params Assembly[]) overloads that scan assemblies for profiles (AutoMapper-style workflow).
  • Added public field mapping — both by convention and via For; read-only (init-only) fields are read but never assigned; properties win on name collisions.

1.0.2 (as FlexMapper)

  • Added net9.0 and net10.0 target frameworks alongside net8.0.

1.0.1 (as FlexMapper)

  • Fixed conversion of Brazilian-format date strings to DateTime (e.g. 20/06/2026 14:20:38).
  • Changed version centralized in Directory.Build.props; local package restore configured through nuget.config.

1.0.0 (as FlexMapper)

  • Added convention mapping, reverse mapping, custom member mapping (For), ignored members (Ignore), custom converters, lists and arrays, nested objects, nullable, enum, Guid, DateTime, DateOnly, TimeOnly, and the ASP.NET Core dependency injection package.
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on TitanMapper:

Package Downloads
TitanMapper.DependencyInjection

ASP.NET Core dependency injection integration for TitanMapper.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.2 106 9/16/2026