系統之間的數據交互往往需要事先定義一些契約,在WCF中我們需要先編寫XSD文件,然後通過自動代碼生成工具自動生成C#對象。對於剛剛接觸契約的人來說,掌握XMLSpy之類的軟件之後確實比手寫XML效率要高,但還是有些學習成本的。此外XML的tag太多,如果設計的類型屬性過多,手寫XSD也不太現實,很難專注於設計。
於是我想能不能先用C#寫好類型,然後自動生成標准格式的XSD呢。經過三天左右的設計和實現,目前實現了以下功能:
1. 支持Class和Enum類型的設計
2. 支持基元類型、自定義類型、泛型列表、自定義類型數組等屬性
3. 支持自定義類型之間的依賴關系
4. 支持契約分組(指定Request/Response分到同一個xsd文件)
5. 支持契約匯總(對於自定義類型,最終體現在一個匯總xsd文件中,並自動引用其它xsd文件)
開源地址:https://github.com/CreateChen/XsdGen。我剛接觸SOA不久,目前只添加了xsd的minOccurs、maxOccurs等基本屬性,對於其它屬性並沒有全部集成進去,集成方式也非常簡單,在Attribute中添加相應的屬性,並在XsdBuilder.cs中添加相應的處理。
先看一下實現的效果,例如我定義了以下類型:FoodOrderRequest、FoodOrderResponse、PayType
using System; using System.Collections.Generic; using XsdAttribute; [assembly: XsdSchema( TargetNamespace = "http://xx.com/framework/soa/sample/v1", XmlNamespace = "http://xx.com/framework/soa/sample/v1", Namespace = "http://xx.com/framework/soa/sample/v1", Common = "http://xx.com/common/types/v1")] [assembly: XsdImport(Id = "SOACommonTypes", Namespace = "http://xx.com/common/types/v1", SchemaLocation = "SOACommonTypes_V1.0.0.xsd")] namespace TestDLL { [XsdComplexType(Annotation = "訂餐申請", FileGroup = "FoodOrder")] public class FoodOrderRequest { [XsdElement(MinOccurs = "1", Annotation = "餐館編號")] public int RestaurantId { get; set; } [XsdElement(MinOccurs = "1", Annotation = "餐館名稱")] public string RestaurantName { get; set; } [XsdElement(Annotation = "訂餐日期")] public DateTime OrderDate { get; set; } [XsdElement(MinOccurs = "0", MaxOccurs = "unbounded", Annotation = "食品編號")] public List<int> FoodId { get; set; } [XsdElement(MinOccurs = "1", Annotation = "業務類型")] public PayType BusinessType { get; set; } } [XsdComplexType(Annotation = "訂餐結果", FileGroup = "FoodOrder")] public class FoodOrderResponse { [XsdElement(MinOccurs = "1", Annotation = "訂單編號")] public int OrderId { get; set; } [XsdElement(Annotation = "預計送達時間")] public DateTime DeliveryTime { get; set; } } [XsdSimpleType(Annotation = "付款類型", FileGroup = "PayType")] public enum PayType { 現金, 支付寶, 微信, 網銀 } }
工具自動為我生成了3個文件:
<?xml version="1.0" encoding="utf-8"?> <xs:schema id="FoodOrder" targetNamespace="http://xx.com/framework/soa/sample/v1" elementFormDefault="qualified" attributeFormDefault="unqualified" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://xx.com/framework/soa/sample/v1" xmlns:ns="http://xx.com/framework/soa/sample/v1" xmlns:common="http://xx.com/common/types/v1"> <xs:include schemaLocation="PayType.xsd" /> <xs:import id="SOACommonTypes" schemaLocation="SOACommonTypes_V1.0.0.xsd" namespace="http://xx.com/common/types/v1" /> <xs:complexType name="FoodOrderRequestType"> <xs:annotation> <xs:documentation>訂餐申請</xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="RestaurantId" type="xs:int" minOccurs="1"> <xs:annotation> <xs:documentation>餐館編號</xs:documentation> </xs:annotation> </xs:element> <xs:element name="RestaurantName" type="xs:string" minOccurs="1"> <xs:annotation> <xs:documentation>餐館名稱</xs:documentation> </xs:annotation> </xs:element> <xs:element name="OrderDate" type="xs:dateTime"> <xs:annotation> <xs:documentation>訂餐日期</xs:documentation> </xs:annotation> </xs:element> <xs:element name="FoodId" type="xs:int" minOccurs="0" maxOccurs="unbounded"> <xs:annotation> <xs:documentation>食品編號</xs:documentation> </xs:annotation> </xs:element> <xs:element name="BusinessType" type="PayType" minOccurs="1"> <xs:annotation> <xs:documentation>業務類型</xs:documentation> </xs:annotation> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="FoodOrderResponseType"> <xs:annotation> <xs:documentation>訂餐結果</xs:documentation> </xs:annotation> <xs:sequence> <xs:element name="OrderId" type="xs:int" minOccurs="1"> <xs:annotation> <xs:documentation>訂單編號</xs:documentation> </xs:annotation> </xs:element> <xs:element name="DeliveryTime" type="xs:dateTime"> <xs:annotation> <xs:documentation>預計送達時間</xs:documentation> </xs:annotation> </xs:element> </xs:sequence> </xs:complexType> </xs:schema>
<?xml version="1.0" encoding="utf-8"?> <xs:schema id="PayType" targetNamespace="http://xx.com/framework/soa/sample/v1" elementFormDefault="qualified" attributeFormDefault="unqualified" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://xx.com/framework/soa/sample/v1" xmlns:ns="http://xx.com/framework/soa/sample/v1" xmlns:common="http://xx.com/common/types/v1"> <xs:import id="SOACommonTypes" schemaLocation="SOACommonTypes_V1.0.0.xsd" namespace="http://xx.com/common/types/v1" /> <xs:simpleType name="PayType" final="restriction"> <xs:restriction base="xs:string"> <xs:enumeration value="現金" /> <xs:enumeration value="支付寶" /> <xs:enumeration value="微信" /> <xs:enumeration value="網銀" /> </xs:restriction> </xs:simpleType> </xs:schema>
<?xml version="1.0" encoding="utf-8"?> <xs:schema id="TestDLL" targetNamespace="http://xx.com/framework/soa/sample/v1" elementFormDefault="qualified" attributeFormDefault="unqualified" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://xx.com/framework/soa/sample/v1" xmlns:ns="http://xx.com/framework/soa/sample/v1" xmlns:common="http://xx.com/common/types/v1"> <xs:import id="SOACommonTypes" schemaLocation="SOACommonTypes_V1.0.0.xsd" namespace="http://xx.com/common/types/v1" /> <xs:include schemaLocation="FoodOrder.xsd" /> <xs:include schemaLocation="PayType.xsd" /> <xs:element name="FoodOrderRequest" nillable="true" type="ns:FoodOrderRequestType" /> <xs:element name="FoodOrderResponse" nillable="true" type="ns:FoodOrderResponseType" /> </xs:schema>
[AttributeUsage(AttributeTargets.Assembly)] public class XsdSchema : Attribute { public string TargetNamespace { get; set; } public string XmlNamespace { get; set; } public string Namespace { get; set; } public string Common { get; set; } //匯總dll中的類型,生成總的xsd private string _packageId; /// <summary> /// 生成XSD的文件名稱 /// </summary> public string PackageId { get { return _packageId; } set { //去除文件名中非法字符 var regex = new Regex(@"\:|\;|\/|\\|\||\,|\*|\?|\""|\<|\>"); _packageId = regex.Replace(value, ""); } } public static XsdSchema Get(Assembly assembly) { return (XsdSchema) GetCustomAttribute(assembly, typeof (XsdSchema)); } }
前幾個是一些命名空間的定義,可以根據自己公司的業務規則進行設計。默認情況下,匯總文件的targetId、文件名稱以及生成的文件夾名稱都是PackageId決定,如果dll沒有指定該值,默認則是dll的Name。
除了定義Schema,每個xsd文件中可能還需要導入一些默認的公共類型,在這種情況下,可以為assembly添加如下Attribute:
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] public class XsdImport : Attribute { public string Id { get; set; } public string SchemaLocation { get; set; } public string Namespace { get; set; } public static IEnumerable<XsdImport> Get(Assembly assembly) { var attributes = GetCustomAttributes(assembly, typeof (XsdImport)); return attributes.Cast<XsdImport>(); } }
[AttributeUsage((AttributeTargets.Class))] public class XsdComplexType : Attribute { private string _fileGroup; /// <summary> /// 生成XSD的文件名稱 /// </summary> public string FileGroup { get { return _fileGroup; } set { //去除文件名中非法字符 var regex = new Regex(@"\:|\;|\/|\\|\||\,|\*|\?|\""|\<|\>"); _fileGroup = regex.Replace(value, ""); } } public string Annotation { get; set; } public static XsdComplexType Get(Type type) { return (XsdComplexType) GetCustomAttribute(type, typeof (XsdComplexType)); } }
其中必須指定FileGroup值,相同的FileGroup值,最後會生成到同一個xsd文件中。
[AttributeUsage((AttributeTargets.Property))] public class XsdElement : Attribute { public string MinOccurs { get; set; } public string MaxOccurs { get; set; } public string Annotation { get; set; } public static XsdElement Get(PropertyInfo propertyInfo) { return (XsdElement) GetCustomAttribute(propertyInfo, typeof (XsdElement)); } }
如果自定義類型的屬性是List或者Array,要將MaxOccurs設為大於0的數或者unbounded。
[AttributeUsage((AttributeTargets.Enum))] public class XsdSimpleType : Attribute { private string _fileGroup; /// <summary> /// 生成XSD的文件名稱 /// </summary> public string FileGroup { get { return _fileGroup; } set { //去除文件名中非法字符 var regex = new Regex(@"\:|\;|\/|\\|\||\,|\*|\?|\""|\<|\>"); _fileGroup = regex.Replace(value, ""); } } public string Annotation { get; set; } public static XsdSimpleType Get(Type type) { return (XsdSimpleType) GetCustomAttribute(type, typeof (XsdSimpleType)); } }
與XsdComplexType類似,相同的FileGroup類型,最後都歸到同一個xsd文件中。
public class XsdFile { public XsdFile() { Imports = new List<XElement>(); Elements = new List<XElement>(); } public XElement Schema { get; set; } /// <summary> /// <para>Assembly級別的Import</para> /// <para>自定義復合對象的Import</para> /// </summary> public List<XElement> Imports { get; set; } /// <summary> /// <para>自定義Class類型</para> /// <para>自定義枚舉類型</para> /// </summary> public List<XElement> Elements { get; set; } public XElement ToXML() { foreach (var import in Imports) { Schema.Add(import); } foreach (var element in Elements) { Schema.Add(element); } return Schema; } }
基本思路是Schema添加所有的Import和Element,最後生成XML。
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Xml.Linq; using XsdAttribute; namespace XsdGen { public class XsdBuilder { private readonly XNamespace _xs = "http://www.w3.org/2001/XMLSchema"; private Assembly _assembly; //Key:FileGroup, Value:對應的XsdFile private Dictionary<string, XsdFile> _xsdFiles; //自定義Class對象的聲明,用於生成匯總xsd private List<XElement> _elements; public void Build(Assembly assembly) { _assembly = assembly; _xsdFiles = new Dictionary<string, XsdFile>(); _elements = new List<XElement>(); XElement[] defaultImports = GetDefaultImports(_assembly).ToArray(); XsdSchema defaultSchema = XsdSchema.Get(_assembly); string directoryName = defaultSchema.PackageId ?? _assembly.GetName().Name; if (!Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } BuildTypes(_assembly); foreach (var item in _xsdFiles) { item.Value.Schema = GetSchema(defaultSchema, item.Key); foreach (var import in defaultImports) { item.Value.Imports.Add(import); } //生成XSD文件 string fileName = string.Format("{0}/{1}.xsd", directoryName, item.Key); item.Value.ToXML().Save(fileName); Console.WriteLine("Generate {0}", fileName); } //生成匯總XSD文件 var summaryXsdFile = BuildSummary(defaultSchema, defaultImports, directoryName); string summaryXsdFileName = string.Format("{0}/{1}.xsd", directoryName, directoryName); summaryXsdFile.ToXML().Save(summaryXsdFileName); Console.WriteLine("{0}Generate Summary{0}\n{1}", new String('=', 10), summaryXsdFileName); } private XElement GetSchema(XsdSchema xsdSchema, string id) { var schema = new XElement( _xs + "schema", new XAttribute("id", id), new XAttribute("targetNamespace", xsdSchema.TargetNamespace), new XAttribute("elementFormDefault", "qualified"), new XAttribute("attributeFormDefault", "unqualified"), new XAttribute(XNamespace.Xmlns + "xs", _xs.ToString()) ); if (!string.IsNullOrEmpty(xsdSchema.XmlNamespace)) schema.SetAttributeValue("xmlns", xsdSchema.XmlNamespace); if (!string.IsNullOrEmpty(xsdSchema.Namespace)) schema.SetAttributeValue(XNamespace.Xmlns + "ns", xsdSchema.Namespace); if (!string.IsNullOrEmpty(xsdSchema.Common)) schema.SetAttributeValue(XNamespace.Xmlns + "common", xsdSchema.Common); return schema; } private IEnumerable<XElement> GetDefaultImports(Assembly assembly) { var xsdImports = XsdImport.Get(assembly); return xsdImports.Select(xsdImport => new XElement( _xs + "import", new XAttribute("id", xsdImport.Id), new XAttribute("schemaLocation", xsdImport.SchemaLocation), new XAttribute("namespace", xsdImport.Namespace) )); } private void BuildTypes(Assembly assembly) { var types = assembly.GetTypes(); foreach (var type in types) { string fileGroup; if (type.IsClass) { var element = BuildElement(type); _elements.Add(element); //_xsdFiles[fileGroup].Elements.Add(element); var complexTypeElement = BuildComplexType(type, out fileGroup); _xsdFiles[fileGroup].Elements.Add(complexTypeElement); } else if (type.IsEnum) { var simpleTypeElement = BuildSimpleType(type, out fileGroup); _xsdFiles[fileGroup].Elements.Add(simpleTypeElement); } } } public XElement BuildElement(Type type) { //只有Request或者Response對象類型,末尾自動添加Type string name = (type.Name.EndsWith("Request") || type.Name.EndsWith("Response")) ? type.Name + "Type" : type.Name; return new XElement( _xs + "element", new XAttribute("name", type.Name), new XAttribute("nillable", true), new XAttribute("type", "ns:" + name) ); } private XElement BuildComplexType(Type type, out string fileGroup) { var xsdComplexType = XsdComplexType.Get(type); //添加XSD文件 fileGroup = xsdComplexType.FileGroup; SetDefaultFile(fileGroup); //只有Request或者Response對象類型,末尾自動添加Type string name = (type.Name.EndsWith("Request") || type.Name.EndsWith("Response")) ? type.Name + "Type" : type.Name; var complexTypeElement = new XElement( _xs + "complexType", new XAttribute("name", name) ); if (!string.IsNullOrEmpty(xsdComplexType.Annotation)) { complexTypeElement.Add(new XElement( _xs + "annotation", new XElement(_xs + "documentation", xsdComplexType.Annotation) )); } var sequenceElement = BuildSequence(type); AddProperties(type, sequenceElement); complexTypeElement.Add(sequenceElement); return complexTypeElement; } private XElement BuildSequence(Type type) { var sequence = new XElement(_xs + "sequence");return sequence; } private XsdFile BuildSummary(XsdSchema defaultSchema, XElement[] defaultImports, string packageId) { XsdFile xsdFile = new XsdFile(); xsdFile.Schema = GetSchema(defaultSchema, packageId); foreach (var import in defaultImports) { xsdFile.Imports.Add(import); } //include所有其它自動生成的xsd文件 foreach (var item in _xsdFiles) { xsdFile.Imports.Add(new XElement( _xs + "include", new XAttribute("schemaLocation", item.Key + ".xsd") )); } //添加dll中所有定義的element foreach (var item in _elements) { xsdFile.Elements.Add(item); } return xsdFile; } private void AddProperties(Type type, XElement sequenceElement) { var properties = type.GetProperties(); foreach (var propertyInfo in properties) { var typeName = Common.GetXsdTypeName(propertyInfo.PropertyType); var propertyElement = new XElement( _xs + "element", new XAttribute("name", propertyInfo.Name), new XAttribute("type", typeName) ); var xsdElement = XsdElement.Get(propertyInfo); if (xsdElement != null) { if (!string.IsNullOrEmpty(xsdElement.MinOccurs)) propertyElement.SetAttributeValue("minOccurs", xsdElement.MinOccurs); if (!string.IsNullOrEmpty(xsdElement.MaxOccurs)) propertyElement.SetAttributeValue("maxOccurs", xsdElement.MaxOccurs); if (!string.IsNullOrEmpty(xsdElement.Annotation)) propertyElement.Add(new XElement( _xs + "annotation", new XElement( _xs + "documentation", xsdElement.Annotation ) )); } //判斷是否自定義類型, 添加Import if (!typeName.StartsWith("xs:")) { var parentClassFileGroup = XsdComplexType.Get(type).FileGroup; var propertyClassFileGroup = Common.GetFileGroup(propertyInfo.PropertyType); if (parentClassFileGroup != propertyClassFileGroup) { string importXsd = propertyClassFileGroup + ".xsd"; //判斷是否已經存在該Import if (_xsdFiles[parentClassFileGroup].Imports.All(item => item.Attribute("schemaLocation").Value != importXsd)) { _xsdFiles[parentClassFileGroup].Imports.Add( new XElement( _xs + "include", new XAttribute("schemaLocation", importXsd) ) ); } } } sequenceElement.Add(propertyElement); } } private XElement BuildSimpleType(Type type, out string fileGroup) { var xsdSimpleType = XsdSimpleType.Get(type); //添加XSD文件 fileGroup = xsdSimpleType.FileGroup; SetDefaultFile(fileGroup); var simpleTypeElement = new XElement( _xs + "simpleType", new XAttribute("name", type.Name), new XAttribute("final", "restriction") ); var restrictionElement = new XElement( _xs + "restriction", new XAttribute("base", "xs:string") ); foreach (var val in Enum.GetNames(type)) { restrictionElement.Add( new XElement( _xs + "enumeration", new XAttribute("value", val) ) ); } simpleTypeElement.Add(restrictionElement); return simpleTypeElement; } private void SetDefaultFile(string fileGroup) { if (!_xsdFiles.ContainsKey(fileGroup)) { var xsdFile = new XsdFile(); _xsdFiles[fileGroup] = xsdFile; } } } }
XsdBuilder依次反射出Assembly、Class、Enum、Property的自定義Attribute,根據XSD的規則構造XElement。主要步驟在Builder函數中都有所體現。
現在寫接口契約就很爽了,只要定義好類生成DLL,通過XsdGen.exe就可輕松生成接口契約
http://www.codeproject.com/Articles/2933/Attributes-in-C
https://msdn.microsoft.com/en-us/library/84c42s56%28v=vs.110%29.aspx
https://msdn.microsoft.com/en-us/library/aa719879(v=vs.71).aspx
https://msdn.microsoft.com/en-us/library/y1375e30%28v=vs.110%29.aspx
http://stackoverflow.com/questions/3353699/using-reflection-to-get-all-classes-of-certain-base-type-in-dll
http://stackoverflow.com/questions/1936953/custom-assembly-attributes
http://stackoverflow.com/questions/1168532/xsd-definition-for-enumerated-value
https://msdn.microsoft.com/en-us/library/bb387075.aspx