Talk2Bits.MappingGenerator 0.0.0-alpha.0.29

This is a prerelease version of Talk2Bits.MappingGenerator.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Talk2Bits.MappingGenerator --version 0.0.0-alpha.0.29
NuGet\Install-Package Talk2Bits.MappingGenerator -Version 0.0.0-alpha.0.29
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="Talk2Bits.MappingGenerator" Version="0.0.0-alpha.0.29" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Talk2Bits.MappingGenerator --version 0.0.0-alpha.0.29
#r "nuget: Talk2Bits.MappingGenerator, 0.0.0-alpha.0.29"
#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.
// Install Talk2Bits.MappingGenerator as a Cake Addin
#addin nuget:?package=Talk2Bits.MappingGenerator&version=0.0.0-alpha.0.29&prerelease

// Install Talk2Bits.MappingGenerator as a Cake Tool
#tool nuget:?package=Talk2Bits.MappingGenerator&version=0.0.0-alpha.0.29&prerelease

MappingGenerator

MappingGenerator is C# source generator that allows generating object mapping code on compilation stage.

Having source code for your mappings generated provides the following benefits:

  • Your mappings are always up to date.
  • You see code for all your mappings. Nothing is hidden.
  • All debugging features are available. You can step-in to your mappings, set breakpoints etc.
  • If mapping can't be done or has issues you get compiler errors rather than runtime errors.

Table of contents

How do I get started?

Install Talk2Bits.MappingGenerator nuget package.

Define source and destination:

public class Source
{
    public int Number { get; set; }

    public string Text { get; set; }

    public long BigNumber { get; set; }
}

public class Destination
{
    public int Number { get; set; }

    public string Text { get; set; }

    public long BigNumber { get; set; }
}

Define mapper class:

[MappingGenerator(typeof(Source), typeof(Destination))]
public partial class Mapper
{ }

Rebuild your project

Use generated mapping:

var source = new Source();
var mapper = new Mapper();
var result = mapper.Map(source);

Features

Basic mapping

public class Source
{
    public int Number { get; set; }

    public string Text { get; set; }

    public long BigNumber { get; set; }
}

public class Destination
{
    public int Number { get; set; }

    public string Text { get; set; }

    public long BigNumber { get; set; }
}

[MappingGenerator(typeof(Source), typeof(Destination))]
public partial class Mapper
{ }

Generated code (removed redundant parts and added comments for clarity):

partial class Mapper : IMapper<Source, Destination>
{
    public Mapper()
    {
    }

    private Destination CreateDestination(Source source)
    {
        return new Destination()
        {};
    }

    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        var result = CreateDestination(source);

        // Properties are matched by name and type.
        result.Number = source.Number;
        result.Text = source.Text;

        // Explicit cast generated.
        result.BigNumber = (int)source.BigNumber;
        AfterMap(source, result);
        return result;
    }

    partial void AfterMap(Source source, Destination result);
}

Keeping eye on missing mappings

You can control how MappingGenerator behaves if it was not able to map everything in destination object with MissingMappingBehavior parameter. Options are:

  • Warning (Default). Produce compilation warning.
  • Ignore. Do nothing.
  • Error. Produce compilation error.

For example the following code will produce compilation error because it can't find source for B.Val:


public class A {}

public class B 
{ 
    public string Val { get; set; }
}

[MappingGenerator(typeof(A), typeof(B), MissingMappingBehavior = MissingMappingBehavior.Error)]
public partial class Mapper 
{
}

Compilation error:

Mapping generator 'Mapper': Failed to resolve mapping for type 'B' property 'Val'.

Ignore destination property

public class Source
{
    public int Number { get; set; }

    public string Text { get; set; }

    public long BigNumber { get; set; }
}

public class Destination
{
    public int Number { get; set; }

    public string Text { get; set; }

    public long BigNumber { get; set; }
}

