Marai Light Mapper

NuGet License License

A lightweight, high-performance object mapper for .NET applications. Map between entities, DTOs, and view models with zero configuration using convention-based automatic mapping or custom mapping logic.

Table of Contents

What is Marai.LightMapper?

Marai.LightMapper is a convention-based object mapper that eliminates repetitive mapping code while maintaining performance and simplicity.

Why LightMapper?

Traditional Approach (Manual Mapping):

C#
public UserDto MapToDto(User user)
{
    return new UserDto
    {
        Id = user.Id,
        Name = user.Name,
        Email = user.Email,
        CreatedAt = user.CreatedAt
        // ... 20+ properties to map manually
    };
}

LightMapper Approach:

C#
// Define mapping intention
public class UserDto : IMapFrom<User>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

// Use mapper
var dto = _mapper.Map<UserDto>(user);  // Done!
Key Benefits
  • Zero Boilerplate – No manual property assignments
  • Convention-Based – Automatic mapping by property name
  • Type-Safe – Compile-time safety with generics
  • Lightweight – Minimal dependencies, single-file implementation
  • High Performance – Reflection-based with caching
  • Extensible – Custom mapping logic when needed
  • Bidirectional – Map both ways with IMapFrom and IMapTo

Features

Core Features
  • Automatic Property Mapping – Maps properties by name (case-insensitive)
  • Convention-Based Discovery – Assembly scanning for mapping registration
  • Bidirectional MappingIMapFrom<T> and IMapTo<T> support
  • Custom Mapping Logic – Override MapFrom() or MapTo() for complex scenarios
  • Singleton Lifetime – Optimized for performance
  • Type Safety – Generic interfaces ensure compile-time validation
  • Exception Safety – Clear error messages when mappings are missing
What’s Included
  • IMapFrom<TSource> – Map FROM source type TO this type
  • IMapTo<TDestination> – Map FROM this type TO destination type
  • IMapper – Central mapper interface
  • Automatic DI registration with assembly scanning
  • MappingNotFoundException – Exception when no mapping found

Installation

Using .NET CLI

Bash
dotnet add package Marai.LightMapper

Using Package Manager Console

Bash
Install-Package Marai.LightMapper

Using PackageReference

Add to your .csproj file:

XML
<PackageReference Include="Marai.LightMapper" Version="1.0.0-Alpha.1" />

Requirements

  • .NET 10.0 or later
  • Microsoft.Extensions.DependencyInjection

Dependency Injection Setup

ASP.NET Core Web API
C#
// Program.cs
using Marai.LightMapper;

var builder = WebApplication.CreateBuilder(args);

// Register Marai.LightMapper with assemblies containing mappings
builder.Services.AddMaraiLightMapper(
    typeof(Program).Assembly  // Current assembly
);

builder.Services.AddControllers();

var app = builder.Build();
app.MapControllers();
app.Run();
Multi-Project Solution
C#
// Program.cs
using Marai.LightMapper;
using MyApp.Application;       // DTOs assembly
using MyApp.Domain;            // Entities assembly

var builder = WebApplication.CreateBuilder(args);

// Register mappings from multiple assemblies
builder.Services.AddMaraiLightMapper(
    typeof(Program).Assembly,      // API layer
    typeof(UserDto).Assembly,      // Application layer (DTOs)
    typeof(User).Assembly          // Domain layer (Entities)
);

var app = builder.Build();
app.Run();
Console Application
C#
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Marai.LightMapper;

var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services) =>
    {
        // Register LightMapper
        services.AddMaraiLightMapper(typeof(Program).Assembly);
        
        // Register other services
        services.AddDbContext(ApplicationDbContext)();
    })
    .Build();

await host.RunAsync();
What Gets Registered

The AddMaraiLightMapper() method:

  1. Registers IMapper as SingletonMapper implementation
  2. Registers MappingRegistry as Singleton → Stores all mappings
  3. Scans assemblies for types implementing IMapFrom<T> or IMapTo<T>
  4. Auto-registers mappings discovered via assembly scanning

Core Concepts

IMapFrom

Marker interface indicating a type can be mapped FROM a source type.

Definition

C#
public interface IMapFrom<TSource>
{
    void MapFrom(TSource source)
    {
        // Default: automatic property mapping
        // Override to provide custom logic
    }
}
How It Works

When a type implements IMapFrom<TSource>:

  • Source Type: TSource (the type you’re mapping FROM)
  • Destination Type: The implementing type (the type you’re mapping TO)
  • Direction: TSourceThisType
