Generate mock XML file from C# class hierachy - c#

In order to generate UBL-order documents in XML, I have created 44 classes in C# using Xml.Serialization. The class consist of a root class "OrderType" which contains a lot of properties (classes), which again contains more properties.
In order to test that the classes always will build a XML document that will pass a validation. I want a XML file containing all the possible nodes (at least once) the hierarchy of Classes/Properties can build.
A very reduced code example:
[XmlRootAttribute]
[XmlTypeAttribute]
public class OrderType
{
public DeliveryType Delivery { get; set; }
//+ 50 more properties
public OrderType(){}
}
[XmlTypeAttribute]
public class DeliveryType
{
public QuantityType Quantity { get; set; }
//+ 10 more properties
public DeliveryType (){}
}
I have already tried to initialise some properties in some of the constructors and it works fine, but this method would take a whole week to finish.
So! Is there a smart an quick method to generate a Mock XML document with all properties initialized?
It's ok that the outer nodes just are defined e.g.:
< Code />

Well, sometimes you have to do it on your own:
using System;
using System.IO;
using System.Xml.Serialization;
using System.Reflection;
namespace extanfjTest
{
class Program
{
static void Main(string[] args)
{
OrderType ouo = new OrderType(DateTime.Now);
SetProperty(ouo);
XmlSerializer ser = new XmlSerializer(typeof(OrderType));
FileStream fs = new FileStream("C:/Projects/group.xml", FileMode.Create);
ser.Serialize(fs, ouo);
fs.Close();
}
public static void SetProperty(object _object)
{
if (_object == null)
{ return; }
foreach (PropertyInfo prop in _object.GetType().GetProperties())
{
if ("SemlerServices.OIOUBL.dll" != prop.PropertyType.Module.Name)
{ continue; }
if (prop.PropertyType.IsArray)
{
var instance = Activator.CreateInstance(prop.PropertyType.GetElementType());
Array _array = Array.CreateInstance(prop.PropertyType.GetElementType(), 1);
_array.SetValue(instance, 0);
prop.SetValue(_object, _array, null);
SetProperty(instance);
}
else
{
var instance = Activator.CreateInstance(prop.PropertyType);
prop.SetValue(_object, instance, null);
SetProperty(instance);
}
}
}
}
}

Related

Saving objects with constructor to xml

Hello everyone i need some help what do i need to change in this code to be able to serialize (save) this file?
I'm trying to create a inventory file that will save crafted items (this one in particular saves crafted components) i have a almost identical code for saving crafted weapons. Searching the net i couldn't find a way to serialize this object because it uses parameters, but i need to add data into that list and save it. Is there a way around it ? if yes how ? THANK YOU !
the error im getting is
InvalidOperationException: ComponentDB.ItemEntry cannot be serialized because it does not have a parameterless constructor.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
using System;
using System.Linq;
public class ComponentDB : MonoBehaviour
{
public ItemDatabase itemDB;
public string Slot;
public static ComponentDB ins;
void Awake()
{
ins = this;
Slot = "Slot1";
}
public void SaveItems()
{
XmlSerializer serializer = new XmlSerializer(typeof(ItemDatabase));
FileStream stream = new FileStream(Application.dataPath + "/StreamingAssets/Save/" + Slot + "CraftedComp.xml", FileMode.Create);
serializer.Serialize(stream, itemDB);
stream.Close();
}
public void LoadItems()
{
XmlSerializer serializer = new XmlSerializer(typeof(ItemDatabase));
FileStream stream = new FileStream(Application.dataPath + "/StreamingAssets/Save/" + Slot + "CraftedComp.xml", FileMode.Open);
itemDB = serializer.Deserialize(stream) as ItemDatabase;
stream.Close();
}
[Serializable]
public class ItemEntry
{
public string Name;
public string Data;
public int Amount;
public ItemEntry(string iName, string idata, int iAmount)
{
Name = iName;
Data = idata;
Amount = iAmount;
}
}
[Serializable]
public class ItemDatabase
{
public List<ItemEntry> list = new List<ItemEntry>();
}
public void ManageItemsInv(string input_name, string input_list, int input_amount)
{
ins.itemDB.list.Add(new ItemEntry(input_name, input_list, input_amount));
}
}
Your code are almost OK, except: you missed a parameterless constructor for ItemEntry, then you could not serialize it.
Solution is quite simple: create a parameterless constructor in ItemEntry class:
[Serializable]
public class ItemEntry
{
public string Name;
public string Data;
public int Amount;
//parameterless constructor for XmlSerializer
public ItemEntry()
{
}
public ItemEntry(string iName, string idata, int iAmount)
{
Name = iName;
Data = idata;
Amount = iAmount;
}
}

