Здравствуйте. Ситуация: есть модель(дисериализованный файл), довольно большая. В ней много строк и вложенных объектов. (Модель YML каталога).
[XmlRoot("yml_catalog")]
public class YMLCatalog
{
///
///
///
///
При валидации модели, в случае ошибки, я получаю (при помощи магии. См. краткое описание в комментариях) путь к ошибке. Например для YML файла с ошибкой в теге price, 0го оффера, в виде отсутствия цены, я получаю путь: shop/offers/offer['0 или 1', смотря как удобнее. Для XPath лучше 1]/price Value: Текст ошибки. (Value - поле со значением тега, аналогично может быть для атрибутов)
Нас интересует: shop/offers/offer[0]/price
По этому пути, мне нужно определить номер строки, где находиться этот тег. Чтобы указать пользователю где ошибка.
Что я пробовал:
1) Сделал через регулярные выражения. Сначала последовательно нахожу индекс элементов по пути, как получу индекс начала тега Price в файле. Затем собираю список строк в файле и нахожу номер строки для вывода, относительно всего файла. - Это работает, но меня не устраивает. Слишком медленно, а у меня могут быть очень большие файлы
2) Попробовал сделать через XPath. Т.е например можно легко получить элемент тега вот так:
XmlDocument document = new XmlDocument();
document.LoadXml(text);
var node = document.SelectSingleNode("//shop/offers/offer[1]/price");// это для примера
3) Есть вариант с созданием своего дессериализатора + атрибутов валидации для него, чтобы к каждому полю в модели приписывать в метаданных IXmlLineInfo - который в стандартном XmlSerializer к сожалению используется только при выдаче информации об ошибке - но это долго, а те готовые, что я нашел, не поддерживаются в net core... UPD: также в таком случае требуется поддержка не только составных объектов, но и простых + простых атрибутов (string, int, long и тп)
Вопрос 1: XPath как-нибудь позволяет получить кол-во элементов выше (или лучше их список) ? Вроде комбинировать preceding и ancestor нельзя (ссылка) - Но опять же, даже если это получиться, придется искать индекс, создавая список строк файла. - Не очень хорошо, хотелось бы узнать номер строки ошибки другим способом
Пробовал "//shop/offers/offer[ 1 ]/price/preceding::*", но в таком случае игнорируются все элементы выше shop, почему не знаю. (когда использовал count(...), получал 27 элементов) - Думаю это всяко лучше только регулярных выражений
Знаете еще способ? Прошу в ответы:)
Главный вопрос: Так как лучше всего найти номер строки ошибки, имея XML и путь к тегу?
P.S Производительность решения приветствуется)
Ответ
Не выдержал, решил дать свой ответ.
Во-первых, XmlSerializer выдаёт ошибку не только при ошибке синтаксиса XML (например, незакрытый тег), но и в том случае, если тип данных не соответствует ожидаемому (например, свойство Price имеет тип decimal, а в узле Price пусто или нечисловая строка). При этом в выбрасываемом исключении содержится сообщение, содержащее номер и позицию ошибки:
В документе XML (4, 18) присутствует ошибка.
На мой взгляд, это уже фактически решает вопрос. Строка с ошибкой известна, можно её исправлять.
Чуть погуглив, я легко выяснил, что для документов YML имеется схема. Скачать её можно здесь
При валидации по схеме будет выдаваться описание ошибки наподобие следующего:
Элемент "Price" недействителен: значение "" недействительно с точки зрения его типа данных "http://www.w3.org/2001/XMLSchema:decimal" - Строка "" не является допустимым значением Decimal.
Это даёт огромное удобство: пользователь легко поймёт и исправит ошибку.
Сама валидация делается в несколько строк:
var xs = new XmlSerializer(typeof(Test));
var settings = new XmlReaderSettings { ValidationType = ValidationType.Schema };
var schemas = new XmlSchemaSet();
schemas.Add(null, "schema.xsd");
settings.Schemas = schemas;
settings.ValidationEventHandler += Settings_ValidationEventHandler;
using (var xr = XmlReader.Create("test.xml", settings))
//while (xr.Read()) ;
item = (Test)xs.Deserialize(xr);
private void Settings_ValidationEventHandler(object sender, ValidationEventArgs e)
{
Console.WriteLine(e.Severity);
Console.WriteLine(e.Message);
Console.WriteLine(e.Exception.LineNumber + " " + e.Exception.LinePosition);
}
В данном коде валидация происходит в процессе чтения xml. То есть нет лишних проходов/загрузок, всё максимально эффективно по скорости и объёму памяти.
Если нужно только валидация без десериализации, то раскомментируйте строку с while, закомментировав следующую.
В том подходе, который используете вы сейчас, всё делается, имхо, очень неэффективно.
Десериализация - отдельно (проход по xml, потребление памяти).
Валидация - отдельно (ещё проход, ещё память).
Поиск строки с ошибкой с помощью XPath - ещё одна загрузка xml в XmlDocument/XDocument (опять полный парсинг xml, опять тратится время и память).
Кроме того, если xml синтаксически некорректен (незакрытый тег), то он вообще не загрузится в XmlDocument/XDocument и вы никогда не узнаете, где же ошибка...
Кроме того, вы вручную расставляете атрибуты валидации и пишете код валидации... Лишняя работа.
Комментариев нет:
Отправить комментарий