Syntax
C#
// UserDto can be mapped FROM User
public class UserDto : IMapFrom<User>
{
    // Properties are automatically mapped by name
}
Automatic Mapping Example
C#
// Entity
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public DateTime CreatedAt { get; set; }
}

// DTO with automatic mapping
public class UserDto : IMapFrom<User>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    // CreatedAt not included - won't be mapped
}

// Usage
var user = new User 
{ 
    Id = 1, 
    Name = "John Doe", 
    Email = "john@example.com",
    CreatedAt = DateTime.UtcNow
};

var dto = _mapper.Map<UserDto>(user);
// dto.Id = 1
// dto.Name = "John Doe"
// dto.Email = "john@example.com"
Custom Mapping Example
C#
public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDate { get; set; }
}

// DTO with custom mapping logic
public class UserProfileDto : IMapFrom<User>
{
    public int Id { get; set; }
    public string FullName { get; set; }  // Computed property
    public int Age { get; set; }          // Computed property

    // Custom mapping implementation
    public void MapFrom(User source)
    {
        Id = source.Id;
        FullName = $"{source.FirstName} {source.LastName}";  // Combine names
        Age = DateTime.UtcNow.Year - source.BirthDate.Year;  // Calculate age
    }
}

// Usage
var user = new User 
{ 
    Id = 1, 
    FirstName = "John", 
    LastName = "Doe",
    BirthDate = new DateTime(1990, 5, 15)
};

var profile = _mapper.Map<UserProfileDto>(user);
// profile.Id = 1
// profile.FullName = "John Doe"
// profile.Age = 35 (as of 2026)
When to Use IMapFrom

Use IMapFrom when:

  • Mapping FROM entities TO DTOs
  • Mapping FROM domain models TO view models
  • The destination type controls the mapping logic
  • You want to keep mapping logic in the DTO layer

IMapTo

Marker interface indicating a type can be mapped TO a destination type.

Definition

C#
public interface IMapTo<TDestination>
{
    void MapTo(TDestination destination)
    {
        // Default: automatic property mapping
        // Override to provide custom logic
    }
}

How It Works

When a type implements IMapTo<TDestination>:

  • Source Type: The implementing type (the type you’re mapping FROM)
  • Destination Type: TDestination (the type you’re mapping TO)
  • Direction: ThisTypeTDestination

Syntax

C#
// CreateUserRequest can be mapped TO User
public class CreateUserRequest : IMapTo<User>
{
    // Properties are automatically mapped by name
}
C#
// Request model
public class CreateProductRequest : IMapTo<Product>
{
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
}

// Entity
public class Product
{
    public int Id { get; set; }  // Not in request, won't be mapped
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public DateTime CreatedAt { get; set; }  // Not in request
}

// Usage
var request = new CreateProductRequest
{
    Name = "Laptop",
    Description = "Gaming laptop",
    Price = 1299.99m
};

var product = _mapper.Map<Product>(request);
// product.Name = "Laptop"
// product.Description = "Gaming laptop"
// product.Price = 1299.99
// product.Id = 0 (default)
// product.CreatedAt = default

Custom Mapping Example

C#
public class UpdateUserRequest : IMapTo<User>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }

    // Custom mapping implementation
    public void MapTo(User destination)
    {
        destination.FirstName = FirstName;
        destination.LastName = LastName;
        destination.Email = Email?.ToLower();  // Normalize email
        destination.UpdatedAt = DateTime.UtcNow;  // Set timestamp
    }
}

// Usage
var request = new UpdateUserRequest
{
    FirstName = "Jane",
    LastName = "Smith",
    Email = "JANE@EXAMPLE.COM"
};

var user = _mapper.Map<User>(request);
// user.FirstName = "Jane"
// user.LastName = "Smith"
// user.Email = "jane@example.com" (lowercased)
// user.UpdatedAt = 2026-02-22 (current time)

When to Use IMapTo

Use IMapTo when:

  • Mapping FROM requests TO entities
  • Mapping FROM DTOs TO domain models
  • The source type controls the mapping logic
  • You want to keep mapping logic in the request/DTO layer

The central mapper interface for executing mappings.

Definition

C#
public interface IMapper
{
    // Generic mapping
    TDestination Map<TDestination>(object source);
    
    // Non-generic mapping
    object Map(object source, Type destinationType);
}

Methods

Map<TDestination>(object source)

