c# - deserialize - ¿Cómo escribir un comentario en un archivo XML cuando se utiliza el XmlSerializer?
serializer c# (3)
Tengo un objeto Foo que serializo a una secuencia XML.
public class Foo {
// The application version, NOT the file version!
public string Version {get;set;}
public string Name {get;set;}
}
Foo foo = new Foo { Version = "1.0", Name = "Bar" };
XmlSerializer xmlSerializer = new XmlSerializer(foo.GetType());
Esto funciona rápido, fácil y hace todo lo que se requiere actualmente.
El problema que tengo es que necesito mantener un archivo de documentación separado con algunos comentarios menores. Como en el ejemplo anterior, el Name
es obvio, pero la Version
es la versión de la aplicación y no la versión del archivo de datos como se podría esperar en este caso. Y tengo muchas cosas más similares que quiero aclarar con un comentario.
Sé que puedo hacer esto si creo manualmente mi archivo XML con la función WriteComment()
, pero ¿hay un atributo posible o una sintaxis alternativa que pueda implementar para poder seguir usando la funcionalidad del serializador?
Esto es posible utilizando la infraestructura predeterminada haciendo uso de las propiedades que devuelven un objeto de tipo XmlComment
y marcando esas propiedades con [XmlAnyElement("SomeUniquePropertyName")]
.
Es decir, si añades una propiedad a Foo
como esta:
public class Foo
{
[XmlAnyElement("VersionComment")]
public XmlComment VersionComment { get { return new XmlDocument().CreateComment("The application version, NOT the file version!"); } set { } }
public string Version { get; set; }
public string Name { get; set; }
}
Se generará el siguiente XML:
<Foo>
<!--The application version, NOT the file version!-->
<Version>1.0</Version>
<Name>Bar</Name>
</Foo>
Sin embargo, la pregunta está pidiendo más que esto, es decir, alguna forma de buscar el comentario en un sistema de documentación. Lo siguiente logra esto usando métodos de extensión para buscar la documentación basada en el nombre de la propiedad de comentario reflejado:
public class Foo
{
[XmlAnyElement("VersionXmlComment")]
public XmlComment VersionXmlComment { get { return GetType().GetXmlComment(); } set { } }
[XmlComment("The application version, NOT the file version!")]
public string Version { get; set; }
[XmlAnyElement("NameXmlComment")]
public XmlComment NameXmlComment { get { return GetType().GetXmlComment(); } set { } }
[XmlComment("The application name, NOT the file name!")]
public string Name { get; set; }
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class XmlCommentAttribute : Attribute
{
public XmlCommentAttribute(string value)
{
this.Value = value;
}
public string Value { get; set; }
}
public static class XmlCommentExtensions
{
const string XmlCommentPropertyPostfix = "XmlComment";
static XmlCommentAttribute GetXmlCommentAttribute(this Type type, string memberName)
{
var member = type.GetProperty(memberName);
if (member == null)
return null;
var attr = member.GetCustomAttribute<XmlCommentAttribute>();
return attr;
}
public static XmlComment GetXmlComment(this Type type, [CallerMemberName] string memberName = "")
{
var attr = GetXmlCommentAttribute(type, memberName);
if (attr == null)
{
if (memberName.EndsWith(XmlCommentPropertyPostfix))
attr = GetXmlCommentAttribute(type, memberName.Substring(0, memberName.Length - XmlCommentPropertyPostfix.Length));
}
if (attr == null || string.IsNullOrEmpty(attr.Value))
return null;
return new XmlDocument().CreateComment(attr.Value);
}
}
Para lo cual se genera el siguiente XML:
<Foo>
<!--The application version, NOT the file version!-->
<Version>1.0</Version>
<!--The application name, NOT the file name!-->
<Name>Bar</Name>
</Foo>
Notas:
El método de extensión
XmlCommentExtensions.GetXmlCommentAttribute(this Type type, string memberName)
asume que la propiedad de comentario se llamaráxxxXmlComment
dondexxx
es la propiedad "real". Si es así, puede determinar automáticamente el nombre de la propiedad real marcando el atributomemberName
entrante conCallerMemberNameAttribute
. Esto se puede anular manualmente pasando el nombre real.Una vez que se conoce el tipo y el nombre del miembro, el método de extensión busca el comentario relevante buscando un atributo
[XmlComment]
aplicado a la propiedad. Esto podría reemplazarse con una búsqueda en caché en un archivo de documentación separado.Si bien aún es necesario agregar las propiedades
xxxXmlComment
para cada propiedad que se pueda comentar, es probable que esto sea menos oneroso que implementar directamenteIXmlSerializable
que es bastante complicado, puede provocar errores en la deserialización y puede requerir una serialización anidada de propiedades secundarias complejas. .Para asegurarse de que cada comentario preceda a su elemento asociado, consulte Control de orden de serialización en C # .
Para que
XmlSerializer
una propiedad, debe tener tanto un getter como un setter. Así le di al comentarista de propiedades las propiedades que no hacen nada.
Trabajando el violín .Net .
No es posible utilizar la infraestructura por defecto. Necesita implementar IXmlSerializable
para sus propósitos.
Implementación muy simple:
public class Foo : IXmlSerializable
{
[XmlComment(Value = "The application version, NOT the file version!")]
public string Version { get; set; }
public string Name { get; set; }
public void WriteXml(XmlWriter writer)
{
var properties = GetType().GetProperties();
foreach (var propertyInfo in properties)
{
if (propertyInfo.IsDefined(typeof(XmlCommentAttribute), false))
{
writer.WriteComment(
propertyInfo.GetCustomAttributes(typeof(XmlCommentAttribute), false)
.Cast<XmlCommentAttribute>().Single().Value);
}
writer.WriteElementString(propertyInfo.Name, propertyInfo.GetValue(this, null).ToString());
}
}
public XmlSchema GetSchema()
{
throw new NotImplementedException();
}
public void ReadXml(XmlReader reader)
{
throw new NotImplementedException();
}
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class XmlCommentAttribute : Attribute
{
public string Value { get; set; }
}
Salida:
<?xml version="1.0" encoding="utf-16"?>
<Foo>
<!--The application version, NOT the file version!-->
<Version>1.2</Version>
<Name>A</Name>
</Foo>
Otra forma, tal vez sea preferible: serializar con el serializador predeterminado, luego realizar el procesamiento posterior, es decir, actualizar XML, por ejemplo, utilizando XDocument
o XmlDocument
.
Probablemente tarde a la fiesta, pero tuve problemas cuando intentaba deserializar el uso de la solución Kirill Polishchuk. Finalmente, decidí editar el XML después de serializarlo y la solución parece:
public static void WriteXml(object objectToSerialize, string path)
{
try
{
using (var w = new XmlTextWriter(path, null))
{
w.Formatting = Formatting.Indented;
var serializer = new XmlSerializer(objectToSerialize.GetType());
serializer.Serialize(w, objectToSerialize);
}
WriteComments(objectToSerialize, path);
}
catch (Exception e)
{
throw new Exception($"Could not save xml to path {path}. Details: {e}");
}
}
public static T ReadXml<T>(string path) where T:class, new()
{
if (!File.Exists(path))
return null;
try
{
using (TextReader r = new StreamReader(path))
{
var deserializer = new XmlSerializer(typeof(T));
var structure = (T)deserializer.Deserialize(r);
return structure;
}
}
catch (Exception e)
{
throw new Exception($"Could not open and read file from path {path}. Details: {e}");
}
}
private static void WriteComments(object objectToSerialize, string path)
{
try
{
var propertyComments = GetPropertiesAndComments(objectToSerialize);
if (!propertyComments.Any()) return;
var doc = new XmlDocument();
doc.Load(path);
var parent = doc.SelectSingleNode(objectToSerialize.GetType().Name);
if (parent == null) return;
var childNodes = parent.ChildNodes.Cast<XmlNode>().Where(n => propertyComments.ContainsKey(n.Name));
foreach (var child in childNodes)
{
parent.InsertBefore(doc.CreateComment(propertyComments[child.Name]), child);
}
doc.Save(path);
}
catch (Exception)
{
// ignored
}
}
private static Dictionary<string, string> GetPropertiesAndComments(object objectToSerialize)
{
var propertyComments = objectToSerialize.GetType().GetProperties()
.Where(p => p.GetCustomAttributes(typeof(XmlCommentAttribute), false).Any())
.Select(v => new
{
v.Name,
((XmlCommentAttribute) v.GetCustomAttributes(typeof(XmlCommentAttribute), false)[0]).Value
})
.ToDictionary(t => t.Name, t => t.Value);
return propertyComments;
}
[AttributeUsage(AttributeTargets.Property)]
public class XmlCommentAttribute : Attribute
{
public string Value { get; set; }
}