DotNetSlackers: ASP.NET News for lazy Developers

Showing posts with label dynamic and generic mapping. Show all posts
Showing posts with label dynamic and generic mapping. Show all posts

Tuesday, October 13, 2015

Automapper – Dynamic and Generic Mapping

In Automapper, we normally have 1 to 1 mapping defined but I have a case whereas the incoming stream as a json payload which then I cast it as a dynamic (using JObject parse) and in one of the property within the payload it defined which object that it needs to cast into. Lets take a look at the sample below

Input
Json payload to create a city
1
2
3
4
5
6
7
8
9
10
11
{
    "requestId": "C4910016-C30D-415C-89D3-D08D724429A6",
    "messageType": "CITY_CREATED",
    "categoryName": "categoryA",
    "metadata": {
        "city": "sydney",
        "state": "NSW",
        "postcode": "2000",
        "country": "australia"
    }
}
at the same time we can also have a Json payload to create a staff
1
2
3
4
5
6
7
8
9
10
11
12
{
  "requestId":"C4910016-C30D-415C-89D3-D08D724429A6",
  "messageType": "STAFF_CREATED",
  "categoryName": "categoryB",
  "staffDetail": {
    "name": "fransiscus",
    "dateOfBirth": "01/01/1950"
  },
  "location" : {
    "cityId" : "1"
  }
}
So what we are doing in here, all the message will go into payload property (it can contain any object) and we add some extra information/header/metadata on the parent level

Desired Outputs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
    "messageType": "CITY_CREATED",
    "payload": {
        "city": "sydney",
        "state": "NSW",
        "postcode": "2000",
        "country": "australia"
    },
    "provider": "abc",
    "providerRequestId": "C4910016-C30D-415C-89D3-D08D724429A6",
    "receivedAt": "2015-09-30T23:53:58.6118521Z",
    "lastUpdated": "2015-09-30T23:53:58.6128283Z",
    "lastUpdater": "Transformer",
    "attempt": 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
    "messageType": "STAFF_CREATED",
    "payload": {
        "staffName": "fransiscus",
        "dateOfBirth": "01/01/1950",
        "cityId": "1"
    },
    "provider": "abc",
    "providerRequestId": "C4910016-C30D-415C-89D3-D08D724429A6",
    "receivedAt": "2015-09-30T23:53:58.6118521Z",
    "lastUpdated": "2015-09-30T23:53:58.6128283Z",
    "lastUpdater": "Transformer",
    "attempt": 0
}
To map this to a concrete class 1:1 mapping is straight forward and easy. The problem here is that the “messageType” is the one that decided which object that it should be

Automapper Configuration:
1. POCO object
abstract class that stores all the metadata
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Metadata
    {
        public string MessageType { get; set; }
        public string Provider { get; set; }
        public string ProviderRequestId { get; set; }
        public DateTime ReceivedAt { get; set; }
        public DateTime LastUpdated { get; set; }
        public string LastUpdater { get; set; }
        public int Attempt { get; set; }
        public List<string> Errors { get; set; }
    }
1
2
3
4
5
6
7
public class City
   {
       public string CityName { get; set; }
       public string State { get; set; }
       public string PostCode { get; set; }
       public string Country { get; set; }
   }
1
2
3
4
5
6
public class StaffDetail
    {
        public string Name { get; set; }
        public string DateOfBirth { get; set; }
        public int CityId { get; set; }
    }
1
2
3
4
public class Message<T> : Metadata where T : class
    {
        public T Payload { get; set; }
    }

 
2. Lets create a TypeConverter for the base class which is Metadata and from this converter it will return the derived class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MetadataTypeConverter : TypeConverter<dynamic, Metadata>
    {
protected override Metadata ConvertCore(dynamic source)
        {
            Metadata metadata;
            var type = (string)source.messageType.Value;
            switch (type)
            {
                case "STAFF_CREATED":
                    metadata = new Message<StaffDetail> { Payload = Mapper.Map<dynamic, StaffDetail>(source) };
                    break;
                case "CITY_CREATED":
                    metadata = new Message<City> { Payload = Mapper.Map<dynamic, City>(source) };
                    break;
                default: throw new Exception(string.Format("no mapping defined for {0}", source.messageType.Value));
            }
            metadata.ProviderRequestId = source.requestId;
            metadata.Topic = string.Format("{0}.{1}.pregame",
                producerTopicName,
                source.categoryName ?? source.competition.categoryName);
            metadata.Provider = "My Provider";
            metadata.MessageType = source.messageType;
            metadata.ReceivedAt = DateTime.UtcNow;
            metadata.LastUpdated = DateTime.UtcNow;
            metadata.LastUpdater = "Transformer";
            metadata.Attempt = 0;
            return metadata;
        }
    }
3. Lets create a TypeConverter for the derived class which are Staff and City
1
2
3
4
5
6
7
8
9
10
11
12
13
public class CityTypeConverter : TypeConverter<dynamic, City>
    {
        protected override City ConvertCore(dynamic source)
        {
            City city = new City();
            city.CityName = source.metadata.city;
            city.State = source.metadata.state;
            city.Postcode = source.metadata.postcode;
            city.Country = source.metadata.country;
            return city;
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
public class StaffDetailTypeConverter : TypeConverter<dynamic, StaffDetail>
   {
       protected override StaffDetail ConvertCore(dynamic source)
       {
           StaffDetail staffdetail = new StaffDetail();
           staffdetail.Name = source.staffDetail.name;
           staffdetail.DateOfBirth = source.staffDetail.dateOfBirth;
           staffdetail.CityId = source.location.cityId;
           return staffdetail;
       }
   }
3. Define the Automapper mapping in the configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class WhafflMessageMapping : Profile
    {
        public override string ProfileName
        {
            get
            {
                return this.GetType().Name;
            }
        }
        protected override void Configure()
        {
            this.CreateMap()
                .ConvertUsing(new MetadataTypeConverter());
            this.CreateMap()
                .ConvertUsing(new StaffDetailTypeConverter());
            this.CreateMap()
                .ConvertUsing(new CityTypeConverter());
        }
        private Metadata BuildWhafflMessage(dynamic context)
        {
            var type = ((string)context.messageType.Value);
            switch (type)
            {
                case "STAFF_CREATED":
                    return new Message { Payload = Mapper.Map(context) };
                case "CITY_CREATED:
                    return new Message { Payload = Mapper.Map(context) };
                default: throw new Exception(string.Format("no mapping defined for {0}", context.messageType.Value));
            }
        }
    }