Maps an object to a destination type using generics.

Parameters:

  • source – The source object to map from

Returns:

  • Instance of TDestination with mapped properties

Throws:

  • ArgumentNullException – If source is null
  • MappingNotFoundException – If no mapping is registered

Example:

C#
public class UserService
{
    private readonly IMapper _mapper;

    public UserService(IMapper mapper)
    {
        _mapper = mapper;
    }

    public UserDto GetUser(User user)
    {
        return _mapper.Map<UserDto>(user);
    }
}
Map(object source, Type destinationType)

Maps an object to a destination type using reflection (non-generic).

Parameters:

  • source – The source object to map from
  • destinationType – The type to map to

Returns:

  • Object instance of destination type

Throws:

  • ArgumentNullException – If source is null
  • MappingNotFoundException – If no mapping is registered

Example:

C#
public object MapDynamic(object source, string targetTypeName)
{
    var destinationType = Type.GetType(targetTypeName);
    return _mapper.Map(source, destinationType);
}
Usage in Controllers
C#
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IMapper _mapper;
    private readonly ApplicationDbContext _db;

    public UsersController(IMapper mapper, ApplicationDbContext db)
    {
        _mapper = mapper;
        _db = db;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetUser(int id)
    {
        var user = await _db.Users.FindAsync(id);
        
        if (user == null)
            return NotFound();

        var dto = _mapper.Map<UserDto>(user);
        return Ok(dto);
    }

    [HttpGet]
    public async Task<IActionResult> GetAllUsers()
    {
        var users = await _db.Users.ToListAsync();
        
        // Map collection
        var dtos = users.Select(u => _mapper.Map<UserDto>(u)).ToList();
        
        return Ok(dtos);
    }

    [HttpPost]
    public async Task<IActionResult> CreateUser(CreateUserRequest request)
    {
        var user = _mapper.Map<User>(request);
        user.CreatedAt = DateTime.UtcNow;
        
        _db.Users.Add(user);
        await _db.SaveChangesAsync();
        
        var dto = _mapper.Map<UserDto>(user);
        return CreatedAtAction(nameof(GetUser), new { id = user.Id }, dto);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateUser(int id, UpdateUserRequest request)
    {
        var user = await _db.Users.FindAsync(id);
        
        if (user == null)
            return NotFound();

        var updatedUser = _mapper.Map<User>(request);
        user.FirstName = updatedUser.FirstName;
        user.LastName = updatedUser.LastName;
        user.Email = updatedUser.Email;
        user.UpdatedAt = DateTime.UtcNow;
        
        await _db.SaveChangesAsync();
        
        var dto = _mapper.Map<UserDto>(user);
        return Ok(dto);
    }
}
Usage in Services
C#
public class ProductService
{
    private readonly IMapper _mapper;
    private readonly ApplicationDbContext _db;

    public ProductService(IMapper mapper, ApplicationDbContext db)
    {
        _mapper = mapper;
        _db = db;
    }

    public async Task<List<ProductDto>> GetAllProductsAsync()
    {
        var products = await _db.Products
            .Where(p => p.IsActive)
            .ToListAsync();

        return products.Select(p => _mapper.Map<ProductDto>(p)).ToList();
    }

    public async Task<int> CreateProductAsync(CreateProductRequest request)
    {
        var product = _mapper.Map<Product>(request);
        product.CreatedAt = DateTime.UtcNow;
        product.IsActive = true;

        _db.Products.Add(product);
        await _db.SaveChangesAsync();

        return product.Id;
    }
}

Mapping Strategies

Automatic Mapping

LightMapper automatically maps properties by name (case-insensitive) when both source and destination have matching property names with compatible types.

How It Works
  1. Scan source properties – Find all readable public properties
  2. Scan destination properties – Find all writable public properties
  3. Match by name – Case-insensitive comparison
  4. Type compatibility check – Ensure destination type can accept source value
  5. Copy values – Transfer property values
Property Matching Rules
C#
public class Source
{
    public int Id { get; set; }           // → Matches destination.Id
    public string Name { get; set; }      // → Matches destination.name (case-insensitive)
    public string EMAIL { get; set; }     // → Matches destination.Email
    public DateTime CreatedAt { get; set; }  // No match in destination (ignored)
}

public class Destination : IMapFrom<Source>
{
    public int Id { get; set; }           // Matched
    public string name { get; set; }      // Matched (case-insensitive)
    public string Email { get; set; }     // Matched
    public string Description { get; set; }  // No match in source (remains default)
}
Type Compatibility
C#
public class Source
{
    public int Age { get; set; }          // int
    public string Name { get; set; }      // string
    public DateTime Date { get; set; }    // DateTime
}

public class Destination : IMapFrom<Source>
{
    public int Age { get; set; }          // int = int (compatible)
    public object Name { get; set; }      // object = string (compatible)
    public string Date { get; set; }      // string ≠ DateTime (incompatible, skipped)
}
Collection Mapping
C#
public class UserDto : IMapFrom<User>
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// Map list
var users = await _db.Users.ToListAsync();
var dtos = users.Select(u => _mapper.Map<UserDto>(u)).ToList();

// Map IEnumerable
IEnumerable<UserDto> MapUsers(IEnumerable<User> users)
{
    return users.Select(u => _mapper.Map<UserDto>(u));
}

Custom Mapping

Override the MapFrom() or MapTo() method for custom mapping logic.

Custom MapFrom
C#
public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<Order> Orders { get; set; }
    public DateTime BirthDate { get; set; }
}

