Aspnetcore: Customize serialization for IJSRuntime.InvokeAsync

Created on 29 Jul 2019  ยท  55Comments  ยท  Source: dotnet/aspnetcore

Before I write anything else.. I have already created an issue a while ago with pretty much the same problem (which I will copy to here as well) but with a solution that didn't really make sense. I now have a concrete idea/solution which is a lot better than what I suggested in my previous request.

Issue (similar to old issue)

Because IJSRuntime.InvokeAsync now uses System.Text.Json instead of Newtonsoft.Json (preview6+) the custom serialization I implemented with Newtonsoft.Json doesn't work anymore. Specifically null values aren't ignored anymore.
Even though it says in the issue I linked..

Users may use a JSON serializer of their choice.

.. the IJSRuntime doesn't use Newtonsoft.Json if you add Newtonsoft.Json in the ConfigureServices method using AddNewtonsoftJson on the IMvcBuilder.

I have already made a comment in the discussion for this issue describing the issue and what I already tried (back then I didn't know this wasn't the default behaviour). In this comment I also link to my SO question where you can find further information on the attempts I've made (same as before, I assumed this wasn't default behaviour).

These issues and questions might mention the need to be able to use Newtonsoft.Json with IJSRuntime. This is not anymore the case as I want to migrate all the custom serialization to System.Text.Json anyway as soon as the API allows all the desired customizations I require.

Request / Idea

Now instead of allowing users to customize _what_ serializer is used (replace System.Text.Json.JsonSerializer), I only want to talk about the JsonSerializerOptions (customize System.Text.Json.JsonSerializer).
I have examined the source code for IJSRuntime and the corresponding implementation(s). From that I have found out that the base class of all implementations (JSRuntimeBase) uses the class JsonSerializerOptionsProvider (see Line 67) which always holds the same JsonSerializerOptions. This can't be influenced by a user of the API.

Now how would you allow the user to customize those JsonSerializerOptions?
It seems like it would be as easy adding another optional parameter to the InvokeAsync methods of type JsonSerializerOptions with a default value of null. For documentation purposes it might be a good idea to make JsonSerializerOptionsProvider public instead of internal so you could say something along the lines of:

if not provided <see cref="JsonSerializerOptionsProvider.DefaultOptions"> will be used.

This seems to already be quite detailed and is of course not up to me to decide, I just wanted to bring up the idea.

I realize that my old request was a bit too broad as well as probably too expensive to implement currently. This however seems much more lightweight and with a clear solution. I think it would be a great addition to this API as it can be a great advantage to be able to customize how the IJSRuntime serializes your arguments.
The biggest advantage for me (and probably one of the biggest use cases overall) would be that I could set IgnoreNullValues to true which is of course not the default value.

affected-medium area-blazor enhancement severity-major

Most helpful comment

some temporally solution

public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);

            ConfigureServices(builder.Services);

            builder.RootComponents.Add<App>("app");

            var host = builder.Build();

            ConfigureProviders(host.Services);

            await host.RunAsync();
        }

        public static void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions();
        }

        public static void ConfigureProviders(IServiceProvider services)
        {
            try
            {
                var jsRuntime = services.GetService<IJSRuntime>();
                var prop = typeof(JSRuntime).GetProperty("JsonSerializerOptions", BindingFlags.NonPublic | BindingFlags.Instance);
                JsonSerializerOptions value = (JsonSerializerOptions)Convert.ChangeType(prop.GetValue(jsRuntime, null), typeof(JsonSerializerOptions));
                value.PropertyNamingPolicy = null;
                value.IgnoreNullValues = true;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"SOME ERROR: {ex}");
            }
        }
    }

All 55 comments

Thanks for contacting us, @Joelius300.
Why do you want null-values ignored?
In general, we don't plan to make the serialization customizable in Blazor, however per-type customizations are allowed today using Sytem.Text.Json. Here is a sample: https://github.com/aspnet/AspNetCore/blob/master/src/Components/Components/src/ElementReference.cs#L15

I need the null values ignored because I'm writing a Chart.js port for blazor (github-repo) and Chart.js has issues if a value is null instead of undefined. Here's a comment from an issue of a similar project where it is also mentioned that null values cause trouble for Chart.Js.
Btw here is the issue for this problem in my repo.
The current workaround uses the following steps (the code is in this file):

  1. Serialize with json.net with options to ignore all null values (this could also be done with System.Text.Json)
  2. Deserialize with json.net to an ExpandoObject which now of course doesn't have the properties that were null anymore. As far as I know System.Text.Json currently doesn't support deserializing to ExpandoObject so I have to use json.net for that.
  3. Restore important property-values that were lost during serialization. This includes multiple DotNetObjectRefs that are of course really important for the whole thing to work correctly.
  4. Turn the ExpandoObject into a Dictionary<string, object> recursively since System.Text.Json doesn't support serializing ExpandoObject.
  5. Invoke the javascript function with the Dictionary<string, object> as parameter using IJSRuntime.InvokeAsync. All the null properties of the initial object are of course not present in this dictionary and thus will be interpreted as undefined rather than null, making the whole interop work at all.

If I was able to use JsonSerializerOptions with IgnoreNullValues = true on IJSRuntime.InvokeAsync, I could directly invoke the javascript with the original object as parameter. This would allow me to not only remove all the dependencies to json.net but also to remove 4 out of 5 steps, making this process much simpler and less ugly (I don't like ExpandoObject).

Hope this helps :)

Ps. Thanks for the sample for how to use a custom converter with System.Text.Json, I was not able to find a good one myself.

Are there any updates on this?
Do you still require more information or am I just too impatient? :)
@mkArtakMSFT

@Joelius300, you should be able to write a JSON converter and apply to your model types to do this for you. Hope this helps: https://docs.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonconverter-1?view=netcore-3.0

I need this for every class in the model (it should be _global_) and the model is a huge with tons of nested and derived classes. It's a port for chart.js which means I have to model these huge json objects that can be used to configure a chart. If I were to create a custom converter for every single one, it would not only take a huge amount of time but also flood the project with converters.
I might be able to create a single converter with a factory used for every class in the tree but since I cannot add them to the Converters used by IJSRuntime (options are not exposed), I will have to add a JsonConverterAttribute to every single model-class. Not only will almost every class in my project have this attribute, it will also create colisions with existing converters (there are a lot of those as well) because you can only define one custom converter (makes sense).

