12

It's my understanding that you can use this setting to get around the issue of getting the following error when you have circular references defined in your object model:

JsonException: A possible object cycle was detected which is not supported. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32.

However I have not been able to implement it successfully to get it to work. If anyone can provide detailed instructions on what needs to be done it would be much appreciated!

I thought about switching the application to using Newtonsoft.JSON but from what I've read this is not doable in a Blazor WebAssembly application?

Update 12/12/2020

The closest articles I had found in trying to figure out how to implement ReferenceHandler.Preserve were these:

https://github.com/dotnet/runtime/issues/42584

https://github.com/dotnet/aspnetcore/issues/28286

Based on these articles I tried implementing the following solutions, neither of which worked...

First attempt I implemented the following code in the Startup.cs class in my Server project:

services.AddControllersWithViews().AddJsonOptions(options =>
            {
                options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve;
            });

Second attempt I implemented the following code in the Startup.cs class in my Server project:

services.AddControllersWithViews(options =>
            {
                options.OutputFormatters.RemoveType<SystemTextJsonOutputFormatter>();
                options.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonSerializerOptions(JsonSerializerDefaults.Web)
                {
                    ReferenceHandler = ReferenceHandler.Preserve
                }));
            });

Update 12/12/2020 11:12 AM CST

After changing my Server project to target .NET 5 and trying both code options from above i now get the following type of error on EVERY page in my application:

Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: The JSON value could not be converted to BusinessManager.Shared.Models.EmploymentBenefit[]. Path: $ | LineNumber: 0 | BytePositionInLine: 1.

Steps To Reproduce Original Problem

Create a new Blazor WebAssembly application

In the Shared project define a parent class that has a collection of child objects as follows:

public virtual List<Child> Children{ get; set; } = new List<Child>();

In the Child class define a property that references its parent as follows:

public virtual Parent Parent{ get; set; }

Then, I use entity framework to generate the database objects. Create a web api function that returns the parent and its child objects as such:

[HttpGet("{id}")]
        public async Task<IActionResult> Get(Guid id)
        {
            var returnValue = await db.Parents
                .Include(aa => aa.Children)
                .FirstOrDefaultAsync(aa => aa.ParentId== id);
            return Ok(returnValue);
        }

And then try to render the parent and child collection on a page by calling this web api function.

