Majipro.Converter 2.1.0

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

Converter

Library for manual class mapping. This library helps you with type mapping using dependency-injected services (Converters).

What it is

This library helps you to build dependency-injected services that can perform object mapping. We call these services "Converters" because they are performing object conversion from one type to another. The fundamental idea was to build some easy manual mapping tool that will enforce a normalized way, how mapping is done. This tool is an alternative to a common solution for manual mapping that uses static methods. Since each mapper (we call it Converter to avoid confusion with Automapper) is an independent dependency-injected service, you can use full advantage of that (or you don't have to if you consider that as a disadvantage) — so you can mock converters in tests, or you can leverage them by injecting other services.

How to install it

dotnet add package Majipro.Converter

One package is the whole setup. It carries the library, the abstractions and the source generator (as a Roslyn analyzer), so there is no second package to reference and no <Analyzer> to write by hand.

Contents

How to use it

Read the sections below to get familiar with the basic usage of this library.

How to register it

Register converters for assembly, where MyClass is:

builder.Services.AddConverting<MyClass>();

Register converters for assembly, where MyClass and YourClass is:

builder.Services.AddConverting(typeof(MyClass).Assembly, typeof(YourClass).Assembly);

How to configure it

You can use configureOptions when you calling AddConverting or AddConverting<T>, such as:

builder.Services.AddConverting<MyClass>(options => 
{
    options.FormatProvider = new NumberFormatInfo();
    options.NumberStyles = NumberStyles.AllowThousands;
    options.ServiceLifetime = ServiceLifetime.Scoped;
    // And more
});

How to create your own converter

Consider you want to convert instance database entity Entity:

public sealed record Entity
{
    public string Firstname { get; init; }
    public string Lastname { get; init; }
}

To newly created instance domain instance of Domain

public sealed record Domain
{
    public string Firstname { get; init; }
    public string Lastname { get; init; }
}

For this you have to create converting service, such as EntityToDomainConverter:

internal sealed class EntityToDomainConverter : IConverter<Entity, Domain>
{
    public Domain Convert(Entity from)
    {
        if (from == null) // Or you can just relay on anotations 
        {
            throw new ArgumentNullException(nameof(from)); // Or do whatever 
        }
        
        return new Domain
        {
            Firstname = from.Firstname,
            Lastname = from.Lastname
        }
    }
}

If you have a such converter in the place, and you have the converting library registered using AddConverting, you can now just call conversion:

public sealed class BusinessLogicService
{
    private readonly IConvertingService _convertingService;
    
    public BusinessLogicService(IConvertingService convertingService)
    {
        _convertingService = convertingService;
    }
    
    public Domain GetDomains()
    {
        var entity = StaticHelper.GetEntities();
        
        // This call will call your IConverter<Entity, Domain> converter
        var domain = _convertingService.Convert<Entity, Domain>(entity);
        
        return domain;
    }
}

How to customize

You can configure library behaviour globally using the ConvertOptions parameter during service registration such as:

builder.Services.AddConverting<MyClass>(options => 
{
    options.FormatProvider = new NumberFormatInfo();
    options.NumberStyles = NumberStyles.AllowThousands;
    options.ServiceLifetime = ServiceLifetime.Scoped;
    // And more
});

This setting is available to you by IConverterOptions singleton service, that you can inject into other dependency-injected services, including Converters:

internal sealed class EntityToDomainConverter : IConverter<Entity, Domain>
{
    private readonly IConverterOptions _converterOptions;

    public EntityToDomainConverter(IConverterOptions converterOptions)
    {
        _converterOptions = converterOptions;
    }
    
    public Domain Convert(Entity from)
    {
        throw new NotImplementedException();
    }
}

Build in Converters

Available build in converters

Library offers build in Converters that you can use using standard API such as _convertingService.Convert<TFrom, TTo>(). Lifetime of these converters is fixed and it is always Singleton. See the list of build in conversions:

From To Options
string bool
string byte
string int
string long
string DateTime FormatProvider, DateTimeStyles
string DateTimeOffset FormatProvider, DateTimeStyles
string decimal FormatProvider, NumberStyles
string double FormatProvider, NumberStyles
string float FormatProvider, NumberStyles

How to override

It is recommended to override default converters by manually registering them before you call AddConverting, since AddConverting uses TryAdd registering internally:

builder.Services.AddSingleton<IConverter<string, bool>, MyStringToBoolConverter>(); // Your overriden converter
builder.Services.AddConverting<MyClass>();

Reference Converters

A Converter creates a new instance. A Reference Converter fills one you already hold — an entity your change tracker knows about, for instance — so its identity and everything the Converter does not touch survive the conversion. Implement IReferenceConverter<TFrom, TTo>, which extends IConverter<TFrom, TTo>, so one class carries both conversions:

internal sealed class DomainToEntityReferenceConverter : IReferenceConverter<Domain, Entity>
{
    public Entity Convert(Domain from)
    {
        return Convert(from, new Entity());
    }

    public Entity Convert(Domain from, Entity to)
    {
        if (from == null) // There is nothing to enhance the instance with
        {
            return to;
        }

        to.Firstname = from.Firstname;
        to.Lastname = from.Lastname;

        return to;
    }
}

Call it through the two argument overload:

var entity = await _repository.GetAsync(id);

// This call will call your IReferenceConverter<Domain, Entity> converter
entity = _convertingService.Convert<Domain, Entity>(domain, entity);

Because IReferenceConverter<TFrom, TTo> is an IConverter<TFrom, TTo>, registering it covers both calls and does not count as a duplicate registration of the pair.

Collections and dictionaries

IConvertingService converts collections for you. The item Converter is resolved once and applied to every element, so you only ever write the Converter for a single item:

IEnumerable<Domain> domains = _convertingService.Convert<Entity, Domain>(entities);
IList<Domain> list = _convertingService.Convert<Entity, Domain>(entityList);
ISet<Domain> set = _convertingService.Convert<Entity, Domain>(entitySet);

// Dictionaries convert the key and the value, each with its own Converter
IDictionary<Guid, Domain> map = _convertingService.Convert<string, Entity, Guid, Domain>(entityMap);

A null source returns null. Every overload has a nullFallback counterpart if you want something else:

IList<Domain> never = _convertingService.Convert<Entity, Domain>(entityList, () => new List<Domain>());

Getting a concrete type back

Convert returns the interface you asked for. ConvertExplicitly extension methods return the concrete type, which is what you usually want at the edge of a method:

List<Domain> list = _convertingService.ConvertExplicitly<Entity, Domain>(entityList);
Domain[] array = _convertingService.ConvertExplicitly<Entity, Domain>(entityArray);
Dictionary<string, Domain> map = _convertingService.ConvertExplicitly<Entity, Domain>(entityMap);
IReadOnlyList<Domain> readOnly = _convertingService.ConvertExplicitly<Entity, Domain>(readOnlyEntities);

List<T>, T[], IReadOnlyList<T>, Dictionary<TKey, T> and IReadOnlyDictionary<TKey, T> are covered, dictionary keys being string, int or Guid.

Async conversions

Every Convert method on IConvertingService has an async counterpart ConvertAsync that returns ValueTask<T> and accepts an optional CancellationToken. This applies to single-object, reference, collection (ISet, IList, IEnumerable), and dictionary conversions.

How to create an async converter

Implement IAsyncConverter<TFrom, TTo> instead of IConverter<TFrom, TTo>:

internal sealed class EntityToDomainAsyncConverter : IAsyncConverter<Entity, Domain>
{
    private readonly IExternalService _externalService;

    public EntityToDomainAsyncConverter(IExternalService externalService)
    {
        _externalService = externalService;
    }

    public async Task<Domain> ConvertAsync(Entity from, CancellationToken cancellationToken)
    {
        var additionalData = await _externalService.GetDataAsync(from.Id, cancellationToken);

        return new Domain
        {
            Firstname = from.Firstname,
            Lastname = from.Lastname,
            AdditionalData = additionalData
        };
    }
}

How to create an async reference converter

Implement IAsyncReferenceConverter<TFrom, TTo> instead of IReferenceConverter<TFrom, TTo>:

internal sealed class EntityToDomainAsyncReferenceConverter : IAsyncReferenceConverter<Entity, Domain>
{
    public async Task<Domain> ConvertAsync(Entity from, CancellationToken cancellationToken)
    {
        // Required by IAsyncConverter<TFrom, TTo> (base interface)
        throw new NotSupportedException();
    }

    public async Task<Domain> ConvertAsync(Entity from, Domain to, CancellationToken cancellationToken)
    {
        to.Firstname = from.Firstname;
        to.Lastname = from.Lastname;
        return to;
    }
}

How to call async conversions

Use ConvertAsync instead of Convert:

public sealed class BusinessLogicService
{
    private readonly IConvertingService _convertingService;

    public BusinessLogicService(IConvertingService convertingService)
    {
        _convertingService = convertingService;
    }

    public async Task<Domain> GetDomainAsync(CancellationToken cancellationToken)
    {
        var entity = await GetEntityAsync();

        // Single object
        var domain = await _convertingService.ConvertAsync<Entity, Domain>(entity, cancellationToken);

        return domain;
    }

    public async Task<IList<Domain>> GetDomainsAsync(CancellationToken cancellationToken)
    {
        var entities = await GetEntitiesAsync();

        // Collection — converts each item using the converter
        var domains = await _convertingService.ConvertAsync<Entity, Domain>(entities, cancellationToken);

        return domains;
    }
}

Sync and async interoperability

Sync and async converters are interoperable:

  • If you call Convert (sync) and only an IAsyncConverter is registered, the library will block on the async converter.
  • If you call ConvertAsync and only an IConverter (sync) is registered, the sync result is wrapped in a ValueTask.

This means you can migrate converters from sync to async (or vice versa) without changing the calling code.

Generated Converters

You do not have to write a Converter for every pair. The package ships a source generator that writes the obvious ones for you at compile time, and AddConverting picks them up the same way it picks up yours — they are ordinary classes in the assembly being compiled.

How it works

The generator looks at your Convert<TFrom, TTo> / ConvertAsync<TFrom, TTo> call sites. Every pair it finds that nobody wrote a Converter for gets one generated, in namespace Majipro.Converter.Generated:

// You write this call, and nothing else
var domain = _convertingService.Convert<Entity, Domain>(entity);
// The generator writes this
public sealed class EntityToDomainConverter : IReferenceConverter<Entity, Domain>
{
    public Domain Convert(Entity from)
    {
        if (from == null)
        {
            return default;
        }

        return new Domain
        {
            Firstname = from.Firstname,
            Lastname = from.Lastname
        };
    }

    public Domain Convert(Entity from, Domain to) { /* the same values, assigned to to */ }
}

Generation is recursive: a property whose type is itself a mappable pair is converted through IConvertingService, and that nested pair gets a generated Converter too.

What it can convert

Shape Example Notes
Property of the same type string to string Also implicit conversions — Guid to Guid?, int to long, differing nullable annotations
Nested objects Address to AddressDto Converted through IConvertingService, the nested pair is generated as well
Closed generic types Wrapper<Guid, Person> to Wrapper<Guid, PersonDto> Mapped like any other class. The type arguments have to be types — the same wrapper closed over a type parameter of the method calling Convert is skipped, see below
A pair the compiler already takes IProducer<Dog> to IProducer<Animal> (out T), IConsumer<Animal> to IConsumer<Dog> (in T), int to long, int to int? Variance, an implicit reference or numeric conversion, an implicit operator of your own — the Converter hands the value back. A pair that can also be mapped property by property is mapped into a new instance instead
Collections List<Address> to List<AddressDto> Source is any IEnumerable<T>, target is IEnumerable<>, ICollection<>, IList<>, IReadOnlyCollection<>, IReadOnlyList<> or List<>
Scalar to its text int to string, DateTime to string Written as System.Convert.ToString
Text to a scalar string to int Only for the build in Converters listed above, so FormatProvider and the other options apply
Enum to enum EntityState to DomainState Matched by member name, so the two sides may number their members differently
Enum to string EntityState to string
Nullable<T> on either side int? to string Unwrapped, the Converter still takes and returns the nullable type

Both conversions are generated wherever they make sense — Convert(from) creating a new instance and Convert(from, to) filling one you already hold (see Reference converters). The second one is left out when the target has init only properties or is a value type, since there is no instance to enhance in that case.

What it refuses

The generator never guesses. When it recognizes a pair but can not write it deterministically, it fails the build with an error instead of generating a Converter that silently loses data:

Diagnostic Means
MC0002 An enum member on the source has no counterpart of that name on the target
MC0003 A writable target property has nothing to fill it — the source has no property of that name, or the two types are not convertible
MC0004 The pair itself can not be written, and no Converter for it is implemented in this assembly or in any assembly it references — the Convert call would compile and throw at runtime, so it fails the build instead
MC0001 Anything else that went wrong while generating

A pair you ask for always ends one of three ways, and never in silence: a Converter is generated for it, one you wrote or one the library ships answers it, or the build fails. The two exceptions are a pair whose sides are the same type, which IConvertingService handles by itself, and a call site whose type is a generic type parameter, which is not a pair until the method is used.

One shape ends on MC0003 for a reason that is not really about the property it names: a generic type constructed over itself, a Node<T> whose property is a Node<Node<T>>, asks for a bigger Converter without end. Names whose type arguments nest deeper than eight are not mapped, so such a property is reported as one nothing fills rather than being generated forever.

Where it looks for your Converters

Across assemblies, which is what makes MC0004 a statement about the pair rather than about the project it was asked for in. The generator looks for implementations — the same thing AddConverting looks for at runtime — in the assembly being compiled and in every referenced assembly that references this library. So the ordinary shape, Converters in a class library and Convert calls in an application, is seen for what it is: the pair is implemented, nothing is generated for it, and nothing is reported about it.

That cuts both ways, and the second half is the one to know: nothing is generated for a pair that is already implemented, wherever it was implemented. The assembly holding those Converters is therefore one you have to hand to AddConverting — the generated assembly carries nothing for that pair:

builder.Services.AddConverting(typeof(EntityToDomainConverter).Assembly, typeof(Program).Assembly);

Two shapes stay outside of this, and neither is found by a type scan at runtime either, so MC0004 can name a pair you consider answered:

Shape Why it is not seen What to do
A Converter registered as a factory or an instance — AddSingleton<IConverter<int, long>>(sp => ...) There is no class implementing the interface to find Write it as a class, or suppress the pair by writing a Converter that delegates to it
A Converter that is internal to a referenced assembly The compiler does not show a referenced assembly's internals to this compilation, while that assembly's own AddConverting registers it at runtime Make it public, or move it into the assembly holding the call site

How to take over

Write the Converter yourself, in this assembly or in any assembly it references. A pair you implemented — IConverter<TFrom, TTo>, IAsyncConverter<TFrom, TTo> or a Reference Converter extending either — is skipped by the generator entirely, so the two never collide. That is the answer to every refusal: MC0002 and MC0003 are the shapes only you can decide, and MC0004 is the pair nobody has written yet.

Options reference

Option Default Applies to
ServiceLifetime Singleton The lifetime every Converter found by AddConverting is registered with. Build in Converters are always Singleton
FormatProvider CultureInfo.InvariantCulture string to number, DateTime and DateTimeOffset
NumberStyles NumberStyles.Any string to decimal, double, float
DateTimeStyles DateTimeStyles.None string to DateTime, DateTimeOffset

The options are one singleton IConverterOptions, so a Converter of yours can inject it and read the same settings the build in ones read.

Advanced scenarios

Inject IConvert<TFrom, TTo> directly

Since Converters are dependency-injected services, it is possible to inject them independently without using IConvertingService

public sealed class BusinessLogicService
{
    private readonly IConverter<Entity, Domain> _converter;
    
    public BusinessLogicService(IConverter<Entity, Domain> converter)
    {
        _converter = converter;
    }
    
    public Domain GetDomains()
    {
        var entity = StaticHelper.GetEntities();
        
        // This call will call your IConverter<Entity, Domain> converter
        var domain = _converter.Convert<Entity, Domain>(entity);
        
        return domain;
    }
}

IConvertingService overriding

It is also possible to override IConvertingService in the same way how you can override default Converter, if the provided implementation does not fit to your needs:

builder.Services.AddSingleton<IConvertingService, MyConvertingService>(); // Your overriden converting service
builder.Services.AddConverting<MyClass>();

Troubleshooting

What you see What it means
There is no conversion from 'X' to 'Y' at runtime Nothing is registered for the pair. The generator reports the pairs it can see as MC0004 at build time, so what is left is what it can not see: the assembly holding the Converter was not passed to AddConverting, or the call site is generic (a type parameter is not a pair until the method is used)
Duplicate converter registrations found at startup Two classes claim the same From -> To pair. A Reference Converter and the IConverter it extends are one registration and are not this
MC0002, MC0003, MC0004 at build time The generator refuses the pair, see What it refuses. Write the Converter yourself, as a class — in this assembly or in one it references
The generated Converter is not found at runtime It is generated into the assembly holding the call site, so that assembly has to be one of the ones you hand to AddConverting
A hand written Converter is duplicated by a generated one The Converter is internal to a referenced assembly, so the compilation it is generated into can not see it. Make it public, or move it next to the call site

About the AI-generated parts

This library is written with the help of Claude Code, and it is worth saying plainly which parts:

  • Converter.Generator (the source generator) is largely AI-generated, together with its test suite in Converter.Generator.Test and the architecture notes in CLAUDE.md. The design decisions are reviewed and owned by a human; a lot of the code writing them down is not hand typed.
  • The runtime library (Converter, Converter.Abstrations) is mostly hand written, with AI help in the build in Converters, tests and packaging.

Everything is reviewed before it is merged and covered by tests, but treat that as context when reading the code or reporting a bug.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  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. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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
2.1.0 101 9/30/2026
2.0.0 82 9/24/2026
1.3.2 1,129 3/30/2026
1.3.1 137 3/22/2026
1.3.0 103 3/19/2026
1.1.0 468 6/26/2025
1.0.1-extnsions.1 45 6/22/2025
1.0.0 204 6/16/2025