Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve null handling #1614

Open
latonz opened this issue Nov 27, 2024 · 4 comments
Open

Improve null handling #1614

latonz opened this issue Nov 27, 2024 · 4 comments
Assignees
Labels
bug Something isn't working

Comments

@latonz
Copy link
Contributor

latonz commented Nov 27, 2024

Currently, Mapperly upgrades non-nullable reference types to annotated nullable reference types early in the mapping pipeline. Additionally, when nullables are involved, Mapperly incorporates null handling into the mapping process and continues the pipeline with non-nullable types. This approach centralizes null handling, making it simple and consistent.

However, this design has led to several bugs, indicating the need for a more robust solution. We need to define a clear concept to address these issues and improve the handling of nullable reference types without compromising simplicity or maintainability.

Related Bugs:

See also #1117 (comment).

@latonz latonz added the bug Something isn't working label Nov 27, 2024
@latonz latonz self-assigned this Dec 8, 2024
@TonEnfer
Copy link
Contributor

TonEnfer commented Dec 9, 2024

I think that when doing this task, you should also take into account similar conversions from #1117:

Source Is nullable Target Is nullable Expected result
class A {} true class B { static B Create(A source) } false target = global::B.Create(source ?? throw new ArgumentNullException(nameof(source)));
class A {} false class B { static B? Create(A source) } false target = global::B.Create(source) ?? throw new ArgumentNullException();
class A {} false struct B { static B? Create(A source) } false target = global::B.Create(source) ?? throw new ArgumentNullException();

or

target = global::B.Create(source).GetValueOrDefault();

Also pay attention to the results of these two tests:

public void MapNonNullableValueTypeToNullableValueType()
{
var source = TestSourceBuilder.Mapping("B", "A?", "struct A { public static A FromB(B source) => new(); }; struct B {}");
TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (global::A?)global::A.FromB(source);");
}
[Fact]
public void MapNonNullableValueTypeToNullableValueTypeWithNullableMethodParameter()
{
var source = TestSourceBuilder.Mapping("B", "A?", "struct A { public static A FromB(B? source) => new(); }; struct B {}");
TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return global::A.FromB(source);");
}

@CoreDX9
Copy link

CoreDX9 commented Dec 24, 2024

Can null value assignments be configured separately for each property?
Like this:

[AllowNullPropertyAssignment(false, Include = ["ArrayProp1", "ListProp2"])]
public static partial void UpdateEntity(this UpdateDto dto, MyEntity entity);

This way, null values can be used in the collection navigation properties to indicate that the collection navigation properties are not modified when update entity using EF Core.

@latonz
Copy link
Contributor Author

latonz commented Jan 6, 2025

This is not yet possible.

@johnnyelwailer
Copy link

johnnyelwailer commented Jan 24, 2025

Greetings. I have a related use case.
Consider this code:

    public class SourceItem
    {
        public required string Name { get; set; }
    }

    public class DestinationItem
    {
        public required string Name { get; set; }
    }

    public class Source
    {
        public SourceItem[]? Items { get; set; }
    }
    
    public class Destination
    {
        public required DestinationItem[] Items { get; set; }
    }
    
    [Mapper(ThrowOnPropertyMappingNullMismatch = false, ThrowOnMappingNullMismatch = false)]
    public partial class TestMapper
    {
        public partial Destination Map(Source source);
    }

It generates the following method

        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.2.0.0")]
        public partial global::<Api.Destination Map(global::Api.Mapping.Source source)
        {
            var target = new global::Api.Mapping.Destination()
            {
                Items = source.Items != null ? MapToDestinationItemArray(source.Items) : throw new System.ArgumentNullException(nameof(source.Items)),
            };
            return target;
        }

BUT, at the same time, it emits a compilation error:
RMG002: Api.Mapping.DestinationItem[] has no accessible parameterless constructor

I guess the best solution to just try to use a empty collection initalizer?
Like

Items = source.Items != null ? MapToDestinationItemArray(source.Items) : []

Currently I can workaround this problem by manually adding the following code:

        public partial DestinationItem Map(SourceItem sourceItem);
        public DestinationItem[] Map(SourceItem[]? sourceItems)
        {
            return sourceItems?.Select(this.Map).ToArray() ?? [];
        }

Thanks for considering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants