SData的网址是https://github.com/knat/SData。
数据交换方案可以分为两类:有纲要(schema)的和无纲要的。有纲要的数据交换方案有Google的Protocol Buffers,Microsoft的Bond以及SData,纲要编译器在编译时刻把纲要与编程语言进行映射,也就是通过纲要生成编程语言代码,此类方案是静态类型化的。无纲要的数据交换方案有JSON(我知道存在JSON schema,但它并不在编译阶段起作用),序列化器(serializer)在运行时刻把数据与编程语言进行映射,此类方案是动态类型化的。静态类型化的数据交换方案的优点是类型安全和高性能,缺点是不够灵活,这和静态类型化的编程语言与动态类型化的编程语言的差异类似。
SData的纲要语言优雅强大,面向对象,类型丰富,代码生成机制优美灵活。下面是通过示例来介绍SData。
1)你需要Visual Studio 2015。
2)下载并安装最新的SData VSIX package(SData-*.vsix)。
3)打开VS 2015,新建一个Console Application,卸载项目并编辑csproj文件,将下面的代码插入到文件末尾:
<!--Begin SData-->
<Import Project="$([System.IO.Directory]::GetFiles($([System.IO.Path]::Combine($([System.Environment]::GetFolderPath(SpecialFolder.LocalApplicationData)), `MicrosoftVisualStudio14.0Extensions`)), `SData.targets`, System.IO.SearchOption.AllDirectories))" />
<!--End SData-->
4)加载项目,打开"Add New Item"对话框 -> Visual C# Items -> SData, 新建一个SData纲要文件,将下面的代码拷贝到该文件中:
//Biz.sds。SData纲要文件的扩展名是sds
namespace "http://example.com/business"//名称空间由URI标识
{
class Person abstract/*抽象类不能拥有实例*/ key Id//指定某属性为类的键,该属性值必须唯一
{
Id/*属性名*/ as Int32//属性类型
Name as String
Phones as list<String>//list是个有序集合
RegDate as nullable<DateTimeOffset>//可空类型可以接受null值
}
class Customer extends Person//继承
{
//每个属性在类中必须拥有唯一的名字
Reputation as Reputation
Orders as nullable<set<Order>>//set是个无序集合,每个条目必须唯一,即Order.Id必须唯一
}
enum Reputation as Int32//枚举的underlying类型
{
None = 0
Bronze = 1
Silver = 2
Gold = 3
Bad = -1
}
class Order key Id
{
Id as Int64
Amount as Decimal
IsUrgent as Boolean
}
class Supplier extends Person
{
BankAccount as String
Products as map<Int32/*key*/, String/*value*/>//map是个无序的key-value集合,每个key必须唯一
}
}
namespace "http://example.com/business/api"
{
//要引用另一个名称空间的成员,需使用import指令
import "http://example.com/business"/*名称空间URI*/ as biz/*为URI取个别名,可选*/
class DataSet
{
People as set<Person>
ETag as Binary
}
}
编译项目时,SData纲要编译器会检查纲要文件的正确性:
5)将SData runtime library NuGet package添加到项目中:
PM> Install-Package SData -Pre
6)在C#文件中,使用SData.SchemaNamespaceAttribute
特性指定纲要名称空间到C#名称空间的映射:
//Program.cs
using SData;
[assembly: SchemaNamespace("http://example.com/business"/*纲要名称空间URI*/,
"Example.Business"/*C#名称空间名字*/)]
//所有的纲要名称空间必须被映射
[assembly: SchemaNamespace("http://example.com/business/api", "Example.Business.API")]
编译项目时,SData纲要编译器在检查了纲要文件的正确性后,会解析C#文件,并在__SDataGenerated.cs文件中生成代码,打开并查看该文件。
7)使用生成的代码非常简单,将下面的代码拷贝到Program.cs中:
//Program.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using SData;
using Example.Business;
using Example.Business.API;
[assembly: SchemaNamespace("http://example.com/business", "Example.Business")]
[assembly: SchemaNamespace("http://example.com/business/api", "Example.Business.API")]
class Program
{
static void Main()
{
//重要:在程序初始化时调用SData_Assembly_Name.Initialize()以初始化元数据
SData_ConsoleApplication1.Initialize();
var ds = new DataSet
{
People = new HashSet<Person>
{
new Customer
{
Id = 1, Name = "Tank", RegDate = DateTimeOffset.Now,
Phones = new List<string> { "1234567", "2345678"},
Reputation = Reputation.Bronze,
Orders = new HashSet<Order>
{
new Order { Id = 1, Amount = 436.99M, IsUrgent = true},
new Order { Id = 2, Amount = 98.77M, IsUrgent = false},
}
},
new Customer
{
Id = 2, Name = "Mike",
Phones = new List<string>(),
Reputation = Reputation.Gold,
},
new Supplier
{
Id = 3, Name = "Eric", RegDate = DateTimeOffset.UtcNow,
Phones = new List<string> {"7654321" },
BankAccount="11223344", Products = new Dictionary<int, string>
{
{ 1, "Mountain Bike" },
{ 2, "Road Bike" },
}
}
},
ETag = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 },
};
using (var writer = new StreamWriter("DataSet.txt"))
{
ds.Save(writer, " ", "
");
}
DataSet result;
var context = new LoadingContext();
using (var reader = new StreamReader("DataSet.txt"))
{
if (!DataSet.TryLoad("**DataSet.txt**", //filePath只是个标识符
reader, context, out result))
{
foreach (var diag in context.DiagnosticList)
{
Console.WriteLine(diag.ToString());
}
Debug.Assert(false);
}
}
}
}
8)打开并查看DataSet.txt(注释是我添加的):
//SData数据格式示例
//一个数据文件必须包含且仅包含一个类数据
<a0/*alias*/ = @"http://example.com/business"/*URI*/> {//类DataSet的数据
People = [//list或set类型的数据
//因为类Person是抽象的,类型指示器'(alias::className)'用来指定数据的类类型
(a0::Customer) {
Id = 1,
Name = @"Tank",
Phones = [
@"1234567",
@"2345678",
],
RegDate = "2015-07-31T11:19:26.7854059+08:00",
Reputation = a0::Reputation.Bronze,
Orders = [
{
Id = 1,
Amount = 436.99,
IsUrgent = true,
},
{
Id = 2,
Amount = 98.77,
IsUrgent = false,
},
],
},
(a0::Customer) {
Id = 2,
Name = @"Mike",
Phones = [
],
Reputation = a0::Reputation.Gold,
//如果某属性的类型是nullable,则该属性可以缺失,否则必须出现
//允许未知的属性
},
(a0::Supplier) {
Id = 3,
Name = @"Eric",
Phones = [
@"7654321",
],
RegDate = "2015-07-31T03:19:26.8010317+00:00",
BankAccount = @"11223344",
Products = #[//map类型的数据
1 = @"Mountain Bike",
2 = @"Road Bike",
],
},
],
ETag = "AQIDBAUGBwg=",
}
9)在行var context = new LoadingContext();
设置一个断点,当程序运行到断点时,打开修改保存DataSet.txt文件,比如删除行Name = @"Tank",
,因为属性Name
的类型不是nullable
,即该属性是必须的,TryLoad()
将会失败,下面的诊断信息将打印在控制台:
Error -293: Property 'Name' missing.
**DataSet.txt**: (23,9)-(23,9)
10)因为每个生成的C#类都标注了partial
修饰符,自定义代码可以添加到C#类中:
//my.cs
namespace Example.Business
{
partial class Person : SomeClass, ISomeInterface
{
public int MyProperty { get; set; }
public abstract void MyMethod();
}
partial class Customer
{
//注意:非抽象类必须有无参构造方法
public override void MyMethod() { }
}
}
11)可以添加自定义验证:
//my.cs
using System;
using SData;
public class MyLoadingContext : LoadingContext
{
public bool CheckCustomerReputation { get; set; }
public override void Reset()
{
base.Reset();
//...
}
}
public enum MyDiagnosticCode
{
PhonesIsEmpty = 1,
BadReputationCustomer,
}
namespace Example.Business
{
partial class Person
{
//OnLoading() is called by the serializer just after the object is created
private bool OnLoading(LoadingContext context, TextSpan textSpan)
{
Console.WriteLine("Person.OnLoading()");
return true;
}
//OnLoaded() is called just after all properties are set
private bool OnLoaded(LoadingContext context, TextSpan textSpan)
{
Console.WriteLine("Person.OnLoaded()");
if (Phones.Count == 0)
{
context.AddDiagnostic(DiagnosticSeverity.Error,
(int)MyDiagnosticCode.PhonesIsEmpty, "Phones is empty.", textSpan);
return false;
}
return true;
//if error diagnostics are added to the context, the method must return false.
//if any OnLoading() or OnLoaded() returns false, TryLoad() will return false immediately
}
}
partial class Customer
{
//the serializer will call base method(Person.OnLoading()) first
private bool OnLoading(LoadingContext context, TextSpan textSpan)
{
Console.WriteLine("Customer.OnLoading()");
return true;
}
//the serializer will call base method(Person.OnLoaded()) first
private bool OnLoaded(LoadingContext context, TextSpan textSpan)
{
Console.WriteLine("Customer.OnLoaded()");
var myContext = (MyLoadingContext)context;
if (myContext.CheckCustomerReputation && Reputation == Business.Reputation.Bad)
{
context.AddDiagnostic(DiagnosticSeverity.Warning,
(int)MyDiagnosticCode.BadReputationCustomer, "Bad reputation customer.",
textSpan);
//if non-error diagnostics are added to the context, the method should return true.
}
return true;
}
}
}
//Program.cs
//...
var context = new MyLoadingContext() { CheckCustomerReputation = true };
using (var reader = new StreamReader("DataSet.txt"))
{
if (!DataSet.TryLoad("**DataSet.txt**", reader, context, out result))
//...
12)使用SData.SchemaClassAttribute
特性将纲要类显式映射到C#类,使用SData.SchemaPropertyAttribute
特性将纲要属性显式映射到C#属性/域,SData纲要编译器会解析这些特性:
//my.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using SData;
namespace Example.Business
{
[SchemaClass("Person"/*schema class name*/)]
partial class Contact
{
[SchemaProperty("RegDate"/*schema property name*/)]
public DateTimeOffset? RegistrationDate { get; internal set; }
//same-named schema property and C# property/field are mapped implicitly
public string Name { get; internal set; }
[SchemaProperty("Phones")]
private Collection<string> _phones;
public Collection<string> Phones
{
get { return _phones ?? (_phones = new Collection<string>()); }
}
//list<T> can be mapped to System.Collections.Generic.ICollection<T> or implementing class
//set<T> can be mapped to System.Collections.Generic.ISet<T> or implementing class
//map<TKey, TValue> can be mapped to System.Collections.Generic.IDictionary<TKey, TValue>
// or implementing class
}
//same-named schema class and C# class are mapped implicitly
partial class Supplier
{
public IDictionary<int, string> Products { get; internal set; }
}
}
namespace Example.Business.API
{
partial class DataSet
{
[SchemaProperty("People")]
public ISet<Contact> Contacts { get; set; }
}
}
SData.SchemaNamespaceAttribute
,SData.SchemaClassAttribute
和SData.SchemaPropertyAttribute
是编译时特性,它们与运行时无关,这算是元编程。
更多信息请访问https://github.com/knat/SData。