¿La mejor manera de obtener InnerXml de un XElement?

147

¿Cuál es la mejor manera de obtener el contenido del bodyelemento mixto en el siguiente código? El elemento puede contener XHTML o texto, pero solo quiero su contenido en forma de cadena. El XmlElementtipo tiene la InnerXmlpropiedad que es exactamente lo que busco.

El código tal como está escrito casi hace lo que quiero, pero incluye el elemento circundante <body>... </body>, que no quiero.

XDocument doc = XDocument.Load(new StreamReader(s));
var templates = from t in doc.Descendants("template")
                where t.Attribute("name").Value == templateName
                select new
                {
                   Subject = t.Element("subject").Value,
                   Body = t.Element("body").ToString()
                };
Mike Powell
fuente

Respuestas:

208

Quería ver cuál de estas soluciones sugeridas funcionaba mejor, así que realicé algunas pruebas comparativas. Por interés, también comparé los métodos LINQ con el antiguo método System.Xml sugerido por Greg. La variación fue interesante y no era lo que esperaba, ya que los métodos más lentos son más de 3 veces más lentos que los más rápidos .

Los resultados ordenados por más rápido a más lento:

  1. CreateReader - Instance Hunter (0.113 segundos)
  2. System.Xml simple y antiguo - Greg Hurlman (0.134 segundos)
  3. Agregado con concatenación de cadenas - Mike Powell (0.324 segundos)
  4. StringBuilder - Vin (0.333 segundos)
  5. String.Join on array - Terry (0.360 segundos)
  6. String.Concat on array - Marcin Kosieradzki (0.364)

Método

Usé un solo documento XML con 20 nodos idénticos (llamado 'sugerencia'):

<hint>
  <strong>Thinking of using a fake address?</strong>
  <br />
  Please don't. If we can't verify your address we might just
  have to reject your application.
</hint>

Los números que se muestran como segundos arriba son el resultado de extraer el "XML interno" de los 20 nodos, 1000 veces seguidas, y tomar el promedio (media) de 5 ejecuciones. No incluí el tiempo que tardó en cargar y analizar el XML en un XmlDocument(para el método System.Xml ) oXDocument (para todos los demás).

