Skip to content

BuildAdapter().AdaptToType<T>() does not include all expected properties when source is using inheritance #776

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

Open
crates-barrels opened this issue Feb 21, 2025 · 6 comments

Comments

@crates-barrels
Copy link

crates-barrels commented Feb 21, 2025

While rewriting some code to use the async implementation of adapt using package Mapster.Async v2.0.1, I stubled upon an issue where not all expected source properties were copied to the result class.
However, the code was working as expected when I was using .Adapt<T>().

I've tracked it down to the source class being inherited from a base class and using a method where the parameter is of this base class type.
The following code can be used (e.g. in a console application) to reproduce the issue and show the difference in behavior between .Adapt<T>() and AdaptToType<T>():

using Mapster;
using Newtonsoft.Json;

namespace MapsterBug
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // sample source object
            var myImplementation = new MyImplementation
            {
                InterfaceProperty = 123,
                FirstImplementationProperty = 789,
                SecondImplementationProperty = "test"
            };

            // adapt using .Adapt<T>(): the result is as expected
            var resultAdapt = myImplementation.Adapt<MyDto>();

            Console.WriteLine("Result of myImplementation.Adapt<MyDto>():");
            Console.WriteLine(JsonConvert.SerializeObject(resultAdapt, Formatting.Indented));
            Console.WriteLine("\r\n-----------------------------------------\r\n");

            // adapt using .AdaptToType<T>(): the result is as expected
            var resultAdaptToType = myImplementation.BuildAdapter().AdaptToType<MyDto>();

            Console.WriteLine("Result of myImplementation.BuildAdapter().AdaptToType<MyDto>():");
            Console.WriteLine(JsonConvert.SerializeObject(resultAdaptToType, Formatting.Indented));
            Console.WriteLine("\r\n-----------------------------------------\r\n");

            // adapt using .Adapt<T>() inside a method where the parameter is of the interface type: the result is as expected
            var resultAdaptWithInterface = AdaptWithInterface(myImplementation);

            Console.WriteLine("Result of myImplementation.Adapt<MyDto>() in method with interface:");
            Console.WriteLine(JsonConvert.SerializeObject(resultAdaptWithInterface, Formatting.Indented));
            Console.WriteLine("\r\n-----------------------------------------\r\n");

            // adapt using .AdaptToType<T>() inside a method where the parameter is of the interface type:
            // the result only includes the interface property, but it does not include the properties of the implementation
            var resultAdaptToTypeWithInterface = AdaptToTypeWithInterface(myImplementation);

            Console.WriteLine("Result of myInterface.BuildAdapter().AdaptToType<MyDto>() in method with interface:");
            Console.WriteLine(JsonConvert.SerializeObject(resultAdaptToTypeWithInterface, Formatting.Indented));
        }

        static MyDto AdaptWithInterface(IMyInterface myInterface) => myInterface.Adapt<MyDto>();
        static MyDto AdaptToTypeWithInterface(IMyInterface myInterface) => myInterface.BuildAdapter().AdaptToType<MyDto>();
    }

    public interface IMyInterface
    {
        public int InterfaceProperty { get; set; }
    }

    public class MyImplementation : IMyInterface
    {
        public int InterfaceProperty { get; set; }
        public int FirstImplementationProperty { get; set; }
        public string SecondImplementationProperty { get; set; }
    }

    public class MyDto
    {
        public int InterfaceProperty { get; set; }
        public int FirstImplementationProperty { get; set; }
        public string SecondImplementationProperty { get; set; }
    }
}

This code is using AdaptToType<T>() to not complicate things by using an additional package (Mapster.Async), but the AdaptToTypeAsync<T>() method has the same behavior.
When running the application, the output is as follows:

Result of myImplementation.Adapt<MyDto>():
{
  "InterfaceProperty": 123,
  "FirstImplementationProperty": 789,
  "SecondImplementationProperty": "test"
}

-----------------------------------------

Result of myImplementation.BuildAdapter().AdaptToType<MyDto>():
{
  "InterfaceProperty": 123,
  "FirstImplementationProperty": 789,
  "SecondImplementationProperty": "test"
}

-----------------------------------------

Result of myImplementation.Adapt<MyDto>() in method with interface:
{
  "InterfaceProperty": 123,
  "FirstImplementationProperty": 789,
  "SecondImplementationProperty": "test"
}

-----------------------------------------

Result of myInterface.BuildAdapter().AdaptToType<MyDto>() in method with interface:
{
  "InterfaceProperty": 123,
  **"FirstImplementationProperty": 0,**
  **"SecondImplementationProperty": null**
}

Results 1, 2 and 3 are as expected, but the last result only includes the IMyInterface property (InterfaceProperty) and not the additional MyImplementation properties (FirstImplementationProperty and SecondImplementationProperty) marked in bold.

Versions used in code sample:

  • .NET 8
  • Mapster 7.4.0
  • Newtonsoft.Json 13.0.3
@DocSvartz
Copy link

DocSvartz commented Feb 21, 2025

@crates-barrels implementation and inheritance are different things.
The interfaces cannot provide access to those Members that are not declared in it.

.Adapt working With runtime type of instance.
.AdaptToType Working with Generic (declared type) from instance

 var source = new MyImplementation() 
 {
     InterfaceProperty = 123,
     FirstImplementationProperty = 789,
     SecondImplementationProperty = "test"
 };

IMyInterface Isource = source;
var result = source.BuildAdapter().AdaptToType<MyDto>();  // work
var resultInterface = source.Adapt<IMyInterface,MyDto>(); // not work 

var resultImplementation = Isource.Adapt<MyDto>(); //  work because Isource.GetType() == typeof(MyImplementation)

var t = Isource.BuildAdapter().AdaptToType<MyDto>(); //  it equal  .Adapt<IMyInterface,MyDto>

@DocSvartz
Copy link

@andrerav @stagep If I understand correctly, asynchronous work cannot work the same way as it works for the synchronous variant.

In order to call .GetType() we will need to wait for the Asynchronous call to complete in any case.
Which will lead to synchronous execution of the code.

@crates-barrels
Copy link
Author

crates-barrels commented Feb 24, 2025

Thank you for the reply @DocSvartz and I'm sorry for using the wrong terminology.
My use case is using inheritance and it behaves the same as the sample code where an interface is used.

Anyway, I expected both methods to behave the same way, but I understand this is not the case because the underlying code is based on different things (runtime using .GetType() and the other using generics)?

Ultimately I would like to use .AfterMappingAsync() in a TypeAdapterConfig.
For this, I have to use the BuildAdapter().AdaptToTypeAsync() alternative since executing .Adapt() results in the following exception:

System.InvalidOperationException: 'Mapping contains async function, please use BuildAdapter.AdaptToTypeAsync instead'

But when using that method, its behavior is different as described.
For now, I'm using a workaround by using the synchronous .AfterMapping() method and .Wait()'ing on an async-method, which is not very clean...

Building further on the sample code, the following is similar to my current workaround where I would like to use .AfterMappingAsync() instead of .AfterMapping() in the TypeAdapterConfig and remove the .Wait() call:

using Mapster;
using Newtonsoft.Json;

namespace MapsterBug
{
    internal class Program
    {
        static void Main(string[] args)
        {
            TypeAdapterConfig<MyImplementation, MySecondDto>.NewConfig()
                .Map(dest => dest.IntProp, src => src.InterfaceProperty)
                .Map(dest => dest.FirstImpProp, src => src.FirstImplementationProperty)
                .AfterMapping((src, dest) => FillSecondImpPropAsync(src, dest).Wait());
                //.AfterMappingAsync(FillSecondImpPropAsync); // this would be the desired way

            // sample source object
            var myImplementation = new MyImplementation
            {
                InterfaceProperty = 123,
                FirstImplementationProperty = 789,
                SecondImplementationProperty = "test"
            };

            var resultAdaptToSecondDto = AdaptSecondDtoWithInterface(myImplementation);

            Console.WriteLine(JsonConvert.SerializeObject(resultAdaptToSecondDto, Formatting.Indented));
        }

        private static MySecondDto AdaptSecondDtoWithInterface(IMyInterface myInterface) => myInterface.Adapt<MySecondDto>();

        private static async Task FillSecondImpPropAsync(MyImplementation src, MySecondDto dest)
        {
            await Task.Delay(1000); // simulate a long-running operation

            dest.SecondImpProp = src.SecondImplementationProperty;
        }
    }

    public interface IMyInterface
    {
        public int InterfaceProperty { get; set; }
    }

    public class MyImplementation : IMyInterface
    {
        public int InterfaceProperty { get; set; }
        public int FirstImplementationProperty { get; set; }
        public string SecondImplementationProperty { get; set; }
    }

    public class MySecondDto
    {
        public int IntProp { get; set; }
        public int FirstImpProp { get; set; }
        public string SecondImpProp { get; set; }
    }
}

Is there another way to achieve this without having to create a method overload for every possible source type?

@DocSvartz
Copy link

DocSvartz commented Feb 24, 2025

Have you tried something like this configuration?

var source = new MyImplementation()
{
    InterfaceProperty = 123,
    FirstImplementationProperty = 789,
    SecondImplementationProperty = "test"
};

IMyInterface Isource = source;

TypeAdapterConfig<MyImplementation, MySecondDto>.NewConfig()
   .Map(dest => dest.IntProp, src => src.InterfaceProperty)
   .Map(dest => dest.FirstImpProp, src => src.FirstImplementationProperty)
   .AfterMappingAsync((src, dest) => FillSecondImpPropAsync(src, dest));

TypeAdapterConfig<IMyInterface, MyDto>.NewConfig()
    .Include<MyImplementation, MyDto>();

TypeAdapterConfig<IMyInterface, MySecondDto>.NewConfig()
   .Include<MyImplementation, MySecondDto>();

var result = await source.BuildAdapter().AdaptToTypeAsync<MyDto>(); // working
var resultInterface = source.Adapt<IMyInterface, MyDto>(); // working

var s = Isource.GetType();

var tMyDto = await Isource.BuildAdapter().AdaptToTypeAsync<MyDto>(); // working
var tMySecondDto = await Isource.BuildAdapter().AdaptToTypeAsync<MySecondDto>(); // working


Update:
It working

@DocSvartz
Copy link

@crates-barrels

Anyway, I expected both methods to behave the same way, but I understand this is not the case because the underlying code is based on different things (runtime using .GetType() and the other using generics)?

Yes, These adapters detect the source type differently.

Most likely you will need to register all implementations that may be at the source, as in example.

TypeAdapterConfig<IMyInterface, MyDto>.NewConfig()
.Include<MyImplementation, MyDto>()
.Include<MyImplementation1, MyDto>()
.Include<MyImplementation2, MyDto>();
......

@DocSvartz
Copy link

@andrerav @stagep
Technically it is possible to do the SourceType calculation
on an Instance basis when using this adapter .

Of the obvious problems that may arise are:

  1. handling null value;
  2. I do not fully understand whether AdaptToTypeAsync will actually work asynchronously.

What do you think about this?

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

2 participants