Search code examples
c#.netautomapper

can AutoMapper map to derived/runtime property types from Dictionary<string, object>?


AutoMapper includes support for mapping from a Dictionary<string, object> to an object graph per https://docs.automapper.org/en/stable/Dynamic-and-ExpandoObject-Mapping.html

It also has mapping inheritance support for runtime polymorphism per https://docs.automapper.org/en/stable/Mapping-inheritance.html#runtime-polymorphism

It's not clear if/how AutoMapper could support mapping parts of a property (child object) that's a derived instance at runtime and the parts we're trying to set aren't part of the base class.

I've tried various combinations of creating mappings from Dictionary<string, object> to Base and Derived with Include or IncludeAllDerived but no such luck thus far. Admittedly, those are documented more for the case of the source types having similar inheritance hierarchy instead of the more dynamic / less structured source of the Dictionary<string, object>

The "mapper.Map(propertyImport, propertyDestination);" call is just included to show that we can map to the derived instance at runtime just fine (makes sense, as the definition of the type of SomeProperty isn't involved in that scenario) but trying to map a dictionary to the parent object fails because it only supports mapping based on the declared property type of Base instead of the runtime type of Derived.

Note: we don't know ahead of time which properties will be in the dictionary and which properties may be derived types at runtime, so the goal is more to have it 'Just Work' based on the runtime types of the properties instead of being limited to what the defined types of the properties are.

Call to "mapper.Map(objectImport, objectDestination);" throws:

AutoMapper.AutoMapperMappingException: Error mapping types.

Mapping types:
Dictionary`2 -> SomeObjectWithAProperty
[...]
 ---> System.ArgumentOutOfRangeException: Cannot find member Baz of type UserQuery+Base. (Parameter 'name')

Source (running as a linqpad script, but should work fine as a console app AFAIK) is:

void Main()
{
    var objectDestination = new SomeObjectWithAProperty
    {
        SomeProperty = new Derived(100m, 10, "bar"),
    };
    var propertyDestination = objectDestination.SomeProperty;

    var objectImport = new Dictionary<string, object>
    {
        ["SomeProperty.Foo"] = 123,
        ["SomeProperty.Bar"] = "blah",
        ["SomeProperty.Baz"] = 200m,
    };
    var propertyImport = new Dictionary<string, object>
    {
        ["Foo"] = 123,
        ["Bar"] = "blah",
        ["Baz"] = 200m,
    };

    var mapper = new MapperConfiguration(mc =>
    {
        //mc.CreateMap<Dictionary<string, object>, Base>().IncludeAllDerived();
        //mc.CreateMap<Dictionary<string, object>, Derived>();
        //mc.CreateMap<Dictionary<string, object>, Base>()
        //    .IncludeAllDerived();
        //mc.CreateMap<Dictionary<string, object>, Base>()
        //    .Include<Dictionary<string, object>, Derived>();
    }).CreateMapper();

    mapper.Map(propertyImport, propertyDestination);
    mapper.Map(objectImport, objectDestination);
}

public class SomeObjectWithAProperty
{
    public Base SomeProperty { get; set; }
}

public record Base(int Foo, string Bar);
public record Derived(decimal Baz, int Foo, string Bar) : Base(Foo, Bar);

Solution

  • You have to add a custom map for member:

    mc.CreateMap<Dictionary<string, object>, SomeObjectWithAProperty>().ForPath(p => (p.SomeProperty as Derived).Baz , opt => opt.MapFrom(q => q["SomeProperty.Baz"] ));
    

    Entire code:

    using AutoMapper;
    
    class q77096931
    {
        public static void Main(string[] args)
        {
            var objectDestination = new SomeObjectWithAProperty
                {
                    SomeProperty = new Derived(100m, 10, "bar"),
                };
    
            var objectImport = new Dictionary<string, object>
            {
                ["SomeProperty.Bar"] = "insideproperty",
                ["SomeProperty.Baz"] = 200m,
            };
    
            var mapper = new MapperConfiguration(mc =>
            {
                mc.CreateMap<Dictionary<string, object>, SomeObjectWithAProperty>().ForPath(p => (p.SomeProperty as Derived).Baz , opt => opt.MapFrom(q => q["SomeProperty.Baz"] ));
            }).CreateMapper();
    
            Console.WriteLine("Original value of SomeProperty.Baz = " + (objectDestination.SomeProperty as Derived).Baz.ToString());
            mapper.Map(objectImport, objectDestination);
            Console.WriteLine("Mapped value of SomeProperty.Baz = " + (objectDestination.SomeProperty as Derived).Baz.ToString());
    
            
        }
    }
    

    Output:

    Original value of SomeProperty.Baz = 100
    Mapped value of SomeProperty.Baz = 200