Вставка строки XML в существующий документ с помощью C# ломает все

Я использую C# с классами System.Xml для управления существующим XML-документом.

В частности, мне нужно вставить фрагменты XML, которые у меня есть в строковой форме, в определенные узлы документа.

Для этого я создаю новый элемент, а затем устанавливаю свою XML-строку в его свойстве InnerXml, например:

using System.Xml;

string nsUri = "http://myorg/myns";
string baseDocument = @$"<?xml version = ""1.0"" encoding = ""utf-16""?><Root xmlns = ""{nsUri}""></Root>";

XmlDocument doc = new XmlDocument();
doc.LoadXml(baseDocument);
var nsm = new XmlNamespaceManager(doc.NameTable);
nsm.AddNamespace("default", nsUri);

var target = doc.CreateElement("Target", nsUri);
target.InnerXml= @"
    <Sub1>This is a subnode</Sub1>
    <Sub2>Here is another subnode</Sub2>
";
doc.DocumentElement.AppendChild(target);

doc.Save(@"C:\text.xml");

Глядя на вывод, вот результат:

<?xml version = "1.0" encoding = "utf-16"?>
<Root xmlns = "http://myorg/myns">
  <Target>
    <Sub1 xmlns = "">This is a subnode</Sub1>
    <Sub2 xmlns = "">Here is another subnode</Sub2>
</Target>
</Root>

Как видите, есть проблема: каждому подузлу внутри вставленной мной строки XML присваивается пустой атрибут xmlns: xmlns = "".

Это само по себе уже проблема, но гораздо хуже то, что после этого документ оказывается в несогласованном состоянии, и дальнейшие манипуляции приводят к странным результатам. Например, если после этого я делаю:

var sub1 = target.SelectSingleNode("default:Sub1", nsm);

тогда sub1 будет null.

Итак, видимо, это неправильный способ сделать это, и проблема возникает, когда я переназначаю InnerXml. Можете ли вы сказать мне, как правильно это сделать?

Некоторые примечания:

  • Я знаю, что могу создавать элементы и атрибуты один за другим вместо того, чтобы просто выгружать строку XML внутри InnerXml, но фрагменты XML, которые мне нужно вставить, очень длинные, и было бы абсурдно вручную воссоздавать их в коде.
  • Я пробовал сделать это с XmlDocumentFragment, но результат точно такой же

Задавая вопрос, вам необходимо предоставить минимальный воспроизводимый пример: Отредактируйте исходный вопрос и предоставьте следующее: (1) Образец правильно сформированного XML-файла. (2) Что вам нужно сделать, то есть логика, и ваш код пытается ее реализовать. (3) Желаемый результат основан на примере данных в № 1 выше.

Yitzhak Khabinsky 17.05.2024 18:52

Лучше использовать LINQ to XML API. Он доступен в .Net Framework с 2007 года.

Yitzhak Khabinsky 17.05.2024 18:53

@YitzhakKhabinsky: ты прочитал вопрос? Код, который я предоставил, ЯВЛЯЕТСЯ минимальным воспроизводимым примером, он ДЕЙСТВИТЕЛЬНО содержит правильно сформированный входной XML, и вы можете запустить его напрямую, и он выдаст выходные данные, которые я показал. Что касается использования LINQ to XML: можно ли смешивать эти два подхода? Поскольку существующая кодовая база довольно велика и везде используется System.Xml, я, к сожалению, не могу ее полностью переписать, даже если бы захотел.

Master_T 17.05.2024 19:05

@ user246821: эти решения, похоже, не применимы к моему вопросу... как я уже сказал: я хочу вставить в документ весь (потенциально очень длинный) фрагмент XML, решения, которые вы связали, создают отдельные элементы и атрибуты один за другим, что в моем случае было бы безумием

Master_T 17.05.2024 19:08

К сожалению, неясно, чего вы пытаетесь достичь. Вы упомянули, что мне нужно вставить фрагменты XML, которые у меня есть в строковой форме, в определенные узлы документа. Однако предоставленный вами код этого не демонстрирует. Где находится переменная, содержащая строковые данные? Если переменная содержит жестко запрограммированные значения, которые вы добавили без использования переменной, почему эта информация вообще находится в строке? Откуда это?

user246821 17.05.2024 19:49

Похоже на ответ из Как изменить или унаследовать пространство имен по умолчанию при настройке InnerXml действительно работает: target.SetInnerXmlAndInheritDefaultNamespace(innerXml); дает желаемый результат, см. dotnetfiddle.net/fIw5W6.

dbc 17.05.2024 20:57

Одно замечание по поводу SetInnerXmlAndInheritDefaultNamespace: вам необходимо добавить элемент target к его родительскому элементу перед установкой внутреннего XML. Если вы этого не сделаете, вы можете получить избыточные объявления пространства имен. В любом случае отметить как дубликат?

dbc 17.05.2024 21:20

@dbc: +1, в итоге я использовал более простое решение, предложенное ниже, но я проверил ответ, на который вы ссылаетесь, и он также работает (единственный недостаток, как вы сказали, заключается в том, что он работает только в том случае, если узел, который вы внедряете XML-код уже добавлен к его последнему родительскому узлу, поэтому вы должны помнить, что всегда следует использовать этот порядок)

Master_T 17.05.2024 22:28
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
9
100
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Я не вижу действительно простого способа смешать текст DOM и XML, подобный этому (ни один API высокого уровня не поддерживает фрагменты XML), но выбрав и используя XmlReader, вы можете это сделать. Также перехожу на XDocument, EG

