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

Posted

技术标签:

【中文标题】带有选项字段的 F# 记录在 Asp.Net WebApi 2.x 应用程序中无法正确反序列化【英文标题】:F# record with option field doesn't deserialize properly in Asp.Net WebApi 2.x app 【发布时间】:2020-02-01 05:23:02 【问题描述】:

我有一个支持面向 .Net 4.5.1 的 WebApi 2.x 的 C# Asp.Net MVC (5.2.7) 应用程序。 我正在试验 F#,并在解决方案中添加了一个 F# 库项目。 Web 应用引用 F# 库。

现在,我希望能够让 C# WebApi 控制器返回 F# 对象并保存 F# 对象。我在序列化带有选项字段的 F# 记录时遇到问题。代码如下:

C# WebApi 控制器:

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:

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:

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 并且如果它是 None 则为 null。反之亦然,null -> None,value -> Some value。

序列化工作正常:

[
    
        "Name": "Scorpion King",
        "Age": 30,
        "Children": 3
    ,
    
        "Name": "Popeye",
        "Age": 40,
        "Children": null
    
]

但是,当我发布到 url 时,Person 参数被序列化为 null,并且不会调用 ReadJson 方法。我使用 Postman(Chrome 应用程序)通过选择 Body -> x-www-form-urlencoded 进行发布。我设置了三个参数:Name=Blob,Age=103,Children=2。

在 WebApiConfig.cs 我有:

config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new Newtonsoft.Json.FSharp.OptionConverter());

但是,如果我只是拥有这个并从 Children 字段中删除 JsonConverter 属性,它似乎没有任何效果。

这是发送到服务器的内容:

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 方法。

有什么想法吗?

谢谢

更新:在 cmets 中正确指出我没有发布 application/json 有效负载。我做到了,序列化仍然无法正常工作。

更新 2: 经过更多测试,这可行:

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 代码(我没有使用 postman):

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# 类:

   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; 
    

【问题讨论】:

嗯,Name=Blob&amp;Age=103&amp;Children=2 不是 JSON,它是 application/x-www-form-urlencoded。请参阅 this answer 到 What is the difference between form-data, x-www-form-urlencoded and raw in the Postman Chrome application?,其中显示了 Content-Type: application/x-www-form-urlencodedContent-Type: application/json。我认为你需要发送 JSON 来调用你的 JSON 转换器。 可能是重复的? How to send JSON Object from POSTMAN to Restful webservices 和 postman - post application/json data. 我没有时间,但我准备用网页测试一下。 Postman 似乎有各种各样的问题 - 实际上我在使用完整的应用程序时遇到了问题,然后我切换到 chrome 应用程序,因为它无法正常工作。 你们是对的!但是我提交了一个 application/json 有效载荷,仍然没有调用 ReadJson 方法。 我测试了它,它工作得很好。这 return Json(Test.deserializePerson($"\"Name\":\"Blob\",\"Age\":100,\"Children\":2")); 正确反序列化并调用 ReadJson 其中let deserializePerson (str:string) = JsonConvert.DeserializeObject&lt;Person&gt;(str) 【参考方案1】:

我根据这篇帖子找到了一个修复方法:https://***.com/a/26036775/832783。

我在我的 WebApiConfig 注册方法中添加了这一行:

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

我仍然需要调查这里发生的事情。

更新 1:

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

【讨论】:

【参考方案2】:

只是提供另一个选项,如果你愿意升级框架版本,你可以使用新的System.Text.Json API,它应该很好而且很快,FSharp.SystemTextJson 包为你提供了一个自定义转换器,支持记录,受歧视的工会等。

【讨论】:

很高兴知道。不幸的是,我被 .Net 框架 4.5.1 的大型网络应用程序困住了。这可以插入到 WebApi 上下文中吗?

以上是关于带有选项字段的 F# 记录在 Asp.Net WebApi 2.x 应用程序中无法正确反序列化的主要内容,如果未能解决你的问题,请参考以下文章

使用多个字段过滤/搜索 - ASP.NET MVC

多个过滤器搜索没有多个选项-ASP.NET MVC

创建 F# ASP.NET Core Web 应用程序需要啥 Visual Studio 2017 选项?

带有参数化超链接的 ASP.NET 引导可切换选项卡

ASP.NET MVC 2 在哪里放置逻辑

ASP.NET MVC 获取带有用户 ID 的文件上传名称并应用于单独的字段