Serialization: Dynamic class names

I have already tried various possibilities but maybe I am just too tired of seeing the solution -.-
I have an xml structure like this:
<diagnosisList>
<diagnosis>
<surgery1>
<date>1957-08-13</date>
<description>a</description>
<ops301>0-000</ops301>
</surgery1>
<surgery2>
<date>1957-08-13</date>
<description>a</description>
<ops301>0-000</ops301>
</surgery2>
<surgery...>
</surgery...>
</diagnosis>
</diagnosisList>
As you see there is a variable number of surgeries. I have a class "surgery" containing the XML elements.
class Surgery
{
[XmlElement("date")]
public string date { get; set; }
[XmlElement("description")]
public string description { get; set; }
[XmlElement("ops301")]
public string ops301 { get; set; }
public Surgery()
{
}
}
and a class diagnosis creating the structure by adding the surgery class to the constructor.
diagnosis.cs
class Diagnosis
{
[XmlElement("surgery")]
public Surgery surgery
{
get;
set;
}
public Diagnosis(Surgery Surgery)
{
surgery = Surgery;
}
}
I need to be able to serialize the class name of the surgery dynamically by adding a number before serialization happens.
does anybody know a way to achieve that?
any help is really appreciated :)
Kind regards
Sandro
-- EDIT
I create the whole structure starting from my root class "Import". this class then will be passed to the serializer. So I cannot use XMLWriter in the middle of creation of the structure. I Need to create the whole structure first and finally it will be serialized:
private static void XmlFileSerialization(Import import)
{
string filename = #"c:\dump\trauma.xml";
// default file serialization
XmlSerializer<Import>.SerializeToFile(import, filename);
XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces();
namespaces.Add("", "");
XmlWriterSettings settings = new XmlWriterSettings();
settings.Encoding = Encoding.UTF8;
settings.Indent = true;
settings.IndentChars = "\t";
XmlSerializer<Import>.SerializeToFile(import, filename, namespaces, settings);
}
and then in the Method "SerializeToFile"
public static void SerializeToFile(T source, string filename, XmlSerializerNamespaces namespaces, XmlWriterSettings settings)
{
if (source == null)
throw new ArgumentNullException("source", "Object to serialize cannot be null");
XmlSerializer serializer = new XmlSerializer(source.GetType());
using (XmlWriter xmlWriter = XmlWriter.Create(filename, settings))
{
System.Xml.Serialization.XmlSerializer x = new System.Xml.Serialization.XmlSerializer(typeof(T));
x.Serialize(xmlWriter, source, namespaces);
}
}
}
What I Need is to be able to instantiate a variable number of classes based on the main class "Surgery". The class must have a variable Name, i.e.
surgery1, surgery2, surgery3, etc.
This cannot be changed because this is given by the Institution defining the XML structure.
the class must be accessible by its dynamic Name because the property in the class must be set.
so:
surgery1.Property = "blabla";
surgery2. Property = "babla";
etc.
I am even thinking about using T4 methods to create this part of code, but there must be another way to achieve dynamic class names.
I also thought of creating instances with variable names of the class by using reflection:
System.Reflection.Assembly.GetExecutingAssembly().CreateInstance(string className)
But this doesn't work actually -.-
Does anybody have a hint and could put me in the right direction?
I think, you could try implement methods from IXmlSerializable in object contains diagnosisList.
Try to use custom xml writer and reader.
public class SurgeryWriter : XmlTextWriter
{
public SurgeryWriter(string url) : base(url, Encoding.UTF8) { }
private int counter = 1;
public override void WriteStartElement(string prefix, string localName, string ns)
{
if (localName == "surgery")
{
base.WriteStartElement(prefix, "surgery" + counter, ns);
counter++;
}
else
base.WriteStartElement(prefix, localName, ns);
}
}
public class SurgeryReader : XmlTextReader
{
public SurgeryReader(string url) : base(url) { }
public override string LocalName
{
get
{
if (base.LocalName.StartsWith("surgery"))
return "surgery";
return base.LocalName;
}
}
}
Classes:
[XmlRoot("diagnosisList")]
public class DiagnosisList
{
[XmlArray("diagnosis")]
[XmlArrayItem("surgery")]
public Surgery[] Diagnosis { get; set; }
}
[XmlRoot("surgery")]
public class Surgery
{
[XmlElement("date", DataType = "date")]
public DateTime Date { get; set; }
[XmlElement("description")]
public string Description { get; set; }
[XmlElement("ops301")]
public string Ops301 { get; set; }
}
Use:
var xs = new XmlSerializer(typeof(DiagnosisList));
DiagnosisList diagnosisList;
using (var reader = new SurgeryReader("test.xml"))
diagnosisList = (DiagnosisList)xs.Deserialize(reader);
using (var writer = new SurgeryWriter("test2.xml"))
xs.Serialize(writer, diagnosisList);
Don't mix XML and C#.
You don't need dynamic names in the C# code!
If you need an arbitrary number of instances of a class, create them in a loop and place it in any collection.
var surgeries = new List<Surgery>();
for (int i = 0; i < 10; i++)
{
var surgery = new Surgery();
surgeries.Add(surgery);
}
Later you can access them by index or by enumerating.
surgeries[5]
foreach (var surgery in surgeries)
{
// use surgery
}
As you can see no need dynamic names!
Alternatively, use the dictionary with arbitrary names as keys.
var surgeryDict = new Dictionary<string, Surgery>();
for (int i = 0; i < 10; i++)
{
var surgery = new Surgery();
surgeryDict["surgery" + i] = surgery;
}
Access by name:
surgeryDict["surgery5"]

