Mapsicle.EntityFramework 1.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Mapsicle.EntityFramework --version 1.1.0
                    
NuGet\Install-Package Mapsicle.EntityFramework -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Mapsicle.EntityFramework" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Mapsicle.EntityFramework" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Mapsicle.EntityFramework" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Mapsicle.EntityFramework --version 1.1.0
                    
#r "nuget: Mapsicle.EntityFramework, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Mapsicle.EntityFramework@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Mapsicle.EntityFramework&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Mapsicle.EntityFramework&version=1.1.0
                    
Install as a Cake Tool

Mapsicle 🍦

NuGet Downloads License: MPL 2.0

ko-fi

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 Mapsicle
Mapsicle.EntityFramework EF Core ProjectTo<T>() Mapsicle.Fluent
Mapsicle.Validation FluentValidation integration Mapsicle.Fluent
Mapsicle.NamingConventions Naming convention support Mapsicle.Fluent

"The fastest mapping is the one you don't have to configure."


πŸš€ Why Switch from AutoMapper?

⚠️ AutoMapper is now commercial software. As of version 13+, AutoMapper requires a paid license. Mapsicle is 100% free and MPL 2.0 licensed forever.

Feature Mapsicle AutoMapper
License MPL 2.0 (Free) Commercial
Dependencies 0 5+
Setup Required None Profiles, DI
Circular Refs Handled Crash
Binary Size ~25KB ~500KB+
Memory Bounded LRU Option No
Cache Statistics Yes No

🚦 Quick Start

Complete Example (Copy & Paste)

using Mapsicle;

// 1. Define your types
public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

public class UserDto
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

// 2. Map - that's it! No configuration needed
var user = new User { Id = 1, FirstName = "John", LastName = "Doe", Email = "john@example.com" };
var dto = user.MapTo<UserDto>();  // FirstName and LastName copied automatically

// 3. Map collections
List<User> users = GetUsers();
List<UserDto> dtos = users.MapTo<UserDto>();  // Entire list mapped

Requirements: .NET Standard 2.0+ or .NET 6.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)
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

πŸ“Š Benchmark Results

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

Core Mapping Performance

Scenario Manual Mapsicle AutoMapper Winner
Single Object 31 ns 59 ns 72 ns ⭐ Mapsicle (+22%)
Flattening 14 ns 29 ns 56 ns ⭐ Mapsicle (+93%)
Collection (100) 3.5 ΞΌs 5.5 ΞΌs 4.0 ΞΌs AutoMapper

Edge Case Performance

Scenario Mapsicle AutoMapper Notes
Deep Nesting (15 levels) βœ… Safe βœ… Safe Both handle with MaxDepth
Circular References βœ… Handled ❌ Crashes Mapsicle wins
Large Collection (10K) 4 ms 4 ms Comparable
Parallel (1000 threads) βœ… Thread-safe βœ… Thread-safe Lock-free reads

Performance Optimizations (v1.1+)

Optimization Improvement Status
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 βœ…

Memory & Cache Statistics (v1.1+)

// Enable memory-bounded caching
Mapper.UseLruCache = true;
Mapper.MaxCacheSize = 1000;  // Default

// Monitor cache performance
var stats = Mapper.CacheInfo();
Console.WriteLine($"Cache entries: {stats.Total}");
Console.WriteLine($"Hit ratio: {stats.HitRatio:P1}");  // Only when LRU enabled
Console.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

Smoke Test Results (10,000 mappings)

βœ“ Core: 10,000 mappings in 19ms
βœ“ Fluent: 10,000 mappings in 10ms
βœ“ Deep nesting (10 levels): 1,000 mappings in 3ms
βœ“ Large collection (10,000 items): 4ms