[MappingGenerator(typeof(Source), typeof(Destination))]
[MappingGeneratorPropertyIgnore(nameof(Destination.BigNumber))]
public partial class Mapper
{ }

Generated code (removed redundant parts and added comments for clarity):

partial class Mapper : IMapper<Source, Destination>
{
    public Mapper()
    {
    }

    private Destination CreateDestination(Source source)
    {
        return new Destination()
        {};
    }

    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        var result = CreateDestination(source);

        result.Number = source.Number;
        result.Text = source.Text;

        // No mapping for BigNumber property.

        AfterMap(source, result);
        return result;
    }

    partial void AfterMap(Source source, Destination result);
}

Override property matching behavior

public class Source
{
    public int Number { get; set; }

    public string SourceText { get; set; }
}

public class Destination
{
    public int Number { get; set; }

    public string DestinationText { get; set; }
}

[MappingGenerator(typeof(Source), typeof(Destination))]
[MappingGeneratorPropertyMapping(nameof(Source.SourceText), nameof(Destination.DestinationText))]
public partial class Mapper
{ }

Generated code (removed redundant parts and added comments for clarity):

partial class Mapper : IMapper<Source, Destination>
{
    public Mapper()
    {
    }

    private Destination CreateDestination(Source source)
    {
        return new Destination()
        {};
    }

    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        var result = CreateDestination(source);

        result.Number = source.Number;

        // SourceText mapped to DestinationText.
        result.DestinationText = source.SourceText;

        AfterMap(source, result);
        return result;
    }

    partial void AfterMap(Source source, Destination result);
}

Provide custom mapping for destination property

You can provide custom mapping for property by adding function:

<DESTINATION-PROPERTY-TYPE> Map<DESTINATION-PROPERTY-NAME>(Source source)
{}
public class Source
{
    public int Number { get; set; }

    public string SourceText { get; set; }
}

public class Destination
{
    public int Number { get; set; }

    public string DestinationText { get; set; }
}

[MappingGenerator(typeof(Source), typeof(Destination))]
public partial class Mapper
{ 
    private string MapDestinationText(Source source)
    {
        return "Custom" + source.SourceText;
    }
}

Generated code (removed redundant parts and added comments for clarity):

partial class Mapper : IMapper<Source, Destination>
{
    public Mapper()
    {
    }

    private Destination CreateDestination(Source source)
    {
        return new Destination()
        {};
    }

    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        var result = CreateDestination(source);

        result.Number = source.Number;

        // User provided mapping used.
        result.DestinationText = MapDestinationText(source);

        AfterMap(source, result);
        return result;
    }

    partial void AfterMap(Source source, Destination result);
}

Executing post mapping logic

MappingGenerator generates partial AfterMap method which is called after mapping is done.

public class Source
{
    public int Number { get; set; }

    public string Text { get; set; }
}

public class Destination
{
    public int Number { get; set; }

    public string Text { get; set; }
}

[MappingGenerator(typeof(Source), typeof(Destination))]
public partial class Mapper
{ 
    private partial void AfterMap(Source source, Destination result);
    {
        result.Number = result.Number + 100;
    }
}

Generated code (removed redundant parts and added comments for clarity):

partial class Mapper : IMapper<Source, Destination>
{
    public Mapper()
    {
    }

    private Destination CreateDestination(Source source)
    {
        return new Destination()
        {};
    }

    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        var result = CreateDestination(source);

        result.Number = source.Number;
        result.Text = source.Text;

        // Will call your AfterMap method.
        AfterMap(source, result);
        return result;
    }

    partial void AfterMap(Source source, Destination result);
}

Providing custom destination object constructor

You can control have destination object is constructed by adding function:

<DESTINATION-TYPE> CreateDestination(Source source)
{}
public class Source
{
    public int Number { get; set; }

    public string Text { get; set; }
}

public class Destination
{
    public Destination(string input) 
    {}

    public int Number { get; set; }

    public string Text { get; set; }
}

