KObjectMapper 0.0.0-alpha-2
dotnet add package KObjectMapper --version 0.0.0-alpha-2
NuGet\Install-Package KObjectMapper -Version 0.0.0-alpha-2
<PackageReference Include="KObjectMapper" Version="0.0.0-alpha-2" />
<PackageVersion Include="KObjectMapper" Version="0.0.0-alpha-2" />
<PackageReference Include="KObjectMapper" />
paket add KObjectMapper --version 0.0.0-alpha-2
#r "nuget: KObjectMapper, 0.0.0-alpha-2"
#:package KObjectMapper@0.0.0-alpha-2
#addin nuget:?package=KObjectMapper&version=0.0.0-alpha-2&prerelease
#tool nuget:?package=KObjectMapper&version=0.0.0-alpha-2&prerelease
KObjectMapper
KObjectMapper is a simple, intuitive, and effective open-source object-to-object mapping library for C# and .NET.
It supports two mapping modes — Implicit (extension methods, zero configuration) and Explicit (mapper instance) — and a rich profile-based configuration system for production-grade mapping pipelines.
Pre-release notice: KObjectMapper is currently published as an alpha package (0.0.0-alpha-1). The API is still evolving and may include breaking changes between alpha versions. Do not use in production workloads yet.
Installation
dotnet add package KObjectMapper --version 0.0.0-alpha-1
For newer alpha drops, increment the suffix (for example:
0.0.0-alpha-2,0.0.0-alpha-3).
Quick Start
Implicit mapping (extension methods)
Implicit mapping uses extension methods and requires no mapper instance or configuration.
using KObjectMapper.Extensions;
var customer = new Customer { Id = 25, FirstName = "Will", LastName = "Smith" };
CustomerDto dto = new();
customer.MapTo(dto); // customer → dto
dto.MapFrom(customer); // equivalent reverse direction
Explicit mapping (mapper instance)
using KObjectMapper;
var mapper = Mapper.Create();
// Map into an existing instance
mapper.Map<Customer, CustomerDto>(customer, dto);
// Map to a new instance
CustomerDto dto = mapper.Map<Customer, CustomerDto>(customer);
// Map collections
IEnumerable<CustomerDto> dtos = mapper.Map<Customer, CustomerDto>(customers);
Profile-Based Configuration
Profiles give you explicit, reusable, and versionable mapping rules per source/target pair.
Defining a profile
using KObjectMapper.Configuration;
public class CustomerProfile : MappingProfile
{
protected override void Configure()
{
CreateMap<Customer, CustomerDto>()
.ForMember(src => src.FullName, tgt => tgt.Name) // rename
.Ignore(tgt => tgt.InternalCode); // skip member
}
}
Registering profiles
using KObjectMapper.DependencyInjection;
// Register individual profiles
builder.Services.AddKObjectMapper(options =>
{
options.AddProfile<CustomerProfile>();
});
// Or scan an assembly for all profiles
builder.Services.AddKObjectMapper(options =>
{
options.AddProfilesFromAssembly(typeof(CustomerProfile).Assembly);
});
Inject and use IObjectMapper anywhere:
using KObjectMapper.Abstractions;
public class CustomersController(IObjectMapper mapper)
{
public CustomerDto Get(Customer customer)
=> mapper.Map<Customer, CustomerDto>(customer);
}
Null-Handling Policy
Control how null source values are handled — globally or per map.
// Global policy
builder.Services.AddKObjectMapper(options =>
{
options.WithNullPolicy(NullMappingPolicy.Ignore);
});
// Per-map policy with a null substitute
CreateMap<Order, OrderDto>()
.WithNullPolicy(NullMappingPolicy.Ignore)
.SubstituteNullWith(tgt => tgt.Status, "Unknown");
| Policy | Behaviour |
|---|---|
Propagate (default) |
Null source value is written to the target |
Ignore |
Null source values are skipped; target retains its value |
Strict Mode and Startup Validation
Enable strict mode to fail fast when no type map is registered for a requested pair.
builder.Services.AddKObjectMapper(options =>
{
options.EnableStrictMode();
options.AddProfile<CustomerProfile>();
});
With strict mode on, calling mapper.Map<A, B>(...) for an unregistered pair throws InvalidOperationException at the call site rather than silently falling back to reflection-based mapping.
Startup validation also catches structural problems — such as target types with no accessible parameterless constructor — and surfaces them as a structured MappingProfileValidationException with an Errors collection before the application starts serving traffic.
Type Converters
Register custom converters for complex domain transformations that go beyond Convert.ChangeType.
Built-in converters
TypeConverters provides ready-made converters for common patterns:
using KObjectMapper.Converters;
// string → int, long, double, decimal, bool, Guid, DateTime, DateTimeOffset
// string → enum (with optional case-insensitive parsing)
// int → enum
TypeConverters.StringToInt32
TypeConverters.StringToGuid
TypeConverters.StringToDateTimeOffset
TypeConverters.StringToEnum<MyEnum>()
TypeConverters.Int32ToEnum<MyEnum>()
Custom converters
Implement ITypeConverter<TSource, TTarget>:
using KObjectMapper.Abstractions;
public class MoneyConverter : ITypeConverter<decimal, string>
{
public string Convert(decimal source) => source.ToString("C2");
}
Register globally or per map:
// Global — applies to all maps
options.AddConverter<decimal, string>(new MoneyConverter());
// Per map — takes precedence over global
CreateMap<Invoice, InvoiceDto>()
.AddConverter<decimal, string>(new MoneyConverter());
Enum Conversion Safety
Use EnumConverter for safe, validated enum conversions that surface failures explicitly instead of silently corrupting data.
using KObjectMapper.Converters;
// String → enum (case-insensitive)
var converter = EnumConverter.FromString<Status>(ignoreCase: true);
// Int → enum
var converter = EnumConverter.FromInt32<Status>();
// Wrap as ITypeConverter and register
CreateMap<OrderDto, Order>()
.AddConverter<string, Status>(
EnumConverter.FromString<Status>(ignoreCase: true).AsTypeConverter());
AsTypeConverter() throws InvalidOperationException with a descriptive message when the source value cannot be mapped to a valid enum member. Use EnumConverter directly when you need the structured EnumConversionResult<TEnum> (with IsSuccess, Value, and Error properties) for non-throwing error handling.
Structured Mapping Results
Use the non-throwing TryMap API to get a structured result instead of an exception on mapping failure.
MappingResult result = mapper.TryMap(source, target);
if (!result.IsSuccess)
{
foreach (MappingError error in result.Errors)
Console.WriteLine($"{error.MemberPath}: {error.Reason}");
}
Each MappingError includes MemberPath, SourceType, TargetType, and Reason.
Observability hooks
Register callbacks for logging and metrics without coupling your mapping code to a specific logging framework:
builder.Services.AddKObjectMapper(options =>
{
options.WithOnMappingError(err => logger.LogError("Mapping failed: {Reason}", err.Reason));
options.WithOnMappingCompleted(result => metrics.Record(result));
});
Collection Mapping Strategies
The collection Map overload supports three merge modes via CollectionMappingOptions.
using KObjectMapper.Collections;
// Replace (default) — replaces target collection with source items
mapper.Map<Source, Target>(sourceList, targetList);
// Append — adds source items after existing target items
var options = new CollectionMappingOptions<Source, Target>()
.WithMergeMode(CollectionMergeMode.Append);
mapper.Map(sourceList, targetList, options);
// MergeByKey — add/update/remove by key selector
var options = new CollectionMappingOptions<Source, Target>()
.WithMergeMode(CollectionMergeMode.MergeByKey)
.WithKeySelector(src => src.Id, tgt => tgt.Id);
mapper.Map(sourceList, targetList, options);
| Mode | Behaviour |
|---|---|
Replace |
Target collection is replaced by source items (default) |
Append |
Source items are appended after existing target items |
MergeByKey |
Matched items are updated; unmatched target items are removed; new source items are added |
MergeByKey throws InvalidOperationException when key selectors are not configured.
Queryable Projection
Project IQueryable<TSource> to IQueryable<TTarget> using EF Core translation-friendly Expression<Func<TSource, TTarget>> expressions built via Expression.MemberInit.
using KObjectMapper.Abstractions;
// Project a queryable (e.g. EF Core DbSet)
IQueryable<CustomerDto> query = mapper.ProjectTo<Customer, CustomerDto>(dbContext.Customers);
// Compose with LINQ
var results = await query.Where(d => d.IsActive).ToListAsync();
// Retrieve the raw expression for manual use
Expression<Func<Customer, CustomerDto>> expr =
mapper.GetProjectionExpression<Customer, CustomerDto>();
Expressions are cached per type pair. A ProjectionException (with SourceType and TargetType context) is thrown when no mappable properties exist or the source is null.
Nested Graph and Circular Reference Handling
Map deep object graphs safely with configurable reference preservation and depth limits.
using KObjectMapper.Configuration;
var options = new GraphMappingOptions()
.WithReferencePreservation() // reuse already-mapped target instances
.WithMaxDepth(32); // throw InvalidOperationException beyond this depth
mapper.Map(source, target, options);
- Circular reference detection — objects already in the visited set are not re-entered, preventing
StackOverflowException. - Reference preservation — when the same source instance appears multiple times in the graph, the same target instance is reused.
- MaxDepth — defaults to 64; throws
InvalidOperationExceptionwhen exceeded.
Async Mapping and Cancellation
Use IAsyncObjectMapper for async pipelines with CancellationToken support.
using KObjectMapper.Abstractions;
IAsyncObjectMapper asyncMapper = serviceProvider.GetRequiredService<IAsyncObjectMapper>();
// Async map (throws OperationCanceledException if token is already cancelled)
await asyncMapper.MapAsync(source, target, cancellationToken);
// Non-throwing async variant
MappingResult result = await asyncMapper.TryMapAsync(source, target, cancellationToken);
Custom async converters
Implement IAsyncTypeConverter<TSource, TTarget> for expensive async transformations:
public class PriceLookupConverter : IAsyncTypeConverter<ProductId, ProductDto>
{
public async Task<ProductDto> ConvertAsync(ProductId source, CancellationToken ct)
{
var price = await _priceService.GetAsync(source.Id, ct);
return new ProductDto { Price = price };
}
}
// Register
builder.Services.AddKObjectMapper(options =>
{
options.AddAsyncConverter<ProductId, ProductDto>(new PriceLookupConverter(_priceService));
});
IAsyncObjectMapper is registered as a singleton in the DI container automatically when AddKObjectMapper is called.
Sensitive Data Guards
Protect sensitive properties from being mapped by annotating them with [Sensitive] and configuring a SensitiveMappingPolicy.
[Sensitive] on a property
using KObjectMapper.Configuration;
public class UserDto
{
public string Username { get; set; }
[Sensitive]
public string PasswordHash { get; set; }
[Sensitive]
public string SocialSecurityNumber { get; set; }
}
[Sensitive] on a class
Marking an entire class [Sensitive] excludes all its properties from mapping:
[Sensitive]
public class AuditRecord
{
public string Actor { get; set; }
public string Action { get; set; }
}
SensitiveMappingPolicy.ExcludeMarked (default)
Only members explicitly annotated with [Sensitive] are excluded. All other members are mapped normally.
builder.Services.AddKObjectMapper(options =>
{
options.SetSensitivePolicy(SensitiveMappingPolicy.ExcludeMarked);
options.AddProfile<UserProfile>();
});
SensitiveMappingPolicy.DefaultDeny
All members are excluded unless explicitly allowed with AllowMember. Use this for high-security contexts where you want an opt-in allowlist.
builder.Services.AddKObjectMapper(options =>
{
options.SetSensitivePolicy(SensitiveMappingPolicy.DefaultDeny);
options.AddProfile<UserProfile>();
});
AllowMember for DefaultDeny mode
public class UserProfile : MappingProfile
{
protected override void Configure()
{
CreateMap<User, UserDto>()
.AllowMember(tgt => tgt.Username)
.AllowMember(tgt => tgt.Email);
// PasswordHash and all other members remain excluded
}
}
Per-map sensitive policy
Override the global policy for a specific map:
CreateMap<User, PublicUserDto>()
.SetSensitivePolicy(SensitiveMappingPolicy.ExcludeMarked);
Constructor-Based and Immutable Mapping
Map into immutable types, C# records, and init-only properties by enabling constructor parameter matching.
// Per map
public class OrderProfile : MappingProfile
{
protected override void Configure()
{
CreateMap<OrderSource, OrderRecord>().AllowConstructorMapping();
}
}
// Or globally
builder.Services.AddKObjectMapper(options =>
{
options.AllowConstructorMapping();
options.AddProfile<OrderProfile>();
});
When enabled, the mapper selects the constructor with the most parameters, matches each parameter by name (case-insensitive) to a source property, and uses default(T) for any unmatched parameter. This supports:
- C# records —
record Person(string FirstName, string LastName, int Age) - Value objects — types with a parameterized constructor and read-only properties
init-only properties — set viaPropertyInfo.SetValue, which bypasses the compile-time restriction
Use mapper.Map<TSource, TTarget>(source) (single-argument overload) to get a newly constructed target instance.
Private Setter Support
Map into DDD entities and aggregates that encapsulate state via private setters by opting in with AllowPrivateSetters().
// Globally
builder.Services.AddKObjectMapper(options =>
{
options.AllowPrivateSetters();
options.AddProfile<OrderProfile>();
});
// Per map
CreateMap<OrderSource, Order>().AllowPrivateSetters();
When opted in, PropertyInfo.SetValue is used to bypass access modifiers at runtime — no additional reflection overhead. When not opted in, private-setter properties are silently skipped and the target retains its default value.
// DDD entity — private setters populated by the mapper when opted in
public class Order
{
public Guid Id { get; private set; }
public string Status { get; private set; } = string.Empty;
public decimal Total { get; private set; }
}
Failure Modes and Edge Cases
| Scenario | Behaviour |
|---|---|
| Strict mode on, no map registered for a type pair | InvalidOperationException thrown at the call site |
Source is null with NullMappingPolicy.Propagate (default) |
null is written to the target member |
Source is null with NullMappingPolicy.Ignore |
Target member retains its existing value |
| Target type has no accessible parameterless constructor | MappingProfileValidationException thrown at startup validation before the app serves traffic |
[Sensitive] on a class |
All properties of that class are excluded from mapping |
MergeByKey used without key selectors configured |
InvalidOperationException thrown when the map is executed |
ProjectTo / GetProjectionExpression with no mappable properties |
ProjectionException thrown with SourceType and TargetType context |
MaxDepth exceeded during graph mapping |
InvalidOperationException thrown at the depth-limit node |
MapAsync called with an already-cancelled CancellationToken |
OperationCanceledException thrown immediately |
AllowConstructorMapping() enabled but target has no parameterized constructor |
Falls back to normal property mapping |
AllowPrivateSetters() not enabled, target has only private setters |
All properties silently skipped; target remains at defaults |
Source Generator
KObjectMapper includes an optional Roslyn source generator that produces static mapper classes at compile time, eliminating reflection overhead on hot paths.
Setup
Add an <Analyzer> reference to KObjectMapper.SourceGenerator in your project file:
<ItemGroup>
<Analyzer Include="path/to/KObjectMapper.SourceGenerator.dll" />
</ItemGroup>
Enabling source generation
Enable globally via DI registration:
builder.Services.AddKObjectMapper(options =>
{
options.EnableSourceGeneration();
options.AddProfile<CustomerProfile>();
});
Enable per map inside a profile:
CreateMap<Customer, CustomerDto>().UseSourceGeneration();
When generation is unavailable for a type pair, the runtime mapper falls back to the reflection pipeline automatically.
Constraints
- Only public, non-static, non-indexer properties with matching names are mapped.
- Source and target property types must be directly assignable.
- Unsupported members emit
KOM001orKOM002warnings at build time and are excluded from the generated mapper.
Troubleshooting
To view the generated files on disk, see docs/specs/GeneratorDebugging.md.
| Diagnostic | Meaning |
|---|---|
KOM001 |
A source property has no matching property on the target type and will not be mapped. |
KOM002 |
A property exists on both types but the source type is not assignable to the target type. |
Dependency Injection — Full Example
using KObjectMapper.DependencyInjection;
using KObjectMapper.Configuration;
using KObjectMapper.Converters;
builder.Services.AddKObjectMapper(options =>
{
options.EnableStrictMode();
options.SetGlobalNullPolicy(NullMappingPolicy.Ignore);
options.SetSensitivePolicy(SensitiveMappingPolicy.ExcludeMarked);
options.AddConverter<string, Status>(
EnumConverter.FromString<Status>(ignoreCase: true).AsTypeConverter());
options.EnableSourceGeneration();
options.ConfigureGraph(g => g.WithReferencePreservation().WithMaxDepth(32));
options.AllowConstructorMapping();
options.AllowPrivateSetters();
options.WithOnMappingError(err => logger.LogError("Mapping failed: {Reason}", err.Reason));
options.AddProfilesFromAssembly(typeof(CustomerProfile).Assembly);
});
Namespace Reference
| Namespace | Contents |
|---|---|
KObjectMapper |
Mapper, AsyncMapper |
KObjectMapper.Abstractions |
IObjectMapper, IAsyncObjectMapper, ITypeConverter<,>, IAsyncTypeConverter<,>, IQueryableMapper |
KObjectMapper.Collections |
CollectionMergeMode, CollectionMappingOptions<,> |
KObjectMapper.Configuration |
MappingProfile, MappingProfileOptions, MappingTypeMapConfiguration, NullMappingPolicy, GraphMappingOptions, MappingProfileValidationException |
KObjectMapper.Converters |
TypeConverters, EnumConverter |
KObjectMapper.DependencyInjection |
ServiceCollectionExtensions (AddKObjectMapper) |
KObjectMapper.Extensions |
Implicit mapping extension methods |
KObjectMapper.Projections |
ProjectionException |
KObjectMapper.Security |
SensitiveAttribute, SensitiveMappingPolicy, SensitiveDataGuard |
Please see the contributing guide for project status and contribution policy.
| Product | Versions 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. |
-
net8.0
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 | |
|---|---|---|---|
| 0.0.0-alpha-2 | 165 | 7/25/2026 | |
| 0.0.0-alpha-1 | 72 | 7/23/2026 | |
| 0.0.0-0.0.1.20230123120554 | 251 | 1/23/2023 | |
| 0.0.0-0.0.1.20230123120546 | 228 | 1/23/2023 | |
| 0.0.0-0.0.1.20230123120518 | 227 | 1/23/2023 | |
| 0.0.0-0.0.1.20230123120511 | 229 | 1/23/2023 | |
| 0.0.0-0.0.1.20230123120503 | 226 | 1/23/2023 | |
| 0.0.0-0.0.1.20230123120459 | 219 | 1/23/2023 | |
| 0.0.0-0.0.1.20230122082047 | 231 | 1/22/2023 | |
| 0.0.0-0.0.1.20230122082038 | 224 | 1/22/2023 | |
| 0.0.0-0.0.1.20230122081512 | 225 | 1/22/2023 | |
| 0.0.0-0.0.1.20230122081506 | 220 | 1/22/2023 | |
| 0.0.0-0.0.1.20230122081302 | 225 | 1/22/2023 | |
| 0.0.0-0.0.1.20230122081254 | 218 | 1/22/2023 | |
| 0.0.0-0.0.1.20230122080704 | 223 | 1/22/2023 | |
| 0.0.0-0.0.1.20230122080656 | 227 | 1/22/2023 | |
| 0.0.0-0.0.1.20230122080652 | 229 | 1/22/2023 | |
| 0.0.0-0.0.1.20230122080644 | 227 | 1/22/2023 | |
| 0.0.0-0.0.1.20230122080401 | 222 | 1/22/2023 | |
| 0.0.0-0.0.1.20230122080354 | 224 | 1/22/2023 |