Even if I managed to get this to work with all these converter-attributes (which I honestly doubt), after fixing all the colisions etc, it would be kind of a bodge, and not a particularly good looking one. From looking at the source of JSRuntime, it's clear that now you're not using the provider anymore. Instead you're creating the options directly in the constructor. You then store it in a protected internal property which makes it inaccessible outside the assembly unless you decide to fully implement your own js-runtime (can't derive from the ones used, they're internal).

I have already used JsonConverter and JsonConverterFactory but I'm not quite sure what you expect me to do. Adding all of these attributes looks and feels horrible plus it creates colisions. Do you have a concrete idea how to achieve this? I believe that you won't ever consider making the serializer-options accessable, correct?

Clearing the milestone so we can discuss if we want to enable support for customing serialization options per JS invocation.

We're moving this issue to Backlog to collect more feedback around this ask.

I also need this feature - having control over the serialization options is necessary in various scenarios, when you want to customize the serialization/deserialization mechanism. Having to do this manually for each model is tedious. Can you provide some more details if this is a problem?

My concrete case is similar to:

    public class CounterBase : ComponentBase
    {
        [Inject]
        internal IJSRuntime JSRuntime { get; set; }


        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);
            InvokeJsMethod();
        }

        private ValueTask<object> InvokeJsMethod()
        {
            var data = new Dictionary<string, object>();

            data["person"] = new Model() { Value = "test1" };

            return JSRuntime.InvokeAsync<object>("testMethod", data);
        }
    }

    public class Model
    {
        public string Value { get; set; }
    }

The value in the JS method ("testMethod") has its property camel-cased:

[
  {
    "person": {
      "value": "test1" <- "value" instead of "Value"
    }
  }
]

But the props of the person object should be in the same casing as in the original model. This is due to the default camel-case serialization.

Another use-case which will probably be very common is going to be introduced once https://github.com/dotnet/corefx/issues/38650 passes. I don't know what the best default value would be in the options you're using internally since I've had many scenarios where I needed polymorphic serialization but also many others where I didn't want it to be polymorphic. I think this is also an option which would be customized a lot.

This feature would also useful as a workaround for https://github.com/dotnet/corefx/issues/38348
System.Text.Json does currently not support F# types (but plans to do so in the future).

With https://github.com/Tarmil/FSharp.SystemTextJson, you can generally work around this. But not in Blazor because you can't inject the necessary JsonSerializerOptions

On second thought, simply copying the extension class and adjusting the code works nicely for my usecase.

I wanted to give another shot at Blazor, and I encountered a similar issue when interfacing with a third-party JavaScript library. They were not handling null properties but were handling missing properties.

I would love to have access to the JsonSerializerOptions instance used by the JSRuntime or an attribute that would allow me to decorate my types/properties, telling them to [IgnoreNullOrWhateverTheNameThatYouChoose].

I spent a few hours digging into your code to find out that despite all of that focus on dependency injection, you decided to new the JsonSerializerOptions in the JSRuntime constructor and assigned it to an inaccessible property of that abstract class. That class have one or more concretions, but they are all internal, so inaccessible to us mortals (heuuuu... I meant simple application developers).

Well, even if WebAssemblyJSRuntime was accessible, converting/casting the IJSRuntime to JSRuntime (or worst to WebAssemblyJSRuntime) would not be a super-strong solution. WebAssemblyJSRuntime is sealed so I could not inherit from it to update the options, which we should not have to do anyway; another not so strong/impossible solution...

image

To fix my problem, I just sent untyped anonymous objects to JavaScript, but that's not a solution for a real project (mine is a small experiment, so I don't mind losing the types). It would have been so much easier to just be able to configure the JsonSerializerOptions and set the IgnoreNullValues to true. It would also follow your own patterns about how to configure options, making it easier for everyone (i.e. being linear throughout the .Net Core ecosystem).

That said, I think that you should focus on better interoperability with JavaScript. There are countless libraries already implemented, and it would be a shame not to be able to use them when needed. Yes, at some point, more and more Blazor libraries will appear, but I believe that it would help kickstart Blazor if we could just use JS stuff easily...

Keep up the good work on making .Net a better place!

some temporally solution

public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);

            ConfigureServices(builder.Services);

            builder.RootComponents.Add<App>("app");

            var host = builder.Build();

            ConfigureProviders(host.Services);

            await host.RunAsync();
        }

        public static void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions();
        }

        public static void ConfigureProviders(IServiceProvider services)
        {
            try
            {
                var jsRuntime = services.GetService<IJSRuntime>();
                var prop = typeof(JSRuntime).GetProperty("JsonSerializerOptions", BindingFlags.NonPublic | BindingFlags.Instance);
                JsonSerializerOptions value = (JsonSerializerOptions)Convert.ChangeType(prop.GetValue(jsRuntime, null), typeof(JsonSerializerOptions));
                value.PropertyNamingPolicy = null;
                value.IgnoreNullValues = true;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"SOME ERROR: {ex}");
            }
        }
    }

Hello everyone, I understand that the use of serialization with System.Text.Json is faster and you want to impose yourself. But there are quite a few things to improve about this library. After several days wasted trying to make a component that can interact with Google Maps using JsRuntime and trying to pass a class with parameters that I need only the parameters that contain data to be serialized. I think it is important to consider that serialization can be configured with JsRuntime or HttpClient, since it is quite important to be able to configure the needs as it happens with Asp.Net core, that through .AddNewtonsoftJson () you can change the library to use in the serializer or as options.JsonSerializerOptions.IgnoreNullValues โ€‹โ€‹= true) that allows you to change the serialization options.
In my opinion I think it is important to consider the implementation in Blazor of systems to be able to configure serialization systems both in JSRuntime and in HttpClient. IMHO consider implementing these configuration options, I think they will help a lot.

I also think that ability to customize serialization would be useful.

Recently I had issue with trying to use prefix-free.js polyfill, where it crashed Blazor since it returned null value where Blazor expected integer value.

It would be good to have ability to set "IgnoreNullValues" option for that.

Moving this to the Next sprint planning for reevaluation

Just because it hasn't been mentioned yet.

