SData:优雅的数据交换方案

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.SchemaNamespaceAttributeSData.SchemaClassAttributeSData.SchemaPropertyAttribute是编译时特性,它们与运行时无关,这算是元编程。

更多信息请访问https://github.com/knat/SData

原文地址:https://www.cnblogs.com/knat/p/4691482.html