Skip to content

Mapsicle

CINuGetDownloadsLicense: MPL 2.0

ko-fi

Mapsicle is a high-performance, modular object mapping ecosystem for .NET. Choose only what you need:

PackagePurposeDependencies
MapsicleZero-config mappingNone
Mapsicle.FluentFluent configuration + ProfilesMapsicle
Mapsicle.EntityFrameworkEF Core ProjectTo<T>()Mapsicle.Fluent
Mapsicle.ValidationFluentValidation integrationMapsicle.Fluent
Mapsicle.NamingConventionsNaming convention supportMapsicle.Fluent
Mapsicle.JsonJSON serialization integrationMapsicle.Fluent
Mapsicle.AspNetCoreASP.NET Core Minimal API helpersMapsicle.Validation
Mapsicle.CachingMemory/Distributed cache supportMapsicle.Fluent
Mapsicle.AuditChange tracking/diff detectionMapsicle.Fluent
Mapsicle.DataAnnotationsDataAnnotations validationMapsicle.Fluent
Mapsicle.DapperDapper query-and-map helpersMapsicle.Fluent
Mapsicle.SerilogMapping diagnostics via SerilogMapsicle

The core Mapsicle package has zero dependencies. Extension packages introduce their respective third-party dependencies (listed in the table above).

Zero configuration by default. Configure only where a mapping is genuinely not conventional.


Why Choose Mapsicle?

AutoMapper 15.0 and later are no longer permissively licensed. They are governed by RPL-1.5 or a licence agreement from Lucky Penny Software, which includes a free Community License for those who qualify. Earlier versions keep their original licence. RPL-1.5 is strong reciprocal: its source obligations reach software that is only deployed internally, not just software you distribute. Check the terms against your own situation rather than taking this paragraph as advice. Mapsicle is MPL 2.0.

When Mapsicle is the right choice, and when it is not

Speed is the weakest argument for this library, so it is not the one to lead with. Against a source generator Mapsicle loses on speed and always will. What it offers is mappings that do not have to be known when you compile.

Reach for Mapsicle when:

SituationWhy the alternatives do not fit
Mapping a Dictionary<string, object> into a typeMapperly has nothing to generate against. AutoMapper needs the pair configured.
A collection whose items have different runtime typesSame: nothing to generate, and the shape is only known at runtime.
Types arriving from plugins, reflection or configurationCompile-time generation is not available at all.
Hundreds of DTOs and no appetite for a CreateMap per pairMapsicle maps by convention with no setup. AutoMapper throws when it reaches an unconfigured pair. AssertConfigurationIsValid() validates the maps you configured, so it does not catch a pair you never registered.
Object graphs that contain cyclesMapsicle returns the default at MaxDepth with no configuration. AutoMapper needs PreserveReferences() or MaxDepth(...), and an unhandled cycle can still overflow the stack. Mapperly needs UseReferenceHandling.
The licence has to be permissiveThis is the reason most people are reading this page.

Reach for something else when:

SituationChoose
Every mapping is known at compile time and you will declare themMapperly. 2.5x to 3x faster and indistinguishable from hand-written code.
Collection throughput is what your workload is bounded byMapperly, then AutoMapper. Mapsicle is 1.33x slower than AutoMapper here.
You need AOT with no runtime code generationMapperly. Mapsicle compiles expression trees at first use.

Mapsicle is about 1.4x faster than AutoMapper on single objects, on both x64 and arm64. That is real and it is measured below, but it is a supporting argument rather than the reason to switch.

Quick Comparison

FeatureMapsicleAutoMapperMapperly
LicenseMPL 2.0RPL-1.5, or a Lucky Penny Software agreement (a free Community License exists)MIT
ArchitectureRuntime + CachingRuntime + ExpressionsSource Generator
Setup RequiredNoneProfiles, DIPartial class
Dependencies0 (core)5+0 (compile-time)
Compile-time SafetyPartialNoFull
AOT CompatiblePartialNoYes
Circular RefsHandled by defaultOpt in via PreserveReferences() or MaxDepth(...)Opt in via UseReferenceHandling
Memory BoundedLRU OptionNoN/A
Cache StatisticsYesNoN/A
Integrated ValidationYesNoNo
ASP.NET Core HelpersYesNoNo

Detailed Comparison: Mapsicle vs AutoMapper vs Mapperly

Core Mapping Features

FeatureMapsicleAutoMapperMapperly
Convention-based mapping
Flattening (Address.CityAddressCity)
Custom member mappingForMember()ForMember()[MapProperty]
Ignore members[IgnoreMap]Ignore()[MapperIgnore]
Reverse mappingReverseMap()ReverseMap()✅ (define both)
Before/After map hooks
Type convertersCreateConverter<>()ConvertUsing()✅ User methods
Inheritance/PolymorphismInclude<>()Include<>()
Nested object mapping
Collection mapping
Constructor mappingConstructUsing()ConstructUsing()✅ (automatic)

Configuration & Organization

FeatureMapsicleAutoMapperMapperly
Profile supportMapsicleProfileProfile❌ (partial classes)
Fluent configuration❌ (attributes)
Attribute-based config[MapFrom]
Static zero-config APIobj.MapTo<T>()
DI-friendlyIMapperIMapper
Assembly scanningN/A

Extension Packages

Package/FeatureMapsicleAutoMapperMapperly
EF Core ProjectToMapsicle.EntityFramework✅ Built-in✅ (expressions)
FluentValidationMapsicle.Validation
DataAnnotationsMapsicle.DataAnnotations
JSON serializationMapsicle.Json
ASP.NET CoreMapsicle.AspNetCore
CachingMapsicle.CachingN/A
Audit/Change trackingMapsicle.Audit
Naming conventions✅ 5 conventions✅ Built-inNamingStrategy

Naming Convention Support

ConventionMapsicleAutoMapperMapperly
PascalCase
camelCase
snake_case
kebab-case
SCREAMING_SNAKE_CASE

Performance Characteristics

AspectMapsicleAutoMapperMapperly
First map overheadMediumMedium-HighNone
Subsequent mapsFastFastFastest
Memory footprintLow-MediumMediumLowest
Startup time impactLowMediumNone
AOT compatiblePartialNoYes

When to Use Each

ScenarioRecommendation
Maximum performance, AOT requiredMapperly
Compile-time safety is criticalMapperly
Quick prototyping, zero setupMapsicle (static API)
Need integrated validationMapsicle
Existing AutoMapper codebaseAutoMapper (if licensed) or migrate
Budget-conscious / OSS projectMapsicle or Mapperly
Complex mapping configurationsAutoMapper or Mapsicle (fluent)
ASP.NET Core Minimal APIsMapsicle (AspNetCore package)
Need audit trail of changesMapsicle (Audit package)

Code Comparison

Mapsicle (Static - Zero Config)

vardto=user.MapTo<UserDto>();

Mapsicle (Fluent)

varconfig=newMapperConfiguration(cfg =>cfg.CreateMap<User,UserDto>());varmapper=config.CreateMapper();vardto=mapper.Map<UserDto>(user);

AutoMapper

