关于json:在Asp.Net WebApi 2.x应用程序中带有选项字段的F#记录未正确反序列化

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; }
    }


只是提供了另一个选择,如果您愿意升级框架版本,则可以使用新的System.Text.Json API,这些API应该又好又快,并且FSharp.SystemTextJson包为您提供了一个支持记录的自定义转换器,可以区分记录 工会等


我根据这篇文章找到了一个修复程序:https://stackoverflow.com/a/26036775/832783。

我在WebApiConfig Register方法中添加了以下这一行:

1
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new DefaultContractResolver();

我仍然必须调查这里发生的事情。

更新1:

事实证明,在ApiController方法中调用Json(...)会创建一个新的JsonSerializerSettings对象,而不是使用global.asax中的默认设置。 这意味着使用Json()结果时,添加到该处的所有转换器对序列化到json都没有任何影响。