Запись F # с полем параметров не десериализована должным образом в приложении Asp.Net WebApi 2.x - PullRequest
2 голосов
/ 03 октября 2019

У меня есть приложение C # Asp.Net MVC (5.2.7) с поддержкой WebApi 2.x для .Net 4.5.1. Я экспериментирую с F #, и я добавил в решение проект библиотеки F #. Веб-приложение ссылается на библиотеку F #.

Теперь я хочу, чтобы контроллер C # WebApi возвращал объекты F #, а также сохранял объекты F #. У меня проблемы с сериализацией записи F # с полем Option. Вот код:

Контроллер 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 -> 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. Я установил три параметра: Имя = Blob, Возраст = 103, Дети = 2.

В WebApiConfig.cs у меня есть:

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

Однако, если у меня это просто есть, и удалитеАтрибут JsonConverter из поля Children, похоже, не имеет никакого эффекта.

Вот что отправляется на сервер:

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

Итак, я не знаю, что не так,почему десериализатор преобразует объект в ноль. Если я уберу поле опций, оно будет работать нормально. Также, если я удалю поле Children из полезной нагрузки, оно также будет работать нормально. Я не понимаю, почему метод ReadJson в OptionCoverter не вызывается.

Есть идеи?

Спасибо

Обновление: в комментариях было правильно указано, что я не сделалразместить приложение / 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, который я использовал для тестирования запроса(Я не использовал почтальон):

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

Ответы [ 2 ]

0 голосов
/ 04 октября 2019

Просто добавьте другую опцию, если вы хотите обновить версию фреймворка, вы можете использовать новые System.Text.Json API, которые должны быть хорошими и быстрыми, а пакет FSharp.SystemTextJson предоставляет вам специальный конвертер с поддержкойзаписи, дискриминируемые союзы и т. д.

0 голосов
/ 03 октября 2019

Я нашел исправление, основанное на этом посте: https://stackoverflow.com/a/26036775/832783.

Я добавил в свой метод регистрации WebApiConfig эту строку:

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

Мне еще предстоит выяснить, что здесь происходит.

Обновление 1:

Оказывается, что вызов Json (...) в методе ApiController создает новый объект JsonSerializerSettings вместо использования набора по умолчанию в global.asax. Это означает, что любые добавленные конвертеры не влияют на сериализацию в json при использовании результата Json ().

...