Mapsicle is a high-performance, modular object mapping ecosystem for .NET. Choose only what you need:
| Package | Purpose | Dependencies |
|---|---|---|
| Mapsicle | Zero-config mapping | None |
| Mapsicle.Fluent | Fluent configuration + Profiles | Mapsicle |
| Mapsicle.EntityFramework | EF Core ProjectTo<T>() | Mapsicle.Fluent |
| Mapsicle.Validation | FluentValidation integration | Mapsicle.Fluent |
| Mapsicle.NamingConventions | Naming convention support | Mapsicle.Fluent |
| Mapsicle.Json | JSON serialization integration | Mapsicle.Fluent |
| Mapsicle.AspNetCore | ASP.NET Core Minimal API helpers | Mapsicle.Validation |
| Mapsicle.Caching | Memory/Distributed cache support | Mapsicle.Fluent |
| Mapsicle.Audit | Change tracking/diff detection | Mapsicle.Fluent |
| Mapsicle.DataAnnotations | DataAnnotations validation | Mapsicle.Fluent |
| Mapsicle.Dapper | Dapper query-and-map helpers | Mapsicle.Fluent |
| Mapsicle.Serilog | Mapping diagnostics via Serilog | Mapsicle |
The core
Mapsiclepackage 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.
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.
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:
| Situation | Why the alternatives do not fit |
|---|---|
Mapping a Dictionary<string, object> into a type | Mapperly has nothing to generate against. AutoMapper needs the pair configured. |
| A collection whose items have different runtime types | Same: nothing to generate, and the shape is only known at runtime. |
| Types arriving from plugins, reflection or configuration | Compile-time generation is not available at all. |
Hundreds of DTOs and no appetite for a CreateMap per pair | Mapsicle 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 cycles | Mapsicle 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 permissive | This is the reason most people are reading this page. |
Reach for something else when:
| Situation | Choose |
|---|---|
| Every mapping is known at compile time and you will declare them | Mapperly. 2.5x to 3x faster and indistinguishable from hand-written code. |
| Collection throughput is what your workload is bounded by | Mapperly, then AutoMapper. Mapsicle is 1.33x slower than AutoMapper here. |
| You need AOT with no runtime code generation | Mapperly. 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.
| Feature | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| License | MPL 2.0 | RPL-1.5, or a Lucky Penny Software agreement (a free Community License exists) | MIT |
| Architecture | Runtime + Caching | Runtime + Expressions | Source Generator |
| Setup Required | None | Profiles, DI | Partial class |
| Dependencies | 0 (core) | 5+ | 0 (compile-time) |
| Compile-time Safety | Partial | No | Full |
| AOT Compatible | Partial | No | Yes |
| Circular Refs | Handled by default | Opt in via PreserveReferences() or MaxDepth(...) | Opt in via UseReferenceHandling |
| Memory Bounded | LRU Option | No | N/A |
| Cache Statistics | Yes | No | N/A |
| Integrated Validation | Yes | No | No |
| ASP.NET Core Helpers | Yes | No | No |
| Feature | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| Convention-based mapping | ✅ | ✅ | ✅ |
Flattening (Address.City → AddressCity) | ✅ | ✅ | ✅ |
| Custom member mapping | ✅ ForMember() | ✅ ForMember() | ✅ [MapProperty] |
| Ignore members | ✅ [IgnoreMap] | ✅ Ignore() | ✅ [MapperIgnore] |
| Reverse mapping | ✅ ReverseMap() | ✅ ReverseMap() | ✅ (define both) |
| Before/After map hooks | ✅ | ✅ | ✅ |
| Type converters | ✅ CreateConverter<>() | ✅ ConvertUsing() | ✅ User methods |
| Inheritance/Polymorphism | ✅ Include<>() | ✅ Include<>() | ✅ |
| Nested object mapping | ✅ | ✅ | ✅ |
| Collection mapping | ✅ | ✅ | ✅ |
| Constructor mapping | ✅ ConstructUsing() | ✅ ConstructUsing() | ✅ (automatic) |
| Feature | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| Profile support | ✅ MapsicleProfile | ✅ Profile | ❌ (partial classes) |
| Fluent configuration | ✅ | ✅ | ❌ (attributes) |
| Attribute-based config | ✅ [MapFrom] | ✅ | ✅ |
| Static zero-config API | ✅ obj.MapTo<T>() | ❌ | ❌ |
| DI-friendly | ✅ IMapper | ✅ IMapper | ✅ |
| Assembly scanning | ✅ | ✅ | N/A |
| Package/Feature | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| EF Core ProjectTo | ✅ Mapsicle.EntityFramework | ✅ Built-in | ✅ (expressions) |
| FluentValidation | ✅ Mapsicle.Validation | ❌ | ❌ |
| DataAnnotations | ✅ Mapsicle.DataAnnotations | ❌ | ❌ |
| JSON serialization | ✅ Mapsicle.Json | ❌ | ❌ |
| ASP.NET Core | ✅ Mapsicle.AspNetCore | ❌ | ❌ |
| Caching | ✅ Mapsicle.Caching | ❌ | N/A |
| Audit/Change tracking | ✅ Mapsicle.Audit | ❌ | ❌ |
| Naming conventions | ✅ 5 conventions | ✅ Built-in | ✅ NamingStrategy |
| Convention | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| PascalCase | ✅ | ✅ | ✅ |
| camelCase | ✅ | ✅ | ✅ |
| snake_case | ✅ | ✅ | ✅ |
| kebab-case | ✅ | ❌ | ❌ |
| SCREAMING_SNAKE_CASE | ✅ | ❌ | ❌ |
| Aspect | Mapsicle | AutoMapper | Mapperly |
|---|---|---|---|
| First map overhead | Medium | Medium-High | None |
| Subsequent maps | Fast | Fast | Fastest |
| Memory footprint | Low-Medium | Medium | Lowest |
| Startup time impact | Low | Medium | None |
| AOT compatible | Partial | No | Yes |
| Scenario | Recommendation |
|---|---|
| Maximum performance, AOT required | Mapperly |
| Compile-time safety is critical | Mapperly |
| Quick prototyping, zero setup | Mapsicle (static API) |
| Need integrated validation | Mapsicle |
| Existing AutoMapper codebase | AutoMapper (if licensed) or migrate |
| Budget-conscious / OSS project | Mapsicle or Mapperly |
| Complex mapping configurations | AutoMapper or Mapsicle (fluent) |
| ASP.NET Core Minimal APIs | Mapsicle (AspNetCore package) |
| Need audit trail of changes | Mapsicle (Audit package) |
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);Features not found in AutoMapper or Mapperly:
- Static zero-config API:
user.MapTo<UserDto>()- no setup required - Built-in validation integration: Map + validate in one call with FluentValidation or DataAnnotations
- Audit/diff tracking: Track what changed during mapping with
MapWithAudit<T>() - Caching integration: Cache mapped results with
IMemoryCache/IDistributedCache - ASP.NET Core IResult helpers:
MapValidateAndReturn<T, TValidator>() - JSON map-and-serialize:
MapToJson<T>(),MapFromJson<T>() - LRU cache option: Memory-bounded cache for long-running applications
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 mappedRequirements: .NET Standard 2.0+ or .NET 6.0+ (Mapsicle.AspNetCore and Mapsicle.EntityFramework require .NET 8.0+)
Installation:dotnet add package Mapsicle
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)
| Scenario | Packages Needed |
|---|---|
| Simple POCO mapping | Mapsicle |
| API DTOs with transformations | Mapsicle.Fluent |
| EF Core with SQL projection | Mapsicle.EntityFramework |
| Map + validate DTOs | Mapsicle.Validation |
| snake_case ↔ PascalCase mapping | Mapsicle.NamingConventions |
Real benchmarks on Apple M1, .NET 8.0, BenchmarkDotNet v0.13.12:
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:
| Runtime | Manual | Mapsicle | AutoMapper | Mapperly | Mapsicle vs AutoMapper |
|---|---|---|---|---|---|
| x64 Linux (CI runner) | 18.3 ns | 60.4 ns | 82.7 ns | 18.2 ns | 1.37x faster |
| arm64 macOS | 12.2 ns | 33.1 ns | 49.0 ns | 12.5 ns | 1.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:
| Scenario | Mapsicle | AutoMapper | Mapperly | vs AutoMapper |
|---|---|---|---|---|
| Flattening | 38.4 ns | 54.1 ns | 15.2 ns | 1.41x faster |
| Collection (100) | 2,428 ns | 1,823 ns | 1,451 ns | 1.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.
| Scenario | Mapsicle | AutoMapper | Mapperly | Notes |
|---|---|---|---|---|
| Deep Nesting (15 levels) | ✅ Safe | ✅ Safe | ✅ Safe | All handle with limits |
| Circular References | Handled by default | Opt in via PreserveReferences() | Opt in via UseReferenceHandling | Only Mapsicle needs no configuration |
| Large Collection (10K) | 4 ms | 4 ms | ~3.5 ms | Mapperly fastest |
| Parallel (1000 threads) | ✅ Thread-safe | ✅ Thread-safe | ✅ Thread-safe | All thread-safe |
| Cold Start | Medium | Slow | None | Mapperly pre-compiled |
| Optimization | Improvement | Status |
|---|---|---|
| TypedMapperCache<T,D> | Zero-allocation generic cache | ✅ NEW |
| MapTo<TSource,TDest>() | Strongly-typed mapping, no boxing | ✅ NEW |
| Skip depth tracking for simple | No overhead for flat types | ✅ NEW |
| Lock-free cache reads | Eliminates contention | ✅ |
| Collection mapper caching | +20% for collections (v1.1) | ✅ |
| PropertyInfo caching | +15% faster cold starts | ✅ |
| Primitive fast path | Skips depth tracking | ✅ |
| Cached compiled actions | No runtime reflection | ✅ |
| LRU cache option | Memory-bounded in long-run apps | ✅ |
| Collection pre-allocation | Capacity hints for known sizes | ✅ |
// 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}");| Feature | Mapsicle (Unbounded) | Mapsicle (LRU) | AutoMapper |
|---|---|---|---|
| Memory Bounded | ❌ | ✅ | ❌ |
| Cache Statistics | Entry count only | Full stats | ❌ |
| Configurable Limit | ❌ | ✅ | ❌ |
| Lock-Free Reads | ✅ | ✅ | Partial |
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-dependenciespacksMapsicleand fails if the nuspec declares a single dependency.licence-boundaryfails if anything undersrc/references AutoMapper, which is RPL-1.5 or a paid licence. It is compared against intests/, which is never packed.
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# 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.DataAnnotationsusingMapsicle;vardto=user.MapTo<UserDto>();// Single objectList<UserDto>dtos=users.MapTo<UserDto>();// Collectionvarflat=order.MapTo<OrderFlatDto>();// Auto-flatteningpublicclassUserDto{[MapFrom("UserName")]// Map from different propertypublicstringName{get;set;}[IgnoreMap]// Never mappedpublicstringSecret{get;set;}}// 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 cacheusingMapsicle.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();// In Program.csservices.AddMapsicle(cfg =>{cfg.CreateMap<User,UserDto>();},validateConfiguration:true);// In your servicepublicclassUserService(IMappermapper){publicUserDtoGetUser(Useruser)=>mapper.Map<UserDto>(user);}cfg.CreateMap<Order,OrderDto>().BeforeMap((src,dest)=>dest.CreatedAt=DateTime.UtcNow).AfterMap((src,dest)=>dest.WasProcessed=true);cfg.CreateMap<Vehicle,VehicleDto>().Include<Car,CarDto>().Include<Truck,TruckDto>();cfg.CreateMap<Order,OrderDto>().ConstructUsing(src =>OrderFactory.Create(src.Type));cfg.CreateConverter<Money,decimal>(m =>m.Amount);cfg.CreateConverter<Money,string>(m =>$"{m.Currency}{m.Amount}");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();// 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();Post-mapping validation using FluentValidation, validate DTOs immediately after mapping.
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"] }}// 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 invalidresult.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[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);}}Automatic naming convention conversion, map between snake_case, PascalCase, camelCase, and kebab-case!
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| Convention | Example | C# Property |
|---|---|---|
NamingConvention.PascalCase | UserName | Standard C# |
NamingConvention.CamelCase | userName | JavaScript/JSON |
NamingConvention.SnakeCase | user_name | Python/Ruby/SQL |
NamingConvention.KebabCase | user-name | URLs/CSS |
// 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"// Combine with IMapper for convention-based mappingvardto=mapper.MapWithConvention<ApiResponse,UserDto>(apiResponse,NamingConvention.SnakeCase,NamingConvention.PascalCase);// Check if names match across conventionsboolmatch=NamingConvention.NamesMatch("user_name",NamingConvention.SnakeCase,"UserName",NamingConvention.PascalCase);// Result: truepublicclassExternalApiClient{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;}}Structured logging integration for enterprise diagnostics and observability.
usingMapsicle.Serilog;usingSerilog;// Configure Serilog loggervarlogger=newLoggerConfiguration().WriteTo.Console().MinimumLevel.Debug().CreateLogger();// Enable Mapsicle loggingMapsicleLogging.UseSerilog(logger);// 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// 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 75msusing(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"Dapper integration for mapping database query results directly to DTOs.
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 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});// 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();usingvartransaction=connection.BeginTransaction();// Mappings work within transactionsvarusers=connection.QueryAndMap<User,UserDto>("SELECT * FROM Users WHERE Active = 1",transaction:transaction).ToList();transaction.Commit();// Map existing IEnumerable from Dappervarusers=connection.Query<User>("SELECT * FROM Users");vardtos=users.MapTo<User,UserDto>(mapper);| AutoMapper | Mapsicle |
|---|---|
CreateMap<S,D>() | Same. |
ForMember().MapFrom() | Same. |
.Ignore() | Same. |
BeforeMap/AfterMap | Same. |
Include<Derived>() | Same. |
ConstructUsing() | Same. |
services.AddAutoMapper() | services.AddMapsicle() |
_mapper.Map<T>() | mapper.Map<T>() or obj.MapTo<T>() |
Simple mappings (no profiles) → Use core Mapsicle package
Profiles with configuration → Use Mapsicle.FluentEF Core ProjectTo → Use Mapsicle.EntityFramework
dotnet remove package AutoMapper
dotnet remove package AutoMapper.Extensions.Microsoft.DependencyInjection
dotnet add package Mapsicle.Fluent # Includes coreBefore (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);Before:
services.AddAutoMapper(typeof(UserProfile).Assembly);After:
services.AddMapsicle(cfg =>{cfg.CreateMap<User,UserDto>();cfg.CreateMap<Order,OrderDto>();// ... all your mappings},validateConfiguration:true);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>();}❌ Not Supported:
IMemberValueResolverinterface - useResolveUsing(func)insteadITypeConverterinterface - useCreateConverter<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
- Circular references: Mapsicle returns the default at
MaxDepthwith no configuration. AutoMapper needsPreserveReferences()orMaxDepth(...)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
Symptom: Destination properties remain default/null after mapping
Causes & Solutions:
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));
Property not readable/writable
// ❌ Won't map (no setter)publicstringName{get;}// ✅ Will mappublicstringName{get;set;}// ✅ Also works (init setter)publicstringName{get;init;}
Type incompatibility
// Check which properties can't mapvarunmapped=Mapper.GetUnmappedProperties<User,UserDto>();Console.WriteLine($"Unmapped: {string.Join(", ",unmapped)}");
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;}}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>();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;}Symptom: Exception thrown or results incorrect
Common Causes:
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();
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));
// 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);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 trueMap 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 declareWhere 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.
❌ 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
- 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
ForMemberexpressions, but notResolveUsingdelegates
- 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
stringdestination yieldsnullrather 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
InvalidOperationExceptionsurfaces asInvalidOperationException, not wrapped inTargetInvocationException. - Unmapped properties: Silent (use
GetUnmappedPropertiesfor validation) - Cache behavior: Default is unbounded (must opt-in to LRU)
| .NET Version | Mapsicle 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 |
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, ordefault(T)if source is null or max depth exceeded
Example:
vardto=user.MapTo<UserDto>();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>();Updates an existing destination object from source.
Parameters:
source- The source objectdestination- The destination object to update
Returns:
TDest- The updated destination (same instance)
Example:
source.Map(existingDto);// Updates existingDto in-placeConverts an object to a dictionary of property name/value pairs.
Returns:
Dictionary<string, object?>- Case-insensitive dictionary
Example:
vardict=user.ToDictionary();Maps a dictionary to an object.
Constraints:
- T must have a parameterless constructor
Example:
varuser=dict.MapTo<User>();- Type:
int - Default:
32 - Description: Maximum recursion depth before returning default value (circular reference protection)
Mapper.MaxDepth=64;- Type:
bool - Default:
false - Description: Enables memory-bounded LRU cache. Clears all caches when changed.
Mapper.UseLruCache=true;- Type:
int - Default:
1000 - Description: Maximum cache entries when UseLruCache is enabled
Mapper.MaxCacheSize=2000;- Type:
Action<string>? - Default:
null - Description: Logger for diagnostic messages (depth warnings, etc)
Mapper.Logger= msg =>_logger.LogDebug(msg);Clears all cached mapping delegates.
Mapper.ClearCache();- Returns:
MapperCacheInfo- Current cache statistics
varstats=Mapper.CacheInfo();Console.WriteLine($"Total: {stats.Total}, Hit Ratio: {stats.HitRatio:P1}");Validates mapping configuration. Throws InvalidOperationException if unmapped properties exist.
Mapper.AssertMappingValid<User,UserDto>();- Returns:
List<string>- Names of destination properties that cannot be mapped
varunmapped=Mapper.GetUnmappedProperties<User,UserDto>();Creates an isolated mapper instance with independent cache and depth tracking.
Parameters:
options- Optional configuration (MaxDepth, Logger, UseLruCache, MaxCacheSize)
Returns:
IDisposablemapper instance
Example:
usingvarmapper=MapperFactory.Create(newMapperOptions{MaxDepth=16,UseLruCache=true,MaxCacheSize=100,Logger=Console.WriteLine});vardto=mapper.MapTo<UserDto>(user);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();ForMember<TMember>()- Configure individual member mappingopt.MapFrom(expr)- Map from custom expressionopt.Ignore()- Don't map this memberopt.Condition(pred)- Conditional mappingopt.ResolveUsing(func)- Custom resolver function
BeforeMap(action)- Execute action before mappingAfterMap(action)- Execute action after mappingInclude<TDerived, TDest>()- Polymorphic mapping supportConstructUsing(factory)- Custom object constructionReverseMap()- Create reverse mappingCreateConverter<TSource, TDest>(converter)- Global type converter
Translates mapping to SQL expression (executed in database).
Parameters:
query- Source EF Core queryableconfig- Optional mapper configuration for custom mappings
Returns:
IQueryable<TDest>- Queryable projection
Example:
vardtos=awaitcontext.Users.Where(u =>u.IsActive).ProjectTo<User,UserDto>(config).ToListAsync();Maps source to destination and validates using the specified validator type.
Type Parameters:
TDest- Destination typeTValidator- FluentValidation validator type (must have parameterless constructor)
Returns:
MapperValidationResult<TDest>- ContainsIsValid,Value,Errors,ErrorsByProperty
Example:
varresult=mapper.MapAndValidate<User,UserDto,UserDtoValidator>(user);if(result.IsValid)returnresult.Value;Maps source to destination and validates using a provided validator instance.
Example:
varvalidator=newUserDtoValidator();varresult=mapper.MapAndValidate<UserDto>(user,validator);Validates an existing object using the specified validator type.
Example:
varresult=dto.Validate<UserDto,UserDtoValidator>();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 propertiesdestConvention- The naming convention of destination properties
Returns:
TDest?- New instance with convention-matched properties
Example:
vardto=apiResponse.MapWithConvention<ApiResponse,UserDto>(NamingConvention.SnakeCase,NamingConvention.PascalCase);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: trueEnables global Serilog integration for Mapsicle mapping operations.
Parameters:
logger- Serilog ILogger instance
Example:
MapsicleLogging.UseSerilog(Log.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.5msMaps 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.2msConfigures the threshold for slow mapping warnings.
Default: 100ms
Example:
MapsicleLogging.SlowMappingThreshold=TimeSpan.FromMilliseconds(50);Executes a SQL query and maps results to destination type.
Overloads:
QueryAndMap<TSource, TDest>(sql, param?, transaction?, commandTimeout?)- Auto-mappingQueryAndMap<TSource, TDest>(sql, MapperConfiguration, param?, ...)- With configurationQueryAndMap<TSource, TDest>(sql, IMapper, param?, ...)- With mapper instance
Returns:
IEnumerable<TDest>- Mapped results
Example:
varusers=connection.QueryAndMap<User,UserDto>("SELECT * FROM Users").ToList();Async version of QueryAndMap.
Example:
varusers=awaitconnection.QueryAndMapAsync<User,UserDto>("SELECT * FROM Users");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});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");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);- ✅ Zero-config convention mapping
- ✅ Collection mapping (List, Array, IEnumerable)
- ✅ Dictionary mapping (object ↔ Dictionary)
- ✅ Flattening (
AddressCity→Address.City) - ✅ Nullable type coercion (
T↔T?) - ✅ 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
- ✅
[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
- ✅ 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
- ✅ ProjectTo with SQL translation
- ✅ ForMember in ProjectTo
- ✅ Flattening in SQL
- ✅ Nested projection
- ✅ Type coercion in queries
- ✅ MapAndValidate with FluentValidation
- ✅ Validator type parameter
- ✅ Validator instance injection
- ✅ Validation result with IsValid, Errors
- ✅ ErrorsByProperty dictionary
- ✅ GetValueOrThrow pattern
- ✅ Validator caching
- ✅ PascalCase convention
- ✅ camelCase convention
- ✅ snake_case convention
- ✅ kebab-case convention
- ✅ MapWithConvention extension
- ✅ ConvertName string extension
- ✅ NamesMatch cross-convention comparison
- ✅ Property mapping cache
- ✅ UseSerilog global integration
- ✅ MapWithLogging extension
- ✅ MapCollectionWithLogging extension
- ✅ Slow mapping warnings
- ✅ MappingLoggingScope for batch operations
- ✅ Structured logging with properties
- ✅ Configurable thresholds
- ✅ QueryAndMap / QueryAndMapAsync
- ✅ QuerySingleAndMap / QuerySingleAndMapAsync
- ✅ QueryFirstAndMap / QueryFirstAndMapAsync
- ✅ Transaction support
- ✅ Custom MapperConfiguration support
- ✅ IMapper instance support
- ✅ MapTo collection extension
dotnet test Mapsicle.sln -c Release runs all of it, including the allocation budgets.
| Package | Tests | What it covers |
|---|---|---|
| Mapsicle | 284 | Core, plus regression, load, fault and untrusted input |
| Mapsicle.NamingConventions | 55 | Naming conventions |
| Mapsicle.Fluent | 39 | Fluent configuration and profiles |
| Mapsicle.Validation | 27 | FluentValidation integration |
| Mapsicle.Json | 26 | JSON serialization |
| Mapsicle.Audit | 26 | Change tracking |
| Mapsicle.Dapper | 25 | Dapper integration |
| Mapsicle.DataAnnotations | 24 | DataAnnotations validation |
| Mapsicle.AspNetCore | 23 | ASP.NET Core helpers |
| Mapsicle.Serilog | 22 | Serilog logging |
| Mapsicle.Caching | 21 | Caching integration |
| Mapsicle.EntityFramework | 19 | EF Core ProjectTo |
| Mapsicle.Performance | 8 | Allocation budgets on warm paths |
| Total | 599 |
Four of those suites exist because a passing test count on its own says very little:
IssueRegressionTestscarries 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.UntrustedInputTestspins the security statements in this README, so a change to any of them fails the build rather than quietly making the documentation wrong.FaultInjectionTestscovers 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.LoadTestsverifies 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.
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
dotnet run --project examples/Mapsicle.ExamplesPRs welcome. Areas for contribution:
- Performance optimizations
- Additional type coercion scenarios
- Documentation improvements
MPL 2.0 License © Arnel Isiderio Robles
Stop configuring. Start mapping.
Free forever. Zero dependencies. Pure performance.