8

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.

hecko84
  • 1,224
  • 1
  • 16
  • 29
  • Automapper already "auto" maps based on property name so you needn't specify those specifically in your mappings. Also, when you do need to map a property because it has a different name, you don't need to keep recreating the mapping each time, you can just chain `ForMember` calls. – Jamie Dixon Oct 04 '13 at 10:55
  • I'm aware of the chaining, during my tries I just have modified it without changing back. Maybe I wasn't clear enough about my intention, but I also want to be able to configure mappings like src.Foo to dest.Bar and I also want to configure this at runtime (or let's say via a restart of the software) without changing my code – hecko84 Oct 04 '13 at 11:01
  • You'd have to roll your own code which reads the config and creates the mappings. Not trivial, but quite straightforward.. – stuartd Oct 04 '13 at 16:33
  • Take a peek at [Automapper profiles](https://github.com/AutoMapper/AutoMapper/wiki/Configuration). Whilst not helping with configuration via XML you could create different profiles with different mappings and load the appropriate profile using a DI container. – Gruff Bunny Oct 06 '13 at 09:23
  • @stuartd: You are saying that it is quite straightforward. Can you provide me with some beta on that? Right now, I'm not quite sure how to tackle this issue. – hecko84 Oct 07 '13 at 06:21
  • You ask in your question HOW to do it, my question for you is WHY do you want to do it this way? – danludwig Oct 07 '13 at 11:51

3 Answers3

9

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>
Community
  • 1
  • 1
hecko84
  • 1,224
  • 1
  • 16
  • 29
3

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.

danludwig
  • 46,965
  • 25
  • 159
  • 237
  • As far as I understand the Profile concept it is a way to create and manage various, lets say styles, of maps. The mapping per profile still has to be defined in code, I'm looking for a way to configure such a profile outside code/the actual application, like you change parameters in a web.config. – hecko84 Oct 07 '13 at 11:43
  • I know what you want to do, and I am telling you that you do not want to do it. But even if you did do it, you would need the XML configuration file in order for your application to run, correct? If that is the case, then the profile config would not be "outside of the actual application", even if it is not done in code. Like I said you can keep these profiles in a different assembly and then include that assembly as one of your app's dependencies, just like you would include the XML file as one of your app's dependencies. I suppose the biggest question I have is WHY do you want it this way? – danludwig Oct 07 '13 at 11:47
  • WHY I have to do it? I received some requirements on the application I have to implemented and one is that the mapping between these objects has to be defined by an operator who is running the application in that way(change after restart is ok). My requirement's interpretation ended in this question (I'm aware of type safety issues, horrible pitfalls in case there are any issues with the XML and so on), in the meanwhile I implemented a small proof-of-concept that is only based on reflection and is working. – hecko84 Oct 07 '13 at 13:38
  • Just some minutes ago I was told that the requirement is obsolete, so I now can do it in a more useful way :) Thank you anyway for your effort – hecko84 Oct 07 '13 at 13:39
  • @danludwig - A mapping change in an XML file may not require a recompile of the application, but of course your solution always requires a code change, which is simply not viable in certain domains. – O.O Feb 28 '14 at 20:09
  • @O.O - even if that is true, this was not a domain-specific question. Your comment also assumes that the deployed code is already bug-free, which may not be true. There are alternatives to continuous deployment that allow you to push compiled code changes without service disruption. Either way, I stand by the points that mapping from XML is brittle (which even the accepted answer confirms), more difficult to unit test, and uses more resources than are necessary to perform such a simple task. At the very least, the XML mapping approach still requires a restart of IIS which will disrupt service. – danludwig Mar 01 '14 at 03:28
2

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

Jimmy
  • 575
  • 5
  • 10