• 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

  • 相关阅读:
    网络状态码含义——(比如204,304, 404, 504,502)
    Vue两个简易代替vuex的方法(eventBus,observable)
    单页面首屏加载慢解决方案
    前端监控和前端埋点
    ES5 和 ES6的继承
    mysql 隔离级别
    mysql 事务
    Seata
    Sentinel Dashboard 部署
    java 垃圾回器
  • 原文地址:https://www.cnblogs.com/knat/p/4691482.html
Copyright © 2020-2023  润新知