在日常的软件开发和使用过程中,我们发现同一套系统的同一配置项在不同的客户环境中是存在各种各样的差异的。在差异较为分散时,如何较好的管理这些差异,使得维护过程能够更加安全和快速,一直在这样那样的困扰着开发者和维护者。
例如,有系统中需要配置日志的记录路径和日志文件的命名方式。默认的日志是放在C盘目录下并以Log_XXX.txt进行命名。
<?xml version=""1.0"" encoding=""utf-8""?>
<LogSetting>
<Path>C:</Path>
<FileName>Log_{0}.txt</FileName>
</LogSetting>
但是在部署至客户时由于不同客户的需求需要对配置进行更改,但是不同的客户可能只改动其中的一部分。A客户希望将日志文件放在Z盘。B客户希望日志文件可以有自定义的命名开头如ZZZ_Log_XXX.txt。
这是我们就希望存在默认设置的配置文档的同时,又会存在一份差异内容的配置文档(以下称为增量文档),通过将两份配置文档的内容进行合并后产生不同客户的配置设定。如下:
<!--客户A的附加代码-->
<?xml version=""1.0"" encoding=""utf-8""?>
<LogSetting>
<Path>Z:</Path>
</LogSetting>
<!--客户B的附加代码-->
<?xml version=""1.0"" encoding=""utf-8""?>
<LogSetting>
<FileName>{1}_Log_{0}.txt</FileName>
</LogSetting>
在部署给不同需求的客户时,只需要将原始配置+不同客户的附加代码同时进行部署,即可实现不同客户间的不同定制配置。
通过如此配置,既可以实现相同内容的公用及增量内容的独立部署,也可以帮助配置管理人员方便的了解客户定制配置的内容。
在这样的背景下,我开发了 XPatchLib 对象增量数据序列化及反序列化器。
通过XPatchLib,可以实现原始对象实例与变更后对象实例间增量内容的序列化(记录增量内容为XML格式)及反序列化(将XML格式的增量内容合并至原始对象实例,使之与变更后对象实例间值相等或引用相等)
Nuget:
https://www.nuget.org/packages/XPatchLib/
Install-Package XPatchLib
帮助文件:https://guqiangjs.github.io/XPatchLib.Net.Doc/
基本用法和输出格式
让我们看一个很简单的例子。开始,我们定义了一个简单的CreditCard类:
public class CreditCard
{
public string CardExpiration { get; set; }
public string CardNumber { get; set; }
public override bool Equals(object obj)
{
CreditCard card = obj as CreditCard;
if (card == null)
{
return false;
}
return string.Equals(this.CardNumber, card.CardNumber)
&& string.Equals(this.CardExpiration, card.CardExpiration);
}
}
以下代码说明如何在两个不同内容之间的CreditCard实例间记录变化信息(增量内容)
首先创建两个类型相同,但内容不同的CreditCard对象。
CreditCard card1 = new CreditCard()
{
CardExpiration = "05/12",
CardNumber = "0123456789"
};
CreditCard card2 = new CreditCard()
{
CardExpiration = "05/17",
CardNumber = "9876543210"
};
调用XPatchSerializer对两个对象的增量内容进行序列化
XPatchSerializer serializer = new XPatchSerializer(typeof(CreditCard));
string context = string.Empty;
using (MemoryStream stream = new MemoryStream())
{
serializer.Divide(stream, card1, card2);
stream.Position = 0;
using (StreamReader stremReader = new StreamReader(stream, Encoding.UTF8))
{
context = stremReader.ReadToEnd();
}
}
经过执行以上代码,context的内容将为:
<?xml version=""1.0"" encoding=""utf-8""?>
<CreditCard>
<CardExpiration>05/17</CardExpiration>
<CardNumber>9876543210</CardNumber>
</CreditCard>
通过以上代码,我们实现了两个同类型的对象实例间,增量的序列化。记录了两个对象之间增量的内容。
下面将介绍如何将已序列化的增量内容附加回原始对象实例,使其与修改后的对象实例形成两个值相同的对象实例。
CreditCard card3 = null;
XPatchSerializer serializer = new XPatchSerializer(typeof(CreditCard));
using (StringReader reader = new StringReader(changedContext))
{
card3 = serializer.Combine(reader, card1) as CreditCard;
}
经过以上代码,可以使新增的 card3 实例的 CardExpiration 属性的值由card1实例中的 "05/12" 变更为增量内容中记录的 "05/17",CardNumber的值也由card1实例中的"0123456789"变更为了增量内容中记录的"9876543210"。如果使用值比较的方式比较 card3 和 card2 两个实例,会发现这两个实例完全相同。
操作简单类型集合
简单类型的集合
public class Warehouse
{
public string Name { get; set; }
public string Address { get; set; }
public string[] Items { get; set; }
}
之后创建两个不同内容的Warehouse对象实例。
Warehouse w1 = new Warehouse()
{
Name = "Company A",
Items = new string[] { "ItemA", "ItemB", "ItemC" }
};
Warehouse w2 = new Warehouse()
{
Name = "Company B",
Items = new string[] { "ItemA", "ItemC", "ItemD" }
};
增量内容结果为:
<?xml version="1.0" encoding="utf-8"?>
<Warehouse>
<Items>
<String Action="Remove">ItemB</String>
<String Action="Add">ItemD</String>
</Items>
<Name>Company B</Name>
</Warehouse>
以上内容表示了,w1与w2之间的增量为Name属性被变更为Company B,同时Items内容集合的ItemB被移除,并增加了ItemD。
那么如果我们定义了w3,将Items设为null会如何?
Warehouse w2 = new Warehouse()
{
Name = "Company B",
Items = null
};
结果如下:
<?xml version="1.0" encoding="utf-8"?>
<Warehouse>
<Items Action="SetNull" />
<Name>Company B</Name>
</Warehouse>
操作复杂类型
为说明复杂类型,我们创建了一个订单类型(OrderInfo),其中包含了自定义的地址信息类型(AddressInfo)
public class AddressInfo
{
public string Country { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public string Zip { get; set; }
public string City { get; set; }
public string Address { get; set; }
}
public class OrderInfo
{
public int OrderId { get; set; }
public decimal OrderTotal { get; set; }
public AddressInfo ShippingAddress { get; set; }
public string UserId { get; set; }
public DateTime Date { get; set; }
}
同样,我们创建两个不同内容的OrderInfo类型的实例
OrderInfo order1 = new OrderInfo(){
OrderId = 1,
OrderTotal = 200.45m,
ShippingAddress = new AddressInfo(){
Country = "China",
Name = "Customer",
Phone = "138-1234-5678",
Zip = "100000",
City = "Beijing",
Address = "",
},
UserId = "1234",
Date = new DateTime(2008,8,8)
};
OrderInfo order2 = new OrderInfo(){
OrderId = 2,
OrderTotal = 180.50m,
ShippingAddress = new AddressInfo(){
Country = "China",
Name = "Customer",
Phone = "138-1234-5678",
Zip = "100000",
City = "Shanghai",
Address = "",
},
UserId = "1234",
Date = new DateTime(2010,4,30)
};
增量内容为:
<?xml version="1.0" encoding="utf-8"?>
<OrderInfo>
<Date>2010-04-30T00:00:00</Date>
<OrderId>2</OrderId>
<OrderTotal>180.50</OrderTotal>
<ShippingAddress>
<City>Shanghai</City>
</ShippingAddress>
</OrderInfo>
操作复杂类型集合
操作复杂类型的集合时,系统通过定义的主键(PrimaryKey)信息来比对集合中的元素是否相同。
用在集合中的复杂类型均需要指定PrimaryKeyAttribute,否则在处理过程中会引发AttributeMissException异常。(也可以通过调用RegisterTypes方法来注册类型的主键)
同时PrimaryKeyAttribute中定义的主键 只能是值类型数据,否则在处理过程中会引发PrimaryKeyException异常。
[PrimaryKey("OrderId")]
public class OrderInfo
{
public int OrderId { get; set; }
public decimal OrderTotal { get; set; }
public AddressInfo ShippingAddress { get; set; }
public string UserId { get; set; }
public DateTime Date { get; set; }
}
public class OrderList
{
public List<OrderInfo> Orders { get; set; }
public string UserId { get; set; }
}
以上代码标记了OrderInfo类型对象的PrimaryKey是OrderInfo属性,在进行增量内容查找的过程中,会通过该属性的值在集合中的元素间进行查找。
同样,我们创建两个不同内容的OrderList类型的实例。
OrderList list1 = new OrderList(){
UserId = "1234",
Orders = new List<OrderInfo>(){
new OrderInfo(){
OrderId = 1,
OrderTotal = 200.45m,
UserId = "1234",
Date = new DateTime(2008,8,8)
},
new OrderInfo(){
OrderId = 2,
OrderTotal = 450.23m,
UserId = "1234",
Date = new DateTime(2008,8,8)
},
new OrderInfo(){
OrderId = 3,
OrderTotal = 185.60m,
UserId = "1234",
Date = new DateTime(2008,8,8)
}
}
};
OrderList list2 = new OrderList(){
UserId = "1234",
Orders = new List<OrderInfo>(){
new OrderInfo(){
OrderId = 1,
OrderTotal = 200.45m,
UserId = "1234",
Date = new DateTime(2008,8,8)
},
new OrderInfo(){
OrderId = 2,
OrderTotal = 230.89m,
UserId = "1234",
Date = new DateTime(2008,8,8)
},
new OrderInfo(){
OrderId = 4,
OrderTotal = 67.30m,
UserId = "1234",
Date = new DateTime(2008,8,8)
}
}
};
增量内容为:
<?xml version="1.0" encoding="utf-8"?>
<OrderList>
<Orders>
<OrderInfo Action="Remove" OrderId="3" />
<OrderInfo OrderId="2">
<OrderTotal>230.89</OrderTotal>
</OrderInfo>
<OrderInfo Action="Add">
<Date>2008-08-08T00:00:00</Date>
<OrderId>4</OrderId>
<OrderTotal>67.30</OrderTotal>
<UserId>1234</UserId>
</OrderInfo>
</Orders>
</OrderList>
RegisterTypes方法
在无法修改类型定义,为其增加或修改 PrimaryKeyAttribute 的情况下,可以在调用 Divide 或 Combine 方法前,调用此方法,传入需要修改的Type及与其对应的主键名称集合。
XPatchSerializer在处理时会按照传入的设置进行处理。
下面的示例使用 RegisterTypes 方法向 XPatchSerializer 注册待处理的类型的主键信息 。
首先有如下的类型定义OrderedItem,该类型并未标记PrimaryKeyAttribute,所以在正常处理集合类型时会抛出AttributeMissException异常。
public class OrderedItem
{
public string Description;
public string ItemName;
public decimal LineTotal;
public int Quantity;
public decimal UnitPrice;
public void Calculate()
{
LineTotal = UnitPrice * Quantity;
}
}
为规避此问题,我们可以在XPatchSerializer初始化之后,调用RegisterTypes方法向其显式注册类型及其对应的主键名称。
private void Divide(string filename)
{
List<OrderedItem> oldItems = new List<OrderedItem>();
List<OrderedItem> newItems = new List<OrderedItem>();
oldItems.Add(new OrderedItem() { ItemName = "Item A", Quantity = 10 });
oldItems.Add(new OrderedItem() { ItemName = "Item B", Quantity = 10 });
newItems.Add(new OrderedItem() { ItemName = "Item A", Quantity = 5 });
newItems.Add(new OrderedItem() { ItemName = "Item C", Quantity = 20 });
XPatchSerializer serializer = new XPatchSerializer(typeof(List<OrderedItem>));
//当OrderItem类型上未标记PrimaryKeyAttribute时,可以通过RegisterTypes方法向系统注册类型与主键的关系
Dictionary<Type, string[]> types = new Dictionary<Type, string[]>();
types.Add(typeof(OrderedItem), new string[] { "ItemName" });
serializer.RegisterTypes(types);
FileStream fs = new FileStream(filename, FileMode.Create);
XmlWriter writer = new XmlTextWriter(fs, Encoding.UTF8);
serializer.Divide(writer, oldItems, newItems);
writer.Close();
}
控制是否序列化默认值
为减小序列化结果的大小,XPatchSerializer的构造函数提供了是否序列化类型默认值的参数设置。
public XPatchSerializer(System.Type pType, bool pSerializeDefalutValue)
当原始对象实例为Null时,才会进行是否序列化默认值的判断。默认设置为不序列化默认值。
以下示例展示了,由不同的参数设定产生的增量结果内容的区别。
首先对原有的CreditCard对象进行修改,增加int类型的参数CardCode。
public class CreditCard
{
public string CardExpiration { get; set; }
public string CardNumber { get; set; }
public int CardCode { get; set; }
}
使用相同的对象实例
CreditCard card1 = new CreditCard()
{
CardExpiration = "05/12",
CardNumber = "0123456789"
CardCode = 0
};
按照默认设置构造XPatchSerializer实例,并进行序列化
XPatchSerializer serializer = new XPatchSerializer(typeof(CreditCard));
context的内容将为:
<?xml version=""1.0"" encoding=""utf-8""?>
<CreditCard>
<CardExpiration>05/17</CardExpiration>
<CardNumber>9876543210</CardNumber>
</CreditCard>
指定需要序列化默认值构造XPatchSerializer实例,并进行序列化
XPatchSerializer serializer = new XPatchSerializer(typeof(CreditCard), true);
context的内容为(多出了<CardCode>0</CardCode>):
<?xml version=""1.0"" encoding=""utf-8""?>
<CreditCard>
<CardCode>0</CardCode>
<CardExpiration>05/17</CardExpiration>
<CardNumber>9876543210</CardNumber>
</CreditCard>
控制DateTime类型的输出格式及处理
XPatchSerializer的构造函数提供了字符串与 System.DateTime 之间转换时,如何处理时间值的参数设置。(XmlDateTimeSerializationMode)
public XPatchSerializer(System.Type pType, System.Xml.XmlDateTimeSerializationMode pMode)