F# record with option field doesn't deserialize properly in Asp.Net WebApi 2.x app
我有一个C#Asp.Net MVC(5.2.7)应用程序,支持针对.Net 4.5.1的WebApi2.x。
我正在尝试F#,并向解决方案中添加了F#库项目。该Web应用程序引用F#库。
现在,我希望能够使C#WebApi控制器返回F#对象并保存F#对象。我在使用Option字段序列化F#记录时遇到麻烦。这是代码:
C#WebApi控制器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using FsLib; namespace TestWebApp.Controllers { [Route("api/v1/Test")] public class TestController : ApiController { // GET api/<controller> public IHttpActionResult Get() { return Json(Test.getPersons); } // GET api/<controller>/5 public string Get(int id) { return"value"; } [AllowAnonymous] // POST api/<controller> public IHttpActionResult Post([FromBody] Test.Person value) { return Json(Test.doSomethingCrazy(value)); } // PUT api/<controller>/5 public void Put(int id, [FromBody]string value) { } // DELETE api/<controller>/5 public void Delete(int id) { } } } |
FsLib.fs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | namespace FsLib open System.Web.Mvc open Option open Newtonsoft.Json module Test = [<CLIMutable>] //[<JsonConverter(typeof<Newtonsoft.Json.Converters.IdiomaticDuConverter>)>] type Person = { Name: string; Age: int; [<JsonConverter(typeof<Newtonsoft.Json.FSharp.OptionConverter>)>] Children: Option<int> } let getPersons = [{Name="Scorpion King";Age=30; Children = Some 3} ; {Name ="Popeye"; Age = 40; Children = None}] |> List.toSeq let doSomethingCrazy (person: Person) = { Name = person.Name.ToUpper(); Age = person.Age + 2 ; Children = if person.Children.IsNone then Some 1 else person.Children |> Option.map (fun v -> v + 1); } let deserializePerson (str:string) = JsonConvert.DeserializeObject<Person>(str) |
这是OptionConverter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | namespace Newtonsoft.Json.FSharp open System open System.Collections.Generic open Microsoft.FSharp.Reflection open Newtonsoft.Json open Newtonsoft.Json.Converters /// Converts F# Option values to JSON type OptionConverter() = inherit JsonConverter() override x.CanConvert(t) = t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<option<_>> override x.WriteJson(writer, value, serializer) = let value = if value = null then null else let _,fields = FSharpValue.GetUnionFields(value, value.GetType()) fields.[0] serializer.Serialize(writer, value) override x.ReadJson(reader, t, existingValue, serializer) = let innerType = t.GetGenericArguments().[0] let innerType = if innerType.IsValueType then typedefof<Nullable<_>>.MakeGenericType([|innerType|]) else innerType let value = serializer.Deserialize(reader, innerType) let cases = FSharpType.GetUnionCases(t) if value = null then FSharpValue.MakeUnion(cases.[0], [||]) else FSharpValue.MakeUnion(cases.[1], [|value|]) |
我想将Option字段序列化为该值(如果它不是None),并且将其序列为null(如果它为None)。反之亦然,null-> None,值-> Some value。
序列化工作正常:
1 2 3 4 5 6 7 8 9 10 11 12 | [ { "Name":"Scorpion King", "Age": 30, "Children": 3 }, { "Name":"Popeye", "Age": 40, "Children": null } ] |
但是,当我发布到url时,Person参数被序列化为null,并且不会调用ReadJson方法。我使用Postman(Chrome应用程序)通过选择"正文"->" x-www-form-urlencoded"进行发布。我设置了三个参数:Name = Blob,Age = 103,Children = 2。
在WebApiConfig.cs中,我有:
1 | config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new Newtonsoft.Json.FSharp.OptionConverter()); |
但是,如果我只有这个,并且从Children字段中删除了JsonConverter属性,它似乎没有任何作用。
这将发送到服务器:
1 2 3 4 5 6 7 | POST /api/v1/Test HTTP/1.1 Host: localhost:8249 Content-Type: application/x-www-form-urlencoded Cache-Control: no-cache Postman-Token: b831e048-2317-2580-c62f-a00312e9103b Name=Blob&Age=103&Children=2 |
因此,我不知道出了什么问题,为什么解串器会将对象转换为null。如果我删除选项字段,它可以正常工作。另外,如果我从有效负载中删除Children字段,则它也可以正常工作。我不明白为什么未调用OptionCoverter的ReadJson方法。
有任何想法吗?
谢谢
更新:在注释中正确地指出了我没有发布application / json负载。我做到了,序列化仍然无法正常工作。
更新2:
经过更多测试后,此方法有效:
1 2 3 4 5 6 7 8 | public IHttpActionResult Post(/*[FromBody]Test.Person value */) { HttpContent requestContent = Request.Content; string jsonContent = requestContent.ReadAsStringAsync().Result; var value = Test.deserializePerson(jsonContent); return Json(Test.doSomethingCrazy(value)); } |
以下是我用于测试请求的Linqpad代码(我没有使用邮递员):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | var baseAddress ="http://localhost:49286/api/v1/Test"; var http = (HttpWebRequest) WebRequest.Create(new Uri(baseAddress)); http.Accept ="application/json"; http.ContentType ="application/json"; http.Method ="POST"; string parsedContent ="{\"Name\":\"Blob\",\"Age\":100,\"Children\":2}"; ASCIIEncoding encoding = new ASCIIEncoding(); Byte[] bytes = encoding.GetBytes(parsedContent); Stream newStream = http.GetRequestStream(); newStream.Write(bytes, 0, bytes.Length); newStream.Close(); var response = http.GetResponse(); var stream = response.GetResponseStream(); var sr = new StreamReader(stream); var content = sr.ReadToEnd(); content.Dump(); |
更新3:
这很好用-即我使用了C#类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public IHttpActionResult Post(/*[FromBody]Test.Person*/ Person2 value) { // HttpContent requestContent = Request.Content; // string jsonContent = requestContent.ReadAsStringAsync().Result; // // var value = Test.deserializePerson(jsonContent); value.Children = value.Children.HasValue ? value.Children.Value + 1 : 1; return Json(value);//Json(Test.doSomethingCrazy(value)); } public class Person2 { public string Name { get; set; } public int Age { get; set; } public int? Children { get; set; } } |
只是提供了另一个选择,如果您愿意升级框架版本,则可以使用新的
我根据这篇文章找到了一个修复程序:https://stackoverflow.com/a/26036775/832783。
我在WebApiConfig Register方法中添加了以下这一行:
1 | config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new DefaultContractResolver(); |
我仍然必须调查这里发生的事情。
更新1:
事实证明,在ApiController方法中调用Json(...)会创建一个新的JsonSerializerSettings对象,而不是使用global.asax中的默认设置。 这意味着使用Json()结果时,添加到该处的所有转换器对序列化到json都没有任何影响。