Nullable Mappings

MappingGenerator respects nullable annotations.

If Source and Destination have same nullable annotations:

public class Source { public string? Value { get; set; } }
public class Destination { public string? Value { get; set; } }

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

Will produce the following generated code (removed redundant parts and added comments for brevity):

partial class Mapper : IMapper<Source, Destination>
{
    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        
        var result = CreateDestination();

        // Destination.Value and Source.Value are both string? type.
        result.Value = source.Value;
        
        return result;
    }
}

If Source and Destination have different nullable annotations:

public class Source { public string? Value { get; set; } }

// Destination.Value is not nullable!
public class Destination { public string Value { get; set; } }

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

Will produce the following generated code (removed redundant parts and added comments for brevity):

partial class Mapper : IMapper<Source, Destination>
{
    public Destination Map(Source source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));
        
        var result = CreateDestination();

        // Destination.Value is not nullable but Source.Value is.
        result.Value = source.Value == null ? throw new MappingNullException(typeof(Source), "Value", typeof(Destination), "Value") : source.Value;
        
        return result;
    }
}

Custom mapper with nullable source:

public class SourceInner { public string? InnerText { get; set; } }
public class DestinationInner { public string? InnerText { get; set; } }

public class Source { public SourceInner? Value { get; set; } }

// Destination.Value is not nullable!
public class Destination { public DestinationInner Value { get; set; } }

[MappingGenerator(typeof(Source<SourceInner?>), typeof(Destination<DestinationInner>))]
public partial class CustomNullableMapper
{
    private IMapper<SourceInner?, DestinationInner> _nullToDefaultMapper = new NullToDefaultMapper();

    // Return predefined result if source is null.
    private class NullToDefaultMapper : IMapper<SourceInner?, DestinationInner>
    {
        public DestinationInner Map(SourceInner? source)
        {
            if (source == null)
                return new DestinationInner { InnerText = "Default" };

            return new DestinationInner { InnerText = source.InnerText };
        }
    }
}

Will produce the following generated code (removed redundant parts and added comments for brevity):

partial class CustomNullableMapper : IMapper<Source<SourceInner?>, Destination<DestinationInner>>
{
    public Destination<DestinationInner> Map(Source<SourceInner?> source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));

        var result = CreateDestination();

        // Inner mapper is fine with source = null, no additional code needed.
        result.Value = this._nullToDefaultMapper.Map(source.Value);
        
        return result;
    }
}

Custom mapper with nullable source and destination:

public class SourceInner { public string? InnerText { get; set; } }
public class DestinationInner { public string? InnerText { get; set; } }

public class Source { public SourceInner? Value { get; set; } }

// Destination.Value is not nullable!
public class Destination { public DestinationInner Value { get; set; } }

[MappingGenerator(typeof(Source<SourceInner?>), typeof(Destination<DestinationInner>))]
public partial class CustomNullableMapper
{
    private IMapper<SourceInner?, DestinationInner?> _nullToDefaultMapper = new NullToDefaultMapper();

    // Return null if source is null.
    private class NullToDefaultMapper : IMapper<SourceInner?, DestinationInner?>
    {
        public DestinationInner? Map(SourceInner? source)
        {
            if (source == null)
                return null;

            return new DestinationInner { InnerText = source.InnerText };
        }
    }
}

Will produce the following generated code (removed redundant parts and added comments for brevity):

#nullable enable

partial class CustomNullableMapper : IMapper<Source<SourceInner?>, Destination<DestinationInner>>
{
    public Destination<DestinationInner> Map(Source<SourceInner?> source)
    {
        if (source == null)
            throw new ArgumentNullException(nameof(source));

        var result = CreateDestination();

        // Inner mapper is fine with source = null but can return null.
        result.Value = this._nullToDefaultMapper.Map(source.Value) 
            ?? throw new MappingNullException(_nullToDefaultMapper.GetType(), typeof(Destination<DestinationInner>), "Value");
        
        return result;
    }
}

#nullable restore