[MappingGenerator(typeof(Source), typeof(Destination))]
public partial class Mapper
{ 
    private Destination CreateDestination(Source source)
    {
        return new Destination("input");
    }
}

Generated code (removed redundant parts and added comments for clarity):

partial class Mapper : IMapper<Source, Destination>
{
    public Mapper()
    {
    }

    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        
        // Your CreateDestination method used.
        var result = CreateDestination(source);

        result.Number = source.Number;
        result.Text = source.Text;

        AfterMap(source, result);
        return result;
    }

    partial void AfterMap(Source source, Destination result);
}

Mapping constructor parameters

MappingGenerator will try to find destination object's constructor that has parameters that can be mapped from source object.

public class Source
{
    public int Number { get; set; }

    public string Text { get; set; }
}

public class Destination
{

    public Destination(string text)
    {
        Text = text;
    }

    public int Number { get; set; }

    public string Text { get; }
}

[MappingGenerator(typeof(Source), typeof(Destination))]
public partial class Mapper
{
}

Generated code (removed redundant parts and added comments for clarity):

partial class Mapper : IMapper<Source, Destination>
{
    public Mapper()
    {
    }

    private Destination CreateDestination(Source source)
    {
        // Parameter 'text' have been mapped to 'source.Text'.
        return new Destination(source.Text)
        {};
    }

    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        var result = CreateDestination(source);

        result.Number = source.Number;

        AfterMap(source, result);
        return result;
    }

    partial void AfterMap(Source source, Destination result);
}

Mapping init only properties

public class Source
{
    public int Number { get; set; }

    public string Text { get; set; }
}

public class Destination
{
    public int Number { get; set; }

    public string Text { get; init; }
}

[MappingGenerator(typeof(Source), typeof(Destination))]
public partial class Mapper
{
}

Generated code (removed redundant parts and added comments for clarity):

partial class Mapper : IMapper<Source, Destination>
{
    public Mapper()
    {
    }

    private Destination CreateDestination(Source source)
    {
        // Init only property 'Text' have been mapped to 'source.Text'.
        return new Destination()
        { 
            Text = source.Text
        };
    }

    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        var result = CreateDestination(source);

        result.Number = source.Number;

        AfterMap(source, result);
        return result;
    }

    partial void AfterMap(Source source, Destination result);
}

Reusing generated mappings

MappingGenerator will reuse other mappings.

public class InnerSource
{
    public string InnerText { get; set; }
}

public class Source
{
    public InnerSource A { get; set; }
    public InnerSource B { get; set; }
}

public class InnerDestination
{
    public string InnerText { get; set; }
}

public class Destination
{
    public InnerDestination A { get; set; }
    public InnerDestination B { get; set; }
}

[MappingGenerator(typeof(Source), typeof(Destination))]
public partial class Mapper
{
}

[MappingGenerator(typeof(InnerSource), typeof(InnerDestination))]
public partial class InnerMapper
{
}

Generated code (removed redundant parts and added comments for clarity):

// Mapping for InnerSource => InnerDestination
partial class InnerMapper : IMapper<InnerSource, InnerDestination>
{
    public Mapper()
    {
    }

    private InnerDestination CreateDestination(Source source)
    {
        return new InnerDestination()
        {};
    }

    public InnerDestination Map(InnerSource source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        var result = CreateDestination(source);

        result.InnerText = source.InnerText;

        AfterMap(source, result);
        return result;
    }

    partial void AfterMap(Source source, Destination result);
}

partial class Mapper : IMapper<Source, Destination>
{
    private IMapper<InnerSource, InnerDestination> innerMapper;

    // Getting InnerMapper.
    public Mapper(IMapper<InnerSource, InnerDestination> innerMapper)
    {
        if (innerMapper == null)
            throw new ArgumentNullException(nameof(innerMapper));

        this.innerMapper = innerMapper;
    }

