SmAutoMapper 1.0.1
See the version list below for details.
dotnet add package SmAutoMapper --version 1.0.1
NuGet\Install-Package SmAutoMapper -Version 1.0.1
<PackageReference Include="SmAutoMapper" Version="1.0.1" />
<PackageVersion Include="SmAutoMapper" Version="1.0.1" />
<PackageReference Include="SmAutoMapper" />
paket add SmAutoMapper --version 1.0.1
#r "nuget: SmAutoMapper, 1.0.1"
#:package SmAutoMapper@1.0.1
#addin nuget:?package=SmAutoMapper&version=1.0.1
#tool nuget:?package=SmAutoMapper&version=1.0.1
MyAutoMapper
Lightweight, high-performance object mapping library for .NET 10 with first-class support for parameterized EF Core projections.
Only external dependency: Microsoft.Extensions.DependencyInjection.Abstractions.
Quick Start
// 1. Register in DI (Program.cs)
builder.Services.AddMapping(typeof(Program).Assembly);
// 2. Use anywhere — no injection needed!
var products = await _db.Products
.ProjectTo<ProductViewModel>(p => p.Set("lang", lang))
.ToListAsync();
// Or without parameters:
var categories = await _db.Categories
.ProjectTo<CategoryDto>()
.ToListAsync();
Features
| Feature | Description |
|---|---|
| Expression projections | Generates Expression<Func<TSource, TDest>> for EF Core IQueryable — only mapped columns appear in SQL |
| Parameterized projections | ParameterSlot<T> uses a closure pattern that EF Core translates to native SQL parameters (@__param_0), preserving the query plan cache |
| In-memory mapping | Compiled Func<TSource, TDest> delegates for fast object-to-object mapping |
| Conventions | Auto-maps by matching property names + flattens nested objects (Address.City → AddressCity) |
| Fluent API | CreateMap<S, D>().ForMember(...).Ignore(...).ConstructUsing(...).ReverseMap() |
| Eager validation | All mappings validated at startup; throws immediately with the full list of issues |
| DI integration | services.AddMapping() with assembly scanning and singleton registration |
Requirements
- .NET 10 SDK (10.0.201+)
- C# 14 (
LangVersion preview)
Fluent API
MappingProfile
public class ProductProfile : MappingProfile
{
public ProductProfile()
{
// Simple mapping with conventions
CreateMap<Product, ProductDto>();
// Custom member mapping
CreateMap<Product, ProductDetailDto>()
.ForMember(d => d.Name, o => o.MapFrom(s => s.Title))
.Ignore(d => d.InternalCode)
.ConstructUsing(src => new ProductDetailDto { Source = "db" })
.ReverseMap();
// Parameterized projection
var lang = DeclareParameter<string>("lang");
CreateMap<Product, ProductViewModel>()
.ForMember(d => d.LocalizedName, o => o.MapFrom(lang,
(src, l) => l == "ru" ? src.NameRu : src.NameUz));
}
}
ProjectTo API
Three levels, from simplest to most explicit:
// 1. Single generic parameter (recommended) — TSource inferred at runtime
source.ProjectTo<ProductDto>();
source.ProjectTo<ProductDto>(p => p.Set("lang", "ru"));
// 2. Two generic parameters — TSource explicit
source.ProjectTo<Product, ProductDto>();
source.ProjectTo<Product, ProductDto>(p => p.Set("lang", "ru"));
// 3. Explicit provider — full control, useful for testing
source.ProjectTo<Product, ProductDto>(projectionProvider);
source.ProjectTo<Product, ProductDto>(projectionProvider, p => p.Set("lang", "ru"));
In-Memory Mapping
var mapper = serviceProvider.GetRequiredService<IMapper>();
var dto = mapper.Map<Product, ProductDto>(product);
Convention-Based Auto-Mapping
When CreateMap<S, D>() is called without ForMember, the compiler automatically matches properties.
Same name (case-insensitive):
Source.Name → Dest.Name
Source.Price → Dest.Price
Nested object flattening:
Source.Address.City → Dest.AddressCity
Source.Address.ZipCode → Dest.AddressZipCode
Explicit ForMember always overrides conventions.
DI Integration
// Auto-scan assembly — finds all MappingProfile subclasses
builder.Services.AddMapping(typeof(Program).Assembly);
// With manual configuration
builder.Services.AddMapping(
cfg => cfg.AddProfile<MyCustomProfile>(),
typeof(SomeProfile).Assembly);
Registers as Singleton:
MapperConfiguration— frozen configurationIMapper— stateless mapperIProjectionProvider— projection provider + staticProjectionProviderAccessor
All mappings validated at registration. On errors: MappingValidationException with the full list of issues.
How Parameterized Projections Work
This is the library's key innovation. Common approaches (string interpolation, constant replacement) break EF Core's query plan cache. MyAutoMapper uses the closure pattern — the same one the C# compiler generates for closures.
Configuration Phase
1. User declares:
var lang = DeclareParameter<string>("lang");
CreateMap<Product, ProductViewModel>()
.ForMember(d => d.LocalizedName, o => o.MapFrom(lang,
(src, l) => l == "ru" ? src.NameRu : src.NameUz))
2. Library generates a dynamic closure holder:
class ClosureHolder_1 { public string lang { get; set; } }
3. Compiler builds an expression with closure pattern:
src => holderInstance.lang == "ru" ? src.NameRu : src.NameUz
Query Phase
1. User calls:
_db.Products.ProjectTo<ProductViewModel>(p => p.Set("lang", "ru"))
2. Library creates a new holder with lang="ru", swaps it in the expression tree.
Tree shape stays IDENTICAL — only the value changes.
3. EF Core translates:
SELECT CASE WHEN @__lang_0 = 'ru' THEN "p"."NameRu" ELSE "p"."NameUz" END
FROM "Products" AS "p"
-- @__lang_0 is a SQL parameter, not an inlined constant!
4. Next call with lang="uz" → EF Core REUSES the query plan.
Only the parameter value changes: @__lang_0 = 'uz'
Example: Web API with Localization
The project samples/MyAutoMapper.WebApiSample is a working ASP.NET Core Web API with EF Core SQLite and parameterized localization.
Profile
public class ProductMappingProfile : MappingProfile
{
public ProductMappingProfile()
{
var lang = DeclareParameter<string>("lang");
CreateMap<Product, ProductViewModel>()
.ForMember(d => d.LocalizedName, o => o.MapFrom(lang,
(src, l) => l == "uz" ? src.NameUz
: l == "lt" ? src.NameLt
: src.NameRu))
.ForMember(d => d.LocalizedDescription, o => o.MapFrom(lang,
(src, l) => l == "uz" ? src.DescriptionUz
: l == "lt" ? src.DescriptionLt
: src.DescriptionRu));
}
}
Controller
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] string lang = "ru")
{
var products = await _db.Products
.ProjectTo<ProductViewModel>(p => p.Set("lang", lang))
.ToListAsync();
return Ok(products);
}
Output
GET /api/products?lang=ru:
[
{"id": 1, "localizedName": "iPhone 16 Pro", "localizedDescription": "Newest Apple smartphone", "price": 12990000.0},
{"id": 2, "localizedName": "Samsung Galaxy S25", "localizedDescription": "Flagship Samsung smartphone", "price": 10490000.0}
]
SQL generated by EF Core:
SELECT CASE WHEN @__lang_0 = 'ru' THEN "p"."NameRu" ELSE "p"."NameUz" END AS "LocalizedName",
"p"."Id", "p"."Price"
FROM "Products" AS "p"
Running the Example
cd samples/MyAutoMapper.WebApiSample
dotnet run
# Swagger UI: http://localhost:5000/swagger
API Endpoints
| Endpoint | Description |
|---|---|
GET /api/products?lang=ru |
All products with localization |
GET /api/products/{id}?lang=ru |
Single product by Id |
GET /api/products/by-category/{id}?lang=uz |
Products by category |
GET /api/categories/tree?lang=ru |
Category tree with hierarchy |
GET /api/categories/flat?lang=lt |
Flat category list |
Testing
# Unit tests
dotnet test tests/MyAutoMapper.UnitTests
# Integration tests (EF Core SQLite)
dotnet test tests/MyAutoMapper.IntegrationTests
# All tests
dotnet test
Benchmarks
Compares MyAutoMapper vs AutoMapper (16.1.1) vs Mapster (10.0.3) vs manual mapping.
cd tests/MyAutoMapper.Benchmarks
# All benchmarks (5-15 min)
dotnet run -c Release -- --filter *
# Specific benchmark
dotnet run -c Release -- --filter *SimpleMappingBenchmark*
# Quick run (less accurate, 1-3 min)
dotnet run -c Release -- --filter * --job short
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26200.8037)
Unknown processor
.NET SDK 10.0.201
[Host] : .NET 10.0.5 (10.0.526.15411), X64 RyuJIT AVX2
DefaultJob : .NET 10.0.5 (10.0.526.15411), X64 RyuJIT AVX2
Categories=Simple
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|------------- |----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:|
| Manual | 3.491 ns | 0.0136 ns | 0.0113 ns | 1.00 | 0.00 | 0.0031 | 48 B | 1.00 |
| Mapster | 8.687 ns | 0.0659 ns | 0.0584 ns | 2.49 | 0.02 | 0.0031 | 48 B | 1.00 |
| SmAutoMapper | 14.566 ns | 0.2508 ns | 0.2224 ns | 4.17 | 0.06 | 0.0030 | 48 B | 1.00 |
| AutoMapper | 25.699 ns | 0.0652 ns | 0.0578 ns | 7.36 | 0.03 | 0.0030 | 48 B | 1.00 |
Important: always run with
-c Release. Debug builds produce inaccurate results.
Building and Running
# Build
dotnet build
# Run tests
dotnet test
# Run Web API example
cd samples/MyAutoMapper.WebApiSample
dotnet run
# Pack for NuGet
dotnet pack -c Release
dotnet pack src/MyAutoMapper/SmAutoMapper.csproj -c Release -o ./nupkg
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.5)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.5)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.