ASP.NET Web API編程——序列化與內容協商

1 多媒體格式化器javascript

多媒體類型又叫MIME類型,指示了數據的格式。在HTTP協議中多媒體類型描述了消息體的格式。一個多媒體類型包括兩個字符串:類型和子類型。html

例如:java

text/htmlimage/pngapplication/jsonapplication/pdfweb

 

請求的Content-Type標頭指定消息體的格式,指示接收者應如何解析消息體內容。json

例如:請求告知服務端請求數據類型爲HTML, XHTML, or XMLapi

請求:Accept: text/html,application/xhtml+xml,application/xml服務器

響應:app

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[] { ',', '\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-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/

部分示例來自於該網站

 

轉載與引用請註明出處。

時間倉促,水平有限,若有不當之處,歡迎指正。
相關文章
相關標籤/搜索