public class UserSummaryDto : IMapFrom<User>
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public int TotalOrders { get; set; }
    public int Age { get; set; }

    // Custom mapping logic
    public void MapFrom(User source)
    {
        Id = source.Id;
        FullName = $"{source.FirstName} {source.LastName}";
        TotalOrders = source.Orders?.Count ?? 0;
        Age = DateTime.UtcNow.Year - source.BirthDate.Year;
    }
}
Custom MapTo
C#
public class CreateUserRequest : IMapTo<User>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }

    // Custom mapping logic
    public void MapTo(User destination)
    {
        destination.FirstName = FirstName?.Trim();
        destination.LastName = LastName?.Trim();
        destination.Email = Email?.ToLower();
        destination.PasswordHash = BCrypt.HashPassword(Password);  // Hash password
        destination.CreatedAt = DateTime.UtcNow;
        destination.IsActive = true;
    }
}
Mixing Automatic and Custom
C#
public class ProductDto : IMapFrom<Product>
{
    // Automatically mapped
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    // Custom properties
    public string PriceFormatted { get; set; }
    public string Status { get; set; }

    public void MapFrom(Product source)
    {
        // Auto-map matching properties
        Id = source.Id;
        Name = source.Name;
        Price = source.Price;
        
        // Custom logic
        PriceFormatted = $"${source.Price:F2}";
        Status = source.IsActive ? "Available" : "Discontinued";
    }
}

Complete Examples

Example 1: Simple Entity to DTO Mapping

C#
// ===== Entity =====
public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string PasswordHash { get; set; }  // Sensitive data
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
    public bool IsActive { get; set; }
}

// ===== DTO (Automatic Mapping) =====
public class UserDto : IMapFrom<User>
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public bool IsActive { get; set; }
    // PasswordHash excluded for security
}

// ===== Service =====
public class UserService
{
    private readonly IMapper _mapper;
    private readonly ApplicationDbContext _db;

    public async Task<List<UserDto>> GetAllUsersAsync()
    {
        var users = await _db.Users.ToListAsync();
        return users.Select(u => _mapper.Map<UserDto>(u)).ToList();
    }

    public async Task<UserDto> GetUserByIdAsync(int id)
    {
        var user = await _db.Users.FindAsync(id);
        return user != null ? _mapper.Map<UserDto>(user) : null;
    }
}

// ===== Controller =====
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly UserService _userService;

    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        var users = await _userService.GetAllUsersAsync();
        return Ok(users);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(int id)
    {
        var user = await _userService.GetUserByIdAsync(id);
        return user != null ? Ok(user) : NotFound();
    }
}

Example 2: Request to Entity Mapping (Create)

C#
// ===== Request Model =====
public class CreateProductRequest : IMapTo<Product>
{
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }

    // Custom mapping to set defaults
    public void MapTo(Product destination)
    {
        destination.Name = Name;
        destination.Description = Description;
        destination.Price = Price;
        destination.CategoryId = CategoryId;
        destination.CreatedAt = DateTime.UtcNow;
        destination.IsActive = true;
        destination.Stock = 0;  // Default stock
    }
}

// ===== Entity =====
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
    public int Stock { get; set; }
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
}

// ===== Response DTO =====
public class ProductDto : IMapFrom<Product>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
    public bool IsActive { get; set; }
}

