Отражение — setValue из глубокого контекста

#c# #dynamic #reflection #.net-5 #system.text.json

#c# #динамический #отражение #.net-5 #system.text.json

Вопрос:

Я столкнулся с проблемой, несомненно, из-за недостатка знаний в процессе отражения, при попытке установить «сложную» иерархию классов на основе файлов Json.

Вот моя основная модель :

 public class Names
{
    public Weapons Weapons { get; set; }
    public Armors Armors { get; set; }
    public Utilities Utilities { get; set; }
    public Names()
    {
        Weapons = new Weapons();
        Armors = new Armors();
        Utilities = new Utilities();
    }
}
 

У каждого из них есть список подмоделей, подобных этому:

 public class Weapons
{
    public BattleAxe BattleAxe { get; set; } = new BattleAxe();
    public Bomb_Missile Bomb_Missile { get; set; } = new Bomb_Missile();
    // etc... Around 20 to 25
}
 

И, наконец, завершенная модель, которая является точным эквивалентом каждого файла json, но может иметь очень разные свойства :

 public class BattleAxe
{
    public string[] Normal { get; set; } = new string[0];
    public string[] DescriptiveAdjective { get; set; } = new string[0];
    public string[] Material { get; set; } = new string[0];
    public string[] Type { get; set; } = new string[0];
    public string[] Title { get; set; } = new string[0];
    public string[] Of { get; set; } = new string[0];
    public string[] NormalForTitle { get; set; } = new string[0];
}
 

Поскольку десериализатор MS Json не поддерживает преобразование в $type, как Newtonsoft ранее, я попытался заполнить значения, используя отражение, тоже так (я удалил всю нулевую проверку для удобочитаемости кода) :

 public static void Load()
{
    Names = new Names();
    foreach (var category in Names.GetType().GetProperties())
    {
        if (category is not null amp;amp; !(category.GetGetMethod()?.IsStatic ?? false))
        {
            var categoryType = category.PropertyType;
            foreach (var item in category.PropertyType.GetProperties())
            {
                var itemType = item.PropertyType;
                var subTypeData = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(File.ReadAllText($"./Assets/Names/{categoryType.Name}/{itemType.Name}.json"));
                var concreteObj = Activator.CreateInstance(itemType);
                foreach (var key in subTypeData.Keys)
                {
                    if (itemType.GetProperty(key) is not null amp;amp; concreteObj is not null)
                    {
                        var prop = concreteObj.GetType().GetProperty(key);
                        var convertedValue = ConvertJsonType(subTypeData[key], subTypeData[key].ValueKind, out var isReferenceType);
                        // It fails here
                        prop.SetValue(
                            isReferenceType ? convertedValue : null,
                            !isReferenceType ? convertedValue : null
                        );
                    }
                }
                item.SetValue(concreteObj, null);
            }
        }
    }
}
 

Таким образом, он завершается ошибкой в prop.SetValue(...) самом глубоком объекте в иерархии с разной ошибкой в зависимости от типа устанавливаемого значения.
Если это ссылка, она выдает System.Reflection.TargetException : 'Object does not match target type' Exception
И если это значение, оно выдает System.Reflection.TargetException : 'Non-static method requires a target.'
Зная, что у меня нет проблем с десериализацией, как показано здесь, только тот факт, что я использую динамический тип (и мой инстинкт подсказывает мне, что это на самом деле проблема …)
отладка

Я не добавляю ConvertJsonType(...) тело, поскольку оно функционально и действительно просто

Меня больше интересует «почему», чем «как», поэтому, если вы можете объяснить мне «теорию» проблемы, это очень помогло бы 🙂

Спасибо!

PS: Я знаю, что могу упростить вещи более читаемым / производительным способом, но я должен достичь этого с помощью reflection для личного обучения 🙂 То же самое для System.Text.Пространство имен Json, я не собираюсь для этого возвращаться к Newtonsoft

Ответ №1:

При вызове SetValue(instance, value) вы должны передать объект, свойство которого должно быть установлено.

Это дикое предположение, но вы могли бы попробовать это:

 prop.SetValue(concreteObj,
              !isReferenceType ? convertedValue : null);
 

Потому что вы хотите заполнить свойства concreteObj , а не само значение.

Если вы посмотрите на объект prop , это было возвращаемое значение concreteObj.GetType().GetProperty(key); . Если вы посмотрите на это близко, GetProperty это метод, Type который не привязан ни к одному экземпляру. Вот почему вам нужно передать экземпляр объекта в качестве первого параметра.


Я имею в виду это в положительном смысле: itemType.GetProperty(key) вызывается на каждой итерации, это будет одно и то же значение на каждой итерации, вы можете привести его к циклу.

Комментарии:

1. Вроде того же ответа, что и Guru, так что это действительно было неправильное использование setValue . Я просто нахожу его объяснение более привлекательным для меня, но это всего лишь вопрос стиля, оба были правильным решением, спасибо 🙂 Что касается вашего itemType.GetProperty(key) комментария, как я мог бы получить его перед циклом в качестве его цели, чтобы проверить, правильно ли текущий ключ из моего объекта json, который я использую, присутствует в моем классе модели? Я знаю, что это дорого, но у меня нет простой идеи сократить его использование

2. Только мой ответ был перед его. По крайней мере, вы его нашли.

Ответ №2:

Поскольку состояние документов TargetException выдается, когда:

Тип obj не соответствует целевому типу, или свойство является свойством экземпляра, но obj является null .

Передача null для obj in SetValue допустима, когда вы пытаетесь установить значение для статического свойства, а не для экземпляра. Тип свойства, являющийся ссылочным, не имеет ничего общего с тем, что свойство является экземпляром или статическим, поэтому ваш вызов должен выглядеть примерно так:

 prop.SetValue(concreteObj, convertedValue);
 

Также ваша item.SetValue(concreteObj, null); не выглядит правильной причиной concreteObj , которая должна быть вторым аргументом в этом вызове. Что-то вроде этого:

 item.SetValue(Names, concreteObj);
 

Также, если вам нужны только свойства экземпляра, вы можете предоставить BindingFlags для получения только свойств экземпляра:

 foreach (var category in Names.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
 

Также я бы сказал, что category is not null проверка избыточна, поэтому в паре с предоставлением BindingFlags вы должны if полностью удалить.

Комментарии:

1. Это действительно было неправильно понято в отношении функции setValue, извините за фиктивную ошибку:/ Спасибо за использование BindingFlags, это более элегантно. Была проблема с item.SetValue(concreteObj, null); тем, как вы сказали, но мне нужно было установить класс category вместо Names . Поскольку у меня еще не было никаких ссылок на него, мне пришлось использовать GetValue (еще один вызов отражения …) для имен. Спасибо за объяснение и советы 🙂