Writing enumerable to csv file

I'm sure its very straightforward but I am struggling to figure out how to write an array to file using CSVHelper.
I have a class for example
public class Test
{
public Test()
{
data = new float[]{0,1,2,3,4};
}
public float[] data{get;set;}
}
i would like the data to be written with each array value in a separate cell. I have a custom converter below which is instead providing one cell with all the values in it.
What am I doing wrong?
public class DataArrayConverter<T> : ITypeConverter
{
public string ConvertToString(TypeConverterOptions options, object value)
{
var data = (T[])value;
var s = string.Join(",", data);
}
public object ConvertFromString(TypeConverterOptions options, string text)
{
throw new NotImplementedException();
}
public bool CanConvertFrom(Type type)
{
return type == typeof(string);
}
public bool CanConvertTo(Type type)
{
return type == typeof(string);
}
}
To further detail the answer from Josh Close, here what you need to do to write any IEnumerable (including arrays and generic lists) in a recent version (anything above 3.0) of CsvHelper!
Here the class under test:
public class Test
{
public int[] Data { get; set; }
public Test()
{
Data = new int[] { 0, 1, 2, 3, 4 };
}
}
And a method to show how this can be saved:
static void Main()
{
using (var writer = new StreamWriter("db.csv"))
using (var csv = new CsvWriter(writer))
{
var list = new List<Test>
{
new Test()
};
csv.Configuration.HasHeaderRecord = false;
csv.WriteRecords(list);
writer.Flush();
}
}
The important configuration here is csv.Configuration.HasHeaderRecord = false;. Only with this configuration you will be able to see the data in the csv file.
Further details can be found in the related unit test cases from CsvHelper.
In case you are looking for a solution to store properties of type IEnumerable with different amounts of elements, the following example might be of any help:
using CsvHelper;
using CsvHelper.Configuration;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace CsvHelperSpike
{
class Program
{
static void Main(string[] args)
{
using (var writer = new StreamWriter("db.csv"))
using (var csv = new CsvWriter(writer))
{
csv.Configuration.Delimiter = ";";
var list = new List<AnotherTest>
{
new AnotherTest("Before String") { Tags = new List<string> { "One", "Two", "Three" }, After="After String" },
new AnotherTest("This is still before") {After="after again", Tags=new List<string>{ "Six", "seven","eight", "nine"} }
};
csv.Configuration.RegisterClassMap<TestIndexMap>();
csv.WriteRecords(list);
writer.Flush();
}
using(var reader = new StreamReader("db.csv"))
using(var csv = new CsvReader(reader))
{
csv.Configuration.IncludePrivateMembers = true;
csv.Configuration.RegisterClassMap<TestIndexMap>();
var result = csv.GetRecords<AnotherTest>().ToList();
}
}
private class AnotherTest
{
public string Before { get; private set; }
public string After { get; set; }
public List<string> Tags { get; set; }
public AnotherTest() { }
public AnotherTest(string before)
{
this.Before = before;
}
}
private sealed class TestIndexMap : ClassMap<AnotherTest>
{
public TestIndexMap()
{
Map(m => m.Before).Index(0);
Map(m => m.After).Index(1);
Map(m => m.Tags).Index(2);
}
}
}
}
By using the ClassMap it is possible to enable HasHeaderRecord (the default) again. It is important to note here, that this solution will only work, if the collection with different amounts of elements is the last property. Otherwise the collection needs to have a fixed amount of elements and the ClassMap needs to be adapted accordingly.
This example also shows how to handle properties with a private set. For this to work it is important to use the csv.Configuration.IncludePrivateMembers = true; configuration and have a default constructor on your class.
Unfortunately, it doesn't work like that. Since you are returning , in the converter, it will quote the field, as that is a part of a single field.
Currently the only way to accomplish what you want is to write manually, which isn't too horrible.
foreach( var test in list )
{
foreach( var item in test.Data )
{
csvWriter.WriteField( item );
}
csvWriter.NextRecord();
}
Update
Version 3 has support for reading and writing IEnumerable properties.

