Indiko.Blocks.Mapping.SimpleMapper
2.2.18
dotnet add package Indiko.Blocks.Mapping.SimpleMapper --version 2.2.18
NuGet\Install-Package Indiko.Blocks.Mapping.SimpleMapper -Version 2.2.18
<PackageReference Include="Indiko.Blocks.Mapping.SimpleMapper" Version="2.2.18" />
<PackageVersion Include="Indiko.Blocks.Mapping.SimpleMapper" Version="2.2.18" />
<PackageReference Include="Indiko.Blocks.Mapping.SimpleMapper" />
paket add Indiko.Blocks.Mapping.SimpleMapper --version 2.2.18
#r "nuget: Indiko.Blocks.Mapping.SimpleMapper, 2.2.18"
#:package Indiko.Blocks.Mapping.SimpleMapper@2.2.18
#addin nuget:?package=Indiko.Blocks.Mapping.SimpleMapper&version=2.2.18
#tool nuget:?package=Indiko.Blocks.Mapping.SimpleMapper&version=2.2.18
Indiko.Blocks.Mapping.SimpleMapper
High-performance object-to-object mapper that compiles all mapping logic to native delegates at startup, producing zero-overhead mapping at runtime.
Features
- Compiled expression trees — mappings are compiled once to
Func<TSource, TDest>delegates at configuration time; runtime mapping is a plain delegate invocation - Convention mapping — properties with matching names and compatible types are mapped automatically without any configuration
- Profile-based organisation — group related mappings into
MappingProfileclasses and scan them by assembly - Rich member configuration —
MapFrom,Ignore,Condition,NullSubstitute,ReverseMap - Type converters, value resolvers, value converters — pluggable extension points for complex transformations
- Collection mapping —
List<T>, arrays, and nested collections handled automatically - Nested object mapping — full object-graph mapping with circular reference tracking
- Expression mapping — translate
Expression<Func<TModel, TResult>>toExpression<Func<TEntity, TResult>>for EF Core query composition - IQueryable projection —
ProjectTo<TDto>for server-side SQL projections - No external dependencies — only
Microsoft.Extensions.DependencyInjection.Abstractionsand the BCL
Installation
dotnet add package Indiko.Blocks.Mapping.SimpleMapper
Quick Start
1. Register with the DI container
using Indiko.Blocks.Mapping.SimpleMapper.Extensions;
// Assembly scan — discovers all MappingProfile subclasses automatically
services.AddSimpleMapper(cfg =>
{
cfg.AddExpressionMapping(); // optional: enables Expression<Func<>> translation
cfg.AddMaps(Assembly.GetEntryAssembly()); // scan for MappingProfile subclasses
});
2. Define a profile
using Indiko.Blocks.Mapping.SimpleMapper.Profiles;
public class OrderProfile : MappingProfile
{
public OrderProfile()
{
CreateMap<Order, OrderDto>()
.ForMember(d => d.CustomerName, opt => opt.MapFrom(s => s.Customer.FullName))
.ForMember(d => d.Total, opt => opt.MapFrom(s => s.Lines.Sum(l => l.Price * l.Qty)))
.ReverseMap();
}
}
3. Inject IMapper and map
using Indiko.Blocks.Mapping.SimpleMapper.Core;
public class OrderService
{
private readonly IMapper _mapper;
public OrderService(IMapper mapper) => _mapper = mapper;
public OrderDto GetDto(Order order) => _mapper.Map<OrderDto>(order);
}
Convention Mapping
Properties whose names match (case-insensitive) and whose types are compatible are mapped without any configuration:
// No ForMember needed — Id, Name, CreatedAt map by convention
CreateMap<Product, ProductDto>();
Supported automatic coercions:
| Source type | Destination type |
|---|---|
string |
int, long, double, decimal, Guid, bool, DateTime |
| Numeric types | Compatible numeric types |
Enum |
int / string (and reverse) |
T |
T? (nullable lift) |
Member Configuration
MapFrom — custom source expression
CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s => s.First + " " + s.Last))
.ForMember(d => d.City, opt => opt.MapFrom(s => s.Address.City))
.ForMember(d => d.OrderCount, opt => opt.MapFrom(s => s.Orders.Count));
Ignore — exclude a member
CreateMap<User, UserDto>()
.ForMember(d => d.PasswordHash, opt => opt.Ignore())
.ForMember(d => d.SecurityStamp, opt => opt.Ignore());
Condition — map only when predicate is true
CreateMap<Product, ProductDto>()
.ForMember(d => d.DiscountedPrice,
opt => opt.Condition(s => s.Discount > 0));
NullSubstitute — provide a default for null source values
CreateMap<Article, ArticleDto>()
.ForMember(d => d.Description, opt => opt.NullSubstitute("No description available"))
.ForMember(d => d.Tags, opt => opt.NullSubstitute(new List<string>()));
ReverseMap
CreateMap<Order, OrderDto>().ReverseMap();
// Generates the reverse TypeMap using convention mapping
All IMapper Overloads
// New instance — destination type inferred from runtime type
TDest Map<TDest>(object source);
// New instance — fully typed
TDest Map<TSource, TDest>(TSource source);
// Update existing instance
TDest Map<TSource, TDest>(TSource source, TDest destination);
// With per-call hooks and Items bag
TDest Map<TDest>(object source,
Action<IMappingOperationOptions<object, TDest>> opts);
TDest Map<TSource, TDest>(TSource source,
Action<IMappingOperationOptions<TSource, TDest>> opts);
TDest Map<TSource, TDest>(TSource source, TDest destination,
Action<IMappingOperationOptions<TSource, TDest>> opts);
Per-call hooks
var dto = _mapper.Map<Order, OrderDto>(order, opts =>
{
opts.BeforeMap((src, dest) => dest.MappedAt = DateTimeOffset.UtcNow);
opts.AfterMap((src, dest) => _logger.LogDebug("Mapped Order {Id}", src.Id));
opts.Items["tenantId"] = _tenantContext.TenantId; // pass data to resolvers
});
Collections
Collections of any registered TypeMap are mapped automatically:
// Maps each element using the Order → OrderDto TypeMap
List<OrderDto> dtos = _mapper.Map<List<OrderDto>>(orderList);
OrderDto[] arr = _mapper.Map<OrderDto[]>(orderList);
Nested collection properties are also handled:
CreateMap<Customer, CustomerDto>();
// CustomerDto.Orders (List<OrderDto>) mapped via Order → OrderDto TypeMap
Nested Objects
Nested objects are mapped recursively. Register a TypeMap for each type that appears in the graph:
CreateMap<Address, AddressDto>();
CreateMap<Customer, CustomerDto>();
// CustomerDto.Address is mapped via Address → AddressDto TypeMap automatically
Circular references (e.g. Order → Customer → Orders) are tracked by object identity and handled safely without infinite loops.
Type Converters
Implement ITypeConverter<TSource, TDest> for full type-to-type conversion:
public class MoneyConverter : ITypeConverter<decimal, MoneyDto>
{
public MoneyDto Convert(decimal source, MoneyDto dest, ResolutionContext ctx)
=> new MoneyDto { Amount = source, Currency = "EUR" };
}
// Register in profile:
CreateMap<decimal, MoneyDto>().ConvertUsing<MoneyConverter>();
Value Resolvers
Implement IValueResolver<TSource, TDest, TMember> for complex single-member resolution:
public class FullNameResolver : IValueResolver<User, UserDto, string>
{
public string Resolve(User source, UserDto dest, string member, ResolutionContext ctx)
=> $"{source.Title} {source.First} {source.Last}".Trim();
}
// Register in profile:
CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.ResolveUsing<FullNameResolver>());
Value Converters
Implement IValueConverter<TSource, TDest> for reusable per-member value conversions:
public class DateToStringConverter : IValueConverter<DateTime, string>
{
public string Convert(DateTime source, ResolutionContext ctx)
=> source.ToString("yyyy-MM-dd");
}
CreateMap<Event, EventDto>()
.ForMember(d => d.Date, opt => opt.ConvertUsing(new DateToStringConverter(), s => s.StartDate));
Expression Mapping
Translate Expression<Func<TModel, TResult>> to Expression<Func<TEntity, TResult>> for use with EF Core Where/OrderBy without loading entities into memory.
Enable in DI registration
services.AddSimpleMapper(cfg =>
{
cfg.AddExpressionMapping(); // required
cfg.AddMaps(assembly);
});
Usage
// Expression written against the DTO/model type
Expression<Func<OrderDto, bool>> modelFilter = dto => dto.CustomerName.Contains("Smith");
// Translated to an entity expression usable with EF Core
Expression<Func<Order, bool>> entityFilter =
_mapper.MapExpression<OrderDto, Order, bool>(modelFilter);
var results = await _db.Orders.Where(entityFilter).ToListAsync();
Supported node types
- Property access chains (
dto.Customer.City) - Binary comparisons (
==,!=,<,>,&&,||) - String methods (
Contains,StartsWith,EndsWith) - Collection predicates (
Any,All,Count,Where) with nested lambda remapping - Constant, closure, and
MemberInitexpressions - Nullable type lifting (
Guid→Guid?coercion)
IQueryable Projection
ProjectTo<TDto> pushes the mapping directly into the SQL query — only the columns needed by the DTO are selected:
// Server-side projection — generates SELECT only the columns OrderDto needs
var dtos = await _db.Orders
.Where(o => o.CustomerId == customerId)
.ProjectTo<OrderDto>(_mapper)
.ToListAsync(cancellationToken);
This avoids loading full entity graphs when you only need a subset of properties.
Configuration Validation
Call AssertConfigurationIsValid() at startup or in CI to catch misconfigured mappings before runtime:
// In Program.cs / startup
var config = new MapperConfiguration(cfg => cfg.AddMaps(assembly));
config.AssertConfigurationIsValid(); // throws if any destination member is unmapped
// In unit tests (recommended for CI)
[Fact]
public void All_profiles_are_valid()
{
var config = new MapperConfiguration(cfg =>
cfg.AddMaps(typeof(OrderProfile).Assembly));
config.AssertConfigurationIsValid(); // fast fail in CI
}
Testing
Instantiate the mapper without DI for focused unit tests:
var config = new MapperConfiguration(cfg => cfg.AddProfile<OrderProfile>());
IMapper mapper = new Mapper(config);
var dto = mapper.Map<OrderDto>(new Order { Id = 1, Customer = new Customer { FullName = "Alice" } });
dto.CustomerName.Should().Be("Alice");
Test a profile in isolation with only the profiles it depends on:
[Fact]
public void OrderProfile_maps_correctly()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<CustomerProfile>(); // dependency
cfg.AddProfile<OrderProfile>();
});
config.AssertConfigurationIsValid();
var mapper = new Mapper(config);
var order = new Order { Id = 42, Lines = [new OrderLine { Price = 10, Qty = 3 }] };
var dto = mapper.Map<OrderDto>(order);
dto.Id.Should().Be(42);
dto.Total.Should().Be(30m);
}
Best Practices
- Register
MapperConfigurationas a singleton — it is thread-safe and expensive to construct; never create it per-request. - One profile per bounded context —
OrderProfile,CustomerProfile,InventoryProfile. - Explicitly ignore security-sensitive members —
opt.Ignore()onPasswordHash,SecurityStamp, etc. Documents intent and satisfies validation. - Prefer
ProjectToover in-memorySelect + MapforIQueryablequeries — avoids loading full entities. - Keep
MapFromexpressions pure and translatable — avoid method calls that EF Core cannot translate to SQL when the TypeMap is used withProjectToorMapExpression. - Register child TypeMaps for every type in collection properties that appears in expression-mapped predicates.
Target Framework
- .NET 10
Related Packages
Indiko.Blocks.Mediation.SimpleMediator— lightweight CQRS mediatorIndiko.Blocks.Mediation.Abstractions— shared mediation contracts
License
MIT — see LICENSE in the repository root.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.0
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Indiko.Blocks.Mapping.SimpleMapper:
| Package | Downloads |
|---|---|
|
Indiko.Blocks.DataAccess.Abstractions
Building Blocks DataAccess Abstractions |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.2.18 | 0 | 3/8/2026 |
| 2.2.17 | 0 | 3/8/2026 |
| 2.2.16 | 0 | 3/8/2026 |
| 2.2.15 | 32 | 3/7/2026 |
| 2.2.13 | 28 | 3/7/2026 |
| 2.2.12 | 29 | 3/7/2026 |
| 2.2.10 | 33 | 3/6/2026 |
| 2.2.9 | 33 | 3/6/2026 |
| 2.2.8 | 32 | 3/6/2026 |
| 2.2.7 | 34 | 3/6/2026 |
| 2.2.5 | 38 | 3/6/2026 |
| 2.2.3 | 36 | 3/6/2026 |
| 2.2.2 | 33 | 3/6/2026 |
| 2.2.1 | 31 | 3/6/2026 |
| 2.2.0 | 33 | 3/6/2026 |