How to implement System.Text.Json.Serialization.ReferenceHandler.Preserve in a Blazor WebAssemby application
Asked Answered
S

3

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:

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.

Snide answered 12/12, 2020 at 4:36 Comment(9)
See the doc pages Proposal: Add mechanism to handle circular references when serializing #30820 and Preserve object references and handle loops and How to preserve references and handle circular references with System.Text.Json.Unfriended
Beyond that, you wrote, However I have not been able to implement it successfully to get it to work. - then can you share a minimal reproducible example showing what did not work? From How to 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.Unfriended
Here is an example of using ReferenceHandler.Preserve: 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 minimal reproducible example with the code you have written that does not work.Unfriended
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.1Snide
Might you edit your question to include code showing how you are invoking the serializer? Incidentally the fiddle 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.Unfriended
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.Snide
Still haven't been able to get it to work. I've updated original post with new information.Snide
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. I'm not familiar with Blazor WebAssembies but maybe something similar is going on here?Unfriended
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.Ivetteivetts
P
0

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

services.AddMvc().AddJsonOptions(o =>
        {
            o.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
        });
Pessimist answered 13/5, 2021 at 0:24 Comment(0)
B
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.

Basiliabasilian answered 25/10, 2021 at 1:15 Comment(0)
C
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();
    }
}
Chiseler answered 9/4, 2023 at 13:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.