How to define AutoMapper mapping outside code i.e.

2019-02-11 05:23发布

问题:

EDIT: Originally I intended to use AutoMapper to achieve my goal, but I had to learn that AutoMapper is not intended to work that way. It gives you the possibility to create profiles but in my case (fully configurable) I would need for each parameter combination one profile, so I came up with an own approach, see answers.

From the AutoMapper wiki I learned to create a simple mapping like

    Mapper.CreateMap<CalendarEvent, CalendarEventForm>().ForMember(dest => dest.Title, opt => opt.MapFrom(src => src.Title));
    Mapper.CreateMap<CalendarEvent, CalendarEventForm>().ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.EventDate.Date));
    Mapper.CreateMap<CalendarEvent, CalendarEventForm>().ForMember(dest => dest.EventHour, opt => opt.MapFrom(src => src.EventDate.Hour));
    Mapper.CreateMap<CalendarEvent, CalendarEventForm>().ForMember(dest => dest.EventMinute, opt => opt.MapFrom(src => src.EventDate.Minute));

For two classes like

public class CalendarEvent
{
    public DateTime EventDate;
    public string Title;
}

public class CalendarEventForm
{
    public DateTime EventDate { get; set; }
    public int EventHour { get; set; }
    public int EventMinute { get; set; }
    public string Title { get; set; }
}

I was now wondering if there is a possibility to define the mapping externally i.e. in an XML file like

<ObjectMapping>
<mapping>
    <src>Title</src>
    <dest>Tile</dest>
</mapping>
<mapping>
    <src>EventDate.Date</src>
    <dest>EventDate</dest>
</mapping>
<mapping>
    <src>EventDate.Hour</src>
    <dest>EventHour</dest>
</mapping>
<mapping>
    <src>EventDate.Minute</src>
    <dest>EventMinute</dest>
</mapping>

