我有一个 C# Asp.Net MVC (5.2.7) 应用程序,它支持面向 .Net 4.5.1 的 WebApi 2.x。 我正在试验 F#,并在解决方案中添加了一个 F# 库项目。 Web 应用引用 F# 库。
现在,我希望能够让 C# WebApi Controller 返回 F# 对象并保存 F# 对象。我在序列化带有选项字段的 F# 记录时遇到问题。这是代码:
C# WebApi Controller :
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 方法。
有什么想法吗?
谢谢
更新:在评论中正确指出我没有发布 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; }
}
最佳答案
我根据这篇文章找到了一个修复:https://stackoverflow.com/a/26036775/832783 .
我在我的 WebApiConfig Register 方法中添加了这一行:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new DefaultContractResolver();
我仍然需要调查这里发生的事情。
更新 1:
事实证明,在 ApiController 方法中调用 Json(...) 会创建一个新的 JsonSerializerSettings 对象,而不是使用 global.asax 中的默认设置。这意味着当使用 Json() 结果时,在那里添加的任何转换器都不会对序列化为 json 产生任何影响。
关于json - 带有选项字段的 F# 记录在 Asp.Net WebApi 2.x 应用程序中未正确反序列化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58209838/