    private Destination CreateDestination(Source source)
    {
        return new Destination()
        {};
    }

    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        var result = CreateDestination(source);

        // Reusing InnerMapper.
        result.A = this.innerMapper(source.A);
        result.B = this.innerMapper(source.B);

        AfterMap(source, result);
        return result;
    }

    partial void AfterMap(Source source, Destination result);
}

Custom type converters

You can provide custom type converters by defining function:

<DESTINATION-TYPE> Convert<ANY-SUFFIX>(<SOURCE-TYPE>)
{}

Note. Custom type conversion will be used for all mapping of type <SOURCE-TYPE> to <DESTINATION-TYPE>.

public class Source
{
    public string Number { get; set; }
}

public class Destination
{
    public int Number { get; set; }
}

[MappingGenerator(typeof(Source), typeof(Destination))]
public partial class Mapper
{ 
    private int Convert(string source)
    {
        return int.Parse(source);
    }
}

Generated code (removed redundant parts and added comments for clarity):

partial class Mapper : IMapper<Source, Destination>
{
    public Mapper()
    {
    }

    private Destination CreateDestination(Source source)
    {
        return new Destination()
        {};
    }

    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        var result = CreateDestination(source);

        // Custom type converter called.
        result.Number = Convert(source.Number);

        AfterMap(source, result);
        return result;
    }

    partial void AfterMap(Source source, Destination result);
}

Open generics

MappingGenerator is able to handle unbounded generic types (open generics). In this case your mapper type definition should have number of generic parameters corresponding to number of unbound generic parameters in both source and destinations types.

For example:

[MappingGenerator(typeof(Source<>), typeof(Destination<>))]
public partial class Mapper<TSource, TDestination>
{}

Is equivalent to Source<TSource> and Destination<TDestination>

[MappingGenerator(typeof(Source<,>), typeof(Destination<,>))]
public partial class Mapper<TSource1, TSource2, TDestination1, TDestination2>
{}

Is equivalent to Source<TSource1, TSource2> and Destination<TDestination1, TDestination2>

When defining mappers for unbound generic types you need to instruct MappingGenerator how TSource can be converted to TDestination (remember, your mapping should be compilable). You can do it by providing type converter method:

[MappingGenerator(typeof(Source<>), typeof(Destination<>))]
public partial class Mapper<TSource, TDestination>
{
    // Now mapping generator knows how to convert TSource => TDestination
    private TDestination Convert(TSource source)
    {
        // Some conversion logic.
    }
}

Or by adding generic constraints:

[MappingGenerator(typeof(Source<>), typeof(Destination<>))]
public partial class Mapper<TSource, TDestination>
    where TSource : TDestination // TSource can be casted implicitly to TDestination
{
}

Complete example:

public class Source<T>
{
    public T A { get; set; }
}

public class Destination<T>
{
    public T A { get; set; }
}

[MappingGenerator(typeof(Source<>), typeof(Destination<>))]
public partial class Mapper<TSource, TDestination>
{
    private TDestination Convert(TSource source)
    {
        // Some conversion logic.
    }
}

Generated code (removed redundant parts and added comments for clarity):

partial class Mapper<TSource, TDestination> : IMapper<TSource, TDestination>
{
    public Mapper()
    {
    }

    private Destination<TDestination> CreateDestination(Source<TSource> source)
    {
        return new Destination<TDestination>()
        {};
    }

    public Destination<TDestination> Map(Source<TSource> source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        var result = CreateDestination(source);

        // Custom type converter called.
        result.A = Convert(source.A);

        AfterMap(source, result);
        return result;
    }

    partial void AfterMap(Source source, Destination result);
}

Arrays and collections

You don't need to define separate mapper for collection or array type, MappingGenerator creates them for you.

Source collection types:

  • IEnumerable<T>
  • ICollection<T>
  • IList<T>
  • Collection<T>
  • HashSet<T>
  • List<T>
  • Arrays

When mapping to an existing collection, the destination collection is cleared first.

