Because [my original question](https://codereview.stackexchange.com/questions/223375/recursive-conversion-from-expandoobject-to-dictionarystring-object) was lacking many details, I have been advised to ask a new question.  
I will repeat the important parts of the original question and add examples etc to hopefully make it clear.  



For [my blazor library](https://github.com/Joelius300/ChartJSBlazor) which is a modification of [this awesome library](https://github.com/mariusmuntean/ChartJs.Blazor) I have to convert an `ExpandoObject` into a `Dictionary<string, object>` since `ExpandoObject`s aren't serialized properly in the newest preview versions of dotnet-core. See [my question related to this](https://stackoverflow.com/questions/56693103/invoke-javascript-method-from-c-sharp-with-dynamic-parameter) for more details.

For this new question I have already improved my code with recommendations from [this](https://codereview.stackexchange.com/a/223380/203991) and [this](https://codereview.stackexchange.com/a/223379/203991) answer on my previous question.  

This is my current (and hopefully already better) approach. I have added summaries to more clearly show my intent but I'm sure these could be improved as well since I'm not very good at documenting my code.  
Also worth mentioning if it's not clear, these changes did not affect the program and everything still works.


`````
/// <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;
        }
    );

`````


Something I didn't mention in detail is why I need this conversion in the first place. This is also the reason [this answer](https://codereview.stackexchange.com/a/223393/203991) probably wont help :/  

I'll explain it with a simplified example:  
`````
public void Demo()
{
    SomeConfig config = new SomeConfig
    {
        Options = new SomeOptions   // it can contain complex types
        {
            SomeInt = 2,            // it can contain primative types
            SomeString = null,      
            Axes = new List<Axis>   // it can contain complex lists
            {
                new Axis(),
                new Axis
                {
                    SomeString = "axisString"
                }
            }
        },
        Data = new SomeData
        {
            Data = new List<int> { 1, 2, 3, 4, 5 },     // it can contain primative lists
            SomeString = "asdf",
            SomeStringEnum = MyStringEnum.Test          // it can contain objects with custom parsing (for JSON.NET, not the parsing that's done when invoking the JS sadly
        }
    };

    // now there are three options for invoking the javascript (I invoke it using IJSRuntime.InvokeAsync)

    // this does not work because there are still nulls and the StringEnum will be parsed as an object instead of string (
    // json.net parses it correctly because of the custom converter but .net does not as it just sees an object)
    InvokeJavascript(config);

    // the nulls are gone and the StringEnum is parsed correctly but 
    // this does not work because .Net (NOT JSON.NET) has to convert the parameter to json which is not possible if the object is an ExpandoObject
    InvokeJavascript(StripNulls(config));

    // this does work because .Net can serialize a Dictionary but not an ExpandoObject. 
    // the nulls are still gone and the StringEnum is parsed correctly
    InvokeJavascript(ConvertExpandoObjectToDictionary(StripNulls(config))); 
}

/// <summary>
/// Returns an object that is equivalent to the given parameter but without any null members.
/// <para>This method is necessary because of the custom parsing for string enums and because for server-side blazor projects, 
/// the interop doesn't work if there are null values (no idea why, this really should be fixed sometime in the future)</para>
/// </summary>
private static ExpandoObject StripNulls(SomeConfig chartConfig)
{
    // Serializing with the custom serializer settings remove null members
    // this cleanChartConfigStr doesn't contain a single null value and also the StringEnums are parsed as strings instead of objects because I use JSON.NET here
    string cleanChartConfigStr = JsonConvert.SerializeObject(chartConfig, JsonSerializerSettings);

    // Get back an ExpandoObject with the clean config - having an ExpandoObject allows us to add/replace members regardless of type
    // which is necessary for preserving the DotNetObjectRefs (see below). Also if it were to be parsed back to the original Type (SomeConfig) 
    // the null values would return (and the string enums would throw errors since I've only implemented writing json but that could be fixed)
    dynamic clearConfigExpando = JsonConvert.DeserializeObject<ExpandoObject>(cleanChartConfigStr, new ExpandoObjectConverter());

    /*
     * There are DotNetObjectRefs in the config which are manually being restored here and assigned to the right property in the dynamic config
     */

    return clearConfigExpando;
}

// serializer settings for json.net to ignore all the null values and use the CamelCaseNamingStrategy
private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore,
    ContractResolver = new DefaultContractResolver
    {
        NamingStrategy = new CamelCaseNamingStrategy(true, false)
    }
};

`````

I can show you the difference when looking at the json that get produced when invoking the javascript with different objects:

**Directly invoking the javascript with the `SomeConfig` instance from the demo results in this json code being used in the javascript:**
```json
{
    "options": {
        "someInt": 2,
        "someString": null,
        "axes": [
            {
                "someString": null
            },
            {
                "someString": "axisString"
            }
        ]
    },
    "data": {
        "data": [
            1,
            2,
            3,
            4,
            5
        ],
        "someString": "asdf",
        "someStringEnum": {}
    }
}
```

**First parsing it with custom parsing (using JSON.NET), then reading it back as `ExpandoObject` (using JSON.NET) and converting that to a `Dictionary<string, object>` results in the following json being used:**
```json
{
    "options": {
        "someInt": 2,
        "axes": [
            {},
            {
                "someString": "axisString"
            }
        ]
    },
    "data": {
        "data": [
            1,
            2,
            3,
            4,
            5
        ],
        "someString": "asdf",
        "someStringEnum": "someTestThing"
    }
}
```
Note that both of these json string were not produced by json.net but by .net itself when invoking the javascript using `IJSRuntime.InvokeAsync`.  

The json of the second option is in this example exactly the same as the intermediate json `cleanChartConfigStr` in the `StripNulls` method. This `cleanChartConfigStr` does not contain the functions and `DotNetObjectRefs` which are added to the `ExpandoObject` later on in the `StripNulls` method.  When using `JSON.stringify` to print the json-string of that `ExpandoObject` (that's the json-string I showed you), you won't see those functions/`DotNetObjectRefs` either but this time because `JSON.stringify` doesn't print them, not because there are non-existent.  
I mention this because otherwise you might think that the `cleanChartConfigStr` as json-object would be equal to the json-object produced by the `ExpandoObject` which is not true (because of those functions and `DotNetObjectRefs`).  

I feel like this part was confusing, I hope you understand what's going on.  


Here are the remaining classes needed for the code to make sense:  
Because you will need the jsRuntime and the blazor project blabla to compile, I have created [a small demo-branch](https://github.com/Joelius300/ChartJSBlazor/tree/DemoCodeReview) on my github. It's ugly code and no, this will not be used anymore it's just so you can try it yourself if you want. The 3 json-strings (the two I showed + the intermediate one) are printed to the console (cmd, not browser) when you access the homepage or one of the chart-pages. Keep in mind that you'll need the newest version of .net core 3 (preview6) installed to run it. I think VS 19 preview is also required.
`````
class SomeConfig
{
    public SomeOptions Options { get; set; }
    public SomeData Data { get; set; }
}

class SomeOptions
{
    public int SomeInt { get; set; }

    public string SomeString { get; set; }

    public List<Axis> Axes { get; set; }
}

class SomeData
{
    public List<int> Data { get; set; }

    public string SomeString { get; set; }

    public MyStringEnum SomeStringEnum { get; set; }
}

class Axis
{
    public string SomeString { get; set; }
}

[JsonConverter(typeof(JsonStringEnumConverter))]
class MyStringEnum
{
    public static MyStringEnum Test => new MyStringEnum("someTestThing");


    private readonly string _value;
    private MyStringEnum(string stringRep) => _value = stringRep;
    public override string ToString() => _value;
}

class JsonStringEnumConverter : JsonConverter<MyStringEnum>
{
    public sealed override bool CanRead => false;
    public sealed override bool CanWrite => true;

    public sealed override MyStringEnum ReadJson(JsonReader reader, Type objectType, MyStringEnum existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException("Don't use me to read JSON");
    }

    public override void WriteJson(JsonWriter writer, MyStringEnum value, JsonSerializer serializer)
    {
        // ToString was overwritten by StringEnum -> safe to just print the string representation
        writer.WriteValue(value.ToString());
    }
}
`````

Now, I have put a lot of time and effort in this question and I hope now there's enough detail. As already mentioned, the real code (not this small demo example) is available on [github](https://github.com/Joelius300/ChartJSBlazor). The most important file for this question would be [ChartJsInterop.cs](https://github.com/Joelius300/ChartJSBlazor/blob/master/ChartJs.Blazor/ChartJS/ChartJsInterop.cs).  

Once again I'm mostly interested in the recursive solution to convert those `ExpandoObject`s. However I'm happy for any critisism so please also mention every small thing that bugs you :)