KObjectMapper 0.0.0-alpha-2

This is a prerelease version of KObjectMapper.
dotnet add package KObjectMapper --version 0.0.0-alpha-2
                    
NuGet\Install-Package KObjectMapper -Version 0.0.0-alpha-2
                    
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="KObjectMapper" Version="0.0.0-alpha-2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="KObjectMapper" Version="0.0.0-alpha-2" />
                    
Directory.Packages.props
<PackageReference Include="KObjectMapper" />
                    
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 KObjectMapper --version 0.0.0-alpha-2
                    
#r "nuget: KObjectMapper, 0.0.0-alpha-2"
                    
#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 KObjectMapper@0.0.0-alpha-2
                    
#: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=KObjectMapper&version=0.0.0-alpha-2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=KObjectMapper&version=0.0.0-alpha-2&prerelease
                    
Install as a Cake Tool

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 InvalidOperationException when 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# recordsrecord Person(string FirstName, string LastName, int Age)
  • Value objects — types with a parameterized constructor and read-only properties
  • init-only properties — set via PropertyInfo.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 KOM001 or KOM002 warnings 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 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
0.0.0-alpha-2 165 7/25/2026
0.0.0-alpha-1 72 7/23/2026 0.0.0-alpha-1 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230123120554 251 1/23/2023 0.0.0-0.0.1.20230123120554 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230123120546 228 1/23/2023 0.0.0-0.0.1.20230123120546 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230123120518 227 1/23/2023 0.0.0-0.0.1.20230123120518 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230123120511 229 1/23/2023 0.0.0-0.0.1.20230123120511 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230123120503 226 1/23/2023 0.0.0-0.0.1.20230123120503 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230123120459 219 1/23/2023 0.0.0-0.0.1.20230123120459 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230122082047 231 1/22/2023 0.0.0-0.0.1.20230122082047 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230122082038 224 1/22/2023 0.0.0-0.0.1.20230122082038 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230122081512 225 1/22/2023 0.0.0-0.0.1.20230122081512 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230122081506 220 1/22/2023 0.0.0-0.0.1.20230122081506 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230122081302 225 1/22/2023 0.0.0-0.0.1.20230122081302 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230122081254 218 1/22/2023 0.0.0-0.0.1.20230122081254 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230122080704 223 1/22/2023 0.0.0-0.0.1.20230122080704 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230122080656 227 1/22/2023 0.0.0-0.0.1.20230122080656 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230122080652 229 1/22/2023 0.0.0-0.0.1.20230122080652 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230122080644 227 1/22/2023 0.0.0-0.0.1.20230122080644 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230122080401 222 1/22/2023 0.0.0-0.0.1.20230122080401 is deprecated because it is no longer maintained and has critical bugs.
0.0.0-0.0.1.20230122080354 224 1/22/2023 0.0.0-0.0.1.20230122080354 is deprecated because it is no longer maintained and has critical bugs.
Loading failed