Automate class generation from an interface using field/property naming conventions

How would I automate the creation of a default implementation of a class from an interface using conventions. In other words, if I have an interface:
public interface ISample
{
int SampleID {get; set;}
string SampleName {get; set;}
}
Is there a snippet, T4 template, or some other means of automatically generating the class below from the interface above? As you can see, I want to put the underscore before the name of the field and then make the field the same name as the property, but lower-case the first letter:
public class Sample
{
private int _sampleID;
public int SampleID
{
get { return _sampleID;}
set { _sampleID = value; }
}
private string _sampleName;
public string SampleName
{
get { return _sampleName;}
set { _sampleName = value; }
}
}
I am not sure if T4 would be the easiest solution here in terms of readability but you can also use another code generation tool at your disposal: the CodeDom provider.
The concept is very straightforward: code consists of building blocks that you put together.
When the time is ripe, these building blocks are then parsed into the language of choice . What you end up with is a string that contains the source code of your newly created program. Afterwards you can write this to a textfile to allow for further use.
As you have noticed: there is no compile-time result, everything is runtime. If you really want compiletime then you should use T4 instead.
The code:
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using System.Reflection;
using System.Text;
namespace TTTTTest
{
internal class Program
{
private static void Main(string[] args)
{
new Program();
}
public Program()
{
// Create namespace
var myNs = new CodeNamespace("MyNamespace");
myNs.Imports.AddRange(new[]
{
new CodeNamespaceImport("System"),
new CodeNamespaceImport("System.Text")
});
// Create class
var myClass = new CodeTypeDeclaration("MyClass")
{
TypeAttributes = TypeAttributes.Public
};
// Add properties to class
var interfaceToUse = typeof (ISample);
foreach (var prop in interfaceToUse.GetProperties())
{
ImplementProperties(ref myClass, prop);
}
// Add class to namespace
myNs.Types.Add(myClass);
Console.WriteLine(GenerateCode(myNs));
Console.ReadKey();
}
private string GenerateCode(CodeNamespace ns)
{
var options = new CodeGeneratorOptions
{
BracingStyle = "C",
IndentString = " ",
BlankLinesBetweenMembers = false
};
var sb = new StringBuilder();
using (var writer = new StringWriter(sb))
{
CodeDomProvider.CreateProvider("C#").GenerateCodeFromNamespace(ns, writer, options);
}
return sb.ToString();
}
private void ImplementProperties(ref CodeTypeDeclaration myClass, PropertyInfo property)
{
// Add private backing field
var backingField = new CodeMemberField(property.PropertyType, GetBackingFieldName(property.Name))
{
Attributes = MemberAttributes.Private
};
// Add new property
var newProperty = new CodeMemberProperty
{
Attributes = MemberAttributes.Public | MemberAttributes.Final,
Type = new CodeTypeReference(property.PropertyType),
Name = property.Name
};
// Get reference to backing field
var backingRef = new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), backingField.Name);
// Add statement to getter
newProperty.GetStatements.Add(new CodeMethodReturnStatement(backingRef));
// Add statement to setter
newProperty.SetStatements.Add(
new CodeAssignStatement(
new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), backingField.Name),
new CodePropertySetValueReferenceExpression()));
// Add members to class
myClass.Members.Add(backingField);
myClass.Members.Add(newProperty);
}
private string GetBackingFieldName(string name)
{
return "_" + name.Substring(0, 1).ToLower() + name.Substring(1);
}
}
internal interface ISample
{
int SampleID { get; set; }
string SampleName { get; set; }
}
}
This produces:
Magnificent, isn't it?
Sidenote: a property is given Attributes = MemberAttributes.Public | MemberAttributes.Final because omitting the MemberAttributes.Final would make it become virtual.
And last but not least: the inspiration of this awesomeness. Metaprogramming in .NET by Kevin Hazzard and Jason Bock, Manning Publications.

How to write a comment to an XML file when using the XmlSerializer?

