Can I make XmlSerializer ignore the namespace (xmlns attribute) on deserialization so that it doesn't matter if the attribute is added or not or even if the attribute is bogus? I know that the source will always be trusted so I don't care about the xmlns attribute.
Yes, you can tell the XmlSerializer to ignore namespaces during de-serialization.
Define an XmlTextReader that ignores namespaces. Like so:
// helper class to ignore namespaces when de-serializing
public class NamespaceIgnorantXmlTextReader : XmlTextReader
{
public NamespaceIgnorantXmlTextReader(System.IO.TextReader reader): base(reader) { }
public override string NamespaceURI
{
get { return ""; }
}
}
// helper class to omit XML decl at start of document when serializing
public class XTWFND : XmlTextWriter {
public XTWFND (System.IO.TextWriter w) : base(w) { Formatting= System.Xml.Formatting.Indented;}
public override void WriteStartDocument () { }
}
Here's an example of how you would de-serialize using that TextReader:
public class MyType1
{
public string Label
{
set { _Label= value; }
get { return _Label; }
}
private int _Epoch;
public int Epoch
{
set { _Epoch= value; }
get { return _Epoch; }
}
}
String RawXml_WithNamespaces = #"
<MyType1 xmlns='urn:booboo-dee-doo'>
<Label>This document has namespaces on its elements</Label>
<Epoch xmlns='urn:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'>0</Epoch>
</MyType1>";
System.IO.StringReader sr;
sr= new System.IO.StringReader(RawXml_WithNamespaces);
var s1 = new XmlSerializer(typeof(MyType1));
var o1= (MyType1) s1.Deserialize(new NamespaceIgnorantXmlTextReader(sr));
System.Console.WriteLine("\n\nDe-serialized, then serialized again:\n");
XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
ns.Add("urn", "booboo-dee-doo");
s1.Serialize(new XTWFND(System.Console.Out), o1, ns);
Console.WriteLine("\n\n");
The result is like so:
<MyType1>
<Label>This document has namespaces on its elements</Label>
<Epoch>0</Epoch>
</MyType1>
If you expect no namespace, but the input has namespaces, then you can set
Namespaces = false
on your XmlTextReader.
Exdended Wolfgang Grinfeld answer (w/o exception handling):
public static Message Convert(XmlDocument doc)
{
Message obj;
using (TextReader textReader = new StringReader(doc.OuterXml))
{
using (XmlTextReader reader = new XmlTextReader(textReader))
{
reader.Namespaces = false;
XmlSerializer serializer = new XmlSerializer(typeof(Message));
obj = (Message)serializer.Deserialize(reader);
}
}
return obj;
}
Solved this by using XmlSerializer Deserialize to read from xml instead from stream. This way before xml is Deserialized, using Regex to remove xsi:type from the xml. Was doing this is Portable Class Library for Cross Platform, so did not had many other options :(. After this the deserialization seems to work fine.
Following code can help,
public static TClass Deserialize<TClass>(string xml) where TClass : class, new()
{
var tClass = new TClass();
xml = RemoveTypeTagFromXml(xml);
var xmlSerializer = new XmlSerializer(typeof(TClass));
using (TextReader textReader = new StringReader(xml))
{
tClass = (TClass)xmlSerializer.Deserialize(textReader);
}
return tClass;
}
public static string RemoveTypeTagFromXml(string xml)
{
if (!string.IsNullOrEmpty(xml) && xml.Contains("xsi:type"))
{
xml = Regex.Replace(xml, #"\s+xsi:type=""\w+""", "");
}
return xml;
}
Why try to make the XmlSerializer forget how XML works? It's a fact of XML that two elements with the same name but different namespaces are different elements.
If you want to process XML that has no namespaces, then you should pre-process it to remove the namespaces, and then pass it to the serializer.
Related
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());
}
}
}
When I serialize the value : If there is no value present in for data then it's coming like below format.
<Note>
<Type>Acknowledged by PPS</Type>
<Data />
</Note>
But what I want xml data in below format:
<Note>
<Type>Acknowledged by PPS</Type>
<Data></Data>
</Note>
Code For this i have written :
[Serializable]
public class Notes
{
[XmlElement("Type")]
public string typeName { get; set; }
[XmlElement("Data")]
public string dataValue { get; set; }
}
I am not able to figure out what to do for achieve data in below format if data has n't assign any value.
<Note>
<Type>Acknowledged by PPS</Type>
<Data></Data>
</Note>
You can do this by creating your own XmlTextWriter to pass into the serialization process.
public class MyXmlTextWriter : XmlTextWriter
{
public MyXmlTextWriter(Stream stream) : base(stream, Encoding.UTF8)
{
}
public override void WriteEndElement()
{
base.WriteFullEndElement();
}
}
You can test the result using:
class Program
{
static void Main(string[] args)
{
using (var stream = new MemoryStream())
{
var serializer = new XmlSerializer(typeof(Notes));
var writer = new MyXmlTextWriter(stream);
serializer.Serialize(writer, new Notes() { typeName = "Acknowledged by PPS", dataValue="" });
var result = Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(result);
}
Console.ReadKey();
}
If you saved your string somewhere (e.g a file) you can use this simple Regex.Replace:
var replaced = Regex.Replace(File.ReadAllText(name), #"<([^<>/]+)\/>", (m) => $"<{m.Groups[1].Value.Trim()}></{m.Groups[1].Value.Trim()}>");
File.WriteAllText(name, replaced);
IMO it's not possibe to generate your desired XML using Serialization. But, you can use LINQ to XML to generate the desired schema like this -
XDocument xDocument = new XDocument();
XElement rootNode = new XElement(typeof(Notes).Name);
foreach (var property in typeof(Notes).GetProperties())
{
if (property.GetValue(a, null) == null)
{
property.SetValue(a, string.Empty, null);
}
XElement childNode = new XElement(property.Name, property.GetValue(a, null));
rootNode.Add(childNode);
}
xDocument.Add(rootNode);
XmlWriterSettings xws = new XmlWriterSettings() { Indent=true };
using (XmlWriter writer = XmlWriter.Create("D:\\Sample.xml", xws))
{
xDocument.Save(writer);
}
Main catch is in case your value is null, you should set it to empty string. It will force the closing tag to be generated. In case value is null closing tag is not created.
Kludge time - see Generate System.Xml.XmlDocument.OuterXml() output thats valid in HTML
Basically after XML doc has been generated go through each node, adding an empty text node if no children
// Call with
addSpaceToEmptyNodes(xmlDoc.FirstChild);
private void addSpaceToEmptyNodes(XmlNode node)
{
if (node.HasChildNodes)
{
foreach (XmlNode child in node.ChildNodes)
addSpaceToEmptyNodes(child);
}
else
node.AppendChild(node.OwnerDocument.CreateTextNode(""))
}
(Yes I know you shouldn't have to do this - but if your sending the XML to some other system that you can't easily fix then have to be pragmatic about things)
You can add a dummy field to prevent the self-closing element.
[XmlText]
public string datavalue= " ";
Or if you want the code for your class then Your class should be like this.
public class Notes
{
[XmlElement("Type")]
public string typeName { get; set; }
[XmlElement("Data")]
private string _dataValue;
public string dataValue {
get {
if(string.IsNullOrEmpty(_dataValue))
return " ";
else
return _dataValue;
}
set {
_dataValue = value;
}
}
}
In principal, armen.shimoon's answer worked for me. But if you want your XML output pretty printed without having to use XmlWriterSettings and an additional Stream object (as stated in the comments), you can simply set the Formatting in the constructor of your XmlTextWriter class.
public MyXmlTextWriter(string filename) : base(filename, Encoding.UTF8)
{
this.Formatting = Formatting.Indented;
}
(Would have posted this as a comment but am not allowed yet ;-))
Effectively the same as Ryan's solution which uses the standard XmlWriter (i.e. there's no need for a derived XmlTextWriter class), but written using linq to xml (XDocument)..
private static void AssignEmptyElements(this XNode node)
{
if (node is XElement e)
{
e.Nodes().ToList().ForEach(AssignEmptyElements);
if (e.IsEmpty)
e.Value = string.Empty;
}
}
usage..
AssignEmptyElements(document.FirstNode);
I have followed a good tutorial that shows how to make an Automation Framework in C# Selenium.
The config file is in XML at the moment, but I wanted some more practice and change it to a .json file.
At the moment we are using the namespace System.Xml.XPath; and my question is, are there a similar for JSON? Lets say "System.Json;" that works the same as my XML reader. So I don't need to refactor to much code, or is it unavoidably?
This is how it works at the moment.
//Initialize
ConfigReader.SetFrameworkSettings();
public class ConfigReader
{
public static void SetFrameworkSettings()
{
XPathItem aut;
string strFilename = Environment.CurrentDirectory.ToString() + "\\Config\\GlobalConfig.xml";
FileStream stream = new FileStream(strFilename, FileMode.Open);
XPathDocument document = new XPathDocument(stream);
XPathNavigator navigator = document.CreateNavigator();
//Get XML Details and pass it in XPathItem type variables
aut = navigator.SelectSingleNode("AutoFramework/RunSettings/AUT");
Settings.AUT = aut.Value.ToString();
}
}
public class Settings
{
public static string AUT { get; set; }
}
Would be awesome if you could just change this two lines
XPathDocument document = new XPathDocument(stream);
XPathNavigator navigator = document.CreateNavigator()
And the XpathItem
Cheers
I would recommend using Newtonsoft.Json which is available from Nuget.
To "reuse" your current code you would have to make some basic SettingsConverter first:
public static class SettingsConverter
{
public static string FromXmlToJson(string xml)
{
Settings settings = null;
// read your xml file and assign values to the settings object
// now you can "serialize" settings object into Json
return JsonSerialization.Serialize(settings);
}
public static string FromJsonToXml(string json)
{
Settings settings = JsonSerialization.Deserialize<MeSettings>(json);
// save settings using your "xml" serialization
return xmlString; // return xml string
}
}
To use these methods I'm using this JsonSerialization helper class :
public static class JsonSerialiation
{
public static string Serialize<T>(T obj)
{
using (MemoryStream stream = new MemoryStream())
{
using (JsonTextWriter writer = new JsonTextWriter(new StreamWriter(stream))
{
JsonSerializer serializer = new JsonSerializer();
serializer.Serializer(writer, obj);
writer.Flush();
stream.Position = 0;
using (StreamReader reader = new StreamREader(stream))
{
return reader.ReadToEnd();
}
}
}
}
public static T Deserialize<T>(string jsonString)
{
using (JsonTextReader reader = new JsonTextReader(new StringReader(str)))
{
JsonSerializer serializer = new JsonSerializer();
return serializer.Deserialize<T>(reader)
}
}
}
Above example requires from you to change your Settings class from static :
public class Settings
{
public static string AUT { get; set; }
}
To instance :
public class Settings
{
public string AUT { get; set; }
}
If you prefer to keep it as static. You should use JObject from Newtonsoft.Json library :
JObject obj = JObject.Parse(jsonString);
Settings.AUT = obj.SelectToken("AUT").Value<string>();
You can always use JsonConvert.Serialize<T>() and JsonConvert.Deserialize<T>() methods instead of the JsonSerialization helper class that I've made but in my opinion the less control you have over your code the bigger the problems will be.
TLDR version
I am serializing objects into XML to match a schema provided by a third party. Their validator requires one of the child objects to have a namespace explicitly declared which matches it's ancestor's namespace . The data is complex enough that I don't want to roll my own serializer for this purpose. How can I force the XMLSerializer class to explicitly render a namespace even though it is technically redundant?
Full version
I am running into an issue where the CoreItemsMkt namespace is not rendered by the XMLSerializer. I believe that this is because both the attribute and the namespaces exactly match the ancestor's namespace that it is inheriting from, therefore the serializer omits it - however, the site validator that this file gets submitted to requires it.
For example:
<?xml version="1.0" encoding="utf-8"?>
<FSAMarketsFeed xmlns="http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2">
<FSAFeedHeader xmlns="http://www.fsa.gov.uk/XMLSchema/FSAFeedCommon-v1-2">
[...contents omitted, this item appears once...]
</FSAFeedHeader>
<FSAMarketsFeedMsg>
<CoreItemsMkt xmlns="http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2"> <!--//This namespace is the issue//-->
[...contents omitted, this item appears multiple times...]
</CoreItemsMkt?
</FSAMarketsFeedMsg>
<FSAMarketsFeedMsg>
<CoreItemsMkt xmlns="http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2"> <!--//This namespace is the issue//-->
[...contents omitted, this item appears multiple times...]
</CoreItemsMkt?
</FSAMarketsFeedMsg>
I'm serializing with a method like this:
var path = GetFilePath();
var ns = new XmlSerializerNamespaces();
ns.Add("", "http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2");
var ser = new XmlSerializer(typeof(FSAMarketsFeed));
var settings = new XmlWriterSettings
{ Encoding = Encoding.UTF8, Indent = true, IndentChars = "\t", NamespaceHandling = NamespaceHandling.Default };
using (var writer = XmlWriter.Create(path, settings))
{
ser.Serialize(writer, GetDataToSerialize(), ns);
}
My root class is defined as:
[XmlType(AnonymousType = true)]
[XmlRoot(Namespace = "http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2", IsNullable = false)]
public class FSAMarketsFeed
{
public FSAMarketsFeed()
{
FSAMarketsFeedMsg = new FSAMarketsFeedMsg[0];
}
[XmlElement("FSAFeedHeader", IsNullable = true, Namespace = "http://www.fsa.gov.uk/XMLSchema/FSAFeedCommon-v1-2")]
public FSAFeedHeader FeedHeader { get; set; }
[XmlElement("FSAMarketsFeedMsg")]
public FSAMarketsFeedMsg[] FSAMarketsFeedMsg { get; set; }
}
The working feed header class:
[XmlType(AnonymousType = true)]
public class FSAFeedHeader
{
[XmlElement("FeedTargetSchemaVersion", IsNullable = true)]
public string FeedTargetSchemaVersion { get; set; }
[XmlElement("Submitter", IsNullable = true)]
public Submitter Submit { get; set; }
[XmlElement("ReportDetails", IsNullable = true)]
public ReportDetails ReportDetail { get; set; }
}
The parent Feed Message Class:
[XmlType(AnonymousType = true)]
public class FSAMarketsFeedMsg
{
[XmlElement("CoreItemsMkt", IsNullable = true, Namespace = "http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2")]
public CoreItemsMkt CoreMarket { get; set; }
[XmlElement("Transaction", IsNullable = true)]
public Transaction Trans { get; set; }
}
Finally, the CoreItemsMkt class which is failing to render its namespace:
[XmlType(Namespace = "http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2", AnonymousType = true)]
public class CoreItemsMkt
{
//[... Children omitted ...]]
}
Tried so far:
Using XMmlType(AnonymousType = true) to try to break the inheritance chain
Explicitly setting xmlns as an XmlAttributeAttribute w/ a hard coded value.
Setting and removing XmlType(Namespace = "http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2") on CoreItemsMkt
Adding and removing XmlElement(Namespace = "the value") on the FSAMArketsFeedMsg's property.
Implementing ISerializable on CoreItmsMkt (Couldn't quite figure out how to get that to work though.)
Stack overflow searches - I've found 1 similar question that was answered with "This is unsupported, change your output namespace." Unfortunately, that answer doesn't work for me.
So, without hand rendering this, is there any way to force the XmlSerializer class to render those namespace attributes on CoreItmsMkt?
Try to use custom XML writer.
public class CustomWriter : XmlTextWriter
{
public CustomWriter(TextWriter writer) : base(writer) { }
public CustomWriter(Stream stream, Encoding encoding) : base(stream, encoding) { }
public CustomWriter(string filename, Encoding encoding) : base(filename, encoding) { }
public override void WriteStartElement(string prefix, string localName, string ns)
{
base.WriteStartElement(prefix, localName, ns);
if (localName == "CoreItemsMkt")
{
base.WriteAttributeString("xmlns",
"http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2");
//base.WriteAttributeString("xmlns", ns);
}
}
}
The custom writer forcibly adds the required attribute to every element with the CoreItemsMkt name.
Usage
using (var customWriter = new CustomWriter(path, Encoding.UTF8))
{
customWriter.Formatting = Formatting.Indented;
customWriter.Indentation = 1;
customWriter.IndentChar = '\t';
ser.Serialize(customWriter, GetDataToSerialize(), ns);
}
You would like to be able to force XmlSerializer to emit redundant xmlns= attributes when serializing specified nested elements. Unfortunately, I don't know of any API to make this happen automatically. You also wrote The data is complex enough that I don't want to roll my own serializer for this purpose so you don't want to have to implement IXmlSerializable on FSAMarketsFeedMsg. (ISerializable is not used by XmlSerializer so implementing it will not help.) Thus you're going to want to do something "semi-manual". There are at least a couple of options for this.
Option 1: Serialize to a temporary XDocument then fix the attributes.
With this solution, you serialize to a temporary XDocument in memory, then add an XAttribute for each desired redundant xmlns=, as follows:
// Generate the temporary XDocument
var ns = Namespaces.GetFSAMarketsFeedNamespace();
var doc = data.SerializeToXDocument(null, ns);
var root = doc.Root;
// Add redundate xmlns= attributes
var name = XName.Get("CoreItemsMkt", Namespaces.FSAMarketsFeed);
var query = doc.Descendants(name); // Could be a more complex query, possibly even an XPath query.
foreach (var element in query)
{
if (!element.Attributes().Any(a => a.IsNamespaceDeclaration))
{
var prefix = element.GetPrefixOfNamespace(element.Name.Namespace);
if (string.IsNullOrEmpty(prefix))
element.Add(new XAttribute("xmlns", element.Name.NamespaceName));
else
element.Add(new XAttribute(XNamespace.Xmlns + prefix, element.Name.NamespaceName));
}
}
// Write the XDocument to disk.
Using the static extension classes:
public static class Namespaces
{
public const string FSAMarketsFeed = #"http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2";
public const string FSAFeedCommon = #"http://www.fsa.gov.uk/XMLSchema/FSAFeedCommon-v1-2";
public static XmlSerializerNamespaces GetFSAMarketsFeedNamespace()
{
var ns = new XmlSerializerNamespaces();
ns.Add("", Namespaces.FSAMarketsFeed);
return ns;
}
}
public static class XObjectExtensions
{
public static T Deserialize<T>(this XContainer element, XmlSerializer serializer)
{
using (var reader = element.CreateReader())
{
serializer = serializer ?? new XmlSerializer(typeof(T));
object result = serializer.Deserialize(reader);
if (result is T)
return (T)result;
}
return default(T);
}
public static XDocument SerializeToXDocument<T>(this T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
{
var doc = new XDocument();
using (var writer = doc.CreateWriter())
{
serializer = serializer ?? new XmlSerializer(obj.GetType());
serializer.Serialize(writer, obj, ns);
}
return doc;
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
{
var doc = obj.SerializeToXDocument(serializer, ns);
var element = doc.Root;
if (element != null)
element.Remove();
return element;
}
}
Which produces the XML:
<FSAMarketsFeed xmlns="http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2">
<FSAFeedHeader xmlns="http://www.fsa.gov.uk/XMLSchema/FSAFeedCommon-v1-2">
<FeedTargetSchemaVersion>value of FeedTargetSchemaVersion</FeedTargetSchemaVersion>
</FSAFeedHeader>
<FSAMarketsFeedMsg>
<CoreItemsMkt xmlns="http://www.fsa.gov.uk/XMLSchema/FSAMarketsFeed-v1-2" />
</FSAMarketsFeedMsg>
</FSAMarketsFeed>
Option 2: Do a nested serialization of CoreMarket using [XmlAnyElement] on its containing type.
Using an [XmlAnyElement] property, a type can serialize and deserialize any arbitrary child element. You can use this functionality to do a nested serialization of CoreMarket with the necessary namespace declarations included.
To do this, modify FSAMarketsFeedMsg as follows:
[XmlType(AnonymousType = true)]
public class FSAMarketsFeedMsg
{
[XmlIgnore]
public CoreItemsMkt CoreMarket { get; set; }
[XmlAnyElement(Name = "CoreItemsMkt", Namespace = Namespaces.FSAMarketsFeed)]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
public XElement CoreMarketXml
{
get
{
return (CoreMarket == null ? null : XObjectExtensions.SerializeToXElement(CoreMarket,
XmlSerializerFactory.Create(typeof(CoreItemsMkt), "CoreItemsMkt", Namespaces.FSAMarketsFeed),
Namespaces.GetFSAMarketsFeedNamespace()));
}
set
{
CoreMarket = (value == null ? null : XObjectExtensions.Deserialize<CoreItemsMkt>(value,
XmlSerializerFactory.Create(typeof(CoreItemsMkt), "CoreItemsMkt", Namespaces.FSAMarketsFeed)));
}
}
// Remainder of properties are left unchanged.
}
In addition to the static extension classes from Option 1, you will need the following to avoid a substantial memory leak:
public static class XmlSerializerFactory
{
static readonly Dictionary<Tuple<Type, string, string>, XmlSerializer> table;
static readonly object padlock;
static XmlSerializerFactory()
{
table = new Dictionary<Tuple<Type, string, string>, XmlSerializer>();
padlock = new object();
}
public static XmlSerializer Create(Type serializedType, string rootName, string rootNamespace)
{
if (serializedType == null)
throw new ArgumentNullException();
if (rootName == null && rootNamespace == null)
return new XmlSerializer(serializedType);
lock (padlock)
{
var key = Tuple.Create(serializedType, rootName, rootNamespace);
XmlSerializer serializer;
if (!table.TryGetValue(key, out serializer))
{
var attr = (string.IsNullOrEmpty(rootName) ? new XmlRootAttribute() { Namespace = rootNamespace } : new XmlRootAttribute(rootName) { Namespace = rootNamespace });
serializer = table[key] = new XmlSerializer(serializedType, attr);
}
return serializer;
}
}
}
Note that the [XmlAnyElement] property will be called for all unknown elements, so if your XML for some reason has unexpected elements, you may get an exception thrown from XObjectExtensions.Deserialize because the root element name is wrong. You may want to catch and ignore exceptions from this method if that is a possibility.
Serialize to disk as you are currently doing. The redundant xmlns= attributes will be present as in Option 1.
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());
}
}
}