The temporary solution provided by @rChavz only works on client-side blazor because there the injected IJSRuntime is a singleton (which imo is an implementation detail and shouldn't be counted on). For server-side (and libraries of course) you'd have to do this modification for every injected IJSRuntime.

My observation is that we're dealing with type incompatibility between JavaScript and .NET. The type undefined is simply not defined with .NET. One way to address it may be to do as suggested in the thread by instructing to not serialize types that are NULL with C#, but what if JavaScript library actually uses null and also undefined? Maybe a more stable approach would be to introduce a new C# type and Convert on that type when serializing. Then it would be possible to instruct to emit values NULL or otherwise, or simply not to emit, e.g. instead of Nullable we had a Blazor specific Undefinable :-)

var person = new Undefinable<Person>() emits undefined
var person = new Undefinable<Person>(null) emits null
var person = new Undefinable<Person>(new Person()) emits person instance

EDIT

I believe this part of the discussion doesn't actually belong in this issue. If there was to be a new type Undefinable or any other means of conveying that a value is undefinable, it would have to go to https://github.com/dotnet/runtime as far as I know.
Also it might not be as straight forward to implement Undefinable as I thought since there is no WriteUndefined method on Utf8JsonWriter.

Original

You make a distinction between null and undefined but what about completely omitted? When you test it with hasOwnProperty, those won't be the same either.
We already have a distinction between null and completely omitted - the IgnoreNullValues option.
Do we really need a distinction between undefined and completely omitted?

If so, instead of adding a new type, I would suggest using an attribute along with a new property in the JsonSerializerOptions. You should also consider that it should be fairly straightforward to implement such a type yourself with a custom converter.
The name for the new JsonSerializerOptions property could be SerializeNullAsUndefined and if true, causes the default converters to emit undefined instead of null. The default is false.
In the same fashion there could be an attribute JsonSerializeNullAsUndefinedAttribute.

Here I should mention that I still think that it would be nice to be able to customize the options on different levels, not only with the JsonSerializerOptions. Having one attribute for each option might not be the best approach but there is no JsonPropertyAttribute as of now, so I'm sure there's a reason for not doing that.

The issue (as from what I see is type incompatibility - not unexpected since we deal with two separate frameworks) can be solved either by introducing artifacts as described in this thread, either via sprinkling the serializer with attributes, or by introducing POCO, e.g. Undefinable<T> for the JS Runtime (Blazor) to deal with (personally I prefer the Undefinable<T> since the proposed serializer attribute kind of lock down to null -> undefined and we don't really know what the target expect, e.g. if null then one behavior, if undefined then another behavior and so on in any combination).

But there is another way to go about this without adding anything either to serializer or to Blazor, and that is to simply use the anonymous type support with .NET. To illustrate, let's assume that we need to ship off an object to some JavaScript library, that accepts both null, undefined or some instance, e.g. let's define the JavaScript type (via typescript declaration) personType as

interface personType {
    age?: number | null,
    name?: string | null,
    address?: string | null
}

Note that the ? tells typescript that we may omit the property, that in turns resolves to undefined with JavaScript (but for fun, please read this thread about undefined).

Let's say that some JavaScript library now depends on this object, and take action due to how the properties are set up (resulting in at least some permutations to consider).

In one instance, I want to push a person object with name and age, but with address as undefined, because it has one behavior with the library, then I can go ahead and do

JSRuntime.InvokeVoidAsync("personLibrary", new { name = "name", age = 42 })

And it resolves to the JavaScript object

{name: 'name', age: 42}

Where the property address is undefined.

Later I want to send the person object, with address set to null, because this has another behavior with the library. Then I can go ahead and do

JSRuntime.InvokeVoidAsync("personLibrary", new { name = "name", age = 42, address = (string)null })

And it resolves to the JavaScript object

{name: 'name', age: 42, address: null}

Another option is to use ExpandoObject, but it doesn't seem to be supported by Blazor WebAssembly.

Good points. I'm biased against the idea of Undefinable (especially on blazor level) because it would bloat up the models in my use case and make them harder to use. If you implement that on blazor level, libraries without a blazor dependency will be much more difficult to customize for serialization. That's why I would tie Undefinable to the serializer (System.Test.Json) if something like that gets implemented.

For your two alternatives, I can actually talk about a real world scenario. I'm working on a blazor port for Chart.js and there we need to a lot of model classes with inheritance, custom serialization and more, so not simple POCO. Since we can't set IgnoreNullValue to true in the options we do the following with the arguments before invoking JavaScript with them:

  • Serialize once with an option to get rid of the null values
  • Deserialize to ExpandoObject (json.net is required for that)
  • Convert ExpandoObject to Dictionary<string, object> recursively because System.Text.Json can't handle ExpandoObject

This is a bit simplified because there are some more things we have to do because our model contains DotNetObjectReferences and polymorphic serialization isn't supported yet. I have already described the full procedure in an earlier comment.

So you're absolutely correct that you can use ExpandoObject to work around this and in fact, it's currently the only solution I know of to support my use-case. Anonymous types will only work for very simple scenarios but definitely not for something like a port of a large js library or any other scenario where you have a big model with inheritance etc.

I tried the ExpandoObject (that seems not supported in Blazor domain, but in .NET Core domain), and I was surprised to see that CamelCase is not working when using Serialize. It behaves as you would simply pass in the Dictionary<string, object> to Serialize.

That said, there is a third option that is available if we don't want to, or able to, use anonymous types. This option is somewhat more advanced, involving building your type dynamically and to pass that object to JSRuntime. It actually works great! The Deserialize works great too. This scenario is supported in Blazor domain, and CamelCase is honored :-)

I have a working solution with polymorphic System.Text.Json using the Convert plugin API, it's not that hard to do. However, I plug that converter into my API, and let my Blazor client send/recv via backend. I do not use that with JSRuntime (as you guys observed before, the serializer is placed somewhere within JSRuntime and not exposed).

Adding an Undefinable<T> class to the framework would mean coupling our C# classes with the serialization process and would introduce a whole new layer of complexity; we already have null, I think that's enough ๐Ÿ˜‰

Back to the basic; requirements are: we need the ability to customize the serialization process.

How? Either with access to the global "ignore null" switch (for global policies), an "ignore null attribute", or another way.

Attributes, while it's linear with the good old days of data annotation, would also tie our classes with the serialization process, meaning either polluted POCOs or additional transitory types (more conversion).

Instead, a new (I think better) way could be a new API that allows configuration similar to the following:

var objectConfigs = new SerializationObjectConfigs<MyObject>(o=> o
    .Property(p => p.IgnorableProp).IgnoreNull()
    .Property(p => p.OtherProp).ConvertUsing(p => MyCustomConverterLogic(p))
);
var json = Convert(myObject, objectConfigs);

Convert represents either the serializer or JSRuntime and could allow passing global settings as well.

We could also capitalize on DI for a more streamlined process, like:

// Startup
services.AddSingleton(new SerializationObjectConfigs<MyObject>(o=> o
    .Property(p => p.IgnorableProp).IgnoreNull()
    .Property(p => p.OtherProp).ConvertUsing(p => MyCustomConverterLogic(p))
));

// Consumer
MyObject myObject = ...;
var json = Convert(myObject);

We could also introduce an ISerializationObjectConfigs<T> interface, but I omitted it for simplicity's sake.

I think that approach would be more flexible and would aim at reusability and flexibility. Those configuration objects could be created globally or locally or wherever we need, decoupling our POCO from the serialization process. We could even distribute configurations in libraries using NuGet.

What do you think?

I also imply deserialization when I say serialization above.

The types name above are not great, but I think they are good enough to explain the idea.

I like your approach, decoupling is great! I also like the option of either pinning the property conversion to some basic transformation IgnoreNull, or dynamically via ConvertUsing (including the undefined case that's needed I hope). It may have the potential to be a candidate for the System.Text.Json serializer, and/or JSRuntime as well.

I'm thinking that this is a good candidate for construct/deconstruct a dynamic object before feeding/reading via JSRuntime :-)

Thanks for good input!

Interesting approach. However, I think now we're just talking about how to create, configure and apply JsonSerializerOptions and serialization attributes and I don't think this belongs here. I say this because I opened this issue for the sole purpose of being able to customize the serialization that the js-interop uses.
I don't think it would make sense to introduce a new system or even a new type like Undefinable. Instead we should just allow passing an instance of JsonSerializerOptions to the IJSRuntime methods. Anything more would be a feature for System.Text.Json and can be proposed on the respective repo. Could a staff member confirm if that's correct?

For me it would be enough to extend the IJSRuntime interface like so:

public interface IJSRuntime
{
    ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args);
    ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args);
}
public interface IJSRuntime
{
    ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args, JsonSerializerOptions options = default);
    ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args, JsonSerializerOptions options = default);
}

@Carl-Hugo Could you tell me why you want such a system _just_ for blazor-js-interop? I think instead you should consider writing an standard extension library to create and configure JsonSerializerOptions in the way you showed, that would be great. Some things you wanted can't be configured through the options like mapping a converter to a property but you could do it with dynamic type generation and the JsonConverterAttribute if you wanted to.

Mmm... if we actually could add to the Convert API (and to other configuration as well) with the serializer, then we could elevate serialization/deserialization! Good point there too :-)

Thanks!

JsRuntime uses System.Text.Json internally and I'm quite sure that won't change anytime soon. That's why I'm almost certain the features you two (@jan-johansson-mr and @Carl-Hugo) are considering may be a good fit for the serializer (System.Text.Json) but not the blazor-js-interop. If implemented for the serializer, they will automatically apply to the blazor-js-interop too. However, there are only disadvantages when implementing it _for_ the blazor-js-interop (not reusable, can't interact with the underlying API, etc).

JsRuntime uses System.Text.Json internally and I'm quite sure that won't change anytime soon. That's why I'm almost certain the features you two (@jan-johansson-mr and @Carl-Hugo) are considering may be a good fit for the serializer (System.Text.Json) but not the blazor-js-interop. If implemented for the serializer, they will automatically apply to the blazor-js-interop too. However, there are only disadvantages when implementing it _for_ the blazor-js-interop (not reusable, can't interact with the underlying API, etc).

The idea is indeed related to System.Text.Json. It could also be nice to have that at the InvokeAsync level (like the config parameter is passed to the serializer). But before JsRuntime comes System.Text.Json...

As for the repo, I'm a bit lost with all of the repo/merged/moved since the beginning; I'll copy/reformulate the idea in dotnet/runtime later.

I got a bit off the track of the "Exposing serializer options to JsRuntime" idea indeed, I forgot the context at "undefined," and my idea got bigger than anticipated while writing it down, I didn't want to highjack/fork your issue (which is crucial to Blazor IMHO).

Please @mkArtakMSFT, @danroth27, @SteveSandersonMS if you have any input of the issue discussed in this thread. Thank you!

Revisiting the idea of separating concern by @Carl-Hugo (after some sleep even) :-) gave me some reflections. There is three ideas floating around (I think), and to sum it up:

  • To pin down transform by attribute (@Joelius300)
  • To pin down transform by type (@jan-johansson-mr)
  • To pin down transform by linq expression (@Carl-Hugo)

All three ideas expresses the concern of where to put the knowledge of the transform. I'm not that convinced that the serializer is the place to put it. The reason for this is that the option of using Convert plugin with System.Text.Json give the option of _how to transform the value_, not the option of _include_ or _exclude_ a certain property. The only way to exclude a property with the serialization is to use the IgnoreNullValues or the IgnoreReadOnlyProperties with the JsonSerializerOptions.

The issue at hand is JavaScript specific, and has nothing to do with how we serialize or deserialize objects. The issue deals with how to handle the undefined object (very much used by JavaScript echo system). Unfortunately we need to handle three cases null, undefined and instance. That is, we also have to be able to pass through null when appropriate.

That's also why the dynamic object came into perspective; to create an object containing all properties with values (null or not) and then feed it to JSRuntime as the JSRuntime next is using System.Text.Json serializer to transform.

All three approaches have some cons and pros, the pro with attribute is to not pollute the type with information about transform concern and to be granular to the property, the con is that it pins down the transform behavior, e.g. always ignore if null. The pro with POCO type is that the transform behavior is set by type construction, the con is that it pollutes the type that is to be transformed. The pro with linq expression is that the concern about transform is placed in an external object and can be either fixed or dynamic, the con is more or less the same as with the attribute if fixed and the overhead to manage the external object when transforming if dynamic (e.g. to tie the configuration to exactly the object instance of concern when transforming).

I don't think we're talking about the same thing. Let me summarize again.

What is already possible

Customizing serialization via attributes. This means:

  • Applying custom converters to your own, non-generic types (JsonConverterAttribute)
  • Using JsonExtensionDataAttribute
  • Ignoring properties completely (JsonIgnoreAttribute)
  • Renaming properties for serialization (JsonPropertyNameAttribute)

What is not possible at the moment

Anything that can't be customized with attributes. There are more but these are the most important ones:

The goal of this feature request

Allow complete customization of the serialization process _within_ System.Text.Json. This means that you should be able to use attributes and JsonSerializerOptions (currently these are all of the available options in System.Text.Json) but there is no need for an additional system.

How to implement

There's only one piece missing, the options. Adding a parameter of type JsonSerializerOptions to the interop methods would fill this gap and allow all the currently impossible customizations I mentioned above.
This should be easy to implement and I'd be willing to submit a PR when this is discussed and approved. The only difficulty I can think of, is how to handle the DotNetObjectReferenceJsonConverterFactory we need for the DotNetObjectReference to be serialized correctly. The best approach I can think of is to create a copy of the options and adding the DotNetObjectReferenceJsonConverterFactory to the Converters.
Although I have invested quite some time in the interop area now, I have not worked with the underlying API yet. If my assumptions are false, please correct me :)

Related but not really part of this feature

The only other blazor-specific thing I can see value in would be a way to invoke JavaScript with a raw json string but I don't know the hidden implications regarding security behind that. Since there are still a lot of features missing in System.Text.Json, some people rely on Newtonsoft.Json and can't migrate yet (like me). Allowing raw json to be passed would make it a lot easier to use Newtonsoft.Json for the interop since you could just serialize the object with Newtonsoft.Json and pass the string directly to the JsRuntime. Currently you have to serialize and deserialize it (with Newtonsoft.Json), which modifies the object structure and types, before you can pass it to JsRuntime which will _again_ serialize it (with System.Text.Json). Needless to say, this hurts performance. I think this should be considered but maybe not as part of this issue.

Anything else like customization with fluent api, in a linq-like manner (as proposed by @Carl-Hugo) should be a new feature considered for System.Text.Json but not blazor.
Also I understand that undefined is JavaScript specific and doesn't exist in json but I haven't been able to imagine a scenario where you need a third option in addition to null and omitted. If you (@jan-johansson-mr) can show me a use-case of such a third option, I might understand what you mean. As of right now, I don't see any value in adding something like that. Still, it wouldn't be part of this issue but I agree that it belongs to blazor and therefore this repo. You should open a new issue where you provide more information and especially motivation about it so the discussion can be separated from this issue. You can of course link to discussions that already happened here and I'll make sure to check out your issue.

Thanks for a nice description! ๐Ÿ‘

My argument was never to emit undefined per se (maybe I could have been more clear about that), my argument was to control when to emit either null, instance or omit the property at transform (the omitted property of interest simply becomes undefined in JavaScript) :-)

Use of attributes pin the transform behavior, thus the property is always ignored if we use JsonIgnoreAttribute . The use of IgnoreNullValues is maybe not such a good option either, since undefined and null can have different meaning when hitting whatever JavaScript resource we may want to use.

Use of converter is doable, but then something (maybe blazor-js-interop on the client) have to look at the property value and decide what to do, e.g. emit or not emit, and so on.

As for now, I have some problems when using JavaScript resources and integrate with an otherwise amazing Blazor environment. I can manage and fix the issues, so that is not an issue, but I'm very curious about what integration surface we may have in the future, opening up an even smoother experience consuming the myriad of JavaScript resources out there in the wild! :-)

My argument was never to emit undefined per se (maybe I could have been more clear about that), my argument was to control when to emit either null, instance or omit the property at transform (the omitted property of interest simply becomes undefined in JavaScript) :-)

undefined means "does not exist/not defined/not there/missing". On the other hand, null means an "object that has a value of null". Telling the serializer to ignore null values on certain properties should be enough to cover that use case (all three use cases):

  • don't serialize nulls = undefined or instance
  • serialize nulls = null or instance

_For a way to reuse a POCO with multiple policies (not using attributes), that's on the serializer's plate._

Summarising

  • The problem outlined in this issue is the inability to access the serializer's options.
  • For the lack of ways to configure serialization, we should take that to one or more other issues.

I think that's what @Joelius300 is saying.

Hi there,

Thanks for input! I walked the road of dynamic object. I took to heart from @Carl-Hugo about decoupling stuff, but even further. I've got a working prototype with all the properties needed (our thread with null, undefined, instance) and it works like a charm. So until there is some other option, this will do more than fine.

In short, by weekend I'll be ready to continue my integration with JavaScript libraries that was hard (not impossible) to do before.

Again, it was a good dialog!! ๐Ÿ‘ ๐Ÿ‘

Hi,

i'm currently also implementing a blazor wrapper and also need the functionality to prevent the serialization of null values.
I also suspect that we will not be the only developers who will encounter this problem when developing a wrapper

Best regards
Sean

Hi @sean-mcleish,

You have the option to send a dictionary to the serializer, and pretty much send _any_ kind of object you want to JavaScript library. We're not bound to send an object as input to the serializer. Following this thread, I was exploring the path of creating dynamic object as input to the serializer. That path turned out to have its drawbacks. The other option is the path I'm now using, and that is to simply translate whatever payload I need for integration with JavaScript library to a Dictionary. This approach is pretty neat and works well. Please try it out if you want. Maybe other options will turn up, but until then this, at least for me, is a pretty good working solution.

The other way around (JavaScript to .NET) is somewhat easier, just do whatever model you need and use that.

Thanks

@jan-johansson-mr

As I've already said, that approach only works for simple objects but causes trouble for custom converters since the object won't be of the original type but Dictionary<string, object> or whatever. There are also performance issues if you have to convert all your objects to dictionaries.

However, I'm also using a similar solution using dictionaries and ExpandoObject since that seems to be the best workaround as of now. I just wanted to comment this since, unless you prove me wrong, I definitely have to disagree that you can _send any kind of object you want to JavaScript library_.

I have now tried a little bit around and I think I have a pre-working solution to do the following things:

  • serialize simple types
  • serialize nullable types
  • serialize IEnumerable
  • serialize IDictionary
  • serialize objects recursively
  • fully customizeable serialization settings

Here is the extension:


Code

public static object PrepareJsInterop<T>(this T obj, JsonSerializerOptions serializerOptions = null)
{
    var type = obj?.GetType();

    // Handle simple types
    if (obj == null || type.IsPrimitive || type == typeof(string))
    {
        return obj;
    }

    // Handle jsonElements
    if (obj is JsonElement jsonElement)
    {
        return jsonElement.PrepareJsonElement();
    }

    // Set default serializer options if necessary
    serializerOptions ??= new JsonSerializerOptions
    {
        IgnoreNullValues = true,
        PropertyNamingPolicy = null
    };

    // Handle all kind of complex objects
    return JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize<object>(obj, serializerOptions))
        .PrepareJsonElement();
}


private static object PrepareJsonElement(this JsonElement obj)
{
    switch (obj.ValueKind)
    {
        case JsonValueKind.Object:
            IDictionary<string, object> expando = new ExpandoObject();
            foreach (var property in obj.EnumerateObject())
            {
                expando.Add(property.Name, property.Value.PrepareJsonElement());
            }

            return expando;
        case JsonValueKind.Array:
            return obj.EnumerateArray().Select(jsonElement => jsonElement.PrepareJsonElement());
        case JsonValueKind.String:
            return obj.GetString();
        case JsonValueKind.Number:
            return obj.GetDecimal();
        case JsonValueKind.True:
            return true;
        case JsonValueKind.False:
            return false;
        case JsonValueKind.Null:
            return null;
        case JsonValueKind.Undefined:
            return null;
        default:
            throw new ArgumentException();
    }
}

If you want to serialize interfaces or abstract classes, you can use the following converter:


Converter

public class PolymorphicConverter : JsonConverterFactory
{
    /// <summary>
    ///     Initializes a new instance of the <see cref="PolymorphicConverter" /> class.
    /// </summary>
    public PolymorphicConverter()
    {

    }

    /// <inheritdoc />
    public override bool CanConvert(Type typeToConvert)
    {
        return typeToConvert.IsInterface || typeToConvert.IsAbstract;
    }

    /// <inheritdoc />
    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        return (JsonConverter)Activator.CreateInstance(
            typeof(PolymorphicConverter<>).MakeGenericType(typeToConvert),
            BindingFlags.Instance | BindingFlags.Public,
            null,
            new object[] {},
            null);
    }
}

public class PolymorphicConverter<T> : JsonConverter<T>
{
    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        var type = value.GetType();
        JsonSerializer.Serialize(writer, value, value.GetType(), options);
    }
}

I have not yet fully tested this code. If you have suggestions for edge cases or possible performance improvements, I would appreciate your feedback.

Have a nice day!

Thanks @sean-mcleish,

It looks like a solution, and if it works for your use case, then I guess it's good. As @Joelius300 pointed out, we have to be careful about performance. But from my point of view it's good to do the job and learn from the experience, and not be afraid to iterate and try different approaches.

.NET does a good job of introspection, but it's always expensive in terms of performance, the less you use dynamic or other constructs in that domain, the better. However, sometimes functionality and correctness is more important than performance.

Have a nice day!

I'm a bit less concerned about performance (don't get me wrong, it's still an important factor) but for many scenarios with js-libraries (not so much for casual use-cases) it's important to be able to use custom converters. The method provided by @sean-mcleish doesn't work for (most) custom converters.

See this fiddle.


Code

using System.Text.Json;
using System.Text.Json.Serialization;
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Reflection;

public static class Program
{
    public static void Main()
    {
        var obj = new Container
        {
            ComplexA = new ComplexA
            {
                StringProp = "asdf",
                IntProp = 100
            },
            ComplexB = new ComplexB("repeat ")
            {
                Times = 3
            }
        };

        string serializedNormal = JsonSerializer.Serialize(obj);
        string serializedPrepared = JsonSerializer.Serialize(PrepareJsInterop(obj));

        Console.WriteLine("Normal: ");
        Console.WriteLine(serializedNormal);
        Console.WriteLine();
        Console.WriteLine("Prepared: ");
        Console.WriteLine(serializedPrepared);
    }   

    public static dynamic PrepareJsInterop<T>(this T obj, bool ignoreNullValues = true)
    {
        var type = obj?.GetType();

        // Handle simple types
        if (obj == null || !type.IsComplex())
        {
            return obj;
        }

        // Handle enumerables
        if (type != typeof(string) && obj is IEnumerable asEnumerable)
        {
            var enumerableType = asEnumerable.GetType();

            if (enumerableType.GenericTypeArguments.Length > 1)
            {
                throw new NotSupportedException("Enumerables with multiple generic arguments are not supported.");
            }

            // Get the item type
            var itemType = enumerableType.GenericTypeArguments[0];

            // Return if it's a simple type or string
            if (!itemType.IsComplex() || itemType == typeof(string))
            {
                return obj;
            }

            // Create a new list with this type using reflection
            var currentValues = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(itemType));

            foreach (var item in asEnumerable)
            {
                var underlyingType = item?.GetType();
                currentValues?.Add(item.PrepareJsInterop(ignoreNullValues));
            }

            return currentValues;
        }

        // Handle complex types except for strings, enumerables
        IDictionary<string, object> expando = new ExpandoObject();

        // Iterate properties
        foreach (var propertyInfo in (obj.GetType()).GetProperties())
        {
            // Take the specified JsonPropertyName, if one has been defined
            var jsonProperty = propertyInfo.GetCustomAttribute<JsonPropertyNameAttribute>();

            // Get the underlying type if it's a nullable type
            var underlyingType = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;

            // Get the value of the current property
            var currentValue = propertyInfo.GetValue(obj);

            // Skip optionally zero values
            if (ignoreNullValues && currentValue == null)
            {
                continue;
            }

            // Run recursively if it's a complex type
            if (underlyingType.IsComplex())
            {
                currentValue = currentValue.PrepareJsInterop(ignoreNullValues);
            }

            expando.Add(jsonProperty == null ? propertyInfo.Name : jsonProperty.Name, currentValue);
        }

        return expando;
    }

    private static bool IsComplex(this Type type)
    {
        return !type.IsPrimitive && !type.IsEnum && type != typeof(string) ||
                type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(type);
    }
}

public class Container
{
    public ComplexA ComplexA {get;set;}
    public ComplexB ComplexB {get;set;}
}

public class ComplexA
{
    public string StringProp {get;set;}
    public int IntProp {get;set;}
}

[JsonConverter(typeof(ComplexBConverter))]
public class ComplexB
{
    private readonly string _value;
    public int Times {get;set;}

    public ComplexB(string value) => _value = value;

    private class ComplexBConverter : JsonConverter<ComplexB>
    {
        public override void Write(Utf8JsonWriter writer, ComplexB value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(string.Concat(Enumerable.Repeat(value._value, value.Times)));
        }

        public override ComplexB Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
        {
            throw new NotImplementedException();
        }
    }
}

Hey @Joelius300,

thanks for the hint and your feedback.

I have now revised the extension so that almost everything should be possible.
I have updated my post above.

Here is the extension:


Code

public static object PrepareJsInterop<T>(this T obj, JsonSerializerOptions serializerOptions = null)
{
    var type = obj?.GetType();

    // Handle simple types
    if (obj == null || type.IsPrimitive || type == typeof(string))
    {
        return obj;
    }

    // Handle jsonElements
    if (obj is JsonElement jsonElement)
    {
        return jsonElement.PrepareJsonElement();
    }

    // Set default serializer options if necessary
    serializerOptions ??= new JsonSerializerOptions
    {
        IgnoreNullValues = true,
        PropertyNamingPolicy = null
    };

    // Handle all kind of complex objects
    return JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize<object>(obj, serializerOptions))
        .PrepareJsonElement();
}


private static object PrepareJsonElement(this JsonElement obj)
{
    switch (obj.ValueKind)
    {
        case JsonValueKind.Object:
            IDictionary<string, object> expando = new ExpandoObject();
            foreach (var property in obj.EnumerateObject())
            {
                expando.Add(property.Name, property.Value.PrepareJsonElement());
            }

            return expando;
        case JsonValueKind.Array:
            return obj.EnumerateArray().Select(jsonElement => jsonElement.PrepareJsonElement());
        case JsonValueKind.String:
            return obj.GetString();
        case JsonValueKind.Number:
            return obj.GetDecimal();
        case JsonValueKind.True:
            return true;
        case JsonValueKind.False:
            return false;
        case JsonValueKind.Null:
            return null;
        case JsonValueKind.Undefined:
            return null;
        default:
            throw new ArgumentException();
    }
}

If you want to serialize interfaces or abstract classes, you can use the following converter:


Converter

public class PolymorphicConverter : JsonConverterFactory
{
    /// <summary>
    ///     Initializes a new instance of the <see cref="PolymorphicConverter" /> class.
    /// </summary>
    public PolymorphicConverter()
    {

    }

    /// <inheritdoc />
    public override bool CanConvert(Type typeToConvert)
    {
        return typeToConvert.IsInterface || typeToConvert.IsAbstract;
    }

    /// <inheritdoc />
    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        return (JsonConverter)Activator.CreateInstance(
            typeof(PolymorphicConverter<>).MakeGenericType(typeToConvert),
            BindingFlags.Instance | BindingFlags.Public,
            null,
            new object[] {},
            null);
    }
}

public class PolymorphicConverter<T> : JsonConverter<T>
{
    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        var type = value.GetType();
        JsonSerializer.Serialize(writer, value, value.GetType(), options);
    }
}

The performance is comparable to the previous version but still not perfect.

Looks promising. I did some perf tests with BenchmarkDotNet.

Tl;dr:

Seems to work fine.
Performance is definitely impacted but how important that is, you have to decide for yourself. In a complex ChartJs.Blazor scenario (which is probably about five times the complexity of case 3), it might actually be a bit too much but I can't test it easily because there's too much other stuff involved (like json.net).


Generic code for for all cases

[MemoryDiagnoser]
public class PrepareForInterop
{
    private static readonly object s_testObj = ...;

    [Benchmark]
    public void SerializeNormal()
    {
        JsonSerializer.Serialize(s_testObj);
    }

    [Benchmark]
    public void SerializePrepared()
    {
        JsonSerializer.Serialize(s_testObj.PrepareJsInterop());
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<PrepareForInterop>();
        Console.WriteLine(summary);
    }
}

Host

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.264 (2004/?/20H1)
AMD Ryzen 7 2700X, 1 CPU, 16 logical and 8 physical cores
.NET Core SDK=5.0.100-preview.4.20258.7
  [Host]     : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
  DefaultJob : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT

Test case 1 - Very simple

private static readonly object s_testObj = 100;
|            Method |     Mean |   Error |  StdDev |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|------------------ |---------:|--------:|--------:|-------:|------:|------:|----------:|
|   SerializeNormal | 252.8 ns | 4.92 ns | 6.05 ns | 0.0439 |     - |     - |     184 B |
| SerializePrepared | 346.0 ns | 2.18 ns | 2.03 ns | 0.0496 |     - |     - |     208 B |

Test case 2 - Annonymous type

private static readonly object s_testObj = new
{
    A = "asdf",
    B = new
    {
        I = 100,
        D = 12345.6789
    },
    C = new[] { 1, 2, 3, 4, 5 },
    F = new object[]
    {
        new
        {
            A = 232,
            B = "asdfasfdasdf"
        },
        Enumerable.Range(1, 10),
        new[]{'f','c','b'}
    }
};
|            Method |         Mean |      Error |     StdDev |   Gen 0 |  Gen 1 | Gen 2 | Allocated |
|------------------ |-------------:|-----------:|-----------:|--------:|-------:|------:|----------:|
|   SerializeNormal |     4.406 us |  0.0809 us |  0.0717 us |  0.2441 |      - |     - |   1.02 KB |
| SerializePrepared | 1,318.251 us | 21.6898 us | 19.2274 us | 13.6719 | 5.8594 |     - |  61.77 KB |

Test case 3 - Custom converter

private static readonly object s_testObj = new Container
{
    ComplexA = new ComplexA
    {
        StringProp = "asdf",
        IntProp = 100
    },
    ComplexB = new ComplexB("repeat ")
    {
        Times = 3
    }
};

public class Container
{
    public ComplexA ComplexA { get; set; }
    public ComplexB ComplexB { get; set; }
}

public class ComplexA
{
    public string StringProp { get; set; }
    public int IntProp { get; set; }
}

