• ASP.NET Web API编程——序列化与内容协商


    1 多媒体格式化器

    多媒体类型又叫MIME类型,指示了数据的格式。在HTTP协议中多媒体类型描述了消息体的格式。一个多媒体类型包括两个字符串:类型和子类型。

    例如:

    text/htmlimage/pngapplication/jsonapplication/pdf

    请求的Content-Type标头指定消息体的格式,指示接收者应如何解析消息体内容。

    例如:请求告知服务端请求数据类型为HTML, XHTML, or XML

    请求:Accept: text/html,application/xhtml+xml,application/xml

    响应:

    HTTP/1.1 200 OK

    Content-Length: 95267

    Content-Type: image/png

    多媒体类型为Web Api指明了如何序列化与反序列化HTTP消息体。Web API内建对XML, JSON, BSONform-urlencoded支持,可以创建多媒体格式化器来自定义格式化方式,自定义的格式化器继承自MediaTypeFormatterBufferedMediaTypeFormatter,其中MediaTypeFormatter使用异步的读写方法,BufferedMediaTypeFormatter使用同步的读写方法。

     

    例:创建CSV格式化器

    定义实体

        public class Product
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string Category { get; set; }
            public decimal Price { get; set; }
        }

    定义ProductCsvFormatter,继承自BufferedMediaTypeFormatter

    public class ProductCsvFormatter : BufferedMediaTypeFormatter
    {
        public ProductCsvFormatter()
        {
            // 添加被支持的多媒体类型
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
        }
    }

     

    重写CanWriteType方法,指明格式化器可序列化的类型

    public override bool CanWriteType(System.Type type)
    {
        //指明可序列化Product
        if (type == typeof(Product))
    {
            return true;
    }
        //指明可序列化IEnumerable<Product>
        else
        {
            Type enumerableType = typeof(IEnumerable<Product>);
            return enumerableType.IsAssignableFrom(type);
        }
    }

     

    重写CanReadType方法,指明格式化器可反序列化的类型

    public override bool CanReadType(Type type)
    {
        //设置为不支持反序列化
        return false;
    }

     

    重写WriteToStream方法,这个方法将序列化数据写入流,若要支持反序列化可重写ReadFromStream方法。

    public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
    {
        using (var writer = new StreamWriter(writeStream))
        {
            var products = value as IEnumerable<Product>;
            if (products != null)
            {
                foreach (var product in products)
                {
                    WriteItem(product, writer);
                }
            }
            else
            {
                var singleProduct = value as Product;
                if (singleProduct == null)
                {
                    throw new InvalidOperationException("Cannot serialize type");
                }
                WriteItem(singleProduct, writer);
            }
        }
    }
    
    // 帮助方法
    private void WriteItem(Product product, StreamWriter writer)
    {
        writer.WriteLine("{0},{1},{2},{3}", Escape(product.Id),
            Escape(product.Name), Escape(product.Category), Escape(product.Price));
    }
    
    static char[] _specialChars = new char[] { ',', '
    ', '
    ', '"' };
    
    private string Escape(object o)
    {
        if (o == null)
        {
            return "";
        }
        string field = o.ToString();
        if (field.IndexOfAny(_specialChars) != -1)
        {
            // Delimit the entire field with quotes and replace embedded quotes with "".
            return String.Format(""{0}"", field.Replace(""", """"));
        }
        else return field;
    }

     

    将多媒体格式化器添加到Web API管道(方法在WebApiConfig类中)

    public static void Register(HttpConfiguration config)
    {
        config.Formatters.Add(new ProductCsvFormatter()); 
    }

     

    字符编码

    多媒体格式化器支持多种编码,例如UTF-8ISO 8859-1

    public ProductCsvFormatter()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
    
        // 新的编码:
        SupportedEncodings.Add(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
        SupportedEncodings.Add(Encoding.GetEncoding("iso-8859-1"));
    }

     

    WriteToStream方法中添加选择编码方式的代码。如果支持反序列化,那么在ReadFromStream方法中同样添加选择编码方式的代码。

    public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
    {
        //调用MediaTypeFormatter.SelectCharacterEncoding选择编码方式,由于ProductCsvFormatter派生自MediaTypeFormatter,所以也就继承了SelectCharacterEncoding这个方法
        Encoding effectiveEncoding = SelectCharacterEncoding(content.Headers);
    
        using (var writer = new StreamWriter(writeStream, effectiveEncoding))
        {
            // Write the object (code not shown)
        }
    }

     

    2 JSON和XML的序列化

    Web API多媒体类型格式化器可以从HTTP消息体中读取CLR对象或将CLR对象写入消息体。Web API框架提供了JSON格式化器和XML格式化器,默认支持JSON和XML序列化。可以在请求的Accept首部字段指定接收的类型。

    例:指定返回JSON字符串

    HttpContent content = new StringContent(JsonConvert.SerializeObject(cont));
    content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
    request.Content = content;
    HttpResponseMessage response = client.SendAsync(request).Result;

    返回结果:

     

    例:指定返回XML字符串

    HttpContent content = new StringContent(JsonConvert.SerializeObject(cont));
    content.Headers.ContentType = new MediaTypeHeaderValue("application/xml");
    request.Content = content;
    HttpResponseMessage response = client.SendAsync(request).Result;

    返回结果:

     

    2.1 JSON格式化器

    JsonMediaTypeFormatter提供对JSON数据的格式化。默认地JsonMediaTypeFormatter使用Json.NET来格式化数据,也可以指定DataContractJsonSerializer来格式化数据。

    var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;

    json.UseDataContractJsonSerializer = true;

     

    序列化

    • 使用Json.NET时,默认地所有的公有类型的字段和属性都会序列化,除非标记了JsonIgnore特性。
    • 可以使用DataContract特性标记数据模型,标记了DataMember特性的属性都会被序列化,即使是私有类型。
    • 只读属性默认被序列化。
    • 默认地,Json.NET的时间字符串为ISO 8601格式,并保持时区。UTC时间含有“Z”字符后缀,本地时间包括时区偏移量。

     

    例:显示本地时间

    控制器

            [HttpPost]
            public IHttpActionResult ModelValid([FromBody]DataModel model)
            {
                new TaskCompletionSource<HttpResponseMessage>();
    
                if (!ModelState.IsValid)
                {
                    throw new HttpResponseException(HttpStatusCode.BadRequest);
                }
                return Ok(model);
            }

    客户端调用:

                HttpClient client = new HttpClient();
                string url = "http://localhost/WebApi_Test/api/account/modelvalid?Field1Name=1name&Field2Name=2name";
                using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url))
                {
                    var cont = new { DT=DateTime.Now};
                    HttpContent content = new StringContent(JsonConvert.SerializeObject(cont));
                    content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                    request.Content = content;
                    HttpResponseMessage response = client.SendAsync(request).Result;
                    Console.WriteLine("状态码:{0}",(int)response.StatusCode);
                    var task = response.Content.ReadAsStringAsync();
                    task.Wait();
                    Console.WriteLine("结果:{0}", task.Result);
                }

    结果:

    • 默认地,Json.NET保留了时区,可以使用DateTimeZoneHandling这一属性改变这种形式。

    例:

    // 转换所有日期为 UTC
    var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    json.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;
    • 若想使用Microsoft JSON 日期格式:

    例:

    var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    json.SerializerSettings.DateFormatHandling 
    = Newtonsoft.Json.DateFormatHandling.MicrosoftDateFormat;
    • 设置Formatting.Indented来支持缩进格式

    例:

    var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    json.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;

    控制器与客户端调用与前例基本一致,缩进的效果为:

    • 为了使JSON字符串属性名称具有驼峰式的风格,设置为CamelCasePropertyNamesContractResolver

    例:

    var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    json.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
    • 匿名类型自动序列化为JSON

    例:控制器操作为Get

    public object Get()
    {
        return new { 
            Name = "Alice", 
            Age = 23, 
            Pets = new List<string> { "Fido", "Polly", "Spot" } 
        };
    }

    调用控制器获得响应中包含:{"Name":"Alice","Age":23,"Pets":["Fido","Polly","Spot"]}

     

    2.2 XML格式化器

    XmlMediaTypeFormatter 提供对XML数据的格式化。默认地,使用DataContractSerializer执行序列化。

    可设置使用XmlSerializer来执行序列化。XmlSerializer支持的类型比DataContractSerializer少,但可以对XML结果做更多地控制。

    var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;

    xml.UseXmlSerializer = true;

    默认地DataContractSerializer行为如下:

    1)所有的公有类型属性或字段都会被序列化(set和get不加修饰),可使用IgnoreDataMember特性将其排除在外。

    2)Privateprotected成员不会序列化。

    3)只读属性不会序列化,但只读的集合属性会被序列化。

    4)类及其成员名称如其定义时所显示的那样,不加改变地被写入XML中。

    5)使用默认的XML名称空间。

    若想要施加更多的控制那么使用DataContract修饰类,使用DataMember修饰其属性。序列化规则如下:

    1)使用DataMember特性修饰成员使其可序列化,即使类属性为私有属性也可将其序列化。

    2)对于使用DataContract特性修饰的类,若不对其属性成员使用DataMember特性,那么就不能序列化。

    3)只读属性不会被序列化。

    4)DataContract中设置Name属性来指定类在XML中的名称。

    5)DataContract中设置NameSpace属性来指定XML名称空间。

    6)DataMember中设置Name属性来指定类属性在XML中的名称。

    时间类型会序列化ISO 8601格式字符串

    使用Indent属性设置缩进格式

    var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;

    xml.Indent = true;

    为不同的CLR类型设置不同的格式化器

    var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;

    // 将XmlSerializer 应用于Product类

    xml.SetSerializer<Product>(new XmlSerializer(typeof(Product)));

    移除JSON或XML格式化器,Register中添加以下代码。

    // Remove the JSON formatter

    config.Formatters.Remove(config.Formatters.JsonFormatter);

    // Remove the XML formatter

    config.Formatters.Remove(config.Formatters.XmlFormatter);

    2.3控制类的循环引用(应避免循环引用)

    例:

    public class Employee
    {
        public string Name { get; set; }
        public Department Department { get; set; }
    }
    
    public class Department
    {
        public string Name { get; set; }
        public Employee Manager { get; set; }
    }
    
    public class DepartmentsController : ApiController
    {
        public Department Get(int id)
        {
            Department sales = new Department() { Name = "Sales" };
            Employee alice = new Employee() { Name = "Alice", Department = sales };
            sales.Manager = alice;
            return sales;
        }
    }

    文件Global.asax中的Application_Start方法中添加如下代码,如果不添加下述代码运行时会报500错误。

    var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
    json.SerializerSettings.PreserveReferencesHandling = 
        Newtonsoft.Json.PreserveReferencesHandling.All;

    结果为:{"$id":"1","Name":"Sales","Manager":{"$id":"2","Name":"Alice","Department":{"$ref":"1"}}}

    对于XML循环引用的问题,有两种解决办法。一是在模型上应用[DataContract(IsReference=true)]特性,二是为DataContractSerializer的构造函数参数preserveObjectReferences赋值为true。

    var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
    var dcs = new DataContractSerializer(typeof(Department), null, int.MaxValue, 
        false, /* preserveObjectReferences: */ true, null);
    xml.SetSerializer<Department>(dcs);

     

    3 ASP.NET Web API 2.1支持BSON

    BSON是二进制序列化格式,与JSON大小相近,对于二进制的文件序列化后比JSON小。BSON数据易扩展,因为元素带有长度字段前缀。解析器能够跳过元素而不对数据解码。编码和解码是高效的,因为数值数据类型被存储为数字,而不是字符串。

    例:不支持BOSN的调用

    var cont = new { Field1Name = "1name", Field2Name = "2name", DT=DateTime.Now};
    HttpContent content = new StringContent(JsonConvert.SerializeObject(cont));
    content.Headers.ContentType = new MediaTypeHeaderValue("application/bson");
    request.Content = content;
    HttpResponseMessage response = client.SendAsync(request).Result;

    结果:

     

    启用BSON格式化器

    设置支持BSON,当客户端请求的Content-Typeapplication/bson时,Web API会使用BSON格式化器。

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.Formatters.Add(new BsonMediaTypeFormatter());
    
            // 其他配置
        }
    }

    为了关联其他多媒体类型与BOSN,应如下设置,例如多媒体类型为“application/vnd.contoso”

    var bson = new BsonMediaTypeFormatter();
    bson.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.contoso"));
    config.Formatters.Add(bson);

     

    例:.NET客户端应用HttpClient使用BSON格式化器。

    static async Task RunAsync()
    {
        using (HttpClient client = new HttpClient())
        {
            client.BaseAddress = new Uri("http://localhost");
    
            // 设置Accept头.
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/bson"));
    
            // 发送请求
            result = await client.GetAsync("api/books/1");
            result.EnsureSuccessStatusCode();
    
            // 使用BSON格式化器反序列化结果
            MediaTypeFormatter[] formatters = new MediaTypeFormatter[] {
                new BsonMediaTypeFormatter()
            };
    
            var book = await result.Content.ReadAsAsync<Book>(formatters);
        }
    }

     

    发送post请求:

    static async Task RunAsync()
    {
        using (HttpClient client = new HttpClient())
        {
            client.BaseAddress = new Uri("http://localhost:15192");
    
            // 设置请求头Content-Type为application/bson
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/bson"));
    
            var book = new Book()
            {
                Author = "Jane Austen",
                Title = "Emma",
                Price = 9.95M,
                PublicationDate = new DateTime(1815, 1, 1)
            };
    
            // 使用BSON格式化器
            MediaTypeFormatter bsonFormatter = new BsonMediaTypeFormatter();
            var result = await client.PostAsync("api/books", book, bsonFormatter);
            result.EnsureSuccessStatusCode();
        }
    }

     

    例:未反序列化BSON结果

    客户端调用

                using(HttpClient client = new HttpClient())
                {
                    client.DefaultRequestHeaders.Accept.Clear();
                    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/bson"));
    
                    string url = "http://localhost/WebApi_Test/api/account/modelvalid?Field1Name=1name&Field2Name=2name";
                    using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url))
                    {
                        var cont = new { Field1Name = "1name", Field2Name = "2name", DT = DateTime.Now };
                        HttpContent content = new StringContent(JsonConvert.SerializeObject(cont));
                        content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                        request.Content = content;
                        HttpResponseMessage response = client.SendAsync(request).Result;
                        Console.WriteLine("状态码:{0}", (int)response.StatusCode);
                        var task = response.Content.ReadAsStringAsync();
                        task.Wait();
                        Console.WriteLine("结果:{0}", task.Result);
                    }
    
                    Console.Read();
                }

    结果:

    客户端序列化,只要改变处理HTTP响应方式即可:

    MediaTypeFormatter[] formatters = new MediaTypeFormatter[] 
    {
         new BsonMediaTypeFormatter()
    };
    
    var task = response.Content.ReadAsAsync<DataModel>(formatters);
    task.Wait();
    var model =  task.Result;

    再次运行获得结果:

     

    序列化顶级原始类型

    BOSN语法中并没有规定如何序列化顶级原始类型,比如int类型,为了突破这一限制,BsonMediaTypeFormatter将顶级原始类型视为一种特殊的情况。在序列化之前将值转换为键值对,键为“Value”。

    例:

    public class ValuesController : ApiController
    {
        public IHttpActionResult Get()
        {
            return Ok(42);
        }
    }

    序列化后的值为:{ "Value": 42 }

     

    4 内容协商

    HTTP中主要的内容协商机制包括如下的请求头:

    Accept:应答中可接受的多媒体类型,如"application/json," "application/xml,"

    Accept-Charset:可接受的字符,如UTF-8ISO 8859-1

    Accept-Encoding:可接受的编码方式,如gzip

    Accept-Language:首先的自然语言,如en-us

    X-Requested-With服务器据此判断请求是否来自于AJAX。

    序列化

    如果Web API控制器操作(Action)返回CLR类型,管道序列化返回值并将其写入HTTP响应消息体。

    例如:

    public Product GetProduct(int id)
    {
        var item = _products.FirstOrDefault(p => p.ID == id);
        if (item == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        return item; 
    }

    发送的请求如下,其中请求接收JSON字符串,即通过Accept: application/json来指定的。

    GET http://localhost.:21069/api/products/1 HTTP/1.1

    Host: localhost.:21069

    Accept: application/json, text/javascript, */*; q=0.01

    响应为:

    HTTP/1.1 200 OK

    Content-Type: application/json; charset=utf-8

    Content-Length: 57

    Connection: Close

    {"Id":1,"Name":"Gizmo","Category":"Widgets","Price":1.99}

    也可返回HttpResponseMessage类型:

    public HttpResponseMessage GetProduct(int id)
    {
        var item = _products.FirstOrDefault(p => p.ID == id);
        if (item == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        return Request.CreateResponse(HttpStatusCode.OK, product);
    }

    内容协商工作原理

    首选,管道从HttpConfiguration对象中获得IContentNegotiator,并从HttpConfiguration.Formatters集合中获得多媒体格式化器列表。

    然后,管道调用IContentNegotiatior.Negotiate,传入待序列化类型、格式化器集合、HTTP请求。Negotiate方法返回两条信息,一是使用了哪个格式化器,二是响应需要的多媒体类型。如果所需的格式化器没有找到,那么Negotiate方法返回NULL,客户端会接受到406(不接受,请求资源不可访问)错误。

    默认的内容协商机制

    DefaultContentNegotiatorIContentNegotiator默认的实现,其选择格式化器的准则为:

    首先,使用MediaTypeFormatter.CanWriteType来验证格式化器是否能够序列化待处理的类型。

    其次,内容协商者会查看每个格式化器,并评估其与HTTP请求的匹配程度。为了评估匹配程度,内容协商会做两件事。

    • 集合SupportedMediaTypes包含了被支持的多媒体类型,内容协商者依据请求头的Accept标头来匹配这个集合。Accept标头可能包含一个范围,例如"text/plain" 可以匹配 text/* */*
    • MediaTypeMapping类提供了匹配HTTP请求的多媒体类型的一般方法。例如它可以匹配自定的HTTP请求头到特定的多媒体类型。

    如果有多个匹配,那么选取质量因数最高的一个匹配。

    例如:

    Accept: application/json, application/xml; q=0.9, */*; q=0.1

    选取质量因数为0.9的,即application/json。

    如果没有匹配,内容协商者试图匹配请求消息体的多媒体类型。

    如果请求包含JSON格式的数据,内容协商者会查找JSON格式化器。

    如果通过以上规则还是无法匹配,内容协商者会选择第一个可以序列化待处理类型的格式化器。

    字符编码方式

    选好格式化器以后,内容协商者会选取最好的字符编码方式,通过查看格式化器的SupportedEncodings属性,并与请求的Accept-Charset标头值进行匹配。

    参考:

    https://docs.microsoft.com/en-us/aspnet/web-api/

    部分示例来自于该网站

    转载与引用请注明出处。
    
    时间仓促,水平有限,如有不当之处,欢迎指正。
  • 相关阅读:
    功能强大表格控件Spread for Windows Forms 中文版发布
    charles 证书在 andriod 7 及更高版本手机上的安装
    Hibernate 开发中问题
    Silverlight中DataForm对数据进行校验
    LightSpeed ORM .NET简单运用
    Hibernate manytomany实现
    C# _lopen判断文件是否正在被使用
    SQL Join
    Castle与Mixin
    Catalysis 的构成部分与框架
  • 原文地址:https://www.cnblogs.com/hdwgxz/p/8778344.html
Copyright © 2020-2023  润新知