一. 我們的需求
你是否和我一樣有如下的困擾:
- 你需要將一個類轉換為XML或JSON存儲或傳輸,但總有你不想存儲或想特殊處理的字段,用序列化器自身的反射功能就看起來頗為雞肋了。
- 與MongoDB等鍵值對數據庫做交互,很多ORM框架都無效了,如何寫一個通用的數據接口層?而且不用去添加丑陋的"MongoIgnore"這樣的attribute?
- 你要將一個對象的屬性“拷貝”到另外一個對象,怎么做?C語言的拷貝構造函數?那太原始了。
- 界面綁定:總有一些綁定表達式,想通過動態的形式提交給框架,而不是寫死在xaml里,那是否又得在C#里寫一堆對象映射的代碼了?
大家肯定都遇到過和我相似的困擾,為了存儲或讀取某個對象(比如從文件或數據庫里讀取),你不得不寫下大量數據類型轉換和賦值語句,而且在程序中不能復用,這樣的代碼到處都是,真是代碼的臭味。
由於鍵值對的概念已經深入人心,於是便有了這樣一個叫做“字典序列化”的接口,該接口不是官方定義的,而是我根據實際需求總結的,同時我發現,它真的非常好用。
廢話不說,先看該接口的定義:
/// <summary> /// 進行字典序列化的接口,方便實現鍵值對的映射關系和重建 /// </summary> public interface IDictionarySerializable { /// <summary> /// 從數據到字典 /// </summary> /// <returns></returns> IDictionary<string, object> DictSerialize(Scenario scenario = Scenario.Database); /// <summary> /// 從字典到數據 /// </summary> /// <param name="dicts">字典</param> /// <param name="scenario">應用場景</param> void DictDeserialize(IDictionary<string, object> dicts, Scenario scenario = Scenario.Database); }
它只有兩個方法,一個是從字典中讀取數據到實體類,另一個是將實體類的數據寫入字典。你要做的,就是讓你的實體類實現這個接口。
值得注意的是,函數參數中有一個Scenario枚舉,該枚舉指定了轉換的場景,例如數據庫和用戶界面,在轉換時可能就有一定的區別,程序員可通過實際情況來確定,具體原因可以看下邊的部分。你可以傳入更多的參數,指示該接口的獨特序列化方法。
二. 如何使用?
先定義一個實體類:Artical, 它是一個簡單的POCO類型, 包含Title等六個屬性,為了節約篇幅,就不在此聲明它了。我們先看如下的代碼:
//define a entity named Artical with following propertynames public void DictDeserialize(IDictionary<string, object> dicts) { Title = (string)dicts["Title"]; PublishTime = (DateTime)dicts["PublishTime"]; Author = (string)dicts["Author"]; ArticalContent = (string)dicts["ArticalContent"]; PaperID = (int)dicts["PaperID"]; ClassID = (string)dicts["ClassID"]; } public IDictionary<string, object> DictSerialize() { var dict = new Dictionary<string, object> { { "Title", this.Title }, { "PublishTime", this.PublishTime.Value }, { "Author", this.Author }, { "ArticalContent", this.ArticalContent }, { "PaperID", this.PaperID.Value }, { "ClassID", this.ClassID } }; return dict; }
它指示了最常用的使用場景。可是讀者可能會注意到以下問題:
Title = (string)dicts["Title"];
(1) 如果字典中沒有對應的屬性項,那么通過索引器讀這個屬性是會報錯的。
(2) 即使有對應的屬性項,可它的值是Null,那么它可能覆蓋掉原本是正常的屬性值。
(3) 類型不統一:這個字典可能是由JSON序列化器提供的,或是由某數據庫的驅動提供的,那么,一個表示時間的字段,傳過來的類型可能是DateTime,也可能是string,如何不用丑陋的代碼做復雜的判斷和轉換呢?
對此,我們添加了一個IDictionary的擴展方法:
public static T Set<T>(this IDictionary<string, object> dict, string key, T oldValue) { object data = null; if (dict.TryGetValue(key, out data)) { Type type = typeof(T); if (type.IsEnum) { var index = (T)Convert.ChangeType(data, typeof(int)); return (index); } if (data == null) { return oldValue; } var value = (T)Convert.ChangeType(data, typeof(T)); return value; } return oldValue; }
於是,從字典中讀取數據(字典反序列化),就可以變得像下面這樣優雅了, 由於編譯器的自動類型推斷功能,連T都不用寫了。如果數據獲取失敗,原有值不受影響。
public void DictDeserialize(IDictionary<string, object> dicts, Scenario scenario = Scenario.Database) { this.Title = dicts.Set("Title", this.Title); this.PublishTime = dicts.Set("PublishTime", this.PublishTime); this.Author = dicts.Set("Author", this.Author); this.ArticalContent = dicts.Set("ArticalContent", this.ArticalContent); this.PaperID = dicts.Set("PaperID", this.PaperID); this.ClassID = dicts.Set("ClassID", this.ClassID); }
可是,又會有人問,如果某屬性的類型是List<T>,比如是string的數組呢?
這個問題會有點復雜,如何保存一個List<string>呢?如果是MongoDB,它的驅動是能直接存儲/返回List<string>的,但如果是文件存儲,一般會存儲成JSON格式,例如["a","b","c","d"]這樣的格式,因此我們可以定義如下的擴展方法:
public static List<T> SetArray<T>(this IDictionary<string, object> dict, string key, List<T> oldValue) { object data = null; if (dict.TryGetValue(key, out data)) { var list = data as List<T>; if (list != null) return list; string str = data.ToString(); if (str=="[]") return oldValue; if (str[0] != '[') return oldValue; return JsonConvert.Import<List<T>>(str); } return oldValue.ToList(); }
這里為了轉換JSON風格的string,用了第三方的JSON轉換庫:Jayrock.Json. 如果你自己有數組和string的轉換方法,不妨可以寫新的擴展方法。
通過該接口,可方便的實現對象間的數據拷貝:
public T Copy<T>(T oldValue) where T : IDictionarySerializable, new() { IDictionary<string, object> data = oldValue.DictSerialize(); var newValue = new T(); newValue.DictDeserialize(data); return newValue; }
三. 如何做數據庫接口層?
有了這樣的接口,我們就可以方便的做一個數據接口層,隔離數據庫和真實類型了,下面依舊以MongoDB為例。如果我們需要獲取表內所有的對象,可用如下的方法:
/// <summary> /// 獲取數據庫中的所有實體 /// </summary> /// <returns></returns> public List<T> GetAllEntitys<T>(string tableName) where T : IDictionarySerializable, new() { if (this.IsUseable == false) { return new List<T>(); } IMongoCollection<Document> collection = this.DB.GetCollection<Document>(tableName); var documents = collection.FindAll().Documents.ToList(); var list = new List<T>(); foreach (Document document in documents) { var user = new T(); user.DictDeserialize(document); list.Add(user); } return list; }
如果想更新或保存一個文檔,可以用如下的代碼:

