riok / mapperly

A .NET source generator for generating object mappings. No runtime reflection.
https://mapperly.riok.app
Apache License 2.0
2.62k stars 142 forks source link

Generic mapping does not work with disabled nullable reference types #1188

Closed DdarkSideE closed 7 months ago

DdarkSideE commented 7 months ago

Describe the bug Seems like generic mapping does not work. Even the example from the site won't work.

Declaration code

class Fruit {}
class Banana : Fruit {}
class Apple : Fruit {}

class BananaDto {}
class AppleDto {}

[Mapper]
partial class Mapper
{
    public static partial TTarget MapFruit<TTarget>(Fruit source);

    private static partial BananaDto MapBanana(Banana source);
    private static partial AppleDto MapApple(Apple source);
}

Actual relevant generated code

    partial class Mapper
    {
        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.4.0.0")]
        public static partial TTarget? MapFruit<TTarget>(global::ConsoleApp2.Fruit? source)
        {
            return source switch
            {
                null => throw new System.ArgumentNullException(nameof(source)),
                _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)),
            };
        }

        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.4.0.0")]
        private static partial global::ConsoleApp2.BananaDto? MapBanana(global::ConsoleApp2.Banana? source)
        {
            if (source == null)
                return default;
            var target = new global::ConsoleApp2.BananaDto();
            return target;
        }

        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.4.0.0")]
        private static partial global::ConsoleApp2.AppleDto? MapApple(global::ConsoleApp2.Apple? source)
        {
            if (source == null)
                return default;
            var target = new global::ConsoleApp2.AppleDto();
            return target;
        }
    }

Expected relevant generated code It should use the MapBanana and MapApple methods to implement MapFruit. Now it basically generates an empty version of the method with no actual mapping.

Environment:

DdarkSideE commented 7 months ago

I am not sure if it is useful, but based on the following provided code, Mapperly generates generic map better. Not sure what is the difference.

Declaration code

namespace A1
{
    public class Class
    {
        public Enum Value { get; set; }
    }

    public enum Enum
    {
        Unknown = 0,
    }
}

namespace A2
{
    public enum Enum
    {
        Unknown = 0,
    }   
}

[Mapper]
partial class Mapper
{
    public partial TTarget Map<TTarget>(Object source);

    private A2.Enum Map(A1.Class source) => MapEnums(source.Value);
    private partial A2.Enum MapEnums(A1.Enum source);
}

Generated code

#nullable enable
partial class Mapper
{
    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.4.0.0")]
    public partial TTarget? Map<TTarget>(object? source)
    {
        return source switch
        {
            global::A1.Enum x when typeof(TTarget).IsAssignableFrom(typeof(global::A2.Enum)) => (TTarget?)(object)MapEnums(x),
            global::A1.Class x when typeof(TTarget).IsAssignableFrom(typeof(global::A2.Enum)) => (TTarget?)(object)Map(x),
            null => throw new System.ArgumentNullException(nameof(source)),
            _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)),
        };
    }

    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.4.0.0")]
    private partial global::A2.Enum MapEnums(global::A1.Enum source)
    {
        return (global::A2.Enum)source;
    }
}
latonz commented 7 months ago

I cannot reproduce this... If I try it locally it generates

    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.4.0.0")]
    public static partial TTarget MapFruit<TTarget>(global::Fruit source)
    {
        return source switch
        {
            global::Banana x when typeof(TTarget).IsAssignableFrom(typeof(global::BananaDto)) => (TTarget)(object)MapBanana(x),
            global::Apple x when typeof(TTarget).IsAssignableFrom(typeof(global::AppleDto)) => (TTarget)(object)MapApple(x),
            null => throw new System.ArgumentNullException(nameof(source)),
            _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)),
        };
    }

which seems to be ok to me. Tested with 3.4.0 and 3.5.0-next.3.

Can you create a repro on github?

DdarkSideE commented 7 months ago

Could you check this, please? I tried it on another PC and it didn't work.

UPD. It seems like it doesn't work if nullable reference types disabled on a project level. When I turn it on, it generates normal code.

Any workarounds how to make it work with the option disabled?

latonz commented 7 months ago

This is a bug, thanks for reporting 😊 It looks like Mapperly doesn't handle disabled nullable reference types not correctly. The only workaround I can think of currently is to enable the nullable reference types for the relevant parts (e.g. the mapper definition file) with #nullable enable.

github-actions[bot] commented 7 months ago

:tada: This issue has been resolved in version 3.5.0-next.4 :tada:

The release is available on:

Your semantic-release bot :package::rocket: