2

I've been stuck on a issue regarding dynamic types combined with jsRuntime invokation.

To have an actual question:
How can I invoke a Javascript function from C# code with a dynamic object as parameter?
If this is not possible, what's the best way to fully convert it so it can be accepted by the InvokeAsync function of a IJSRuntime?

Now, to what I've already tried (and obviously failed with).

I'm using a library from github which implements ChartJS in blazor. I've copied the source instead of using the nuget package since something seems to have been broken in the last updates from blazor or some other dependency.

What I'm doing is I'm invoking a Javascript function from my razor component and I'm also passing in my config for said function. The StripNulls method converts the config (actual type) to a dynamic type without all the properties that were null.

dynamic param = StripNulls(chartConfig);
return jsRuntime.InvokeAsync<bool>("ChartJSInterop.SetupChart", param);

I don't think it's necessary to put the code for the StripNulls method but maybe I'm missing something important so here's the code.

/// Returns an object that is equivalent to the given parameter but without any null member AND it preserves DotNetInstanceClickHandler/DotNetInstanceHoverHandler members intact
///
/// <para>Preserving DotNetInstanceClick/HoverHandler members is important because they contain DotNetObjectRefs to the instance whose method should be invoked on click/hover</para>
///
/// <para>This whole method is hacky af but necessary. Stripping null members is only needed because the default config for the Line charts on the Blazor side is somehow messed up. If this were not the case no null member stripping were necessary and hence, the recovery of the DotNetObjectRef members would also not be needed. Nevertheless, The Show must go on!</para>
/// </summary>
/// <param name="chartConfig"></param>
/// <returns></returns>
private static ExpandoObject StripNulls(ChartConfigBase chartConfig)
{
    // Serializing with the custom serializer settings remove null members
    var cleanChartConfigStr = JsonConvert.SerializeObject(chartConfig, JsonSerializerSettings);

    // Get back an ExpandoObject dynamic with the clean config - having an ExpandoObject allows us to add/replace members regardless of type
    dynamic clearConfigExpando = JsonConvert.DeserializeObject<ExpandoObject>(cleanChartConfigStr, new ExpandoObjectConverter());

    // Restore any .net refs that need to be passed intact
    var dynamicChartConfig = (dynamic) chartConfig;
    if (dynamicChartConfig?.Options?.Legend?.OnClick != null
        && dynamicChartConfig?.Options?.Legend?.OnClick is DotNetInstanceClickHandler)
    {
        clearConfigExpando.options = clearConfigExpando.options ?? new { };
        clearConfigExpando.options.legend = clearConfigExpando.options.legend ?? new { };
        clearConfigExpando.options.legend.onClick = dynamicChartConfig.Options.Legend.OnClick;
    }

    if (dynamicChartConfig?.Options?.Legend?.OnHover != null
        && dynamicChartConfig?.Options?.Legend?.OnHover is DotNetInstanceHoverHandler)
    {
        clearConfigExpando.options = clearConfigExpando.options ?? new { };
        clearConfigExpando.options.legend = clearConfigExpando.options.legend ?? new { };
        clearConfigExpando.options.legend.onHover = dynamicChartConfig.Options.Legend.OnHover;
    }

    return clearConfigExpando;
}

However if I try to invoke the InvokeAsync method with this dynamic object I get the following error:

System.NotSupportedException: 'The collection type 'System.Dynamic.ExpandoObject' is not supported.'

So after some research I stumbeled upon this answer which suggest converting the dynamic object to a Dictionary.
But sadly the exact same error occured with this code:

dynamic dynParam = StripNulls(chartConfig);
Dictionary<string, object> param = new Dictionary<string, object>(dynParam);
return jsRuntime.InvokeAsync<bool>("ChartJSInterop.SetupChart", param);

I then saw in the debug-inspector that even after I created a Dictionary there were still ExpandoObjects inside the Dictionary which probably caused the exception. It fairly surprised me that this convsersion wasn't recursive.

