1

My Controller's Post method always has null property values on parameter when triggered.

Environment:

I'm using ASP.Net Core WebAPI with F#.

Attempt:

I tried to apply a suggestion from this link to my Startup.fs file:

GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ContractResolver <-
        Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()

However, GlobalConfiguration is an unrecognized type.

Here's a larger view of my attempt:

type Startup private () =
    new (configuration: IConfiguration) as this =
        Startup() then
        this.Configuration <- configuration

        // *** I INSERTED THIS BELOW *** //
        GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ContractResolver <-
            Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()

Similar questions:

I did review this link. However, nothing worked.

HTTP Post:

This is the Post method where I keep observing null property values on my parameter:

[<HttpPost>]
member x.Post(request:DataTransfer.Request) = (* "request" parameter is always null *)

    request |> Query.carriers  
            |> function
               | Error _     -> x.StatusCode(500) :> IActionResult
               | Ok carriers -> x.Ok(carriers)    :> IActionResult

The actual type is defined here:

[<CLIMutable>]
type Request = { 
    RequestId : string 
    Customer  : Customer
    ItemQtys  : ItemQty seq
}

Client:

My client app makes the following call:

let client   = WebGateway.httpClient APIBaseAddress
let response = client.PostAsJsonAsync("carriers", request) |> toResult

Here's the request type on the client:

[<CLIMutable>]
type Request = { 
    RequestId : string 
    Customer  : Customer
    ItemQtys  : ItemQty seq
}

UPDATE:

I then attempted to apply a suggestion from this link and also using this reference. It didn't work though.

member this.ConfigureServices(services: IServiceCollection) =
    // Add framework services.
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
            .AddJsonOptions(fun opt -> 

                               let resolver = Newtonsoft.Json.Serialization.DefaultContractResolver()
                               resolver.NamingStrategy <- null
                               opt.SerializerSettings.ContractResolver <- resolver

                           ) |> ignore

Postman:

enter image description here enter image description here

Screenshots:

enter image description here

Partial Success:

The following seems promising:

enter image description here

Hence, the code above results in actual values sent from the client. However, I haven't found a way to convert the object into the data transfer type that I need.

Scott Nimrod
  • 11,206
  • 11
  • 54
  • 118
  • Try using the DefaultContractResolver? See https://stackoverflow.com/questions/22789504/f-json-net-6-0-and-webapi-serialization-of-record-types/26036775#26036775 – Isaac Abraham Jul 23 '19 at 13:26
  • 2
    And what are you sending? Post the JSON and the request details in general, along with what you're using to make that request `HttpClient`/Postman/etc. If you're making the request in code, post that code. – Chris Pratt Jul 23 '19 at 13:27
  • Might also be useful to see the HTTP request you're sending. – Isaac Abraham Jul 23 '19 at 13:27
  • @IsaacAbraham - I'm not able to find the namespace for resolving Formatters.JsonFormatter. Any suggestions? – Scott Nimrod Jul 23 '19 at 13:39
  • @ScottNimrod While I am not verse in f# does this allow for parameter attributes? Could be that you are just missing [FromBody] on the action parameter. – Nkosi Jul 23 '19 at 13:42
  • 1
    @ScottNimrod This also looks like a mixing of versions. `GlobalConfiguration` is not part of asp.net-core. That was from previous version. – Nkosi Jul 23 '19 at 13:45
  • @Nkosi - I've tried that attribute as well. However, some documentation led me to believe that FromBody is for KeyValue pairs and not for a fairly complex structure. – Scott Nimrod Jul 23 '19 at 13:46
  • @ScottNimrod again that is from previous version. in core it is to explicitly tell model binder where to bind data. – Nkosi Jul 23 '19 at 13:46
  • 3
    `member x.Post([]request:DataTransfer.Request)` – Nkosi Jul 23 '19 at 13:48
  • I've tried that without any success. I believe it has to do with me using F#'s record type that isn't getting resolved appropriately. – Scott Nimrod Jul 23 '19 at 13:50
  • @ScottNimrod Perhaps that no longer exists in ASP .NET Core - it is (or was!) part of Newtonsoft.Json. – Isaac Abraham Jul 23 '19 at 14:11
  • @IsaacAbraham I added an update – Scott Nimrod Jul 23 '19 at 14:12
  • Is your request actually send in camal case? – Daniel Hardt Jul 23 '19 at 15:34
  • 1
    An "easy" way to check if it is the record type is to try with a class? – Devon Burriss Jul 23 '19 at 16:35
  • Is your record defined _inside_ a module? This has been known to throw off lots of different reflection-based libraries across the ecosystem (i.e. moving the record might help). – pblasucci Jul 23 '19 at 20:27
  • @pblasucci I tried moving the record type outside of a module but I'm observing the same issue. – Scott Nimrod Jul 23 '19 at 20:39
  • The following retrieves the value: member x.Post([] request:Newtonsoft.Json.Linq.JObject) = ... – Scott Nimrod Jul 23 '19 at 23:13

1 Answers1

1

You don't need to mess with Contract Resolvers in ASP.NET Core to use camel-case and F# record types, as you did with ASP.NET. I would take all of that out, or try creating a simple test project to get it working, then go back and make the appropriate changes to your real project. With no additional configuration, you should be able to get something working just by following these steps:

  1. Run dotnet new webapi -lang F#
  2. Add the following type to ValuesController.fs:
    [<CLIMutable>]
    type Request =
    {
        Name: string
        Value: string        
    }
  1. Change the Post method to be something like this:
    [<HttpPost>]
    member this.Post([<FromBody>] request:Request) =
        this.Created(sprintf "/%s" request.Name, request)
  1. Debug the service to validate that it works by sending the following request in PostMan:
    {
        "name": "test",
        "value": "abc"
    }

You should see the request object echoed back with a 201 response.

Aaron M. Eshbach
  • 6,380
  • 12
  • 22
  • Thanks but I'm still observing null property values on my record parameter even when I reference the example you provided. – Scott Nimrod Jul 23 '19 at 19:07
  • @ScottNimrod Can you update your question with a snap from PostMan or Fiddler of the request body you're sending? – Aaron M. Eshbach Jul 23 '19 at 19:13
  • I'm not an expert with Postman. However, I provided some screenshots. – Scott Nimrod Jul 23 '19 at 19:58
  • @ScottNimrod Ah, ok, I see the problem (at least with the example). You need to use the "raw" option under body type, then use application/json as the content type, and paste in the JSON above. In your real service, are you trying to process JSON data, or a form? If JSON, are you sending the application/json content-type header? – Aaron M. Eshbach Jul 23 '19 at 20:04
  • I attempted to apply your feedback. However, I'm still not able to get Postman to work with HTTP Post. I did update the screenshots though. – Scott Nimrod Jul 23 '19 at 20:29
  • @ScottNimrod Any chance you could upload your sample project? – Aaron M. Eshbach Jul 25 '19 at 12:35
  • I worked around the issue by sending a post request on the client using a string value instead of the CLIMutable record type. Then on the server, I deserialized the string value on the post method into the record type I needed. – Scott Nimrod Jul 25 '19 at 13:21