Search code examples
jsonasp.net-coreasp.net-web-apiasp.net-core-webapiblazor-webassembly

WebAPI : JSON ReferenceHandler.Preserve


Am using Blazor (Hosted) and looking to preserve referencing when sending results back to client. The sample below doesn't really need reference preservation but is my test scenario for a more complex structure that does.

  • Class "Staff" is defined in the Shared project.
  • WebAPI method returns an IEnumerable

That payload looks like this:

[
  {
    "id":"a583baf9-8990-484f-9dc6-e8ea822f49c6",
    "name":"Neil",
    "themeName":"Blue Gray"
  },
  {
    "id":"a7a8e753-c7af-4b29-9242-7b2f5bdac830",
    "name":"Caroline",
    "themeName":"Yellow"
  }
]

Using

var result = await response.Content.ReadFromJsonAsync<IEnumerable<Staff>>();

I am able to get my Staff objects in the client.

Moving on to reference preservation:

I updated StartUp.cs on server to include:

services.AddControllersWithViews()
    .AddJsonOptions(o => 
        o.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve
    );

Result was that the return payload now looks like this:

{
  "$id":"1",
  "$values":
     [
       {
         "$id":"2",
         "id":"a583baf9-8990-484f-9dc6-e8ea822f49c6",
         "name":"Neil",
         "themeName":"Blue Gray"
       },
       {
         "$id":"3",
         "id":"a7a8e753-c7af-4b29-9242-7b2f5bdac830",
         "name":"Caroline",
         "themeName":"Yellow"
       }
     ]
}

Seems correct.

This caused JSON deserialisation exception at line:

var result = await response.Content.ReadFromJsonAsync<IEnumerable<Staff>>();

So, I thought I might need to include reference handling options when deserializing on the client as well. So, changed to:

JsonSerializerOptions options = new JsonSerializerOptions();
options.ReferenceHandler = ReferenceHandler.Preserve;
var result = await response.Content.ReadFromJsonAsync<IEnumerable<Staff>>(options);

I got no errors, but my Enumerable included:

The 2 Staff objects (but will all properties nulled). A 3rd null object in the Enumerable.

Could anyone guide me on what I'm doing wrong?


Solution

  • I have found a solution. This is what appeared to be happening:

    The default configuration for Json serialization on WebAPI appears to be camel case. However, even though this was the case, I had not had any problem serializing shared classes (that use capitalisation) and deserializing on the client, even though the JSON itself was using camel case.

    This started to fail when I added ReferenceHandler.Preserve to my JsonSerializerOptions.

    Updating my Json Options as follows, solved the problem:

    services.AddControlersWithViews()
        .AddJsonOptions(options =>
        {
            options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
            options.JsonSerializerOptions.PropertyNamingPolicy = null // prevent camel case
        }
    

    Alternative approach is to use MvcOptions. I don't claim to know which is prefereable, but both the above and the below seem to give the same outcome.

    services.AddControllersWithViews(options =>
    {
        options.OutputFormatters.RemoveType<SystemTextJsonOutputFormatter>();
        options.OutputFormatters.Add(new SystemTextJsonOutputFormatter(
            new JsonSerializerOptions(JsonSerializerDefaults.Web)
            {
                ReferenceHandler = ReferenaceHandler.Preserve,
                PropertyNamingPolicy = null    // prevent camel casing of Json
            }));
    });
    

    Then on client, when receiving response from WebAPI:

    HttpResponseMessage response = await Http.GetAsync(myapiroute);
    IEnumerable<Staff> staff = response.Content.ReadFromJsonAsync<IEnumerable<Staff>>(
        new JsonSerializerOptions() { ReferenceHandler = ReferenceHandler.Preserve });
    

    And now reference handling appears to cross the boundary from WebAPI to Blazor Client.