and by that influence the creation of the map (XML isn't a reqirement, can be everything else too). For simplicity say types are no issue, so src and dest should be the same otherwise it is ok to fail. The idea behind this is to be very flexible in what should be mapped and where it should be mapped. I was thinking about reflection to get property values based on its name, but this seems to not work. I'm also not sure if this makes sense at all or if I'm missing something important, so help and ideas are appreciated.

回答1:

Finally, I implemented the original requirement by my own although I didn't need it anyways (requirement changed). I'll provide the code here in case somebody needs it (and more or less as proof of concept, as a lot of improvement can still be done) or is interested in in, bear in mind, that the XML is a error prone, crucial component in this approach, property names and types have to match exactly, otherwise it wont work, but with a little GUI to edit the file this should be achieveable (I mean not editing the file manually).

I used code from here and here and added class PropertyMapping to store the mappings read from the XML and also classes Foo and Bar to create a nested data structure, to copy to.

Anyways here is the code, maybe it helps somebody some time:

Main:

public class Program
{
    public static void Main(string[] args)
    {
        // Model
        var calendarEvent = new CalendarEvent
        {
            EventDate = new DateTime(2008, 12, 15, 20, 30, 0),
            Title = "Company Holiday Party"
        };

        MyObjectMapper mTut = new MyObjectMapper(@"SampleMappings.xml");

        Console.WriteLine(string.Format("Result MyMapper: {0}", Program.CompareObjects(calendarEvent, mTut.TestMyObjectMapperProjection(calendarEvent))));

        Console.ReadLine();
    }

    public static bool CompareObjects(CalendarEvent calendarEvent, CalendarEventForm form)
    {
        return calendarEvent.EventDate.Date.Equals(form.EventDate) &&
               calendarEvent.EventDate.Hour.Equals(form.EventHour) &&
               calendarEvent.EventDate.Minute.Equals(form.EventMinute) &&
               calendarEvent.Title.Equals(form.Title);
    }
}

Mapper implementation:

public class MyObjectMapper
{
    private List<PropertyMapping> myMappings = new List<PropertyMapping>();

    public MyObjectMapper(string xmlFile)
    {
        this.myMappings = GenerateMappingObjectsFromXml(xmlFile);
    }

    /*
     * Actual mapping; iterate over internal mappings and copy each source value to destination value (types have to be the same)
     */ 
    public CalendarEventForm TestMyObjectMapperProjection(CalendarEvent calendarEvent)
    {
        CalendarEventForm calendarEventForm = new CalendarEventForm();

        foreach (PropertyMapping propertyMapping in myMappings)
        {
            object originalValue = GetPropValue(calendarEvent,propertyMapping.FromPropertyName);

            SetPropValue(propertyMapping.ToPropertyName, calendarEventForm, originalValue);
        }

        return calendarEventForm;
    }
    /*
     * Get the property value from the source object
     */ 
    private object GetPropValue(object obj, String compoundProperty)
    {
        foreach (String part in compoundProperty.Split('.'))
        {
            if (obj == null) { return null; }

            Type type = obj.GetType();
            PropertyInfo info = type.GetProperty(part);
            if (info == null) { return null; }

            obj = info.GetValue(obj, null);
        }
        return obj;
    }
    /*
     * Set property in the destination object, create new empty objects if needed in case of nested structure
     */ 
    public void SetPropValue(string compoundProperty, object target, object value)
    {
        string[] bits = compoundProperty.Split('.');
        for (int i = 0; i < bits.Length - 1; i++)
        {
            PropertyInfo propertyToGet = target.GetType().GetProperty(bits[i]);

            propertyToGet.SetValue(target, Activator.CreateInstance(propertyToGet.PropertyType));

            target = propertyToGet.GetValue(target, null);               
        }
        PropertyInfo propertyToSet = target.GetType().GetProperty(bits.Last());
        propertyToSet.SetValue(target, value, null);
    }              

    /*
     * Read XML file from the provided file path an create internal mapping objects
     */ 
    private List<PropertyMapping> GenerateMappingObjectsFromXml(string xmlFile)
    {
        XElement definedMappings = XElement.Load(xmlFile);
        List<PropertyMapping> mappings = new List<PropertyMapping>();

        foreach (XElement singleMappingElement in definedMappings.Elements("mapping"))
        {
            mappings.Add(new PropertyMapping(singleMappingElement.Element("src").Value, singleMappingElement.Element("dest").Value));
        }

        return mappings;
    } 
}

My model classes:

public class CalendarEvent
{
    public DateTime EventDate { get; set; }
    public string Title { get; set; }
}

public class CalendarEventForm
{
    public DateTime EventDate { get; set; }
    public int EventHour { get; set; }
    public int EventMinute { get; set; }
    public string Title { get; set; }
    public Foo Foo { get; set; }
}

public class Foo
{
    public Bar Bar { get; set; }

}

public class Bar
{
    public DateTime InternalDate { get; set; }

}

Internal mapping representation:

public class PropertyMapping
{
    public string FromPropertyName;
    public string ToPropertyName;

    public PropertyMapping(string fromPropertyName, string toPropertyName)
    {
        this.FromPropertyName = fromPropertyName;
        this.ToPropertyName = toPropertyName;
    }
}

Sample XML configuration:

<?xml version="1.0" encoding="utf-8" ?>
    <ObjectMapping>
      <mapping>
        <src>Title</src>
        <dest>Title</dest>
       </mapping>
      <mapping>
        <src>EventDate.Date</src>
        <dest>EventDate</dest>
      </mapping>
      <mapping>
        <src>EventDate.Hour</src>
        <dest>EventHour</dest>
      </mapping>
      <mapping>
        <src>EventDate.Minute</src>
        <dest>EventMinute</dest>
      </mapping>
      <mapping>
        <src>EventDate</src>
        <dest>Foo.Bar.InternalDate</dest>
      </mapping>
     </ObjectMapping>


回答2:

You don't want to do what you are asking about. Like @Gruff Bunny says, automapper already has the Profile class which essentially does all of the configuration you are looking for.

Why don't you want to do this with an XML (or other configuration) file?

Firstly, because you will lose the strongly-typed nature of the automapper configurations. You could write code to parse an XML or any other type of file to read the mappings and then call CreateMap based on the textual mappings. But if you do this, then you really need a unit test for each configuration to make sure no exceptions will be thrown at runtime.

Secondly, you say that you want to configure this at runtime. But simply replacing the configuration file will not be sufficient. In order for the CreateMap methods to be invoked again, you need an entry point, which is usually Global.asax in web applications. So after you replace the config file, you will still need to recycle or restart the app for the new config to take place. It won't happen automatically like it does when you replace web.config.

Thirdly, it slows startup time of your application when you do this. It is much faster for the CreateMap calls to happen straight from CLR code than to parse text for mappings.

How can you accomplish different mapping configurations without an XML or other external text file?

With AutoMapper.Profile. There is nothing in AutoMapper or .NET for that matter that says you have to declare your mappings in the same assembly as your application. You could create AutoMapper.Profile classes in another assembly which defines these mappings in a strongly-typed manner. You can then load these Profile classes when you bootstrap automapper. Look for the AutoAutoMapper library in my github account for some helpers that will make this easier.

public class CalendarEventProfile : AutoMapper.Profile
{
    public override void Configure()
    {
        CreateMap<CalendarEvent, CalendarEventForm>()
            //.ForMember(d => d.Title, o => o.MapFrom(s => s.Title)) //redundant, not necessary
            .ForMember(d => d.EventDate, o => o.MapFrom(s => s.EventDate.Date))
            .ForMember(d => d.EventHour, o => o.MapFrom(s => s.EventDate.Hour))
            .ForMember(d => d.EventMinute, o  => o.MapFrom(s => s.EventDate.Minute))
        ;
    }
}

By writing this class you have essentially externalized the mapping configuration in the same manner that you would have by putting it in an XML file. The biggest and most advantageous difference is that this is typesafe, whereas an XML configuration is not. So it is much easier to debug, test, and maintain.



回答3:

This is my implementation using an Excel file to store the mappings in (could be any source file). Works well if you need user to be able to modify how objects are mapped and gives you a visual on what is happening in your app.

https://github.com/JimmyOnGitHub/AutoMapper-LoadMappings/tree/master/LoadMappingExample