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?
- Features
- Installation
- Dependency Injection Setup
- Core Concepts
- Mapping Strategies
- Complete Examples
- Best Practices
- Exception Handling
- Performance
- FAQ
- License
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):
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:
// 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
IMapFromandIMapTo
Features
Core Features
- Automatic Property Mapping – Maps properties by name (case-insensitive)
- Convention-Based Discovery – Assembly scanning for mapping registration
- Bidirectional Mapping –
IMapFrom<T>andIMapTo<T>support - Custom Mapping Logic – Override
MapFrom()orMapTo()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 typeIMapTo<TDestination>– Map FROM this type TO destination typeIMapper– Central mapper interface- Automatic DI registration with assembly scanning
MappingNotFoundException– Exception when no mapping found
Installation
Using .NET CLI
dotnet add package Marai.LightMapperUsing Package Manager Console
Install-Package Marai.LightMapperUsing PackageReference
Add to your .csproj file:
<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
// 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
// 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
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:
- Registers
IMapperas Singleton →Mapperimplementation - Registers
MappingRegistryas Singleton → Stores all mappings - Scans assemblies for types implementing
IMapFrom<T>orIMapTo<T> - Auto-registers mappings discovered via assembly scanning
Core Concepts
IMapFrom
Marker interface indicating a type can be mapped FROM a source type.
Definition
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:
TSource→ThisType
Syntax
// UserDto can be mapped FROM User
public class UserDto : IMapFrom<User>
{
// Properties are automatically mapped by name
}Automatic Mapping Example
// 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
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
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:
ThisType→TDestination
Syntax
// CreateUserRequest can be mapped TO User
public class CreateUserRequest : IMapTo<User>
{
// Properties are automatically mapped by name
}// 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 = defaultCustom Mapping Example
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
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
TDestinationwith mapped properties
Throws:
ArgumentNullException– If source is nullMappingNotFoundException– If no mapping is registered
Example:
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 fromdestinationType– The type to map to
Returns:
- Object instance of destination type
Throws:
ArgumentNullException– If source is nullMappingNotFoundException– If no mapping is registered
Example:
public object MapDynamic(object source, string targetTypeName)
{
var destinationType = Type.GetType(targetTypeName);
return _mapper.Map(source, destinationType);
}Usage in Controllers
[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
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
- Scan source properties – Find all readable public properties
- Scan destination properties – Find all writable public properties
- Match by name – Case-insensitive comparison
- Type compatibility check – Ensure destination type can accept source value
- Copy values – Transfer property values
Property Matching Rules
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
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
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
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
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
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
// ===== 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)
// ===== 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
// ===== 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
// ===== 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 → ProductBest Practices
DO
1. Use DTOs for API Responses
// 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
// Register both assemblies
builder.Services.AddMaraiLightMapper(
typeof(User).Assembly, // Domain
typeof(UserDto).Assembly // Application
);3. Use Custom Mapping for Computed Properties
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)
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)
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
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)
var dtos = users.Select(u => _mapper.Map<UserDto>(u)).ToList();2. Don’t Use Mapper for Simple Assignments
var dto = new SinglePropertyDto { Id = user.Id };3. Don’t Forget to Register Assemblies
builder.Services.AddMaraiLightMapper(
typeof(Program).Assembly,
typeof(UserDto).Assembly
);4. Don’t Map Null Values
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
// Solution: Implement IMapFrom
public class UserDto : IMapFrom<User>
{
public int Id { get; set; }
}2. Assembly not registered
builder.Services.AddMaraiLightMapper(
typeof(Program).Assembly,
typeof(UserDto).Assembly
);3. Wrong mapping direction
public class UserDto : IMapFrom<User>, IMapTo<User> { }Exception Handling Example
catch (MappingNotFoundException ex)
{
_logger.LogError(ex, "Mapping not found: {Message}", ex.Message);
return StatusCode(500, "Configuration error: mapping not available");
}Global Exception Handler
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)
builder.Services.AddMaraiLightMapper(typeof(Program).Assembly);2. Map Collections Efficiently
var dtos = users.Select(u => _mapper.Map<UserDto>(u)).ToList();3. Use Database Projections for Large Queries
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?
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