varconfig=newMapperConfiguration(cfg =>cfg.CreateMap<User,UserDto>());varmapper=config.CreateMapper();vardto=mapper.Map<UserDto>(user);

Mapperly

[Mapper]publicpartialclassUserMapper{publicpartialUserDtoToDto(Useruser);}// Usagevardto=newUserMapper().ToDto(user);

Unique Mapsicle Features

Features not found in AutoMapper or Mapperly:

  1. Static zero-config API: user.MapTo<UserDto>() - no setup required
  2. Built-in validation integration: Map + validate in one call with FluentValidation or DataAnnotations
  3. Audit/diff tracking: Track what changed during mapping with MapWithAudit<T>()
  4. Caching integration: Cache mapped results with IMemoryCache/IDistributedCache
  5. ASP.NET Core IResult helpers: MapValidateAndReturn<T, TValidator>()
  6. JSON map-and-serialize: MapToJson<T>(), MapFromJson<T>()
  7. LRU cache option: Memory-bounded cache for long-running applications

Quick Start

Complete Example (Copy & Paste)

usingMapsicle;// 1. Define your typespublicclassUser{publicintId{get;set;}publicstringFirstName{get;set;}publicstringLastName{get;set;}publicstringEmail{get;set;}}publicclassUserDto{publicintId{get;set;}publicstringFirstName{get;set;}publicstringLastName{get;set;}}// 2. Map - that's it! No configuration neededvaruser=newUser{Id=1,FirstName="John",LastName="Doe",Email="john@example.com"};vardto=user.MapTo<UserDto>();// FirstName and LastName copied automatically// 3. Map collectionsList<User>users=GetUsers();List<UserDto>dtos=users.MapTo<UserDto>();// Entire list mapped

Requirements: .NET Standard 2.0+ or .NET 6.0+ (Mapsicle.AspNetCore and Mapsicle.EntityFramework require .NET 8.0+) Installation:dotnet add package Mapsicle

Which Package Do I Need?

Do you need EF Core query translation (ProjectTo)?
├─ YES → Install: Mapsicle + Mapsicle.Fluent + Mapsicle.EntityFramework
└─ NO
├─ Do you need post-mapping validation?
│ └─ YES → Install: Mapsicle + Mapsicle.Fluent + Mapsicle.Validation
├─ Do you need naming convention support (snake_case ↔ PascalCase)?
│ └─ YES → Install: Mapsicle + Mapsicle.Fluent + Mapsicle.NamingConventions
├─ Do you need custom mapping logic (ForMember, hooks)?
│ └─ YES → Install: Mapsicle + Mapsicle.Fluent
└─ NO → Install: Mapsicle (core only - zero config)
ScenarioPackages Needed
Simple POCO mappingMapsicle
API DTOs with transformationsMapsicle.Fluent
EF Core with SQL projectionMapsicle.EntityFramework
Map + validate DTOsMapsicle.Validation
snake_case ↔ PascalCase mappingMapsicle.NamingConventions

Benchmark Results

Real benchmarks on Apple M1, .NET 8.0, BenchmarkDotNet v0.13.12:

Core Mapping Performance

BenchmarkDotNet, short job, .NET 8, measured on two architectures because one is not evidence of anything portable. Reproduce with dotnet run -c Release --project tests/Mapsicle.Benchmarks -- --core.

Single object, five properties:

RuntimeManualMapsicleAutoMapperMapperlyMapsicle vs AutoMapper
x64 Linux (CI runner)18.3 ns60.4 ns82.7 ns18.2 ns1.37x faster
arm64 macOS12.2 ns33.1 ns49.0 ns12.5 ns1.48x faster

Mapperly is not a competitor, it is a different trade, and it wins the one this table measures. At 18.2 ns against hand-written code's 18.3 ns it is not close to manual, it is indistinguishable from it, because a source generator emits ordinary C# assignments at compile time and leaves no delegate, no cache lookup and no indirection at runtime. Mapsicle and AutoMapper both build an expression tree, compile it, cache it, look it up and invoke through it. That apparatus is the entire 2.5x to 3x gap, and no runtime mapper can close it, because the apparatus is what makes it a runtime mapper.

What you buy with it: Mapperly needs a partial class with a [Mapper] attribute and a declared method for every pair, all known at compile time. It cannot map a Dictionary<string, object> into a type chosen at runtime, or a collection whose items turn out to have different runtime types, because there is nothing for it to generate against. Mapsicle needs no configuration and resolves types as it meets them.

If your mappings are all known at compile time and you are willing to declare them, choose Mapperly. Mapsicle is for the case where they are not, and its comparison is with AutoMapper.

Other scenarios, arm64:

ScenarioMapsicleAutoMapperMapperlyvs AutoMapper
Flattening38.4 ns54.1 ns15.2 ns1.41x faster
Collection (100)2,428 ns1,823 ns1,451 ns1.33x slower

Allocation per operation matches hand-written code for single objects and flattening (48 B and 56 B, the destination and nothing else). On collections Mapsicle allocates 5,696 B against AutoMapper's 6,992 B, about 19 percent less, while taking longer.

Read that collection row rather than skipping it. Mapsicle is slower than AutoMapper mapping collections. If collection throughput is what your workload is bounded by, Mapperly is faster than both.

Two things worth more than the table:

  • The flattening row has wide error bars at this job length. Run it on your hardware before it decides anything.
  • An earlier version of this table claimed 2.1x on single objects and rough parity on collections. Neither held when the benchmark was re-run. It went unnoticed because CI measured the comparison, printed it and exited zero regardless. CI now fails when the comparison changes direction, and it reads BenchmarkDotNet's own summary rather than a stopwatch loop, so the published numbers and the gated numbers come from one source.

Edge Case Performance

ScenarioMapsicleAutoMapperMapperlyNotes
Deep Nesting (15 levels)✅ Safe✅ Safe✅ SafeAll handle with limits
Circular ReferencesHandled by defaultOpt in via PreserveReferences()Opt in via UseReferenceHandlingOnly Mapsicle needs no configuration
Large Collection (10K)4 ms4 ms~3.5 msMapperly fastest
Parallel (1000 threads)✅ Thread-safe✅ Thread-safe✅ Thread-safeAll thread-safe
Cold StartMediumSlowNoneMapperly pre-compiled

Performance Optimizations (v1.1+)

OptimizationImprovementStatus
TypedMapperCache<T,D>Zero-allocation generic cache✅ NEW
MapTo<TSource,TDest>()Strongly-typed mapping, no boxing✅ NEW
Skip depth tracking for simpleNo overhead for flat types✅ NEW
Lock-free cache readsEliminates contention
Collection mapper caching+20% for collections (v1.1)
PropertyInfo caching+15% faster cold starts
Primitive fast pathSkips depth tracking
Cached compiled actionsNo runtime reflection
LRU cache optionMemory-bounded in long-run apps
Collection pre-allocationCapacity hints for known sizes

Memory & Cache Statistics (v1.1+)