/// <summary> /// 更新或增加一個新文檔 /// </summary> /// <param name="entity"></param> /// <param name="tableName">表名 </param> /// <param name="keyName"> </param> /// <param name="keyvalue"> </param> public void SaveOrUpdateEntity(IDictionarySerializable entity, string tableName, string keyName, object keyvalue) { if (this.IsUseable == false) { return; } IMongoCollection<Document> collection = this.DB.GetCollection<Document>(tableName); Document document = collection.FindOne(new Document { { keyName, keyvalue } }); if (document != null) { this.UpdateDocument(entity, document); collection.Save(document); } else { Document doc = this.GetNewDocument(entity); collection.Save(doc); } } private Document GetNewDocument(IDictionarySerializable entity) { IDictionary<string, object> datas = entity.DictSerialize(); var document = new Document(datas); return document; } private void UpdateDocument(IDictionarySerializable data, Document document) { IDictionary<string, object> datas = data.DictSerialize(); foreach (string key in datas.Keys) { document[key] = datas[key]; } }
可以通過泛型或者接口的方法,方便的讀取/存儲這些數據。
完整的驅動層代碼,在這里:

/// <summary> /// Mongo數據庫服務 /// </summary> public class MongoServiceBase { #region Constants and Fields protected IMongoDatabase DB; protected Mongo Mongo; private Document update; #endregion //鏈接字符串 #region Properties public string ConnectionString { get; set; } //數據庫名 public string DBName { get; set; } public bool IsUseable { get; set; } /// <summary> /// 本地數據庫位置 /// </summary> public string LocalDBLocation { get; set; } #endregion #region Public Methods /// <summary> /// 連接到數據庫,只需執行一次 /// </summary> public virtual bool ConnectDB() { var config = new MongoConfigurationBuilder(); config.ConnectionString(this.ConnectionString); //定義Mongo服務 this.Mongo = new Mongo(config.BuildConfiguration()); if (this.Mongo.TryConnect()) { this.IsUseable = true; this.update = new Document(); this.update["$inc"] = new Document("id", 1); this.DB = this.Mongo.GetDatabase(this.DBName); } else { this.IsUseable = false; } return this.IsUseable; } public T Copy<T>(T oldValue) where T : IDictionarySerializable, new() { IDictionary<string, object> data = oldValue.DictSerialize(); var newValue = new T(); newValue.DictDeserialize(data); return newValue; } /// <summary> /// 創建一個自增主鍵索引表 /// </summary> /// <param name="tableName">表名</param> public void CreateIndexTable(string tableName) { if (this.IsUseable == false) { return; } IMongoCollection idManager = this.DB.GetCollection("ids"); Document idDoc = idManager.FindOne(new Document("Name", tableName)); if (idDoc == null) { idDoc = new Document(); idDoc["Name"] = tableName; idDoc["id"] = 0; } idManager.Save(idDoc); } /// <summary> /// 獲取數據庫中的所有實體 /// </summary> /// <returns></returns> public List<T> GetAllEntitys<T>(string tableName) where T : IDictionarySerializable, new() { if (this.IsUseable == false) { return new List<T>(); } IMongoCollection<Document> collection = this.DB.GetCollection<Document>(tableName); List<Document> documents = collection.FindAll().Documents.ToList(); var list = new List<T>(); foreach (Document document in documents) { var user = new T(); user.DictDeserialize(document); list.Add(user); } return list; } /// <summary> /// 獲取一定范圍的實體 /// </summary> /// <param name="tableName"></param> /// <param name="type"></param> /// <param name="mount"></param> /// <param name="skip"></param> /// <returns></returns> public List<IDictionarySerializable> GetAllEntitys(string tableName, Type type) { if (this.IsUseable == false) { return new List<IDictionarySerializable>(); } List<Document> docuemts = this.DB.GetCollection<Document>(tableName).FindAll().Documents.ToList(); var items = new List<IDictionarySerializable>(); foreach (Document document in docuemts) { object data = Activator.CreateInstance(type); var suck = (IDictionarySerializable)data; suck.DictDeserialize(document); items.Add(suck); } return items; } /// <summary> /// 獲取一定范圍的實體 /// </summary> /// <param name="tableName"></param> /// <param name="type"></param> /// <param name="mount"></param> /// <param name="skip"></param> /// <returns></returns> public List<IDictionarySerializable> GetEntitys(string tableName, Type type, int mount, int skip) { if (this.IsUseable == false) { return new List<IDictionarySerializable>(); } List<Document> docuemts = this.DB.GetCollection<Document>(tableName).FindAll().Skip(skip).Limit(mount).Documents.ToList(); var items = new List<IDictionarySerializable>(); foreach (Document document in docuemts) { object data = Activator.CreateInstance(type); var suck = (IDictionarySerializable)data; suck.DictDeserialize(document); items.Add(suck); } return items; } public List<T> GetEntitys<T>(string tableName, int mount, int skip) where T : IDictionarySerializable, new() { if (this.IsUseable == false) { return new List<T>(); } ICursor<Document> collection = this.DB.GetCollection<Document>(tableName).Find(null).Skip(skip).Limit(mount); var users = new List<T>(); foreach (Document document in collection.Documents) { var user = new T(); user.DictDeserialize(document); users.Add(user); } return users; } /// <summary> /// 直接插入一個實體 /// </summary> /// <param name="user"></param> /// <param name="tableName"></param> public bool InsertEntity(IDictionarySerializable user, string tableName, string key, out int index) { if (this.IsUseable == false) { index = 0; return false; } IMongoCollection<Document> collection = this.DB.GetCollection<Document>(tableName); IMongoCollection idManager = this.DB.GetCollection("ids"); Document docID = idManager.FindAndModify(this.update, new Document("Name", tableName), returnNew: true); //下面三句存入數據庫 Document doc = this.GetNewDocument(user); doc[key] = docID["id"]; index = (int)docID["id"]; ; collection.Save(doc); return true; } public void RepairDatabase() { bool local = (this.ConnectionString.Contains("localhost") || this.ConnectionString.Contains("127.0.0.1")); if (local == false) { throw new Exception("MongoDB數據庫不在本地,無法啟動自動數據庫修復"); } var mydir = new DirectoryInfo(this.LocalDBLocation); FileInfo file = mydir.GetFiles().FirstOrDefault(d => d.Name == "mongod.lock"); if (file == null) { throw new Exception("修復失敗,您是否沒有安裝MongoDB數據庫"); } try { File.Delete(file.FullName); string str = CMDHelper.Execute("net start MongoDB"); } catch (Exception ex) { } } /// <summary> /// 更新或增加一個新文檔 /// </summary> /// <param name="entity"></param> /// <param name="tableName">表名 </param> /// <param name="keyName"> </param> /// <param name="keyvalue"> </param> public void SaveOrUpdateEntity( IDictionarySerializable entity, string tableName, string keyName, object keyvalue) { if (this.IsUseable == false) { return; } IMongoCollection<Document> collection = this.DB.GetCollection<Document>(tableName); Document document = collection.FindOne(new Document { { keyName, keyvalue } }); if (document != null) { this.UpdateDocument(entity, document); collection.Save(document); } else { Document doc = this.GetNewDocument(entity); collection.Save(doc); } } public bool TryFindEntity<T>(string tableName, string keyName, object keyvalue, out T result) where T : class, IDictionarySerializable, new() { IMongoCollection<Document> collection = this.DB.GetCollection<Document>(tableName); Document document = collection.FindOne(new Document { { keyName, keyvalue } }); if (document == null) { result = null; return false; } result = new T(); try { result.DictDeserialize(document); } catch (Exception ex) { XLogSys.Print.Error(ex); } return true; } #endregion #region Methods private Document GetNewDocument(IDictionarySerializable entity) { IDictionary<string, object> datas = entity.DictSerialize(); var document = new Document(datas); return document; } private void UpdateDocument(IDictionarySerializable data, Document document) { IDictionary<string, object> datas = data.DictSerialize(); foreach (string key in datas.Keys) { document[key] = datas[key]; } } #endregion }
四.一些問題
下面我們討論一些遇到的問題:
- 為什么不用反射來讀寫字段?而非要顯式的實現這兩個接口呢?
性能是首先要考慮的,而實現這兩個接口意味着更多的控制權和靈活性。 另外,對於很多鍵值對的數據庫來說,“Key”也是要占用存儲空間的,而且占用不少。如果實體類中字段屬性特別長,那么就會占用可觀的存儲,MongoDB就是如此。因此,在讀寫數據庫的場景中,應當保持Key較短。
- IDictionary<string, object> 的object是不是會有性能影響?
由於保存的是String-object的鍵值對形式,因此不可避免的存在裝箱和拆箱操作,在數據量大時,性能損耗肯定還是有的。不過,大部分數據庫驅動不也有大量這種操作么?畢竟只需要讀寫一次即可,性能損失可忽略不計,再說,還有更好的方法么?
- 是否能通過該接口實現LINQ查詢甚至SQL查詢?
應該是可以的,它建立了屬性與名稱的映射關系,因此可以通過該接口實現LINQ和SQL解析器,實現查詢等功能。
- 為什么不用官方的ISerializable接口呢?
原因如第一條,依靠attribute的方法沒有靈活性,同時,對二進制序列化等操作,實現起來也比較困難。
五. 其他應用場景
除了上面介紹的應用場景之外,該接口還能用於如下用途:
- 環境和模塊配置: 可以方便的將環境中所有的配置以鍵值對的方式存儲,並在需要的時候加載
- RPC: 由於實現了到XML/JSON的方便轉換,可以很容易的實現遠程過程調用
- Word文檔導出:該接口包含了鍵值對,因此在Word模板中,可以寫上Key, 通過該接口,一次性全部替換即可,非常方便。
- 界面綁定: 通過該接口實現動態的Binding語法。
一個方便的東西,應該是簡單的。通過該接口獲得的好處非常多,還需要大家去挖掘。值得提出的是,它也能應用在其他語言上,比如JAVA, 用HashMap<String,Object>來實現類似的需求。
如果有任何問題,歡迎討論!