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

Struggling with mapping readonly ICollection property #689

Open
akordowski opened this issue Mar 29, 2024 Discussed in #688 · 19 comments
Open

Struggling with mapping readonly ICollection property #689

akordowski opened this issue Mar 29, 2024 Discussed in #688 · 19 comments

Comments

@akordowski
Copy link

akordowski commented Mar 29, 2024

Discussed in #688

Originally posted by akordowski March 24, 2024
Hi everyone!

I am currently struggling with the mapping of a readonly ICollection property. I followed the instructions but I can't make it work. The readonly collection is not populated with the mapped content. What am I making wrong? Have anyone an idea how I can make it work?

Here the code:

Programm.cs

var config = TypeAdapterConfig.GlobalSettings;
config.Scan(typeof(ChannelMapping).Assembly);
config.Default.UseDestinationValue(m => m.SetterModifier == AccessModifier.None &&
                                        m.Type.IsGenericType &&
                                        m.Type.GetGenericTypeDefinition() == typeof(ICollection<>));

var channelSrc = new MapsterTest.Objects.Source.Channel
{
    ChannelId = "123",
    Thumbnails = new MapsterTest.Objects.Source.ThumbnailDetails
    {
        Default = new MapsterTest.Objects.Source.Thumbnail
        {
            Url = "https://www.youtube.com/default.jpg"
        },
        Medium = new MapsterTest.Objects.Source.Thumbnail
        {
            Url = "https://www.youtube.com/medium.jpg"
        },
        High = new MapsterTest.Objects.Source.Thumbnail
        {
            Url = "https://www.youtube.com/high.jpg"
        }
    }
};

// Thumbnails are mapped correctly to a collection
var thumbnailsDest = channelSrc.Thumbnails.Adapt<ICollection<MapsterTest.Objects.Destination.Thumbnail>>().ToList();

// channelDest.Thumbnails collection is empty
var channelDest = channelSrc.Adapt<MapsterTest.Objects.Destination.Channel>();

ThumbnailMapping.cs

public class ThumbnailMapping : IRegister
{
    public void Register(TypeAdapterConfig config)
    {
        config.ForType<Objects.Source.ThumbnailDetails, ICollection<Objects.Destination.Thumbnail>>()
            .MapWith(src => MapThumbnailDetails(src).ToList());
    }

    private static IEnumerable<Objects.Destination.Thumbnail> MapThumbnailDetails(Objects.Source.ThumbnailDetails thumbnailDetails)
    {
        yield return MapThumbnail(thumbnailDetails.Default, "Default");
        yield return MapThumbnail(thumbnailDetails.Medium, "Medium");
        yield return MapThumbnail(thumbnailDetails.High, "High");
    }

    private static Objects.Destination.Thumbnail MapThumbnail(
        Objects.Source.Thumbnail thumbnail,
        string thumbnailType) =>
        new()
        {
            Type = thumbnailType,
            Url = thumbnail.Url.Trim(),
        };
}

Channel.cs (Destination)

public class Channel
{
    public string ChannelId { get; set; } = default!;
    public ICollection<Thumbnail> Thumbnails { get; } = new List<Thumbnail>();
}

Thumbnail.cs (Destination)

public class Thumbnail
{
    public string Type { get; set; } = default!;
    public string Url { get; set; } = default!;
}

Channel.cs (Source)

public class Channel
{
    public string ChannelId { get; set; } = default!;
    public ThumbnailDetails Thumbnails { get; set; } = default!;
}

ThumbnailDetails.cs (Source)

public class ThumbnailDetails
{
    public Thumbnail? Default { get; set; }
    public Thumbnail? Medium { get; set; }
    public Thumbnail? High { get; set; }
}

Thumbnail.cs (Source)

public class Thumbnail
{
    public string Url { get; set; } = default!;
}
@steingran
Copy link

@akordowski I have the same issue. Did you ever find a solution?

This project is starting to look abandoned — no commits since September 2023, and no release since then. And issues opened are not answered... Is it time to move on to a different mapper?

@akordowski
Copy link
Author

This is how I solved the problem. It's far from being an optimal solution but it works. It was a special case, but it might give you an idea. Hope it helps you.

public class SrcClassMapping : IRegister
{
    public void Register(TypeAdapterConfig config)
    {
        config
            .ForType<DestClass, ICollection<SrcClass>>()
            .MapWith(src => MapCollection(src, null))
            .MapToTargetWith((src, dest) => MapCollection(src, dest));
    }