So I created my own recursive function to fully convert a dynamic object to a Dictionary. I implemented it like this and it seemed to work (it's a very big, nested object but all properties I looked at were fine):

private static Dictionary<string, object> ConvertDynamicToDictonary(IDictionary<string, object> value)
{
    return value.ToDictionary(
        p => p.Key,
        p => 
            p.Value is IDictionary<string, object> 
                ? ConvertDynamicToDictonary((IDictionary<string, object>)p.Value) 
                : p.Value
    );
}

And called like this (no I did not just accidentally pass in the wrong param):

dynamic dynParam = StripNulls(chartConfig);
Dictionary<string, object> param = ConvertDynamicToDictonary(dynParam);
return jsRuntime.InvokeAsync<bool>("ChartJSInterop.SetupChart", param);

This still throws the same exact exception and now I'm very frustrated and have no idea why it's still telling me about ExpandoObject when in my mind I don't see how it may not have been fully converted to a Dictionary<string, object>.
I have no further ideas and hope that some kind internet stanger can help me with this. Maybe there's just something wrong with my recursive solution or a small thing I'm overlooking but I have not managed to find it yet.

Additional information:

Versions:
Everything on the newest preview (.net Core 3, VS 19, C#)

Exception Stack trace:

at System.Text.Json.Serialization.JsonClassInfo.GetElementType(Type propertyType, Type parentType, MemberInfo memberInfo) at System.Text.Json.Serialization.JsonClassInfo.CreateProperty(Type declaredPropertyType, Type runtimePropertyType, PropertyInfo propertyInfo, Type parentClassType, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonClassInfo.AddProperty(Type propertyType, PropertyInfo propertyInfo, Type classType, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonClassInfo..ctor(Type type, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonSerializerOptions.GetOrAddClass(Type classType) at System.Text.Json.Serialization.JsonSerializer.GetRuntimeClassInfo(Object value, JsonClassInfo& jsonClassInfo, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonSerializer.HandleEnumerable(JsonClassInfo elementClassInfo, JsonSerializerOptions options, Utf8JsonWriter writer, WriteStack& state) at System.Text.Json.Serialization.JsonSerializer.Write(Utf8JsonWriter writer, Int32 flushThreshold, JsonSerializerOptions options, WriteStack& state) at System.Text.Json.Serialization.JsonSerializer.WriteCore(PooledByteBufferWriter output, Object value, Type type, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonSerializer.WriteCoreString(Object value, Type type, JsonSerializerOptions options) at System.Text.Json.Serialization.JsonSerializer.ToString[TValue](TValue value, JsonSerializerOptions options) at Microsoft.JSInterop.JSRuntimeBase.InvokeAsync[T](String identifier, Object[] args) at ChartJs.Blazor.ChartJS.ChartJsInterop.SetupChart(IJSRuntime jsRuntime, ChartConfigBase chartConfig)

Joelius
  • 3,839
  • 1
  • 16
  • 36
  • If possible I would rewrite StripNulls not to use any ExpandoObjects at all, and get rid of any and all `dynamic` if you possibly can. If anything in the chain is `dynamic`, effectively everything is ("dynamic contagion"), because nothing about a `dynamic` can be known with certainty at compile time. Have you set a breakpoint and fully expanded `param` at runtime, carefully examining the type of it and every object in it, and every property of every object in it, etc. recursively? – 15ee8f99-57ff-4f92-890c-b56153 Jun 20 '19 at 20:17
  • I tried completely removing the `StripNulls` method but the method is there and hacky for a purpose, it doesn't work without it. If you know how to remove properties from an object without using dynamic that would of course be great but I don't know how. – Joelius Jun 20 '19 at 20:25
  • It sounds to me like you're passing an ExpandoObject, and the error message is telling you not to. My recommendation would be to try not passing one. – 15ee8f99-57ff-4f92-890c-b56153 Jun 20 '19 at 20:26
  • Also no, I have not checked every single property recursively and I believe this is the issue. I'm currently rewriting the `ConvertDynamicToDictonary` method and I have an idea of what the issue might be. I'll update the question if I have success with that so you could look over it. – Joelius Jun 20 '19 at 20:26
  • You can't convert dynamic to dictionary. Dynamic isn't a type of object, it's a kind of reference. You can convert ExpandoObject to dictionary, however. – 15ee8f99-57ff-4f92-890c-b56153 Jun 20 '19 at 20:27
  • Okay I have to elaborate. I'm converting `IDictionary` to `Dictionary` with recursion since [`ExpandoObject` derives from `IDictionary`](https://learn.microsoft.com/en-us/dotnet/api/system.dynamic.expandoobject?view=netframework-4.8). Since `dynamic` seems to be implicitly castable to `IDictionary` as well, I talked about "converting 'dynamic'". – Joelius Jun 20 '19 at 20:30
  • You can treat a `dynamic` to anything as anything at all at compile time, because what `dynamic` does is late binding: A `dynamic` reference to an `ExpandoObject` is basically a dictionary `x` where the key "Foo" can be used like `x.Foo = "some value";` And `ExpandoObject` implements `IDictionary` as part of making that happen (you can't "derive from" an interface, but that's kind of a nit pick). – 15ee8f99-57ff-4f92-890c-b56153 Jun 20 '19 at 20:34
  • 1
    Ooh that makes sense. I've not been using dynamic too much as you can see. Yea I meant implement not derive of course :) – Joelius Jun 20 '19 at 20:36
  • @EdPlunkett Thank you a lot for your insights. I have now found a solution (see my edit). Feel free to tell me anything I did wrong or didn't cover. I'll close the question soon with my own answer. – Joelius Jun 20 '19 at 21:11

1 Answers1

2

Update

I have put this function on CodeReview (see) and I have some improvements. First of all some general stuff but then there is one fatal bug in the current solution. The handling for IEnumerable<object> is wrong. Converting only ExpandoObjects is fine but I completely leave out everything other than ExpandoObject. This is fixed in the new solution.
One thing you might want to do is turn this into an extension method to make it even more clean but in my case I didn't want to do that because I wanted the function to be private. If yours is public you should really consider an extension method.

/// <summary>
/// This method is specifically used to convert an <see cref="ExpandoObject"/> with a Tree structure to a <see cref="Dictionary{string, object}"/>.
/// </summary>
/// <param name="expando">The <see cref="ExpandoObject"/> to convert</param>
/// <returns>The fully converted <see cref="ExpandoObject"/></returns>
private static Dictionary<string, object> ConvertExpandoObjectToDictionary(ExpandoObject expando) => RecursivelyConvertIDictToDict(expando);

/// <summary>
/// This method takes an <see cref="IDictionary{string, object}"/> and recursively converts it to a <see cref="Dictionary{string, object}"/>. 
/// The idea is that every <see cref="IDictionary{string, object}"/> in the tree will be of type <see cref="Dictionary{string, object}"/> instead of some other implementation like <see cref="ExpandoObject"/>.
/// </summary>
/// <param name="value">The <see cref="IDictionary{string, object}"/> to convert</param>
/// <returns>The fully converted <see cref="Dictionary{string, object}"/></returns>
private static Dictionary<string, object> RecursivelyConvertIDictToDict(IDictionary<string, object> value) =>
    value.ToDictionary(
        keySelector => keySelector.Key,
        elementSelector =>
        {
            // if it's another IDict just go through it recursively
            if (elementSelector.Value is IDictionary<string, object> dict)
            {
                return RecursivelyConvertIDictToDict(dict);
            }

            // if it's an IEnumerable check each element
            if (elementSelector.Value is IEnumerable<object> list)
            {
                // go through all objects in the list
                // if the object is an IDict -> convert it
                // if not keep it as is
                return list
                    .Select(o => o is IDictionary<string, object>
                        ? RecursivelyConvertIDictToDict((IDictionary<string, object>)o)
                        : o
                    );
            }

            // neither an IDict nor an IEnumerable -> it's fine to just return the value it has
            return elementSelector.Value;
        }
    );

Original answer

Soo I finally found the answer after many hours. The problem was (kind of expected) the ConvertDynamicToDictionary method.
My recursive solution only checked if there was another IDictionary but what ended up happening was that there was an array of ExpandoObjects somewhere in the tree. After adding this check for IEnumerables it worked and the method now looks like this:

private static Dictionary<string, object> ConvertDynamicToDictonary(IDictionary<string, object> value)
{
    return value.ToDictionary(
        p => p.Key,
        p =>
        {
            // if it's another IDict (might be a ExpandoObject or could also be an actual Dict containing ExpandoObjects) just go trough it recursively
            if (p.Value is IDictionary<string, object> dict)
            {
                return ConvertDynamicToDictonary(dict);
            }

            // if it's an IEnumerable, it might have ExpandoObjects inside, so check for that
            if (p.Value is IEnumerable<object> list)
            {
                if (list.Any(o => o is ExpandoObject))
                { 
                    // if it does contain ExpandoObjects, take all of those and also go trough them recursively
                    return list
                        .Where(o => o is ExpandoObject)
                        .Select(o => ConvertDynamicToDictonary((ExpandoObject)o));
                }
            }

            // neither an IDict nor an IEnumerable -> it's probably fine to just return the value it has
            return p.Value;
        } 
    );
}

I'm happy for any critisism on this function as I don't know if I've covered every possibility. Feel free to tell me anything that catches your eye which could be improved. It definitely works in my case so this will be my answer to my own question.

Joelius
  • 3,839
  • 1
  • 16
  • 36
  • 1
    Glad to help :) was a tough one for me too. – Joelius Jun 21 '19 at 15:59
  • hi Joelius, I think would help in the subject https://blazorise.com/docs/extensions/chart/ – Jawad Sabir Jun 22 '19 at 08:02
  • @JawadSabir this is just another library which could do the Charts. I already have one which I can adjust etc. Maybe I'll use this but I'll doubt it since the one I'm using currently seems to allow for a lot more customization than the one you showed. – Joelius Jun 22 '19 at 08:08
  • @Juelius, Yes I agree, the library is not versatile as expected. However, in the link they say what is changed about Json serialization in Blazor that affect the default ChartJs.Blazor – Jawad Sabir Jun 22 '19 at 08:12
  • Ahh okay I see. As I said I updated the library for myself to the newest versions which seemed to break it. I'm glad I got a fix now, maybe I'll try to create a pull request but not sure about that yet. – Joelius Jun 22 '19 at 08:14
  • 1
    @JawadSabir I have updated the answer. It's really important to use the new solution because there's one fatal bug in the `IEnumerable` part of the old solution which can make you loose entries. – Joelius Jul 06 '19 at 15:08