情境如下,我們定義一個抽象型別Notification保存排程發送通知的資料(包含JobType、ScheduleTime及Message),依發送管道分為電子郵件通知及簡訊通知,故實作成EmailNotification及SMSNotification兩個類別,並各自增加Email及PhoneNo屬性。

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
 
namespace CustCreate
{
    public enum Channels
    {
        Email, SMS
    }
    //通知作業
    public abstract class Notification
    {
        [JsonConverter(typeof(StringEnumConverter))]
        //通知管道
        public Channels JobType { get; protected set; }
        //排程時間
        public DateTime ScheduledTime { get; set; }
        //訊息內容
        public string Message { get; set; }
 
        protected Notification(DateTime time, string msg)
        {
            ScheduledTime = time;
            Message = msg;
        }
 
        protected Notification() { }
    }
    //電子郵件通知
    public class EmailNotification : Notification
    {
        public string Email { get; set; }
        public EmailNotification(DateTime time, string email, string msg)
            : base(time, msg)
        {
            JobType = Channels.Email;
            Email = email;
        }
    }
    //簡訊通知
    public class SMSNotification : Notification
    {
        //電話號碼
        public string PhoneNo { get; set; }
 
        public SMSNotification()
        {
            JobType = Channels.SMS;
        }
 
        public SMSNotification(DateTime time, string phoneNo, string msg)
            : base(time, msg)
        {
            JobType = Channels.SMS;
            PhoneNo = phoneNo;
        }
 
    }
}

依循上述資料結構,我們可輕易產生一個List<Notification>,其中包含EmailNotification及SMSNotification兩種不同型別的物件,用JsonConvert.SerializeObject()簡單轉成JSON:

    var jobs = new List<Notification>();
    jobs.Add(new EmailNotification(
        DateTime.UtcNow, "blah@bubu.blah.boo", "Test 1"));
    jobs.Add(new SMSNotification(
        DateTime.UtcNow, "0912345678", "Test 2"));
    Console.WriteLine(
        JsonConvert.SerializeObject(jobs, Formatting.Indented));
    Console.Read();

產生JSON字串如下:

[
  {
    "Email": "blah@bubu.blah.boo",
    "JobType": "Email",
    "ScheduledTime": "2013-12-13T13:43:25.6881876Z",
    "Message": "Test 1"
  },
  {
    "PhoneNo": "0912345678",
    "JobType": "SMS",
    "ScheduledTime": "2013-12-13T13:43:25.6891803Z",
    "Message": "Test 2"
  }
]

到目前為止非常輕鬆愉快吧? 然後呢? 以前學過的JsonConvert.DeserializeObject<T>都只能轉回單一型別,要怎麼把它反序列化還原回內含EmailNotification及SMSNotification不同型別物件的List<Notification>?

雖然不算常見情境,但這可難不倒偉大的Json.NET!

以下是程式範例,關鍵在於我們得繼承CustomCreationConverter<T>自訂一個轉換Notification物件的轉換器作為DesializeObject時的第二個參數。而在轉換器中可透過覆寫ReadJson()方法,依輸入JSON內容,傳回轉換後的物件。如此,我們就能依JobType動態建立不同型別的物件,再從JSON內容取得屬性值,達成反序列化還原出不同型別物件的目標。

另外,稍早定義物件時有預留伏筆,EmailNotification只提供有參數的建構式,而SMSNotification則支援無參數建構式(可沿用內建的屬性對應機制),以便示範兩種不同做法,細節部分請直接看程式碼及註解。

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
 
namespace CustCreate
{
    class Program
    {
        static string json = @"[
  {
    ""Email"": ""blah@bubu.blah.boo"",
    ""JobType"": ""Email"",
    ""ScheduledTime"": ""2013-12-13T13:43:25.6881876Z"",
    ""Message"": ""Test 1""
  },
  {
    ""PhoneNo"": ""0912345678"",
    ""JobType"": ""SMS"",
    ""ScheduledTime"": ""2013-12-13T13:43:25.6891803Z"",
    ""Message"": ""Test 2""
  }
]";
        //自訂轉換器,繼承CustomCreationConverter<T>
        class NotificationConverter : CustomCreationConverter<Notification>
        {
            //由於ReadJson會依JSON內容建立不同物件,用不到Create()
            public override Notification Create(Type objectType)
            {
                throw new NotImplementedException();
            }
            //自訂解析JSON傳回物件的邏輯
            public override object ReadJson(JsonReader reader, Type objectType, 
                object existingValue, JsonSerializer serializer)
            {
                JObject jo = JObject.Load(reader);
                //先取得JobType,由其決定建立物件
                string jobType = jo["JobType"].ToString();
                if (jobType == "Email")
                {
                    //做法1: 由JObject取出建構式所需參數建構物件
                    var target = new EmailNotification(
                            jo["ScheduledTime"].Value<DateTime>(),
                            jo["Email"].ToString(),
                            jo["Message"].ToString()
                        );
                    return target;
                }
                else if (jobType == "SMS")
                {
                    //做法2: 若物件支援無參數建構式,則可直接透過
                    //       serializer.Populate()自動對應屬性
                    var target = new SMSNotification();
                    serializer.Populate(jo.CreateReader(), target);
                    return target;
                }
                else
                    throw new ApplicationException("Unsupported type: " + jobType);
            }
        }
        static void Main(string[] args)
        {
            //JsonConvert.DeserializeObject時傳入自訂Converter
            var list = JsonConvert.DeserializeObject<List<Notification>>(
                json, new NotificationConverter());
            var item = list[0];
            Console.WriteLine("Type:{0} Email={1}", 
                item.JobType, (item as EmailNotification).Email);
            item = list[1];
            Console.WriteLine("Type:{0} PhoneNo={1}", 
                item.JobType, (item as SMSNotification).PhoneNo);
            Console.Read();
        }
    }
}

就醬,我們就從JSON完整重現原本的List<Notification>囉~

Type:Email Email=blah@bubu.blah.boo
Type:SMS PhoneNo=0912345678

Comments

# by Mars

請問如果email也提供無參數建構式是否就兩種type都可以使用SMS那段程式碼就可行了呢?

# by Jeffrey

to Mars, 是的,EmailNotification是刻意設計以展示必須有建構參數時的做法,否則可用SMSNotification的做法較簡單。

Post a comment