    private static ICollection<SrcClass> MapCollection(
        DestClass? destObj,
        ICollection<SrcClass>? srcCollection)
    {
        srcCollection ??= new List<SrcClass>();

        if (destObj is null)
            return srcCollection;

        // Custom mapping

        return srcCollection;
    }
}

@steingran
Copy link

@akordowski Great, thanks! Will give it a try

@stagep
Copy link

stagep commented Jan 7, 2025

In order to identify issues that are still active, we are closing issues that we believe are either resolved or are dormant. If your issue is still active then please reopen. Thanks.

@stagep stagep closed this as completed Jan 7, 2025
@akordowski
Copy link
Author

@stagep Please reopen, as I can only comment. Thank you.

@stagep stagep reopened this Jan 8, 2025
@DocSvartz
Copy link

Hello @akordowski. Destination Class does it really look like you indicated?

public class Channel
{
    public string ChannelId { get; set; } = default!;
    public ICollection<Thumbnail> Thumbnails { get; } = new List<Thumbnail>();
}

it doesn't have any additional constructors?

@akordowski
Copy link
Author

Yes, the destination class looks exactly like this and don't have any constructors.

@DocSvartz
Copy link

Ok, I'm starting to research this problem.
I'll try to come back soon with an explanation or a solution :)

@DocSvartz DocSvartz self-assigned this Jan 9, 2025
@DocSvartz DocSvartz added improvement and removed bug labels Jan 9, 2025
@DocSvartz
Copy link

DocSvartz commented Jan 9, 2025

Properties without a setter are not updated .

Your code even gets built in and works. But the result is not assigned anywhere.
Because can't call setter in any way for such fields.

Mappingfunction (channelSrc,TChannelDestination)
{
  var result = new ChannelDestination()
  MapThumbnailDetails(src).ToList() // var result.Thumbnails=  The assignment not created or was deleted,
}

This Working because here your function specified in the Map() condition is simply called and the result is returned.

// Thumbnails are mapped correctly to a collection
var thumbnailsDest = channelSrc.Thumbnails.Adapt<ICollection<MapsterTest.Objects.Destination.Thumbnail>>().ToList();

@DocSvartz
Copy link

@andrerav Continuing with the topic raised here #719 .
It seems that users expect such behavior.

@andrerav
Copy link
Contributor

andrerav commented Jan 9, 2025

@DocSvartz If I understand this correctly, the issue is that the values in Thumbnails (which is a read-only list) should be mapped automatically from the source collection. I'm looking at the test for UseDestinationValue, and it appears to work as intended. And that test is in principle the same use case as this issue. The difference being that this issue is configured using TypeAdapterConfig. The test only uses the [UseDestinationValue] attribute, and as far as I can tell there are no tests that uses the TypeAdapterConfig method.

@DocSvartz
Copy link

DocSvartz commented Jan 9, 2025

@andrerav For this example Doesn't work even if you use
[UseDestinationValue]

@DocSvartz
Copy link

DocSvartz commented Jan 9, 2025

@andrerav
It seems MapWith suppresses this settings.
This example works For the TempThumbnails property

[TestClass]
public class WhenReadOnlyInterfaceCollectMapping
{
    [TestMethod]
    public void TestMethod1()
    {
        var config = TypeAdapterConfig.GlobalSettings;
        config.Scan(typeof(WhenReadOnlyInterfaceCollectMapping).Assembly);
        config.Default.UseDestinationValue(m => m.SetterModifier == AccessModifier.None &&
                                                m.Type.IsGenericType &&
                                                m.Type.GetGenericTypeDefinition() == typeof(ICollection<>));


        var channelSrc = new ChannelSource
        {
            ChannelId = "123",
            Thumbnails = new ThumbnailDetailsSource
            {
                Default = new ThumbnailSource
                {
                    Url = "https://www.youtube.com/default.jpg"
                },
                Medium = new ThumbnailSource
                {
                    Url = "https://www.youtube.com/medium.jpg"
                },
                High = new ThumbnailSource  
                {
                    Url = "https://www.youtube.com/high.jpg"
                }
            },

            TempThumbnails = new List<int> (){ 1, 2, 3 }
            
        };

        // Thumbnails are mapped correctly to a collection
        var thumbnailsDest = channelSrc.Thumbnails.Adapt<ICollection<ThumbnailDestination>>().ToList();

        // channelDest.Thumbnails collection is empty
        var channelDest = channelSrc.Adapt<ChannelDestination>();

        //Maping TempThumbnails is work result is List<string> {"1", "2", "3" }



    }

