DotNetSlackers: ASP.NET News for lazy Developers

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));
            }
        }
    }

No comments:

Post a Comment