scooter
  • 353
  • 4
  • 12
  • See the doc pages [Proposal: Add mechanism to handle circular references when serializing #30820](https://github.com/dotnet/runtime/issues/30820) and [Preserve object references and handle loops](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-5-0#preserve-object-references-and-handle-loops) and [How to preserve references and handle circular references with System.Text.Json](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-preserve-references?pivots=dotnet-5-0). – dbc Dec 12 '20 at 05:51
  • Beyond that, you wrote, *However I have not been able to implement it successfully to get it to work.* - then can you share a [mcve] showing what did not work? From [ask]: *Help others reproduce the problem... if your problem is with code you've written, you should include some... Include just enough code to allow others to reproduce the problem.* – dbc Dec 12 '20 at 05:53
  • Here is an example of using `ReferenceHandler.Preserve`: https://dotnetfiddle.net/t2EkHR. **Note that `ReferenceHandler.Preserve` only works in .Net 5 or later.** To diagnose the problem you mention when you wrote *I have not been able to implement it successfully to get it to work* we need to see a [mcve] with the code you have written that does not work. – dbc Dec 12 '20 at 16:32
  • Updated my post with code examples. I'm wondering if the .Net 5 or later aspect isn't at least part of the problem with a Blazor WebAssembly application as I'm not seeing you can use .Net 5 with Blazor WebAssembly although I am finding all the various versions of frameworks a little confusing and hard to keep straight. However, the code compiles fine with my changes in place. I am referencing System.Net.Http.Json version 5.0.0 in my server project. My Client project targets .NET Standard 2.1 and my Server project targets .NET Core 3.1 – scooter Dec 12 '20 at 16:50
  • Might you [edit] your question to include code showing how you are invoking the serializer? Incidentally the fiddle https://dotnetfiddle.net/t2EkHR has some code that prints the current .Net version in runtime, you might be able to use that to confirm you are running under .Net 5. – dbc Dec 12 '20 at 16:53
  • I see i have an option to change my Server project to target .NET 5.0 so i'm going to see if that fixes anything. – scooter Dec 12 '20 at 16:55
  • Still haven't been able to get it to work. I've updated original post with new information. – scooter Dec 12 '20 at 19:55
  • Not sure then. If you are using Entity Framework, I know that Json.NET's `PreserveReferencesHandling` functionality does not work with dynamic proxies, one needs to set `Configuration.ProxyCreationEnabled = false;` e,g, as shown [here](https://stackoverflow.com/a/19468282/3744182). I'm not familiar with Blazor WebAssembies but maybe something similar is going on here? – dbc Dec 12 '20 at 20:04
  • 1
    Your second serialization code works, but it creates json with fields like $id, $values and $ref, which System.Text.Json is not able to deserialize back. – Mark Toman Jan 22 '21 at 00:01

3 Answers3

0

Here's what I did with mine. In Startup.cs add this

services.AddMvc().AddJsonOptions(o =>
        {
            o.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
        });
MADJOE
  • 199
  • 1
  • 9
0

After changing my Server project to target .NET 5 and trying both code options from above i now get the following type of error on EVERY page in my application:

Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: The JSON value could not be converted to BusinessManager.Shared.Models.EmploymentBenefit[]. Path: $ | LineNumber: 0 | BytePositionInLine: 1.

Are you enabling ReferenceHandler.Preserve in your client side as well? Otherwise the JsonSerializer in the client won't be aware of the metadata.

Jozkee
  • 77
  • 2
  • 13
0

For Blazor WASM you need to do the following:

On the server side, add the following code to Program.cs:

builder.Services.AddControllers().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
    options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
});

This code ensures server will handle references as you need.

Note: Instead of AddControllers you can use also AddControllersWithViews

On the client side (Blazor WASM), there is at the moment no way to do it in such a simple way. Or at least I did not find anything. So you need to ensure that for every call of JSON-based API, you will pass a JSON option and use it for deserialization. You can do it with extension methods like this:

public static class HttpClientExtensions
{
    private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions
    {
        ReferenceHandler = ReferenceHandler.Preserve,
        PropertyNameCaseInsensitive = true
    };

    public static Task<TValue> GetFromJsonAsync<TValue>(this HttpClient httpClient, string requestUri)
    {
        return httpClient.GetFromJsonAsync<TValue>(requestUri, JsonSerializerOptions);
    }
}

Or you can do it by custom HTTP client class like this:

using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;

public class JsonHttpClient
{
    private readonly HttpClient _httpClient;
    private readonly JsonSerializerOptions _jsonSerializerOptions;

    public JsonHttpClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
        _jsonSerializerOptions = new JsonSerializerOptions
        {
            ReferenceHandler = ReferenceHandler.Preserve,
            PropertyNameCaseInsensitive = true
        };
    }

    public async Task<T> GetFromJsonAsync<T>(string requestUri)
    {
        var response = await _httpClient.GetAsync(requestUri);
        response.EnsureSuccessStatusCode();

        using var contentStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync<T>(contentStream, _jsonSerializerOptions);
    }
}

and with this second option you also need to add following code to Program.cs builder.Services.AddScoped(sp => new JsonHttpClient(sp.GetRequiredService<HttpClient>()));

and following code where you want to use it (razor page):

@inject JsonHttpClient JsonHttpClient

and

var resultData = await JsonHttpClient.GetFromJsonAsync<MyDataType>("api/someendpoint");

Note: there is only inject of new HTTP Client and exchange of Http to JsonHttpClient.

Here is also code of both classes with other useful methods:

