I'm out of square brackets, but I do have some extensions methods if it's of any interest.
They can traverse endlessly and get an object or set a generic value to the end of a key chain of dictionary. The Get() returns an object, but that's easily fixable to a generic type if necessary.
Since the keys are based on params you could throw in a dynamic arrays of keys if you have a very dynamic environment.
Works like this:
    var exists1 = info.Exists("Gen", "name", "One key too much"); // false
    var exists2 = info.Exists("Gen", "chapters"); // true
    var value1 = info.Get("Gen", "name", "One key too much"); // null
    var value2 = info.Get("Gen", "chapters"); // "50"
    var valueToSet = "myNewValue";
    var successSet1 = info.TrySet(valueToSet, "Gen", "before", "One key too much"); // false
    var successSet2 = info.TrySet(valueToSet, "Gen", "after"); // true | "Gen" -> "after" set to "myNewValue"
    var dictionary1 = info.FindSubDictionary("Gen", "name", "One key too much"); // null
    var dictionary2 = info.FindSubDictionary("Gen", "chapters"); // "Gen" sub dictionary
I haven't tested it much, nor given much thought into what it should return when, but if you find it useful, it could be something to start to tinker around with.
4 extension methods mostly working together, offering Exists, Get, TrySet and (what should be) an internal one to resolve the dictionary tree.
TrySet:
/// <summary>
/// Tries to set a value to any dictionary found at the end of the params keys, or returns false
/// </summary>
public static bool TrySet<T>(this System.Collections.IDictionary dictionary, T value, params string[] keys)
{
    // Get the deepest sub dictionary, set if available
    var subDictionary = dictionary.FindSubDictionary(keys);
    if (subDictionary == null) return false;
    subDictionary[keys.Last()] = value;
    return true;
}
Get:
/// <summary>
/// Returns a value from the last key, assuming there is a dictionary available for every key but last
/// </summary>
public static object Get(this System.Collections.IDictionary dictionary, params string[] keys)
{
    var subDictionary = dictionary.FindSubDictionary(keys);
    if (subDictionary == null) return null; // Or throw
    return subDictionary[keys.Last()];
}
Exists:
/// <summary>
/// Returns whether the param list of keys has dictionaries all the way down to the final key
/// </summary>
public static bool Exists(this System.Collections.IDictionary dictionary, params string[] keys)
{
    // If we have no keys, we have traversed all the keys, and should have dictionaries all the way down.
    // (needs a fix for initial empty key params though)
    if (keys.Count() == 0) return true;
    // If the dictionary contains the first key in the param list, and the value is another dictionary, 
    // return that dictionary with first key removed (recursing down)
    if (dictionary.Contains(keys.First()) && dictionary[keys.First()] is System.Collections.IDictionary subDictionary)
        return subDictionary.Exists(keys.Skip(1).ToArray());
    // If we didn't have a dictionary, but we have multiple keys left, there are not enough dictionaries for all keys
    if (keys.Count() > 1) return false; 
    // If we get here, we have 1 key, and we have a dictionary, we simply check whether the last value exists,
    // thus completing our recursion
    return dictionary.Contains(keys.First());
}
FindSubDictionary:
/// <summary>
/// Returns the possible dictionary that exists for all keys but last. (should eventually be set to private)
/// </summary>
public static System.Collections.IDictionary FindSubDictionary(this System.Collections.IDictionary dictionary, params string[] keys)
{
    // If it doesn't exist, don't bother
    if (!dictionary.Exists(keys)) return null; // Or throw
    // If we reached end of keys, or got 0 keys, return
    if (keys.Count() == 0) return null; // Or throw
    // Look in the current dictionary if the first key is another dictionary.
    return dictionary[keys.First()] is System.Collections.IDictionary subDictionary
        ? subDictionary.FindSubDictionary(keys.Skip(1).ToArray()) // If it is, follow the subdictionary down after removing the key just used
        : keys.Count() == 1 // If we only have one key remaining, the last key should be for a value in the current dictionary. 
            ? dictionary // Return the current dictionary as it's the proper last one
            : null; // (or throw). If we didn't find a dictionary and we have remaining keys, the dictionary tree is invalid
}
Edit:
Noticed you wanted CSV-keys. It should be easy to just construct from above, but I made a quick example using a single csv-key with a bonus optional custom separator.
    var csvResult1 = info.Get("Gen, chapters, One key too much"); // null
    var csvResult2 = info.Get("Gen, chapters"); // "50"
    // With custom separator
    var csvResult3 = info.Get("Gen;chapters;One key too much", separator: ";"); 
    var csvResult4 = info.Get("Gen; chapters", separator: ";"); 
Code:
/// <summary>
/// Returns a value from the last key of a csv string with keys, assuming there is a dictionary available for every key but the last.
/// </summary>
public static object Get(this System.Collections.IDictionary dictionary, string csvKeys, string separator = ",")
{
    if (String.IsNullOrEmpty(csvKeys)) throw new ArgumentNullException("Csv key input parameters is not allowed to be null or empty", nameof(csvKeys));
    var keys = csvKeys.Split(separator).Select(k => k.Trim());
    var subDictionary = dictionary.FindSubDictionary(keys.ToArray());
    if (subDictionary == null) return null; // Or throw
    return subDictionary[keys.Last()];
}
You better test that last one though, I didn't run it. Should run fine though... I just can't give any guarantees it will :)