// Enable memory-bounded cachingMapper.UseLruCache=true;Mapper.MaxCacheSize=1000;// Default// Monitor cache performancevarstats=Mapper.CacheInfo();Console.WriteLine($"Cache entries: {stats.Total}");Console.WriteLine($"Hit ratio: {stats.HitRatio:P1}");// Only when LRU enabledConsole.WriteLine($"Hits: {stats.Hits}, Misses: {stats.Misses}");
FeatureMapsicle (Unbounded)Mapsicle (LRU)AutoMapper
Memory Bounded
Cache StatisticsEntry count onlyFull stats
Configurable Limit
Lock-Free ReadsPartial

The claims are gated

The comparison above is not only published, it is checked. dotnet run -c Release --project tests/Mapsicle.Benchmarks -- --quick runs Mapsicle against AutoMapper and returns a non-zero exit code when a ratio moves outside its bound. CI runs it on every pull request.

Two more gates guard the other half of the pitch:

  • core-has-no-dependencies packs Mapsicle and fails if the nuspec declares a single dependency.
  • licence-boundary fails if anything under src/ references AutoMapper, which is RPL-1.5 or a paid licence. It is compared against in tests/, which is never packed.

Run Benchmarks Yourself

cd tests/Mapsicle.Benchmarks
dotnet run -c Release # Full suite
dotnet run -c Release -- --quick # Smoke test
dotnet run -c Release -- --edge # Edge cases only

Installation

# Core package - zero config
dotnet add package Mapsicle
# Fluent configuration + Profiles (optional)
dotnet add package Mapsicle.Fluent
# EF Core ProjectTo (optional)
dotnet add package Mapsicle.EntityFramework
# FluentValidation integration (optional)
dotnet add package Mapsicle.Validation
# Naming conventions support (optional)
dotnet add package Mapsicle.NamingConventions
# Serilog structured logging (optional)
dotnet add package Mapsicle.Serilog
# Dapper integration (optional)
dotnet add package Mapsicle.Dapper
# JSON serialization (optional)
dotnet add package Mapsicle.Json
# ASP.NET Core Minimal API helpers (optional)
dotnet add package Mapsicle.AspNetCore
# Memory/Distributed caching (optional)
dotnet add package Mapsicle.Caching
# Change tracking/audit (optional)
dotnet add package Mapsicle.Audit
# DataAnnotations validation (optional)
dotnet add package Mapsicle.DataAnnotations

Package 1: Mapsicle (Core)

Basic Mapping

usingMapsicle;vardto=user.MapTo<UserDto>();// Single objectList<UserDto>dtos=users.MapTo<UserDto>();// Collectionvarflat=order.MapTo<OrderFlatDto>();// Auto-flattening

Attributes