I have an object Foo which I serialize to an XML stream.
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());
This works fast, easy and does everything currently required.
The problem I'm having is that I need to maintain a separate documentation file with some minor remarks. As in the above example, Name is obvious, but Version is the application version and not the data file version as one could expect in this case. And I have many more similar little things I want to clarify with a comment.
I know I can do this if I manually create my XML file using the WriteComment() function, but is there a possible attribute or alternative syntax I can implement so that I can keep using the serializer functionality?
This is possible using the default infrastructure by making use of properties that return an object of type XmlComment and marking those properties with [XmlAnyElement("SomeUniquePropertyName")].
I.e. if you add a property to Foo like this:
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; }
}
The following XML will be generated:
<Foo>
<!--The application version, NOT the file version!-->
<Version>1.0</Version>
<Name>Bar</Name>
</Foo>
However, the question is asking for more than this, namely some way to look up the comment in a documentation system. The following accomplishes this by using extension methods to look up the documentation based on the reflected comment property name:
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);
}
}
For which the following XML is generated:
<Foo>
<!--The application version, NOT the file version!-->
<Version>1.0</Version>
<!--The application name, NOT the file name!-->
<Name>Bar</Name>
</Foo>
Notes:
The extension method XmlCommentExtensions.GetXmlCommentAttribute(this Type type, string memberName) assumes that the comment property will be named xxxXmlComment where xxx is the "real" property. If so, it can automatically determine the real property name by marking the incoming memberName attribute with CallerMemberNameAttribute. This can be overridden manually by passing in the real name.
Once the type and member name are known, the extension method looks up the relevant comment by searching for an [XmlComment] attribute applied to the property. This could be replaced with a cached lookup into a separate documentation file.
While it is still necessary to add the xxxXmlComment properties for each property that might be commented, this is likely to be less burdensome than implementing IXmlSerializable directly which is quite tricky, can lead to bugs in deserialization, and can require nested serialization of complex child properties.
To ensure that each comment precedes its associated element, see Controlling order of serialization in C#.
For XmlSerializer to serialize a property it must have both a getter and setter. Thus I gave the comment properties setters that do nothing.
Working .Net fiddle.
Isn't possible using default infrastructure. You need to implement IXmlSerializable for your purposes.
Very simple implementation:
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; }
}
Output:
<?xml version="1.0" encoding="utf-16"?>
<Foo>
<!--The application version, NOT the file version!-->
<Version>1.2</Version>
<Name>A</Name>
</Foo>
Another way, maybe preferable: serialize with default serializer, then perform post-processing, i.e. update XML, e.g. using XDocument or XmlDocument.
Add comment at the end of xml after serialization (magic is to flush xmlWriter).
byte[] buffer;
XmlSerializer serializer = new XmlSerializer(result.GetType());
var settings = new XmlWriterSettings() { Encoding = Encoding.UTF8 };
using (MemoryStream memoryStream = new MemoryStream())
{
using (XmlWriter xmlWriter = XmlWriter.Create(memoryStream, settings))
{
serializer.Serialize(xmlWriter, result);
xmlWriter.WriteComment("test");
xmlWriter.Flush();
buffer = memoryStream.ToArray();
}
}
Probably late to the party but I had problems when I was trying to deserialize using Kirill Polishchuk solution. Finally I decided to edit the XML after serializing it and the solution looks like:
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; }
}
Proposed solution by user dbc looks fine, however it seems to need more manual work to create such comments than using an XmlWriter that knows how to insert comments based on XmlComment attributes.
See https://archive.codeplex.com/?p=xmlcomment - it seems you can pass such a writer to XmlSerializer and thus not have to implement your own serialization which could be tricky.
I did myself end up using dbc's solution though, nice and clean with no extra code. See https://dotnetfiddle.net/Bvbi0N. Make sure you provide a "set" accessor for the comment element (the XmlAnyElement). It doesn't need to have a name btw.
Update: better pass a unique name always, aka use [XmlAnyElement("someCommentElement")] instead of [XmlAnyElement]. Was using the same class with WCF and it was choking upon those XmlAnyElements that didn't have a name provided, even though I had [XmlIgnore, SoapIgnore, IgnoreDataMember] at all of them.
for nested xml, I changed the method this way(for me i was having simple property as string(its possible to make it more complex in the logic)
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);
}
if (propertyInfo.GetValue(this, null).GetType().ToString() != "System.String")
{
XmlSerializer xmlSerializer = new XmlSerializer(propertyInfo.GetValue(this, null).GetType());
xmlSerializer.Serialize(writer, propertyInfo.GetValue(this, null));
}
else
{
writer.WriteElementString(propertyInfo.Name, propertyInfo.GetValue(this, null).ToString());
}
}
}

Categories