// ===== Controller =====
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IMapper _mapper;
    private readonly ApplicationDbContext _db;

    [HttpPost]
    public async Task<IActionResult> CreateProduct(CreateProductRequest request)
    {
        // Map request to entity
        var product = _mapper.Map<Product>(request);
        
        _db.Products.Add(product);
        await _db.SaveChangesAsync();
        
        // Map entity to response DTO
        var dto = _mapper.Map<ProductDto>(product);
        
        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, dto);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(int id)
    {
        var product = await _db.Products.FindAsync(id);
        
        if (product == null)
            return NotFound();
        
        var dto = _mapper.Map<ProductDto>(product);
        return Ok(dto);
    }
}

Example 3: Complex Custom Mapping

C#
// ===== Entities =====
public class Order
{
    public int Id { get; set; }
    public string OrderNumber { get; set; }
    public int CustomerId { get; set; }
    public Customer Customer { get; set; }
    public List<OrderItem> Items { get; set; }
    public OrderStatus Status { get; set; }
    public DateTime CreatedAt { get; set; }
}

public class OrderItem
{
    public int Id { get; set; }
    public int ProductId { get; set; }
    public Product Product { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

// ===== DTO with Complex Mapping =====
public class OrderDetailsDto : IMapFrom<Order>
{
    public int Id { get; set; }
    public string OrderNumber { get; set; }
    public string CustomerName { get; set; }
    public string Status { get; set; }
    public decimal TotalAmount { get; set; }
    public int ItemCount { get; set; }
    public List<OrderItemDto> Items { get; set; }
    public DateTime CreatedAt { get; set; }

    public void MapFrom(Order source)
    {
        Id = source.Id;
        OrderNumber = source.OrderNumber;
        CustomerName = $"{source.Customer.FirstName} {source.Customer.LastName}";
        Status = source.Status.ToString();
        TotalAmount = source.Items.Sum(i => i.Quantity * i.UnitPrice);
        ItemCount = source.Items.Count;
        CreatedAt = source.CreatedAt;
        
        // Map nested collection
        Items = source.Items.Select(i => new OrderItemDto
        {
            ProductName = i.Product.Name,
            Quantity = i.Quantity,
            UnitPrice = i.UnitPrice,
            Subtotal = i.Quantity * i.UnitPrice
        }).ToList();
    }
}

public class OrderItemDto
{
    public string ProductName { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Subtotal { get; set; }
}

// ===== Service =====
public class OrderService
{
    private readonly IMapper _mapper;
    private readonly ApplicationDbContext _db;

    public async Task<OrderDetailsDto> GetOrderDetailsAsync(int orderId)
    {
        var order = await _db.Orders
            .Include(o => o.Customer)
            .Include(o => o.Items)
                .ThenInclude(i => i.Product)
            .FirstOrDefaultAsync(o => o.Id == orderId);

        if (order == null)
            return null;

        return _mapper.Map<OrderDetailsDto>(order);
    }
}

Example 4: Bidirectional Mapping

C#
// ===== Entity =====
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
}

// ===== DTO with Bidirectional Mapping =====
public class ProductDto : IMapFrom<Product>, IMapTo<Product>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }

    // Map FROM Product TO ProductDto
    public void MapFrom(Product source)
    {
        Id = source.Id;
        Name = source.Name;
        Price = source.Price;
        Stock = source.Stock;
    }

    // Map FROM ProductDto TO Product
    public void MapTo(Product destination)
    {
        // Don't overwrite Id (it's database-generated)
        destination.Name = Name;
        destination.Price = Price;
        destination.Stock = Stock;
    }
}

// Both directions work
var dto = _mapper.Map<ProductDto>(product);    // Product → ProductDto
var product2 = _mapper.Map<Product>(dto);       // ProductDto → Product

Best Practices

DO

1. Use DTOs for API Responses

C#
// GOOD: Return DTO, not entity
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
    var user = await _db.Users.FindAsync(id);
    var dto = _mapper.Map<UserDto>(user);  // DTO hides sensitive data
    return Ok(dto);
}

// BAD: Return entity directly
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
    var user = await _db.Users.FindAsync(id);  // Exposes PasswordHash!
    return Ok(user);
}

2. Keep Mapping Types in Same Assembly

C#
// Register both assemblies
builder.Services.AddMaraiLightMapper(
    typeof(User).Assembly,      // Domain
    typeof(UserDto).Assembly    // Application
);

3. Use Custom Mapping for Computed Properties