public static class HttpClientExtensions
{
    private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions
    {
        ReferenceHandler = ReferenceHandler.Preserve,
        PropertyNameCaseInsensitive = true
    };

    public static Task<TValue> GetFromJsonAsync<TValue>(this HttpClient httpClient, string requestUri)
    {
        return httpClient.GetFromJsonAsync<TValue>(requestUri, JsonSerializerOptions);
    }

    public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient httpClient, string requestUri, TValue value)
    {
        return httpClient.PostAsJsonAsync(requestUri, value, JsonSerializerOptions);
    }

    public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient httpClient, string requestUri, TValue value)
    {
        return httpClient.PutAsJsonAsync(requestUri, value, JsonSerializerOptions);
    }

    public static async Task<TResponse> PostAsJsonAsync<TValue, TResponse>(this HttpClient httpClient, string requestUri, TValue value)
    {
        var response = await httpClient.PostAsJsonAsync(requestUri, value, JsonSerializerOptions);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<TResponse>(JsonSerializerOptions);
    }

    public static async Task<TResponse> PutAsJsonAsync<TValue, TResponse>(this HttpClient httpClient, string requestUri, TValue value)
    {
        var response = await httpClient.PutAsJsonAsync(requestUri, value, JsonSerializerOptions);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<TResponse>(JsonSerializerOptions);
    }
}

and

using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

public class JsonHttpClient
{
    private readonly HttpClient _httpClient;
    private readonly JsonSerializerOptions _jsonSerializerOptions;

    public JsonHttpClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
        _jsonSerializerOptions = new JsonSerializerOptions
        {
            ReferenceHandler = ReferenceHandler.Preserve,
            PropertyNameCaseInsensitive = true
        };
    }

    public async Task<T> GetFromJsonAsync<T>(string requestUri)
    {
        var response = await _httpClient.GetAsync(requestUri);
        response.EnsureSuccessStatusCode();

        using var contentStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync<T>(contentStream, _jsonSerializerOptions);
    }

    public async Task<TResponse> PostAsJsonAsync<TRequest, TResponse>(string requestUri, TRequest content)
    {
        using var contentStream = new MemoryStream();
        await JsonSerializer.SerializeAsync(contentStream, content, _jsonSerializerOptions);
        contentStream.Position = 0;

        using var response = await _httpClient.PostAsync(requestUri, new StreamContent(contentStream));
        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync<TResponse>(responseStream, _jsonSerializerOptions);
    }

    public async Task PostAsJsonAsync<TRequest>(string requestUri, TRequest content)
    {
        using var contentStream = new MemoryStream();
        await JsonSerializer.SerializeAsync(contentStream, content, _jsonSerializerOptions);
        contentStream.Position = 0;

        using var response = await _httpClient.PostAsync(requestUri, new StreamContent(contentStream));
        response.EnsureSuccessStatusCode();
    }

    public async Task<TResponse> PutAsJsonAsync<TRequest, TResponse>(string requestUri, TRequest content)
    {
        using var contentStream = new MemoryStream();
        await JsonSerializer.SerializeAsync(contentStream, content, _jsonSerializerOptions);
        contentStream.Position = 0;

        using var response = await _httpClient.PutAsync(requestUri, new StreamContent(contentStream));
        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync<TResponse>(responseStream, _jsonSerializerOptions);
    }

    public async Task PutAsJsonAsync<TRequest>(string requestUri, TRequest content)
    {
        using var contentStream = new MemoryStream();
        await JsonSerializer.SerializeAsync(contentStream, content, _jsonSerializerOptions);
        contentStream.Position = 0;

        using var response = await _httpClient.PutAsync(requestUri, new StreamContent(contentStream));
        response.EnsureSuccessStatusCode();
    }

    public async Task DeleteAsync(string requestUri)
    {
        var response = await _httpClient.DeleteAsync(requestUri);
        response.EnsureSuccessStatusCode();
    }
}
Jan Hlavsa
  • 71
  • 1
  • 4