public class Source
{
    public string Text { get; set; }
}

public class Destination
{
    public string Text { get; set; }
}

[MappingGenerator(typeof(Source), typeof(Destination))]
public partial class Mapper
{ }

Generated code (removed redundant parts and added comments for clarity):

partial class Mapper : 
    IMapper<Source, Destination>, 
    IMapper<IEnumerable<Source>, List<Destination>>, 
    IMapper<IEnumerable<Source>, HashSet<Destination>>, 
    IMapper<IEnumerable<Source>, System.Collections.ObjectModel.Collection<Destination>>, 
    IMapper<IEnumerable<Source>, Destination[]>
{
    public Mapper()
    {
    }

    private Destination CreateDestination(Source source)
    {
        return new Destination()
        {};
    }

    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        var result = CreateDestination(source);
        result.Text = source.Text;
        AfterMap(source, result);
        return result;
    }

    // !!! Explicit interface implementation.
    List<Destination> IMapper<IEnumerable<Source>, List<Destination>>.Map(IEnumerable<Source> source)
    {
        source ??= Enumerable.Empty<Source>();
        return new List<Destination>(source.Select(Map));
    }

    // !!! Explicit interface implementation.
    HashSet<Destination> IMapper<IEnumerable<Source>, HashSet<Destination>>.Map(IEnumerable<Source> source)
    {
        source ??= Enumerable.Empty<Source>();
        return new HashSet<Destination>(source.Select(Map));
    }

    // !!! Explicit interface implementation.
    Collection<Destination> IMapper<IEnumerable<Source>, Collection<Destination>>.Map(IEnumerable<Source> source)
    {
        var result = ((IMapper<IEnumerable<Source>, List<Destination>>)this).Map(source);
        // Collection is wrapper over IList.
        return new Collection<Destination>(result);
    }

    // !!! Explicit interface implementation.
    Destination[] IMapper<IEnumerable<Source>, Destination[]>.Map(IEnumerable<Source> source)
    {
        source ??= Enumerable.Empty<Source>();
        return source.Select(Map).ToArray();
    }

    partial void AfterMap(Source source, Destination result);
}

Usage:

var source = new Source[] { ... };
// Collection mappers require explicit cast.
var mapper = (IMapper<IEnumerable<Source>, List<Destination>)new Mapper(source);
var result = mapper.Map(source);

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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  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.
  • .NETStandard 2.0

    • No dependencies.

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.0.0-preview.26 131 4/19/2022
1.0.0-preview.24 113 4/16/2022
1.0.0-preview.23 109 4/16/2022
1.0.0-preview.16 115 4/16/2022
1.0.0-preview.14 113 4/16/2022
1.0.0-preview.13 115 4/12/2022
1.0.0-preview.8 117 4/11/2022
1.0.0-preview.4 112 4/10/2022
1.0.0-preview.2 109 4/10/2022
1.0.0-preview 114 4/10/2022
0.0.0-alpha.0.78 120 4/10/2022
0.0.0-alpha.0.77 113 4/9/2022
0.0.0-alpha.0.76 115 4/9/2022
0.0.0-alpha.0.73 122 4/7/2022
0.0.0-alpha.0.69 109 4/1/2022
0.0.0-alpha.0.64 106 4/1/2022
0.0.0-alpha.0.62 116 3/31/2022
0.0.0-alpha.0.53 108 3/30/2022
0.0.0-alpha.0.50 102 3/30/2022
0.0.0-alpha.0.47 106 3/30/2022
0.0.0-alpha.0.30 116 3/28/2022
0.0.0-alpha.0.29 114 3/28/2022
0.0.0-alpha.0.28 112 3/28/2022
0.0.0-alpha.0.27 115 3/28/2022
0.0.0-alpha.0.26 111 3/27/2022
0.0.0-alpha.0.25 111 3/27/2022
0.0.0-alpha.0.16 116 3/26/2022