    public class ThumbnailMapping : IRegister
    {
        public void Register(TypeAdapterConfig config)
        {
            config.ForType<ThumbnailDetailsSource, ICollection<ThumbnailDestination>>()
                .MapWith(src => MapThumbnailDetails(src).ToList());
        }

        private static IEnumerable<ThumbnailDestination> MapThumbnailDetails(ThumbnailDetailsSource thumbnailDetails)
        {
            yield return MapThumbnail(thumbnailDetails.Default, "Default");
            yield return MapThumbnail(thumbnailDetails.Medium, "Medium");
            yield return MapThumbnail(thumbnailDetails.High, "High");
        }

        private static ThumbnailDestination MapThumbnail(
            ThumbnailSource thumbnail,
            string thumbnailType) =>
            new()
            {
                Type = thumbnailType,
                Url = thumbnail.Url.Trim(),
            };
    }

    public class ChannelDestination
    {
        public string ChannelId { get; set; } = default!;
        
       // [UseDestinationValue]
        public ICollection<ThumbnailDestination> Thumbnails { get; } = new List<ThumbnailDestination>();
        
      //  [UseDestinationValue]
        public ICollection<string> TempThumbnails { get; } = new List<string>();
    }

    public class ThumbnailDestination
    {
        public string Type { get; set; } = default!;
        public string Url { get; set; } = default!;
    }

    public class ChannelSource
    {
        public string ChannelId { get; set; } = default!;
        public ThumbnailDetailsSource Thumbnails { get; set; } = default!;


        public ICollection<int> TempThumbnails { get; set; } = new List<int>();

    }

    public class ThumbnailDetailsSource
    {
        public ThumbnailSource? Default { get; set; }
        public ThumbnailSource? Medium { get; set; }
        public ThumbnailSource? High { get; set; }
    }

    public class ThumbnailSource
    {
        public string Url { get; set; } = default!;
    }


}



@DocSvartz
Copy link

DocSvartz commented Jan 9, 2025

Mapping Function For the last example

{
    .Block(Mapster.Tests.WhenReadOnlyInterfaceCollectMapping+ChannelDestination $result) {
        .If ($var1 == null) {
            .Return #Label1 { null }
        } .Else {
            .Default(System.Void)
        };
        $result = .New Mapster.Tests.WhenReadOnlyInterfaceCollectMapping+ChannelDestination();
        .Block() {
            $result.ChannelId = $var1.ChannelId;
            .Call System.Linq.Enumerable.ToList(.Call Mapster.Tests.WhenReadOnlyInterfaceCollectMapping+ThumbnailMapping.MapThumbnailDetails($var1.Thumbnails)
            );
            .Invoke (.Lambda #Lambda2<System.Func`3[System.Collections.Generic.ICollection`1[System.Int32],System.Collections.Generic.ICollection`1[System.String],System.Collections.Generic.ICollection`1[System.String]]>)(
                $var1.TempThumbnails,
                $result.TempThumbnails)
        };
        .Return #Label1 { $result };
        .Label
            null
        .LabelTarget #Label1:
    }
}


@andrerav
Copy link
Contributor

andrerav commented Jan 9, 2025

Yes, I see. Could this work with MapToTargetWith() instead?

@DocSvartz
Copy link

DocSvartz commented Jan 9, 2025

I haven't checked, but if the result is passed to it

$result = .New Mapster.Tests.WhenReadOnlyInterfaceCollectMapping+ChannelDestination();

Then the collection mapping mechanism itself must be implemented by the user

@andrerav
Copy link
Contributor

andrerav commented Jan 9, 2025

Right. So the workaround is MapToTargetWith() (and I see that this is the workaround @akordowski suggested earlier in this thread). But the ideal solution is for Mapster to detect UseDestinationValue in the logic for MapWith so that the collection returned from the lambda is copied into the destination collection?

@DocSvartz
Copy link

DocSvartz commented Jan 9, 2025

Although maybe I'm already confused :)

@DocSvartz
Copy link

@andrerav I think yes, that's what was meant

UseDestinationValue == .Invoke (MapToWith, $result.Thumbnails)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants