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

1 多媒体格式化器

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

例如:

text/html、image/png、application/json、application/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, BSON,form-urlencoded支持,可以创建多媒体格式化器来自定义格式化方式,自定义的格式化器继承自MediaTypeFormatter或BufferedMediaTypeFormatter,其中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[] { ',', '\n', '\r', '"' };

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-8或ISO 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)Private和protected成员不会序列化。

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-Type为application/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-8或ISO 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(不接受,请求资源不可访问)错误。

默认的内容协商机制

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

首先,使用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/

部分示例来自于该网站

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

Go中map数据类型3点小知识

1、map数据类型初始化 两种方式:map[string]string{}或make(map[string]string) 2、未初始化的map是nil,它与一...

35560
来自专栏恰童鞋骚年

.NET基础拾遗(4)委托、事件、反射与特性

  委托这个概念对C++程序员来说并不陌生,因为它和C++中的函数指针非常类似,很多码农也喜欢称委托为安全的函数指针。无论这一说法是否正确,委托的的确确实现了和...

9420
来自专栏androidBlog

java 解决文件名重复问题的两种方法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gdutxiaoxu/article/de...

44610
来自专栏大内老A

[ASP.NET MVC]通过对HtmlHelper扩展简化“列表控件”的绑定

在众多表单元素中,有一类<select>元素用于绑定一组预定义列表。传统的ASP.NET Web Form中,它对应着一组重要的控件类型,即ListContro...

20650
来自专栏Android 研究

APK安装流程详解5——Installer、InstallerConnection和Installd守护进程

因为Installer继承自SystemService,所以我们看下Installer的onStart方法 代码在Installer.java 396行

17410
来自专栏好好学java的技术栈

Java基于百度API的图片文字识别(支持中文,英文和中英文混合)

具体文档:http://ai.baidu.com/docs#/OCR-API/e1bd77f3

36720
来自专栏finleyMa

PHP7 新语法总结,更新7.2注意事项

太空船操作符用于比较两个表达式。当$a小于、等于或大于$b时它分别返回-1、0或1

45620
来自专栏iOS开发

iOS开发之 Method Swizzling 深入浅出

如果产品经理突然说:"在所有页面添加统计功能,也就是用户进入这个页面就统计一次"。我们会想到下面的一些方法:

48970
来自专栏DOTNET

ASP.NET Web API编程——模型验证与绑定

1.模型验证 使用特性约束模型属性 可以使用System.ComponentModel.DataAnnotations提供的特性来限制模型。 例如,Requi...

99650
来自专栏技术小讲堂

iBatis.Net(4):DataMapper API

在iBatis.Net中,可以通过SqlMapper实例访问DataMapper API,其实以前写的都是一些iBatis.Net的概念和一些配置的问题,从这一...

338100

扫码关注云+社区

领取腾讯云代金券