[JsonConverter(typeof(ComplexBConverter))]
public class ComplexB
{
    private readonly string _value;
    public int Times { get; set; }

    public ComplexB(string value) => _value = value;

    private class ComplexBConverter : JsonConverter<ComplexB>
    {
        public override void Write(Utf8JsonWriter writer, ComplexB value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(string.Concat(Enumerable.Repeat(value._value, value.Times)));
        }

        public override ComplexB Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
        {
            throw new NotImplementedException();
        }
    }
}
|            Method |         Mean |       Error |      StdDev |  Gen 0 |  Gen 1 | Gen 2 | Allocated |
|------------------ |-------------:|------------:|------------:|-------:|-------:|------:|----------:|
|   SerializeNormal |     962.5 ns |     6.12 ns |     5.72 ns | 0.1736 |      - |     - |     728 B |
| SerializePrepared | 561,230.5 ns | 3,369.15 ns | 3,151.51 ns | 5.8594 | 2.9297 |     - |   27470 B |

Hey @Joelius300,

thanks again for your feedback and for the detailed performance tests!

I have just updated the code and made some optimizations and bug fixes.

Have a nice weekend!

With the last activity here falling back three months, I wanted to check in again.
.NET 5 is getting closer (and I'm excited) so how is this issue doing? Are there updates from the sprint planning? Will it make it into the .NET 5 release? :)

I would like to add my voice to the chorus of devs asking for a way to customize the (de)serialization of JS objects. Because Webassembly has no access to the DOM, I'm forced to use JS libraries for rich UI interactions like drag & drop (interact.js for example). I can wrap these libraries into Blazor components, .Net classes and callbacks, but the JS functions and callbacks return huge objects, some of them several layers deep, including references and circular references. Presently I'm forced to either create separate JS & .Net classes that mirror the library objects, minus the references, or manually remove the references in JS before sending the object to .Net. This is quite inconvenient and adds a lot of work when using these libraries, not to mention having to be aware of every nook and cranny of a JS library just to use pieces of it (having to know what to remove from objects so it serializes properly).

Ideally I would like to be able to:

  • Detect and remove circular references so that the object can be transmitted to .Net

AND/OR

  • Limit the depth of serialization so I can keep the top tier of values only

UPDATE: And another use-case I just bumped into! I do not want to serialize null values. Some JS libs expect "options" objects during their initialization phase, and when I pass these objects as serialized .Net classes, I end up providing a bunch of null values. The nulls supersede the default values that the JS lib would have applied for these particular options.

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

..The nulls supersede the default values that the JS lib would have applied for these particular options.

This is the main issue I was facing in my original post. There doesn't seem to be a usable workaround and serializing everything multiple times isn't feasible for large objects. I'd love some clarity on this. Again, I'll happily submit a PR but as long as the discussion is ongoing, I don't think that is the next step.

..The nulls supersede the default values that the JS lib would have applied for these particular options.

This is the main issue I was facing in my original post. There doesn't seem to be a usable workaround and serializing everything multiple times isn't feasible for large objects. I'd love some clarity on this. Again, I'll happily submit a PR but as long as the discussion is ongoing, I don't think that is the next step.

Honestly I don't see much of a debate anymore, looks like 1 year ago @danroth27 was leaning towards "customizing serialization options per JS invocation.", and then put the idea on the backburner forever. But at least initially your idea seemed to garner interest and wasn't outright rejected.

I'm wondering what alternatives we have other than to customize the options per invocation. We can't customize the options per injected JSRuntime instance, as this will impact an entire app, when in reality the customization was only needed by a lib (and other libs might need different customizations). Other than the entire instance or the invocations, I don't see any other opportunity to customize the options.

For now I'm using the hack above by @rChavz, so it impacts the whole app vs. just my lib (not good).

If all it takes to resolve this is a PR by you, then please do it and be a hero! I fear that the alternative is waiting another year or two until someone at MS has the bandwidth.

FWIW - I already have a PR open (#22317) for some time now but haven't received any MS feedback on it. The implementation is pretty straight forward... not sure what the hang up is. Would love to hear something on it either way.

@StevenRasmussen Thank you. I've actually already commented on it a while back and still stand by the points I made then. Also, if you want to use it, you'll need to rebase it because there are now conflicts.

@Joelius300 - Yes, thanks for your comments... and I agree with them. I was hoping for some MS feedback before proceeding with making any changes... and you're correct, since it's been so long I'll need to rebase and resolve any conflicts. I'm happy to put forth the effort to do so but was hoping for some official feedback as to whether the PR had any merit before investing the time.

That's why I think it's best to put off opening the PR until the discussion here is done :)

I think that can be true in many instances... my hope was that by putting forth something concrete that it would in itself further the discussion. Clearly that hasn't happened so I guess we wait until this gets enough exposure for MS to take notice. Looking at the comments it appears that this is merely a discussion between fellow outsiders without any real feedback/input from MS. The last comment from MS was that it was being put into the Next sprint planning for consideration which was back in April. It doesn't appear that anything has happened since then. I guess we wait :(

Hi chaps.

I have just come here after search for a way to do this, as @Joelius300 I am also using ChartJS.
In my Program.cs I configured the JsonSerializerOptions, this had no effect... Do you think we could make this work? Would that be something worth adding to the framework?

            services.Configure<JsonSerializerOptions>(options =>
            {
                options.IgnoreNullValues = true;
            });

Edit

My solution to my ChartJS issue was to recusivly remove the nulls on the JS side, I used solution 4) from this chap => https://stackoverflow.com/questions/286141/remove-blank-attributes-from-an-object-in-javascript ๐Ÿ˜‰

@LukeTOBrien Don't know if this is allowed but if you're using Blazor and want Chart.js charts, take a look at ChartJs.Blazor ๐Ÿ˜„

@Joelius300 thanks! - Looks good and I can tell you are passionate about it.
My app is kind of a WYSIWYG, so the Chart is in an iframe, so I can only use JS for that. - I like your project thought ๐Ÿ˜„

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rynowak picture rynowak  ยท  3Comments

ipinak picture ipinak  ยท  3Comments

farhadibehnam picture farhadibehnam  ยท  3Comments

rbanks54 picture rbanks54  ยท  3Comments

aurokk picture aurokk  ยท  3Comments