publicclassUserDto{[MapFrom("UserName")]// Map from different propertypublicstringName{get;set;}[IgnoreMap]// Never mappedpublicstringSecret{get;set;}}

Stability Features (NEW!)

// Cycle Detection - no more StackOverflowMapper.MaxDepth=32;// Default, configurable// Validation at startupMapper.AssertMappingValid<User,UserDto>();// LoggingMapper.Logger=Console.WriteLine;// Memory-bounded caching (prevents memory leaks in long-running apps)Mapper.UseLruCache=true;// Enable LRU cacheMapper.MaxCacheSize=1000;// Limit cache entries// Cache statisticsvarstats=Mapper.CacheInfo();Console.WriteLine($"Hit ratio: {stats.HitRatio:P1}");// Scoped instances with isolated cachesusingvarmapper=MapperFactory.Create();vardto=mapper.MapTo<UserDto>(user);// Uses isolated cache

Package 2: Mapsicle.Fluent

Basic Configuration

usingMapsicle.Fluent;varconfig=newMapperConfiguration(cfg =>{cfg.CreateMap<User,UserDto>().ForMember(d =>d.FullName, opt =>opt.MapFrom(s =>$"{s.First}{s.Last}")).ForMember(d =>d.Password, opt =>opt.Ignore()).ForMember(d =>d.Status, opt =>opt.Condition(s =>s.IsActive));});config.AssertConfigurationIsValid();varmapper=config.CreateMapper();

DI Integration (NEW!)

// In Program.csservices.AddMapsicle(cfg =>{cfg.CreateMap<User,UserDto>();},validateConfiguration:true);// In your servicepublicclassUserService(IMappermapper){publicUserDtoGetUser(Useruser)=>mapper.Map<UserDto>(user);}

Lifecycle Hooks (NEW!)

cfg.CreateMap<Order,OrderDto>().BeforeMap((src,dest)=>dest.CreatedAt=DateTime.UtcNow).AfterMap((src,dest)=>dest.WasProcessed=true);

Polymorphic Mapping (NEW!)

cfg.CreateMap<Vehicle,VehicleDto>().Include<Car,CarDto>().Include<Truck,TruckDto>();

Custom Construction (NEW!)

cfg.CreateMap<Order,OrderDto>().ConstructUsing(src =>OrderFactory.Create(src.Type));

Global Type Converters (NEW!)

cfg.CreateConverter<Money,decimal>(m =>m.Amount);cfg.CreateConverter<Money,string>(m =>$"{m.Currency}{m.Amount}");

Package 3: Mapsicle.EntityFramework

ProjectTo<T>() that translates to SQL, no in-memory loading.

usingMapsicle.EntityFramework;vardtos=await_context.Users.Where(u =>u.IsActive).ProjectTo<UserEntity,UserDto>().ToListAsync();// Flattening in SQL: Customer.Name → CustomerNamevarorders=_context.Orders.ProjectTo<OrderEntity,OrderFlatDto>().ToList();

ProjectTo with Fluent Configuration (NEW!)

// ForMember expressions are translated to SQL!varconfig=newMapperConfiguration(cfg =>{cfg.CreateMap<Order,OrderDto>().ForMember(d =>d.CustomerName, opt =>opt.MapFrom(s =>s.Customer.FirstName+" "+s.Customer.LastName)).ForMember(d =>d.Total, opt =>opt.MapFrom(s =>s.Lines.Sum(l =>l.Quantity*l.UnitPrice)));});// These expressions translate to SQL queriesvarorders=_context.Orders.ProjectTo<Order,OrderDto>(config).ToList();

Package 4: Mapsicle.Validation

Post-mapping validation using FluentValidation, validate DTOs immediately after mapping.

Basic Usage

usingFluentValidation;usingMapsicle.Fluent;usingMapsicle.Validation;// 1. Define your validatorpublicclassUserDtoValidator:AbstractValidator<UserDto>{publicUserDtoValidator(){RuleFor(x =>x.Name).NotEmpty().WithMessage("Name is required");RuleFor(x =>x.Email).NotEmpty().EmailAddress();RuleFor(x =>x.Age).GreaterThan(0).WithMessage("Age must be positive");}}// 2. Map and validate in one callvarresult=mapper.MapAndValidate<User,UserDto,UserDtoValidator>(user);if(result.IsValid){returnOk(result.Value);// The mapped DTO}else{returnBadRequest(result.ErrorsByProperty);// { "Email": ["Valid email is required"] }}

API Overview

// Map and validate with validator typevarresult=mapper.MapAndValidate<TSource,TDest,TValidator>(source);// Map and validate with validator instancevarvalidator=newUserDtoValidator();varresult=mapper.MapAndValidate<UserDto>(source,validator);// Validate an existing objectvarresult=dto.Validate<UserDto,UserDtoValidator>();// Get value or throw exceptionvardto=result.GetValueOrThrow();// Throws ValidationException if invalid

Result Properties

result.IsValid// bool - true if validation passedresult.Value // TDest - the mapped object
result.Errors // IList<ValidationFailure> - all validation errors
result.ErrorsByProperty // IDictionary<string, string[]> - errors grouped by property
result.ValidationResult // FluentValidation.Results.ValidationResult - full result

Real-World Example: API Controller

[ApiController][Route("api/users")]publicclassUsersController:ControllerBase{privatereadonlyIMapper_mapper;privatereadonlyIUserRepository_repo;publicUsersController(IMappermapper,IUserRepositoryrepo){_mapper=mapper;_repo=repo;}[HttpPost]publicasyncTask<IActionResult>Create([FromBody]CreateUserRequestrequest){varresult=_mapper.MapAndValidate<CreateUserRequest,UserDto,UserDtoValidator>(request);if(!result.IsValid){returnBadRequest(new{errors=result.ErrorsByProperty});}varuser=await_repo.CreateAsync(result.Value);returnCreatedAtAction(nameof(GetById),new{id=user.Id},user);}}

Package 5: Mapsicle.NamingConventions

Automatic naming convention conversion, map between snake_case, PascalCase, camelCase, and kebab-case!

Basic Usage

usingMapsicle.NamingConventions;// Source uses snake_case (e.g., from Python API or database)publicclassApiResponse{publicintuser_id{get;set;}publicstringfirst_name{get;set;}publicstringemail_address{get;set;}}// Destination uses PascalCase (C# convention)publicclassUserDto{publicintUserId{get;set;}publicstringFirstName{get;set;}publicstringEmailAddress{get;set;}}// Map with naming convention conversionvardto=apiResponse.MapWithConvention<ApiResponse,UserDto>(NamingConvention.SnakeCase,NamingConvention.PascalCase);// dto.UserId == apiResponse.user_id// dto.FirstName == apiResponse.first_name

Built-in Conventions

ConventionExampleC# Property
NamingConvention.PascalCaseUserNameStandard C#
NamingConvention.CamelCaseuserNameJavaScript/JSON
NamingConvention.SnakeCaseuser_namePython/Ruby/SQL
NamingConvention.KebabCaseuser-nameURLs/CSS

Convert Property Names

// Convert a single namevarsnake="UserName".ConvertName(NamingConvention.PascalCase,NamingConvention.SnakeCase);// Result: "user_name"varpascal="first_name".ConvertName(NamingConvention.SnakeCase,NamingConvention.PascalCase);// Result: "FirstName"varcamel="OrderCount".ConvertName(NamingConvention.PascalCase,NamingConvention.CamelCase);// Result: "orderCount"

Use with Fluent Mapper

// Combine with IMapper for convention-based mappingvardto=mapper.MapWithConvention<ApiResponse,UserDto>(apiResponse,NamingConvention.SnakeCase,NamingConvention.PascalCase);

Check Name Matching

// Check if names match across conventionsboolmatch=NamingConvention.NamesMatch("user_name",NamingConvention.SnakeCase,"UserName",NamingConvention.PascalCase);// Result: true

Real-World Example: External API Integration

publicclassExternalApiClient{privatereadonlyHttpClient_http;publicasyncTask<UserDto>GetUserAsync(intid){// External API returns snake_case JSONvarresponse=await_http.GetFromJsonAsync<ExternalUserResponse>($"/users/{id}");// Convert to C# conventionsreturnresponse.MapWithConvention<ExternalUserResponse,UserDto>(NamingConvention.SnakeCase,NamingConvention.PascalCase);}}// External API response (snake_case)publicclassExternalUserResponse{publicintuser_id{get;set;}publicstringfirst_name{get;set;}publicstringlast_name{get;set;}publicstringemail_address{get;set;}publicDateTimecreated_at{get;set;}}// Internal DTO (PascalCase)publicclassUserDto{publicintUserId{get;set;}publicstringFirstName{get;set;}publicstringLastName{get;set;}publicstringEmailAddress{get;set;}publicDateTimeCreatedAt{get;set;}}

Package 6: Mapsicle.Serilog

Structured logging integration for enterprise diagnostics and observability.

Basic Setup

usingMapsicle.Serilog;usingSerilog;// Configure Serilog loggervarlogger=newLoggerConfiguration().WriteTo.Console().MinimumLevel.Debug().CreateLogger();// Enable Mapsicle loggingMapsicleLogging.UseSerilog(logger);

Map with Logging

// Log individual mappingsvardto=user.MapWithLogging<User,UserDto>(logger);// Output: [INF] Mapsicle: Mapped User -> UserDto in 0.5ms// Log collection mappingsvardtos=users.MapCollectionWithLogging<User,UserDto>(logger);// Output: [INF] Mapsicle: Mapped 100 User -> UserDto items in 5.2ms

Slow Mapping Warnings

// Configure slow mapping threshold (default: 100ms)MapsicleLogging.SlowMappingThreshold=TimeSpan.FromMilliseconds(50);// Slow mappings automatically log warningsvardto=largeObject.MapWithLogging<Large,LargeDto>(logger);// Output: [WRN] Mapsicle: Slow mapping detected Large -> LargeDto took 75ms

Scoped Logging for Batch Operations

using(varscope=newMappingLoggingScope(logger,"OrderProcessing")){// All mappings in this scope are logged with the operation contextvarorderDto=order.MapWithLogging<Order,OrderDto>(logger);varitemDtos=items.MapCollectionWithLogging<Item,ItemDto>(logger);}// Output includes: OperationName = "OrderProcessing"

Package 7: Mapsicle.Dapper

Dapper integration for mapping database query results directly to DTOs.

Basic Usage

usingMapsicle.Dapper;usingDapper;// Query and map in one callvarusers=connection.QueryAndMap<User,UserDto>("SELECT * FROM Users").ToList();// With parametersvaruser=connection.QuerySingleAndMap<User,UserDto>("SELECT * FROM Users WHERE Id = @Id",param:new{Id=1});

Async Support

// Async query and mapvarusers=awaitconnection.QueryAndMapAsync<User,UserDto>("SELECT * FROM Users");// Async single resultvaruser=awaitconnection.QuerySingleAndMapAsync<User,UserDto>("SELECT * FROM Users WHERE Id = @Id",param:new{Id=1});

With Custom Configuration

// Use a custom mapper configurationvarconfig=newMapperConfiguration(cfg =>{cfg.CreateMap<User,UserSummaryDto>().ForMember(d =>d.FullName, opt =>opt.MapFrom(s =>$"{s.FirstName}{s.LastName}"));});varusers=connection.QueryAndMap<User,UserSummaryDto>("SELECT * FROM Users",config).ToList();// Or use IMapper instancevarmapper=config.CreateMapper();varusers=connection.QueryAndMap<User,UserSummaryDto>("SELECT * FROM Users",mapper).ToList();

Transaction Support

usingvartransaction=connection.BeginTransaction();// Mappings work within transactionsvarusers=connection.QueryAndMap<User,UserDto>("SELECT * FROM Users WHERE Active = 1",transaction:transaction).ToList();transaction.Commit();

Map Existing Dapper Results

// Map existing IEnumerable from Dappervarusers=connection.Query<User>("SELECT * FROM Users");vardtos=users.MapTo<User,UserDto>(mapper);

Migration from AutoMapper

API Compatibility

AutoMapperMapsicle
CreateMap<S,D>()Same.
ForMember().MapFrom()Same.
.Ignore()Same.
BeforeMap/AfterMapSame.
Include<Derived>()Same.
ConstructUsing()Same.
services.AddAutoMapper()services.AddMapsicle()
_mapper.Map<T>()mapper.Map<T>() or obj.MapTo<T>()

Step-by-Step Migration Guide

1. Identify Your AutoMapper Usage

Simple mappings (no profiles) → Use core Mapsicle package Profiles with configuration → Use Mapsicle.FluentEF Core ProjectTo → Use Mapsicle.EntityFramework

2. Install Packages

dotnet remove package AutoMapper
dotnet remove package AutoMapper.Extensions.Microsoft.DependencyInjection
dotnet add package Mapsicle.Fluent # Includes core

3. Convert Profiles to Configuration

Before (AutoMapper):

publicclassUserProfile:Profile{publicUserProfile(){CreateMap<User,UserDto>().ForMember(d =>d.FullName, opt =>opt.MapFrom(s =>s.FirstName+" "+s.LastName));}}

After (Mapsicle):

// In Program.cs/Startup.csservices.AddMapsicle(cfg =>{cfg.CreateMap<User,UserDto>().ForMember(d =>d.FullName, opt =>opt.MapFrom(s =>s.FirstName+" "+s.LastName));},validateConfiguration:true);

4. Update DI Registration

Before:

services.AddAutoMapper(typeof(UserProfile).Assembly);

After:

services.AddMapsicle(cfg =>{cfg.CreateMap<User,UserDto>();cfg.CreateMap<Order,OrderDto>();// ... all your mappings},validateConfiguration:true);

5. Update Mapping Calls

Before:

publicclassUserService{privatereadonlyIMapper_mapper;publicUserService(IMappermapper)=>_mapper=mapper;publicUserDtoGetUser(Useruser)=>_mapper.Map<UserDto>(user);}

After (same interface!):

publicclassUserService{privatereadonlyIMapper_mapper;publicUserService(IMappermapper)=>_mapper=mapper;// Option 1: Same as AutoMapperpublicUserDtoGetUser(Useruser)=>_mapper.Map<UserDto>(user);// Option 2: Extension method (no DI needed for simple cases)publicUserDtoGetUser(Useruser)=>user.MapTo<UserDto>();}

Known Incompatibilities

Not Supported:

  • IMemberValueResolver interface - use ResolveUsing(func) instead
  • ITypeConverter interface - use CreateConverter<T, U>() instead
  • Conditional mapping with complex predicates
  • MaxDepth per individual mapping (only global Mapper.MaxDepth)

Now Supported (via extension packages):

  • Custom naming conventions → Mapsicle.NamingConventions
  • Post-mapping validation → Mapsicle.Validation

⚠️Behavioral Differences:

  • Circular references: Mapsicle returns the default at MaxDepth with no configuration. AutoMapper needs PreserveReferences() or MaxDepth(...) to handle them.
  • Unmapped properties: Both ignore, but Mapsicle has GetUnmappedProperties<T, U>() for validation
  • Null handling: Both return null for null source, but Mapsicle is more aggressive with null-safe navigation

Troubleshooting

Common Issues

Issue: Properties Not Mapping

Symptom: Destination properties remain default/null after mapping

Causes & Solutions:

  1. Property name mismatch

    // Problem: Source has "UserName", destination has "Name"// Solution 1: Use [MapFrom] attributepublicclassUserDto{[MapFrom("UserName")]publicstringName{get;set;}}// Solution 2: Use Fluent configurationcfg.CreateMap<User,UserDto>().ForMember(d =>d.Name, opt =>opt.MapFrom(s =>s.UserName));
  2. Property not readable/writable

    // ❌ Won't map (no setter)publicstringName{get;}// ✅ Will mappublicstringName{get;set;}// ✅ Also works (init setter)publicstringName{get;init;}
  3. Type incompatibility

    // Check which properties can't mapvarunmapped=Mapper.GetUnmappedProperties<User,UserDto>();Console.WriteLine($"Unmapped: {string.Join(", ",unmapped)}");

Issue: StackOverflowException

Cause: Circular references exceeding MaxDepth (default 32)

Solutions:

// Solution 1: Increase depth limitMapper.MaxDepth=64;// Solution 2: Enable logging to see depth warningsMapper.Logger= msg =>Console.WriteLine($"[Mapsicle] {msg}");// Solution 3: Use [IgnoreMap] to break cyclepublicclassUser{publicintId{get;set;}[IgnoreMap]// Don't map back to parentpublicList<Order>Orders{get;set;}}

Issue: Poor Collection Mapping Performance

Symptom: Mapping 10,000+ items is slow

Solutions:

// ❌ Don't: Map items individuallyforeach(varuserinusers){dtos.Add(user.MapTo<UserDto>());}// ✅ Do: Map entire collectionvardtos=users.MapTo<UserDto>();// 20% faster with cached mapper// ✅ Do: Pre-warm cache at startup for frequently used typesnewUser().MapTo<UserDto>();newOrder().MapTo<OrderDto>();

Issue: Memory Growth in Long-Running Apps

Symptom: Memory usage grows over time

Cause: Unbounded cache with many dynamic type combinations

Solution:

// Enable memory-bounded LRU cacheMapper.UseLruCache=true;Mapper.MaxCacheSize=1000;// Adjust based on # of unique type pairs// Monitor cache performancevarstats=Mapper.CacheInfo();if(stats.HitRatio<0.8){// Consider increasing cache sizeMapper.MaxCacheSize=2000;}

Issue: EF Core ProjectTo Not Working

Symptom: Exception thrown or results incorrect

Common Causes:

  1. Missing configuration

    // ❌ Don't use convention mapping with complex expressionsvardtos=context.Orders.ProjectTo<Order,OrderDto>().ToList();// ✅ Pass configuration for ForMember expressionsvarconfig=newMapperConfiguration(cfg =>{cfg.CreateMap<Order,OrderDto>().ForMember(d =>d.CustomerName, opt =>opt.MapFrom(s =>s.Customer.Name));});vardtos=context.Orders.ProjectTo<Order,OrderDto>(config).ToList();
  2. Non-translatable expressions

    // ❌ Method calls that don't translate to SQLcfg.CreateMap<User,UserDto>().ForMember(d =>d.Name, opt =>opt.ResolveUsing(u =>FormatName(u)));// ✅ Use expressions that translate to SQLcfg.CreateMap<User,UserDto>().ForMember(d =>d.Name, opt =>opt.MapFrom(u =>u.FirstName+" "+u.LastName));

Debugging Tips

// 1. Enable verbose loggingMapper.Logger= msg =>_logger.LogDebug($"[Mapsicle] {msg}");// 2. Validate mapping at startup
#if DEBUGMapper.AssertMappingValid<User,UserDto>();
#endif
// 3. Check configuration in fluent mapperconfig.AssertConfigurationIsValid();// 4. Monitor cache statisticsvarstats=Mapper.CacheInfo();_logger.LogInformation($"Cache: {stats.Total} entries, Hit ratio: {stats.HitRatio:P1}");// 5. Use MapperFactory for isolated testingusingvarmapper=MapperFactory.Create(newMapperOptions{MaxDepth=16,Logger=Console.WriteLine});vardto=mapper.MapTo<UserDto>(user);

Mapping Untrusted Input

A mapper copies every matching property it can. Pointed at a request body it will set anything whose name lines up, including a property the caller had no business setting. This is true of every convention-based mapper, Mapsicle included, and it is worth stating plainly rather than leaving for you to discover:

// An attacker controls the keys.varbody=newDictionary<string,object?>{["Email"]="user@example.com",["IsAdmin"]=true,// not a field the caller should decide};varaccount=body.MapTo<Account>();// account.IsAdmin is now true

Map untrusted input into a DTO that holds only the fields a caller may set, then map that into your entity:

publicclassAccountUpdateDto// no IsAdmin, no Balance{publicstringEmail{get;set;}="";}vardto=body.MapTo<AccountUpdateDto>();dto.Map(existingAccount);// reaches nothing the DTO does not declare

Where a shared type is unavoidable, [IgnoreMap] is an enforceable control and is honoured on every entry point, including the dictionary path.

What Mapsicle does guarantee about untrusted values:

  • Values are copied, never interpreted. Nothing in a string is parsed, executed or sanitised. A value containing SQL, script or format-string syntax arrives byte for byte.
  • A value of the wrong type is dropped, not coerced and not thrown. A caller cannot use a type mismatch to crash a request handler or to smuggle a value through a loose conversion.
  • Unknown keys are ignored rather than throwing.

All of this is covered by UntrustedInputTests in tests/Mapsicle.Tests. A failure there means one of these statements stopped being true.

Known Limitations

Feature Limitations

Not Supported:

  • Async mapping operations
  • Source/destination value injection (context passing)
  • Open generic types
  • Explicit type conversion configuration beyond built-ins

Supported via Extension Packages:

  • Custom naming conventions (PascalCase ↔ snake_case) → Mapsicle.NamingConventions
  • Post-mapping validation → Mapsicle.Validation

⚠️Partial Support:

  • Nested flattening limited to 1 level (Address.City ✅, Address.Street.Line1 ❌)
  • Collection mapping is slower than AutoMapper: 2,428 ns against 1,823 ns for 100 items, while allocating about 19 percent less. Competitive again at 10K+.
  • EF Core ProjectTo works with ForMember expressions, but not ResolveUsing delegates

Behavioral Differences from AutoMapper

  • Circular references: Returns default value instead of throwing exception
  • Null safety: More aggressive null-safe navigation (fewer NullReferenceException). A null reference-typed source mapped to a string destination yields null rather than throwing.
  • Map(destination) is not atomic: it writes properties in order, so a setter that throws part-way leaves the earlier ones already written. Map into a fresh instance if you need all-or-nothing.
  • Exceptions from your accessors propagate unwrapped: a getter throwing InvalidOperationException surfaces as InvalidOperationException, not wrapped in TargetInvocationException.
  • Unmapped properties: Silent (use GetUnmappedProperties for validation)
  • Cache behavior: Default is unbounded (must opt-in to LRU)

Platform Support

.NET VersionMapsicle Support
.NET 8.0✅ Fully supported
.NET 6.0-7.0✅ Via .NET Standard 2.0
.NET 5.0✅ Via .NET Standard 2.0
.NET Core 2.0+✅ Via .NET Standard 2.0
.NET Framework 4.6.1+✅ Via .NET Standard 2.0

API Reference

Core Extensions (using Mapsicle)

MapTo<T>(this object source)

Maps a source object to a new instance of type T.

Parameters:

  • source - The source object to map from

Returns:

  • T? - New instance of T with mapped properties, or default(T) if source is null or max depth exceeded

Example:

vardto=user.MapTo<UserDto>();

MapTo<T>(this IEnumerable source)

Maps a collection to a List.

Parameters:

  • source - The source collection

Returns:

  • List<T> - New list with mapped items (empty if source is null)

Optimization: Pre-allocates capacity if source implements ICollection

Example:

List<UserDto>dtos=users.MapTo<UserDto>();

Map<TDest>(this object source, TDest destination)

Updates an existing destination object from source.

Parameters:

  • source - The source object
  • destination - The destination object to update

Returns:

  • TDest - The updated destination (same instance)

Example:

source.Map(existingDto);// Updates existingDto in-place

ToDictionary(this object source)

Converts an object to a dictionary of property name/value pairs.

Returns:

  • Dictionary<string, object?> - Case-insensitive dictionary

Example:

vardict=user.ToDictionary();

MapTo<T>(this IDictionary<string, object?> source) where T : new()

Maps a dictionary to an object.

Constraints:

  • T must have a parameterless constructor

Example:

varuser=dict.MapTo<User>();

Static Mapper Configuration

Mapper.MaxDepth

  • Type:int
  • Default:32
  • Description: Maximum recursion depth before returning default value (circular reference protection)
Mapper.MaxDepth=64;

Mapper.UseLruCache

  • Type:bool
  • Default:false
  • Description: Enables memory-bounded LRU cache. Clears all caches when changed.
Mapper.UseLruCache=true;

Mapper.MaxCacheSize

  • Type:int
  • Default:1000
  • Description: Maximum cache entries when UseLruCache is enabled
Mapper.MaxCacheSize=2000;

Mapper.Logger

  • Type:Action<string>?
  • Default:null
  • Description: Logger for diagnostic messages (depth warnings, etc)
Mapper.Logger= msg =>_logger.LogDebug(msg);

Mapper.ClearCache()

Clears all cached mapping delegates.

Mapper.ClearCache();

Mapper.CacheInfo()

  • Returns:MapperCacheInfo - Current cache statistics
varstats=Mapper.CacheInfo();Console.WriteLine($"Total: {stats.Total}, Hit Ratio: {stats.HitRatio:P1}");

Mapper.AssertMappingValid<TSource, TDest>()

Validates mapping configuration. Throws InvalidOperationException if unmapped properties exist.

Mapper.AssertMappingValid<User,UserDto>();

Mapper.GetUnmappedProperties<TSource, TDest>()

  • Returns:List<string> - Names of destination properties that cannot be mapped
varunmapped=Mapper.GetUnmappedProperties<User,UserDto>();

MapperFactory

MapperFactory.Create(MapperOptions? options = null)

Creates an isolated mapper instance with independent cache and depth tracking.

Parameters:

  • options - Optional configuration (MaxDepth, Logger, UseLruCache, MaxCacheSize)

Returns:

  • IDisposable mapper instance

Example:

usingvarmapper=MapperFactory.Create(newMapperOptions{MaxDepth=16,UseLruCache=true,MaxCacheSize=100,Logger=Console.WriteLine});vardto=mapper.MapTo<UserDto>(user);

Fluent API (using Mapsicle.Fluent)

MapperConfiguration

varconfig=newMapperConfiguration(cfg =>{cfg.CreateMap<User,UserDto>().ForMember(d =>d.FullName, opt =>opt.MapFrom(s =>s.FirstName+" "+s.LastName)).ForMember(d =>d.Password, opt =>opt.Ignore()).ForMember(d =>d.IsActive, opt =>opt.Condition(s =>s.Status=="Active")).BeforeMap((src,dest)=>Console.WriteLine("Mapping started")).AfterMap((src,dest)=>dest.MappedAt=DateTime.UtcNow).Include<PowerUser,PowerUserDto>().ConstructUsing(src =>newUserDto(src.Id)).ReverseMap();cfg.CreateConverter<Money,decimal>(m =>m.Amount);});config.AssertConfigurationIsValid();varmapper=config.CreateMapper();

Configuration Methods

  • ForMember<TMember>() - Configure individual member mapping

    • opt.MapFrom(expr) - Map from custom expression
    • opt.Ignore() - Don't map this member
    • opt.Condition(pred) - Conditional mapping
    • opt.ResolveUsing(func) - Custom resolver function
  • BeforeMap(action) - Execute action before mapping

  • AfterMap(action) - Execute action after mapping

  • Include<TDerived, TDest>() - Polymorphic mapping support

  • ConstructUsing(factory) - Custom object construction

  • ReverseMap() - Create reverse mapping

  • CreateConverter<TSource, TDest>(converter) - Global type converter


EntityFramework Extensions (using Mapsicle.EntityFramework)

ProjectTo<TSource, TDest>(this IQueryable<TSource> query, MapperConfiguration? config = null)

Translates mapping to SQL expression (executed in database).

Parameters:

  • query - Source EF Core queryable
  • config - Optional mapper configuration for custom mappings

Returns:

  • IQueryable<TDest> - Queryable projection

Example:

vardtos=awaitcontext.Users.Where(u =>u.IsActive).ProjectTo<User,UserDto>(config).ToListAsync();

Validation Extensions (using Mapsicle.Validation)

MapAndValidate<TDest, TValidator>(this IMapper mapper, object? source)

Maps source to destination and validates using the specified validator type.

Type Parameters:

  • TDest - Destination type
  • TValidator - FluentValidation validator type (must have parameterless constructor)

Returns:

  • MapperValidationResult<TDest> - Contains IsValid, Value, Errors, ErrorsByProperty

Example:

varresult=mapper.MapAndValidate<User,UserDto,UserDtoValidator>(user);if(result.IsValid)returnresult.Value;

MapAndValidate<TDest>(this IMapper mapper, object? source, IValidator<TDest> validator)

Maps source to destination and validates using a provided validator instance.

Example:

varvalidator=newUserDtoValidator();varresult=mapper.MapAndValidate<UserDto>(user,validator);

Validate<T, TValidator>(this T value)

Validates an existing object using the specified validator type.

Example:

varresult=dto.Validate<UserDto,UserDtoValidator>();

NamingConventions Extensions (using Mapsicle.NamingConventions)

MapWithConvention<TSource, TDest>(this TSource source, NamingConvention sourceConvention, NamingConvention destConvention)

Maps source to destination with naming convention transformation.

Parameters:

  • sourceConvention - The naming convention of source properties
  • destConvention - The naming convention of destination properties

Returns:

  • TDest? - New instance with convention-matched properties

Example:

vardto=apiResponse.MapWithConvention<ApiResponse,UserDto>(NamingConvention.SnakeCase,NamingConvention.PascalCase);

ConvertName(this string name, NamingConvention from, NamingConvention to)

Converts a property name from one convention to another.

Example:

varsnakeName="UserName".ConvertName(NamingConvention.PascalCase,NamingConvention.SnakeCase);// Result: "user_name"

NamingConvention.NamesMatch(string sourceName, NamingConvention sourceConvention, string destName, NamingConvention destConvention)

Checks if two names match when their conventions are applied.

Example:

boolmatch=NamingConvention.NamesMatch("user_id",NamingConvention.SnakeCase,"UserId",NamingConvention.PascalCase);// Result: true

Serilog Extensions (using Mapsicle.Serilog)

MapsicleLogging.UseSerilog(ILogger logger)

Enables global Serilog integration for Mapsicle mapping operations.

Parameters:

  • logger - Serilog ILogger instance

Example:

MapsicleLogging.UseSerilog(Log.Logger);

MapWithLogging<TSource, TDest>(this TSource source, ILogger logger)

Maps source to destination with timing and structured logging.

Returns:

  • TDest? - Mapped destination object

Example:

vardto=user.MapWithLogging<User,UserDto>(logger);// Logs: Mapsicle: Mapped User -> UserDto in 0.5ms

MapCollectionWithLogging<TSource, TDest>(this IEnumerable<TSource> source, ILogger logger)

Maps a collection with aggregated timing and logging.

Returns:

  • List<TDest> - List of mapped destination objects

Example:

vardtos=users.MapCollectionWithLogging<User,UserDto>(logger);// Logs: Mapsicle: Mapped 100 User -> UserDto items in 5.2ms

MapsicleLogging.SlowMappingThreshold

Configures the threshold for slow mapping warnings.

Default: 100ms

Example:

MapsicleLogging.SlowMappingThreshold=TimeSpan.FromMilliseconds(50);

Dapper Extensions (using Mapsicle.Dapper)

QueryAndMap<TSource, TDest>(this IDbConnection connection, string sql, ...)

Executes a SQL query and maps results to destination type.

Overloads:

  • QueryAndMap<TSource, TDest>(sql, param?, transaction?, commandTimeout?) - Auto-mapping
  • QueryAndMap<TSource, TDest>(sql, MapperConfiguration, param?, ...) - With configuration
  • QueryAndMap<TSource, TDest>(sql, IMapper, param?, ...) - With mapper instance

Returns:

  • IEnumerable<TDest> - Mapped results

Example:

varusers=connection.QueryAndMap<User,UserDto>("SELECT * FROM Users").ToList();

QueryAndMapAsync<TSource, TDest>(this IDbConnection connection, string sql, ...)

Async version of QueryAndMap.

Example:

varusers=awaitconnection.QueryAndMapAsync<User,UserDto>("SELECT * FROM Users");

QuerySingleAndMap<TSource, TDest>(this IDbConnection connection, string sql, ...)

Executes a query expecting a single result and maps it.

Returns:

  • TDest? - Mapped result or null

Example:

varuser=connection.QuerySingleAndMap<User,UserDto>("SELECT * FROM Users WHERE Id = @Id",param:new{Id=1});

QueryFirstAndMap<TSource, TDest>(this IDbConnection connection, string sql, ...)

Executes a query and maps the first result.

Returns:

  • TDest? - First mapped result or null

Example:

varuser=connection.QueryFirstAndMap<User,UserDto>("SELECT * FROM Users ORDER BY CreatedAt DESC");

MapTo<TSource, TDest>(this IEnumerable<TSource>? source, IMapper mapper)

Maps an existing collection using a provided mapper.

Returns:

  • List<TDest> - Mapped results

Example:

varusers=connection.Query<User>("SELECT * FROM Users");vardtos=users.MapTo<User,UserDto>(mapper);

Complete Feature List

Core Features

  • ✅ Zero-config convention mapping
  • ✅ Collection mapping (List, Array, IEnumerable)
  • ✅ Dictionary mapping (object ↔ Dictionary)
  • ✅ Flattening (AddressCityAddress.City)
  • ✅ Nullable type coercion (TT?)
  • ✅ Enum to numeric conversion
  • ✅ Nested object mapping
  • ✅ Case-insensitive property matching
  • ✅ Record type support (positional parameters)
  • ✅ Anonymous type support
  • ✅ Circular reference protection
  • ✅ Thread-safe caching

Advanced Features

  • [MapFrom] attribute
  • [IgnoreMap] attribute
  • ✅ Fluent configuration API
  • ✅ ForMember custom expressions
  • ✅ BeforeMap/AfterMap hooks
  • ✅ Polymorphic mapping (.Include<>)
  • ✅ Custom construction (.ConstructUsing)
  • ✅ Global type converters
  • ✅ Conditional mapping
  • ✅ ReverseMap
  • ✅ DI integration
  • ✅ Configuration validation

Enterprise Features

  • ✅ LRU cache option (memory-bounded)
  • ✅ Cache statistics (hits, misses, ratio)
  • ✅ PropertyInfo caching
  • ✅ Lock-free reads
  • ✅ Isolated mapper instances
  • ✅ Configurable depth limits
  • ✅ Diagnostic logging
  • ✅ Unmapped property detection

EF Core Features

  • ✅ ProjectTo with SQL translation
  • ✅ ForMember in ProjectTo
  • ✅ Flattening in SQL
  • ✅ Nested projection
  • ✅ Type coercion in queries

Validation Features (Mapsicle.Validation)

  • ✅ MapAndValidate with FluentValidation
  • ✅ Validator type parameter
  • ✅ Validator instance injection
  • ✅ Validation result with IsValid, Errors
  • ✅ ErrorsByProperty dictionary
  • ✅ GetValueOrThrow pattern
  • ✅ Validator caching

Naming Convention Features (Mapsicle.NamingConventions)

  • ✅ PascalCase convention
  • ✅ camelCase convention
  • ✅ snake_case convention
  • ✅ kebab-case convention
  • ✅ MapWithConvention extension
  • ✅ ConvertName string extension
  • ✅ NamesMatch cross-convention comparison
  • ✅ Property mapping cache

Serilog Features (Mapsicle.Serilog)

  • ✅ UseSerilog global integration
  • ✅ MapWithLogging extension
  • ✅ MapCollectionWithLogging extension
  • ✅ Slow mapping warnings
  • ✅ MappingLoggingScope for batch operations
  • ✅ Structured logging with properties
  • ✅ Configurable thresholds

Dapper Features (Mapsicle.Dapper)

  • ✅ QueryAndMap / QueryAndMapAsync
  • ✅ QuerySingleAndMap / QuerySingleAndMapAsync
  • ✅ QueryFirstAndMap / QueryFirstAndMapAsync
  • ✅ Transaction support
  • ✅ Custom MapperConfiguration support
  • ✅ IMapper instance support
  • ✅ MapTo collection extension

Test Coverage

dotnet test Mapsicle.sln -c Release runs all of it, including the allocation budgets.

PackageTestsWhat it covers
Mapsicle284Core, plus regression, load, fault and untrusted input
Mapsicle.NamingConventions55Naming conventions
Mapsicle.Fluent39Fluent configuration and profiles
Mapsicle.Validation27FluentValidation integration
Mapsicle.Json26JSON serialization
Mapsicle.Audit26Change tracking
Mapsicle.Dapper25Dapper integration
Mapsicle.DataAnnotations24DataAnnotations validation
Mapsicle.AspNetCore23ASP.NET Core helpers
Mapsicle.Serilog22Serilog logging
Mapsicle.Caching21Caching integration
Mapsicle.EntityFramework19EF Core ProjectTo
Mapsicle.Performance8Allocation budgets on warm paths
Total599

Four of those suites exist because a passing test count on its own says very little:

  • IssueRegressionTests carries one test per fixed defect, named by issue number. Reverted against the pre-fix source, 19 of its 28 fail. The 9 that pass are the controls, and they are supposed to pass either way.
  • UntrustedInputTests pins the security statements in this README, so a change to any of them fails the build rather than quietly making the documentation wrong.
  • FaultInjectionTests covers what happens when a mapping fails: which exception surfaces, whether it is wrapped, whether the destination is left half-written, and whether a failure poisons the cache for later calls.
  • LoadTests verifies every mapping against the input that produced it, because the failure a shared compiled delegate produces under concurrency is a wrong answer rather than an exception.

Allocation budgets run under dotnet test rather than only in a benchmark, so a change that starts boxing a value or allocating a closure on a warm path fails CI instead of being noticed in a profiler later.


Project Structure

Mapsicle/
├── src/
│ ├── Mapsicle/ # Core - zero config
│ ├── Mapsicle.Fluent/ # Fluent + DI + Profiles
│ ├── Mapsicle.EntityFramework/ # EF Core ProjectTo
│ ├── Mapsicle.Validation/ # FluentValidation integration
│ ├── Mapsicle.NamingConventions/ # Naming convention support
│ ├── Mapsicle.Serilog/ # Serilog structured logging
│ ├── Mapsicle.Dapper/ # Dapper integration
│ ├── Mapsicle.Json/ # JSON serialization
│ ├── Mapsicle.AspNetCore/ # ASP.NET Core Minimal API
│ ├── Mapsicle.Caching/ # Memory/Distributed caching
│ ├── Mapsicle.Audit/ # Change tracking/diff
│ └── Mapsicle.DataAnnotations/ # DataAnnotations validation
├── tests/
│ ├── Mapsicle.Tests/
│ ├── Mapsicle.Fluent.Tests/
│ ├── Mapsicle.EntityFramework.Tests/
│ ├── Mapsicle.Validation.Tests/
│ ├── Mapsicle.NamingConventions.Tests/
│ ├── Mapsicle.Serilog.Tests/
│ ├── Mapsicle.Dapper.Tests/
│ ├── Mapsicle.Json.Tests/
│ ├── Mapsicle.AspNetCore.Tests/
│ ├── Mapsicle.Caching.Tests/
│ ├── Mapsicle.Audit.Tests/
│ ├── Mapsicle.DataAnnotations.Tests/
│ └── Mapsicle.Benchmarks/
└── examples/
└── Mapsicle.Examples/ # Working examples for all packages

Run Examples

dotnet run --project examples/Mapsicle.Examples

Contributing

PRs welcome. Areas for contribution:

  • Performance optimizations
  • Additional type coercion scenarios
  • Documentation improvements

License

MPL 2.0 License © Arnel Isiderio Robles


Stop configuring. Start mapping.
Free forever. Zero dependencies. Pure performance.

About

Mapsicle is a high-performance, modular object mapping ecosystem for .NET.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Contributors

Languages