C#
public class OrderDto : IMapFrom<Order>
{
    public int Id { get; set; }
    public decimal TotalAmount { get; set; }  // Computed

    public void MapFrom(Order source)
    {
        Id = source.Id;
        TotalAmount = source.Items.Sum(i => i.Quantity * i.UnitPrice);
    }
}

4. Use IMapFrom for Read Operations (Entity → DTO)

C#
public class UserDto : IMapFrom<User>  // FROM User TO UserDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}

5. Use IMapTo for Write Operations (Request → Entity)

C#
public class CreateUserRequest : IMapTo<User>  // FROM Request TO User
{
    public string Name { get; set; }
    public string Email { get; set; }
}

6. Exclude Sensitive Data from DTOs

C#
public class UserDto : IMapFrom<User>
{
    public int Id { get; set; }
    public string Email { get; set; }
    // PasswordHash NOT included
}
DON’T

1. Don’t Map in Loops (Performance)

C#
var dtos = users.Select(u => _mapper.Map<UserDto>(u)).ToList();

2. Don’t Use Mapper for Simple Assignments

C#
var dto = new SinglePropertyDto { Id = user.Id };

3. Don’t Forget to Register Assemblies

C#
builder.Services.AddMaraiLightMapper(
    typeof(Program).Assembly,
    typeof(UserDto).Assembly
);

4. Don’t Map Null Values

C#
var dto = user != null ? _mapper.Map<UserDto>(user) : null;

Exception Handling

MappingNotFoundException

Thrown when no mapping is registered from source type to destination type.

Common Causes

1. Forgot to implement IMapFrom or IMapTo

C#
// Solution: Implement IMapFrom
public class UserDto : IMapFrom<User>
{
    public int Id { get; set; }
}

2. Assembly not registered

C#
builder.Services.AddMaraiLightMapper(
    typeof(Program).Assembly,
    typeof(UserDto).Assembly
);

3. Wrong mapping direction

C#
public class UserDto : IMapFrom<User>, IMapTo<User> { }

Exception Handling Example

C#
catch (MappingNotFoundException ex)
{
    _logger.LogError(ex, "Mapping not found: {Message}", ex.Message);
    return StatusCode(500, "Configuration error: mapping not available");
}

Global Exception Handler

C#
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
app.UseExceptionHandler(_ => { });

Performance

Benchmarks

LightMapper is optimized for performance using:

  • Singleton lifetime – Mapper and registry never recreated
  • Cached mappings – Mappings discovered once at startup
  • Reflection caching – Property info cached in ConcurrentDictionary
Performance Tips

1. Use Singleton IMapper (Default)

C#
builder.Services.AddMaraiLightMapper(typeof(Program).Assembly);

2. Map Collections Efficiently

C#
var dtos = users.Select(u => _mapper.Map<UserDto>(u)).ToList();

3. Use Database Projections for Large Queries

C#
var dtos = await _db.Users
    .Select(u => new UserDto
    {
        Id = u.Id,
        Name = u.Name,
        Email = u.Email
    })
    .ToListAsync();

FAQ

Q: How is LightMapper different from AutoMapper?

LightMapper:

  • Lightweight (single implementation file)
  • Convention-based with marker interfaces
  • Explicit mapping registration via IMapFrom/IMapTo
  • Blazing fast startup (assembly scanning only)
  • No advanced features (value resolvers, type converters, etc.)

AutoMapper:

  • Feature-rich (profiles, resolvers, converters, projections)
  • Dynamic configuration
  • Heavier dependency
  • Slower startup (extensive configuration)
Q: Can I map between types without implementing interfaces?

No. LightMapper requires types to implement IMapFrom<T> or IMapTo<T> for mapping discovery.

Q: Can I map the same types in both directions?

Yes! Implement both IMapFrom and IMapTo.

Q: How do I map nested objects?

Use custom mapping logic.

Q: Can I inject dependencies into mapping logic?

No. Mapping types are not instantiated via DI. Use a dedicated mapper service for that.

Q: How do I handle null values?
C#
var dto = user != null ? _mapper.Map<UserDto>(user) : null;

No. Map each element individually.

License

This project is licensed under the Marai Proprietary Software License Agreement.

Free for personal and commercial use.

See LICENSE file or visit: https://marai.dev/proprietary-software-license-agreement

Support

  • Documentation: https://github.com/maraisystems/Marai.LightMapper
  • NuGet: https://www.nuget.org/packages/Marai.LightMapper/
  • Email: contact@marai.dev