πŸ’‘ Key Insight: Mapsicle wins on simple/flattened mappings and safety. Both vastly outperform reflection-based approaches.

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 (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

⚑ Package 1: Mapsicle (Core)

Basic Mapping

using Mapsicle;

var dto = user.MapTo<UserDto>();              // Single object
List<UserDto> dtos = users.MapTo<UserDto>();  // Collection
var flat = order.MapTo<OrderFlatDto>();       // Auto-flattening

Attributes

public class UserDto
{
    [MapFrom("UserName")]  // Map from different property
    public string Name { get; set; }

    [IgnoreMap]             // Never mapped
    public string Secret { get; set; }
}

Stability Features (NEW!)

// Cycle Detection - no more StackOverflow
Mapper.MaxDepth = 32;  // Default, configurable

// Validation at startup
Mapper.AssertMappingValid<User, UserDto>();

// Logging
Mapper.Logger = Console.WriteLine;

// Memory-bounded caching (prevents memory leaks in long-running apps)
Mapper.UseLruCache = true;   // Enable LRU cache
Mapper.MaxCacheSize = 1000;  // Limit cache entries

// Cache statistics
var stats = Mapper.CacheInfo();
Console.WriteLine($"Hit ratio: {stats.HitRatio:P1}");

// Scoped instances with isolated caches
using var mapper = MapperFactory.Create();
var dto = mapper.MapTo<UserDto>(user);  // Uses isolated cache

⚑ Package 2: Mapsicle.Fluent

Basic Configuration

using Mapsicle.Fluent;

var config = new MapperConfiguration(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();
var mapper = config.CreateMapper();

DI Integration (NEW!)

// In Program.cs
services.AddMapsicle(cfg =>
{
    cfg.CreateMap<User, UserDto>();
}, validateConfiguration: true);

// In your service
public class UserService(IMapper mapper)
{
    public UserDto GetUser(User user) => 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!

using Mapsicle.EntityFramework;

var dtos = await _context.Users
    .Where(u => u.IsActive)
    .ProjectTo<UserEntity, UserDto>()
    .ToListAsync();

// Flattening in SQL: Customer.Name β†’ CustomerName
var orders = _context.Orders
    .ProjectTo<OrderEntity, OrderFlatDto>()
    .ToList();

ProjectTo with Fluent Configuration (NEW!)

// ForMember expressions are translated to SQL!
var config = new MapperConfiguration(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 queries
var orders = _context.Orders.ProjectTo<Order, OrderDto>(config).ToList();

⚑ Package 4: Mapsicle.Validation

Post-mapping validation using FluentValidationβ€”validate DTOs immediately after mapping!

Basic Usage

using FluentValidation;
using Mapsicle.Fluent;
using Mapsicle.Validation;

// 1. Define your validator
public class UserDtoValidator : AbstractValidator<UserDto>
{
    public UserDtoValidator()
    {
        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 call
var result = mapper.MapAndValidate<User, UserDto, UserDtoValidator>(user);

if (result.IsValid)
{
    return Ok(result.Value);  // The mapped DTO
}
else
{
    return BadRequest(result.ErrorsByProperty);  // { "Email": ["Valid email is required"] }
}

API Overview

// Map and validate with validator type
var result = mapper.MapAndValidate<TSource, TDest, TValidator>(source);

// Map and validate with validator instance
var validator = new UserDtoValidator();
var result = mapper.MapAndValidate<UserDto>(source, validator);

// Validate an existing object
var result = dto.Validate<UserDto, UserDtoValidator>();

// Get value or throw exception
var dto = result.GetValueOrThrow();  // Throws ValidationException if invalid

Result Properties

result.IsValid           // bool - true if validation passed
result.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")]
public class UsersController : ControllerBase
{
    private readonly IMapper _mapper;
    private readonly IUserRepository _repo;

    public UsersController(IMapper mapper, IUserRepository repo)
    {
        _mapper = mapper;
        _repo = repo;
    }

    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateUserRequest request)
    {
        var result = _mapper.MapAndValidate<CreateUserRequest, UserDto, UserDtoValidator>(request);

        if (!result.IsValid)
        {
            return BadRequest(new { errors = result.ErrorsByProperty });
        }

        var user = await _repo.CreateAsync(result.Value);
        return CreatedAtAction(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

using Mapsicle.NamingConventions;

// Source uses snake_case (e.g., from Python API or database)
public class ApiResponse
{
    public int user_id { get; set; }
    public string first_name { get; set; }
    public string email_address { get; set; }
}

// Destination uses PascalCase (C# convention)
public class UserDto
{
    public int UserId { get; set; }
    public string FirstName { get; set; }
    public string EmailAddress { get; set; }
}

// Map with naming convention conversion
var dto = apiResponse.MapWithConvention<ApiResponse, UserDto>(
    NamingConvention.SnakeCase,
    NamingConvention.PascalCase);

// dto.UserId == apiResponse.user_id
// dto.FirstName == apiResponse.first_name

Built-in Conventions

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 Property Names

// Convert a single name
var snake = "UserName".ConvertName(NamingConvention.PascalCase, NamingConvention.SnakeCase);
// Result: "user_name"

var pascal = "first_name".ConvertName(NamingConvention.SnakeCase, NamingConvention.PascalCase);
// Result: "FirstName"

var camel = "OrderCount".ConvertName(NamingConvention.PascalCase, NamingConvention.CamelCase);
// Result: "orderCount"

Use with Fluent Mapper

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

Check Name Matching

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

Real-World Example: External API Integration

public class ExternalApiClient
{
    private readonly HttpClient _http;

    public async Task<UserDto> GetUserAsync(int id)
    {
        // External API returns snake_case JSON
        var response = await _http.GetFromJsonAsync<ExternalUserResponse>($"/users/{id}");

        // Convert to C# conventions
        return response.MapWithConvention<ExternalUserResponse, UserDto>(
            NamingConvention.SnakeCase,
            NamingConvention.PascalCase);
    }
}

// External API response (snake_case)
public class ExternalUserResponse
{
    public int user_id { get; set; }
    public string first_name { get; set; }
    public string last_name { get; set; }
    public string email_address { get; set; }
    public DateTime created_at { get; set; }
}

// Internal DTO (PascalCase)
public class UserDto
{
    public int UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string EmailAddress { get; set; }
    public DateTime CreatedAt { get; set; }
}

πŸ”§ Migration from AutoMapper

API Compatibility

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>()

Step-by-Step Migration Guide

1. Identify Your AutoMapper Usage

Simple mappings (no profiles) β†’ Use core Mapsicle package Profiles with configuration β†’ Use Mapsicle.Fluent EF 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):

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

After (Mapsicle):

// In Program.cs/Startup.cs
services.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:

public class UserService
{
    private readonly IMapper _mapper;

    public UserService(IMapper mapper) => _mapper = mapper;

    public UserDto GetUser(User user) => _mapper.Map<UserDto>(user);
}

After (same interface!):

public class UserService
{
    private readonly IMapper _mapper;

    public UserService(IMapper mapper) => _mapper = mapper;

    // Option 1: Same as AutoMapper
    public UserDto GetUser(User user) => _mapper.Map<UserDto>(user);

    // Option 2: Extension method (no DI needed for simple cases)
    public UserDto GetUser(User user) => 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: AutoMapper throws exception, Mapsicle returns default value
  • 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] attribute
    public class UserDto
    {
        [MapFrom("UserName")]
        public string Name { get; set; }
    }
    
    // Solution 2: Use Fluent configuration
    cfg.CreateMap<User, UserDto>()
        .ForMember(d => d.Name, opt => opt.MapFrom(s => s.UserName));
    
  2. Property not readable/writable

    // ❌ Won't map (no setter)
    public string Name { get; }
    
    // βœ… Will map
    public string Name { get; set; }
    
    // βœ… Also works (init setter)
    public string Name { get; init; }
    
  3. Type incompatibility

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

Cause: Circular references exceeding MaxDepth (default 32)

Solutions:

// Solution 1: Increase depth limit
Mapper.MaxDepth = 64;

// Solution 2: Enable logging to see depth warnings
Mapper.Logger = msg => Console.WriteLine($"[Mapsicle] {msg}");

// Solution 3: Use [IgnoreMap] to break cycle
public class User
{
    public int Id { get; set; }

    [IgnoreMap]  // Don't map back to parent
    public List<Order> Orders { get; set; }
}
Issue: Poor Collection Mapping Performance

Symptom: Mapping 10,000+ items is slow

Solutions:

// ❌ Don't: Map items individually
foreach (var user in users)
{
    dtos.Add(user.MapTo<UserDto>());
}

// βœ… Do: Map entire collection
var dtos = users.MapTo<UserDto>();  // 20% faster with cached mapper

// βœ… Do: Pre-warm cache at startup for frequently used types
new User().MapTo<UserDto>();
new Order().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 cache
Mapper.UseLruCache = true;
Mapper.MaxCacheSize = 1000;  // Adjust based on # of unique type pairs

// Monitor cache performance
var stats = Mapper.CacheInfo();
if (stats.HitRatio < 0.8)
{
    // Consider increasing cache size
    Mapper.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 expressions
    var dtos = context.Orders.ProjectTo<Order, OrderDto>().ToList();
    
    // βœ… Pass configuration for ForMember expressions
    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Order, OrderDto>()
            .ForMember(d => d.CustomerName, opt => opt.MapFrom(s => s.Customer.Name));
    });
    var dtos = context.Orders.ProjectTo<Order, OrderDto>(config).ToList();
    
  2. Non-translatable expressions

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

Debugging Tips

// 1. Enable verbose logging
Mapper.Logger = msg => _logger.LogDebug($"[Mapsicle] {msg}");

// 2. Validate mapping at startup
#if DEBUG
Mapper.AssertMappingValid<User, UserDto>();
#endif

// 3. Check configuration in fluent mapper
config.AssertConfigurationIsValid();

// 4. Monitor cache statistics
var stats = Mapper.CacheInfo();
_logger.LogInformation($"Cache: {stats.Total} entries, Hit ratio: {stats.HitRatio:P1}");

// 5. Use MapperFactory for isolated testing
using var mapper = MapperFactory.Create(new MapperOptions
{
    MaxDepth = 16,
    Logger = Console.WriteLine
});
var dto = mapper.MapTo<UserDto>(user);

⚠️ 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 ~27% slower than AutoMapper for 100-1000 items (competitive on 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)
  • Unmapped properties: Silent (use GetUnmappedProperties for validation)
  • Cache behavior: Default is unbounded (must opt-in to LRU)

Platform Support

.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

πŸ“š 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:

var dto = user.MapTo<UserDto>();

MapTo<T>(this IEnumerable source)

Maps a collection to a List<T>.

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:

var dict = 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:

var user = 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
var stats = 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
var unmapped = 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:

using var mapper = MapperFactory.Create(new MapperOptions
{
    MaxDepth = 16,
    UseLruCache = true,
    MaxCacheSize = 100,
    Logger = Console.WriteLine
});
var dto = mapper.MapTo<UserDto>(user);

Fluent API (using Mapsicle.Fluent)

MapperConfiguration
var config = new MapperConfiguration(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 => new UserDto(src.Id))
        .ReverseMap();

    cfg.CreateConverter<Money, decimal>(m => m.Amount);
});

config.AssertConfigurationIsValid();
var mapper = 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:

var dtos = await context.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:

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

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

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

Example:

var validator = new UserDtoValidator();
var result = mapper.MapAndValidate<UserDto>(user, validator);

Validate<T, TValidator>(this T value)

Validates an existing object using the specified validator type.

Example:

var result = 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:

var dto = 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:

var snakeName = "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:

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

πŸ“ Complete Feature List

Core Features

  • βœ… 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

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

πŸ§ͺ Test Coverage

Package Tests Coverage
Mapsicle 210 Core + Stability
Mapsicle.Fluent 35 Fluent + Enterprise
Mapsicle.EntityFramework 19 EF Core
Mapsicle.Validation 13 FluentValidation
Mapsicle.NamingConventions 55 Naming Conventions
Total 332

πŸ“ Project Structure

Mapsicle/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ Mapsicle/                    # Core - zero config
β”‚   β”œβ”€β”€ Mapsicle.Fluent/             # Fluent + DI
β”‚   β”œβ”€β”€ Mapsicle.EntityFramework/    # EF Core ProjectTo
β”‚   β”œβ”€β”€ Mapsicle.Validation/         # FluentValidation integration
β”‚   └── Mapsicle.NamingConventions/  # Naming convention support
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ Mapsicle.Tests/
β”‚   β”œβ”€β”€ Mapsicle.Fluent.Tests/
β”‚   β”œβ”€β”€ Mapsicle.EntityFramework.Tests/
β”‚   β”œβ”€β”€ Mapsicle.Validation.Tests/
β”‚   β”œβ”€β”€ Mapsicle.NamingConventions.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


<p align="center"> <strong>Stop configuring. Start mapping.</strong><br> <em>Free forever. Zero dependencies. Pure performance.</em> </p>

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.2 77 3/3/2026
1.2.0 101 1/23/2026
1.1.0 91 1/19/2026
1.0.8 175 12/26/2025
1.0.6 180 12/24/2025
1.0.4 95 1/19/2026
1.0.3 98 1/19/2026
1.0.1 98 12/27/2025