问题
I have a C# Asp.Net MVC (5.2.7) app with support for WebApi 2.x targeting .Net 4.5.1. I am experimenting with F# and I added an F# library project to the solution. The web app references the F# library.
Now, I want to be able to have the C# WebApi controller return F# objects and also save F# objects. I have trouble serializing a F# record with an Option field. Here is the code:
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)
Here is the 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|])
I want to serialize the Option field to the value, if it is not None and to null if it is None. And vice-versa, null -> None, value -> Some value.
The serialization works fine:
[
{
"Name": "Scorpion King",
"Age": 30,
"Children": 3
},
{
"Name": "Popeye",
"Age": 40,
"Children": null
}
]
However, when I post to the url, Person parameter is serialized to null and the ReadJson method is not invoked. I used Postman (the chrome app) to post by selecting the Body -> x-www-form-urlencoded. I set up three parameters: Name=Blob,Age=103,Children=2.
In WebApiConfig.cs I have:
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new Newtonsoft.Json.FSharp.OptionConverter());
However, if I just have this and remove the JsonConverter attribute from the Children field, it doesn't seem to have any effect.
This what gets sent to the server:
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
So, I don't know what's wrong, why the deserializer converts the object to null. If I remove the option field it works fine. Also if I remove the Children field from the payload it works fine as well. I do not understand why the ReadJson method of the OptionCoverter is not invoked.
Any ideas?
Thanks
An update: In the comments it was rightly pointed I did not post a application/json payload. I did it and the serialization still doesn't work properly.
Update 2: After more testing, this works:
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));
}
The following is the Linqpad code I used for testing the request (I didn't use 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();
Update 3:
This works just fine - i.e. I used a C# class:
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; }
}
回答1:
I found a fix, based on this post: https://stackoverflow.com/a/26036775/832783.
I added in my WebApiConfig Register method this line:
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new DefaultContractResolver();
I still have to investigate what's happening here.
Update 1:
It turns out that calling Json(...) in the ApiController method creates a new JsonSerializerSettings object instead of using the default set in the global.asax. What this means is that any converters added there don't have any effect on the serialization to json when the Json() result is used.
回答2:
Just to throw another option in, if you're willing to upgrade the framework version you could use the new System.Text.Json
APIs which should be nice and fast, and the FSharp.SystemTextJson
package gives you a custom converter with support for records, discriminated unions etc.
来源:https://stackoverflow.com/questions/58209838/f-record-with-option-field-doesnt-deserialize-properly-in-asp-net-webapi-2-x-a