using System.Xml;
using System.Xml.Linq;

XNamespace ns = "http://myorg/myns";
string baseDocument = @$"<?xml version = ""1.0"" encoding = ""utf-16""?><Root xmlns = ""{ns.ToString()}""></Root>";

XDocument doc = XDocument.Parse(baseDocument);

var target = new XElement(ns + "Target");

var fragment = @"
    <Sub1>This is a subnode</Sub1>
    <Sub2>Here is another subnode</Sub2>
";
var settings = new XmlReaderSettings() { ConformanceLevel = ConformanceLevel.Fragment };
XmlReader reader = XmlReader.Create(new StringReader(fragment), settings);

while (reader.Read())
{
    while (reader.NodeType != XmlNodeType.Element)
    {
        if (!reader.Read()) break;
    }
    if (reader.EOF) break;
    var sr = reader.ReadSubtree(); 
    XElement subNode = XElement.Load(sr);
    subNode.Name = ns + subNode.Name.LocalName;
    target.Add(subNode);
}
doc.Root.Add(target);
Console.WriteLine(doc.ToString());
Ответ принят как подходящий

XmlElement.InnerXml очевидно считает элемент без явного пространства имен принадлежащим пустому пространству имен. Поэтому лучшее решение — устранить проблему в источнике: убедитесь, что все, что генерирует вашу XML-строку, указывает правильное пространство имен для каждого элемента. (Иначе вне контекста непонятно, к какому пространству имен принадлежат <Sub1> и <Sub2>.)

Однако, если это невозможно, и вы готовы предположить, что все элементы без атрибута пространства имен в вашей строке XML принадлежат указанному пространству имен по умолчанию, тогда альтернативный подход — создать фрагмент документа, установить его InnerXml в вашу строку XML. в контексте с желаемым пространством имен по умолчанию, а затем добавьте фрагмент в документ:

// As before...
string nsUri = "http://myorg/myns";
string baseDocument = @$"<?xml version = ""1.0"" encoding = ""utf-16""?><Root xmlns = ""{nsUri}""></Root>";

XmlDocument doc = new XmlDocument();
doc.LoadXml(baseDocument);
var nsm = new XmlNamespaceManager(doc.NameTable);
nsm.AddNamespace("default", nsUri);

// Create the <Target> element using CreateDocumentFragment instead of CreateElement:
var fragment = doc.CreateDocumentFragment();
fragment.InnerXml = @$"<Target xmlns = ""{nsUri}"">
    <Sub1>This is a subnode</Sub1>
    <Sub2>Here is another subnode</Sub2>
  </Target>";
var node = doc.DocumentElement.AppendChild(fragment);
node.Attributes.RemoveNamedItem("xmlns"); // Remove the redundant xmlns attribute on <Target>.

doc.Save(@"C:\text.xml");

Вот результат:

<?xml version = "1.0" encoding = "utf-16"?>
<Root xmlns = "http://myorg/myns">
  <Target>
    <Sub1>This is a subnode</Sub1>
    <Sub2>Here is another subnode</Sub2>
  </Target>
</Root>

Это единственное решение, которое, похоже, работает правильно для моего варианта использования. Я не думал просто удалять xmlns, как будто это просто еще один случайный атрибут, но это имеет смысл, поскольку в корневом документе уже указано пространство имен по умолчанию. Спасибо!

Master_T 17.05.2024 22:27

Вы были почти там. Единственное, что вам нужно — создать узлы subs и добавить их в файл target. Что-то вроде этого.

using System.Xml;
    string nsUri = "http://myorg/myns";
    string baseDocument = @$"<?xml version = ""1.0"" encoding = ""utf-16""?><Root xmlns = ""{nsUri}""></Root>";

    XmlDocument doc = new XmlDocument();
    doc.LoadXml(baseDocument);
    var nsm = new XmlNamespaceManager(doc.NameTable);
    nsm.AddNamespace("default", nsUri);

    var target = doc.CreateElement("Target", nsUri);
    var sub =doc.CreateElement("Sub1", nsUri);
    sub.InnerXml = "This is a subnode";
    target.AppendChild(sub);
    sub = doc.CreateElement("Sub2", nsUri);
    sub.InnerXml = "Here is another subnode";
    target.AppendChild(sub);
    doc.DocumentElement?.AppendChild(target);

    Console.WriteLine(doc.OuterXml);

Код производит именно то, что вы хотите. Вы также можете получить доступ к элементам.

var subBak = target.SelectSingleNode("default:Sub1", nsm);

Вот еще одно решение с использованием LINQ to XML.

С#

void Main()
{
    XDocument xdoc = XDocument.Parse(@"<Root xmlns='http://myorg/myns'>
        </Root>");

    XElement fragment = XElement.Parse(@"<Target>
            <Sub1>This is a subnode</Sub1>
            <Sub2>Here is another subnode</Sub2>
        </Target>");
    xdoc.Root.Add(fragment);

    foreach (var node in xdoc.Descendants()
        .Where(x => x.Name.NamespaceName == ""))
    {
            // Inherit the parent namespace instead
            node.Name = node.Parent.Name.Namespace + node.Name.LocalName;
    }
    xdoc.Save(@"e:\temp.xml");
}

Выход

<?xml version = "1.0" encoding = "utf-8"?>
<Root xmlns = "http://myorg/myns">
  <Target>
    <Sub1>This is a subnode</Sub1>
    <Sub2>Here is another subnode</Sub2>
  </Target>
</Root>

Другие вопросы по теме