Los algoritmos LINQ que utilicé fueron: (C #: todos toman un XElement"padre" y devuelven la cadena XML interna)

CreateReader:

var reader = parent.CreateReader();
reader.MoveToContent();

return reader.ReadInnerXml();

Agregado con concatenación de cadenas:

return parent.Nodes().Aggregate("", (b, node) => b += node.ToString());

StringBuilder:

StringBuilder sb = new StringBuilder();

foreach(var node in parent.Nodes()) {
    sb.Append(node.ToString());
}

return sb.ToString();

String.Join en matriz:

return String.Join("", parent.Nodes().Select(x => x.ToString()).ToArray());

String.Concat en la matriz:

return String.Concat(parent.Nodes().Select(x => x.ToString()).ToArray());

No he mostrado el algoritmo "Plain old System.Xml" aquí, ya que solo está llamando a .InnerXml en los nodos.


Conclusión

Si el rendimiento es importante (por ejemplo, mucho XML, analizado con frecuencia), usaría el CreateReadermétodo de Daniel cada vez . Si solo está haciendo algunas consultas, es posible que desee utilizar el método agregado más conciso de Mike.

Si está utilizando XML en elementos grandes con muchos nodos (tal vez 100), probablemente comenzará a ver el beneficio de usar StringBuildersobre el método Aggregate, pero no más CreateReader. No creo que los métodos Joiny Concatsean más eficientes en estas condiciones debido a la penalidad de convertir una lista grande en una gran matriz (incluso obvio aquí con listas más pequeñas).

Luke Sampson
fuente
La versión de StringBuilder se puede escribir en una línea: var result = parent.Elements (). Aggregate (new StringBuilder (), (sb, xelem) => sb.AppendLine (xelem.ToString ()), sb => sb.ToString ( ))
Softlion
77
Te perdiste parent.CreateNavigator().InnerXml(necesitas using System.Xml.XPathel método de extensión).
Richard
No hubiera pensado que necesitaras el .ToArray()interior .Concat, pero parece que lo hace más rápido
drzaus
En caso de que no se desplaza a la parte inferior de estas respuestas: Consideremos sólo separar el contenedor / root desde .ToString()por esta respuesta . Parece aún más rápido ...
drzaus
2
Realmente deberías envolver eso var reader = parent.CreateReader();en una declaración de uso.
BrainSlugs83
70

Creo que este es un método mucho mejor (en VB, no debería ser difícil de traducir):

Dado un XElement x:

Dim xReader = x.CreateReader
xReader.MoveToContent
xReader.ReadInnerXml
Cazador de instancias
fuente
¡Agradable! Esto es mucho más rápido que algunos de los otros métodos propuestos (los probé todos; vea mi respuesta para más detalles). Aunque todos hacen el trabajo, este lo hace más rápido, ¡incluso se ve más rápido que System.Xml.Node.InnerXml!
Luke Sampson el
44
XmlReader es desechable, así que no olvides envolverlo con el uso, por favor (editaría la respuesta yo mismo si supiera VB).
Dmitry Fedorkov
19

¿Qué tal usar este método de "extensión" en XElement? trabajó para mi !

public static string InnerXml(this XElement element)
{
    StringBuilder innerXml = new StringBuilder();

    foreach (XNode node in element.Nodes())
    {
        // append node's xml string to innerXml
        innerXml.Append(node.ToString());
    }

    return innerXml.ToString();
}

O use un poco de Linq

public static string InnerXml(this XElement element)
{
    StringBuilder innerXml = new StringBuilder();
    doc.Nodes().ToList().ForEach( node => innerXml.Append(node.ToString()));

    return innerXml.ToString();
}

Nota : El código anterior tiene que usarse element.Nodes()en lugar de element.Elements(). Algo muy importante para recordar la diferencia entre los dos. element.Nodes()te da todo XText, XAttributeetc., pero XElementsolo un elemento.

Vin
fuente
15

Con todo el crédito debido a aquellos que descubrieron y probaron el mejor enfoque (¡gracias!), Aquí está envuelto en un método de extensión:

public static string InnerXml(this XNode node) {
    using (var reader = node.CreateReader()) {
        reader.MoveToContent();
        return reader.ReadInnerXml();
    }
}
Todd Menier
fuente
10

Mantenlo simple y eficiente:

String.Concat(node.Nodes().Select(x => x.ToString()).ToArray())
  • El agregado es ineficaz para la memoria y el rendimiento al concatenar cadenas
  • Usar Join ("", sth) es usar una matriz de cadenas dos veces más grande que Concat ... Y se ve bastante extraño en el código.
  • Usar + = parece muy extraño, pero aparentemente no es mucho peor que usar '+'; probablemente se optimizaría con el mismo código, porque el resultado de la asignación no se usa y el compilador puede eliminarlo de forma segura.
  • StringBuilder es tan imperativo, y todos saben que el "estado" innecesario apesta.
Marcin Kosieradzki
fuente
7

Terminé usando esto:

Body = t.Element("body").Nodes().Aggregate("", (b, node) => b += node.ToString());
Mike Powell
fuente
Eso hará mucha concatenación de cadenas: prefiero que Vin use el StringBuilder. El foreach manual no es negativo.
Marc Gravell
Este método realmente me salvó hoy, tratando de escribir un XElement con el nuevo constructor y ninguno de los otros métodos se lo prestaba fácilmente, mientras que este lo hizo. ¡Gracias!
delliottg
3

Personalmente, terminé escribiendo un InnerXmlmétodo de extensión usando el método Agregado:

public static string InnerXml(this XElement thiz)
{
   return thiz.Nodes().Aggregate( string.Empty, ( element, node ) => element += node.ToString() );
}

Mi código de cliente es tan breve como lo sería con el antiguo espacio de nombres System.Xml:

var innerXml = myXElement.InnerXml();
Martin RL
fuente
2

@ Greg: Parece que has editado tu respuesta para que sea una respuesta completamente diferente. A lo que mi respuesta es sí, podría hacer esto usando System.Xml pero esperaba mojarme los pies con LINQ to XML.

Dejaré mi respuesta original a continuación en caso de que alguien más se pregunte por qué no puedo usar la propiedad .Value de XElement para obtener lo que necesito:

@Greg: la propiedad Value concatena todo el contenido de texto de cualquier nodo secundario. Entonces, si el elemento del cuerpo contiene solo texto, funciona, pero si contiene XHTML obtengo todo el texto concatenado pero ninguna de las etiquetas.

Mike Powell
fuente
Me encontré con este mismo problema y pensé que era un error: tenía contenido 'mixto' (es decir <root>random text <sub1>child</sub1> <sub2>child</sub2></root>) que se convirtió random text childchildenXElement.Parse(...).Value
drzaus
1

// el uso de Regex podría ser más rápido para simplemente recortar la etiqueta de elemento de inicio y fin

var content = element.ToString();
var matchBegin = Regex.Match(content, @"<.+?>");
content = content.Substring(matchBegin.Index + matchBegin.Length);          
var matchEnd = Regex.Match(content, @"</.+?>", RegexOptions.RightToLeft);
content = content.Substring(0, matchEnd.Index);
usuario950851
fuente
1
ordenado. incluso más rápido que el uso justo IndexOf:var xml = root.ToString(); var begin = xml.IndexOf('>')+1; var end = xml.LastIndexOf('<'); return xml.Substring(begin, end-begin);
drzaus
0

¿Es posible usar los objetos de espacio de nombres System.Xml para hacer el trabajo aquí en lugar de usar LINQ? Como ya mencionó, XmlNode.InnerXml es exactamente lo que necesita.

Greg Hurlman
fuente
0

Preguntándome si (observe que me deshice de b + = y solo tengo b +)

t.Element( "body" ).Nodes()
 .Aggregate( "", ( b, node ) => b + node.ToString() );

podría ser un poco menos eficiente que

string.Join( "", t.Element.Nodes()
                  .Select( n => n.ToString() ).ToArray() );

No estoy 100% seguro ... pero mirando Aggregate () y string.Join () en Reflector ... creo lo leí como Aggregate solo agregando un valor de retorno, así que esencialmente obtienes:

cadena = cadena + cadena

versus string.Join, tiene alguna mención allí de FastStringAllocation o algo así, lo que me hace pensar que la gente de Microsoft podría haber puesto un aumento de rendimiento adicional allí. Por supuesto, mi .ToArray () llama a mi negado eso, pero solo quería ofrecer otra sugerencia.


fuente
0

¿ya sabes? Lo mejor que puede hacer es volver a CDATA :( Estoy buscando soluciones aquí, pero creo que CDATA es, con mucho, el más simple y barato, no el más conveniente para desarrollar con

Ayyash
fuente
0
var innerXmlAsText= XElement.Parse(xmlContent)
                    .Descendants()
                    .Where(n => n.Name.LocalName == "template")
                    .Elements()
                    .Single()
                    .ToString();

Hará el trabajo por ti

Vinod Srivastav
fuente
-2
public static string InnerXml(this XElement xElement)
{
    //remove start tag
    string innerXml = xElement.ToString().Trim().Replace(string.Format("<{0}>", xElement.Name), "");
    ////remove end tag
    innerXml = innerXml.Trim().Replace(string.Format("</{0}>", xElement.Name), "");
    return innerXml.Trim();
}
Shivraj
fuente
Y también si el elemento tiene algún atributo o incluso solo un espacio demasiado, la lógica falla.
Christoph