I have written an Interface for writing a very very simple Plugin. In fact it is just a class that is loaded at runtime out of a dll file and is stored as Property in another class. That class that stores the interface has to get serialized. As example this is my serialized object:
<?xml version="1.0" encoding="utf-8"?><MD5HashMapper xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.namespace.net" />
But now If i want to load that Object I get an Exception:
As example :
{"<MD5HashMapper xmlns='http://www.vrz.net/Vrz.Map'> was not expected."}
So does anyone has an idea how to solve that problem?
Code:
I have an Interface named IMap that is shared in a dll file to create Addins based on that interface:
public interface IMap
{
object Map(object input);
}
I also have different Mappers (you can pass an input through them and they modify the output). All Mappers are derived from:
[XmlInclude(typeof(ConstMapper))]
[XmlInclude(typeof(FuncMapper))]
[XmlInclude(typeof(IdentMapper))]
[XmlInclude(typeof(NullMapper))]
[XmlInclude(typeof(RefMapper))]
[XmlInclude(typeof(VarMapper))]
[XmlInclude(typeof(TableMapper))]
[XmlInclude(typeof(AddinMapper))]
public class MapperBase:ComponentBase,IMap
{ public virtual object Map(object input) {
throw new NotImplementedException("Diese Methode muss überschrieben werden");
}
public override string ToString() {
return ShortDisplayName;
}
}
Just forget ComponentBase. It is not important for this...
Now i also have a AddinMapper. The main function of that mapper is to cast create MapperBase Object out of the IMap object:
And that is exactly that class I want to seralize including the properties of the Mapper Property (type IMap).
public class AddinMapper : MapperBase
{
private static MapperBase[] _mappers;
const string addinDirectory = #"Addin\Mappers\";
//Mappers from *.dll files are loaded here:
[XmlIgnore]
public static MapperBase[] Mappers
{
get
{
if (_mappers == null)
{
List<MapperBase> maps = new List<MapperBase>();
foreach (string dll in Directory.GetFiles(addinDirectory, "*.dll"))
{
if (Path.GetFileName(dll) != "IMap.dll")
{
var absolutePath = Path.Combine(Environment.CurrentDirectory, dll);
Assembly asm = Assembly.LoadFile(absolutePath);
foreach (Type type in asm.GetTypes().ToList().Where(p => p.GetInterface("IMap") != null))
{
maps.Add(new AddinMapper((IMap)Activator.CreateInstance(type)));
}
}
}
Mappers = maps.ToArray();
}
return _mappers;
}
set
{
_mappers = value;
}
}
IMap _base;
public string MapperString { get; set; }
[XmlIgnore()]
public IMap Mapper
{
get
{
if (_base == null)
{
Type type = null;
foreach (MapperBase mapperBase in Mappers)
{
if (mapperBase is AddinMapper && ((AddinMapper)mapperBase).Mapper.GetType().FullName == _mapperName)
{
type = (mapperBase as AddinMapper).Mapper.GetType();
break;
}
}
if (type != null)
{
XmlSerializer serializer = new XmlSerializer(type);
using (StringReader reader = new StringReader(MapperString))
{
Mapper = (IMap)serializer.Deserialize(reader);
}
}
}
return _base;
}
private set
{
_base = value;
StoreMapperString();
}
}
string _mapperName;
[System.ComponentModel.Browsable(false)]
public string MapperName
{
get
{
return _mapperName;
}
set
{
_mapperName = value;
}
}
public AddinMapper(IMap baseInterface) : this()
{
Mapper = baseInterface;
_mapperName = baseInterface.GetType().FullName;
}
public AddinMapper()
{
}
public override object Map(object input)
{
return Mapper.Map(input);
}
public override string ToString()
{
return Mapper.ToString();
}
private void StoreMapperString()
{
MemoryStream memstream = new MemoryStream();
XmlStore.SaveObject(memstream, Mapper);
using (StreamReader reader = new StreamReader(memstream))
{
memstream.Position = 0;
MapperString = reader.ReadToEnd();
}
}
}
An example for such a addin would be:
public class ReplaceMapper : IMap
{
public string StringToReplace { get; set; }
public string StringToInsert { get; set; }
public object Map(object input)
{
if (input is string)
{
input = (input as string).Replace(StringToReplace, StringToInsert);
}
return input;
}
}
And the Problem is I want to save the Settings like StringToReplace,... as xml
I ve solved my problem:
I really don t know why but take a look at this article: http://www.calvinirwin.net/2011/02/10/xmlserialization-deserialize-causes-xmlns-was-not-expected/
(if link is dead later)
XmlRootAttribute xRoot = new XmlRootAttribute();
xRoot.ElementName = elementName;
xRoot.IsNullable = true;
XmlSerializer ser = new XmlSerializer(typeof(MyObject), xRoot);
XmlReader xRdr = XmlReader.Create(new StringReader(xmlData));
MyObject tvd = (MyObject)ser.Deserialize(xRdr);
Now the important thing: It does not matter if you don t get an excption on serialization. You have to add the XmlRootAttribute on both ways: Serialisation and Deserialization.
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());
}
}
}
I serialized a class to XML. But deserialization to the same class type is failing when schema validation is enabled.
Here is what I'm doing:
creating an object from the serializable class
Serializing that object to XML
Gets the schema from the that object
Adds that schema to validation
deserialize with out validation
deserialize with XMLschema validation
In step six, it is failing...
Here in this code sample, method with validation is failing:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
namespace Deserialize
{
public class Program
{
static string filepath = "TestSerilize.xml";
private static object oSchema;
private static XmlReaderSettings oXmlReaderSettings;
static void Main(string[] args)
{
MyObject oMyobject = new MyObject();
oMyobject.MyObjectType = "MyCustomType";
List<Items> olistItems = new List<Items>();
Items oItems = new Items();
oItems.key = "test123";
oItems.value = "testvalue";
olistItems.Add(oItems);
oMyobject.Items = olistItems;
Saveobjecttofile(oMyobject, filepath);
dynamic objDeserialized = null;
objDeserialized = GetObjFormfileWithoutValidation(filepath, oMyobject.GetType());
objDeserialized = GetObjFormfileWithValidation(filepath, oMyobject.GetType());
}
private static dynamic GetObjFormfileWithValidation(string filepath, Type type)
{
XmlReaderSettings oXmlReaderSettings = new XmlReaderSettings();
oXmlReaderSettings.ValidationType = ValidationType.Schema;
dynamic oSchema = GetSchemaFromType(type);
oXmlReaderSettings.Schemas.Add(oSchema);
XmlReader oXmlReader = null;
if (oSchema != null)
{
oXmlReader = XmlReader.Create(filepath, oXmlReaderSettings);
}
else
{
oXmlReader = XmlReader.Create(filepath);
}
object obj = null;
try
{
XmlSerializer oXmlSerializer = new XmlSerializer(type);
obj = oXmlSerializer.Deserialize(oXmlReader);
}
finally
{
oXmlReader.Close();
}
return obj;
}
private static XmlSchema GetSchemaFromType(Type type)
{
var oSoapReflectionImporter = new SoapReflectionImporter();
var oXmlTypeMapping = oSoapReflectionImporter.ImportTypeMapping(type);
var oXmlSchemas = new XmlSchemas();
var oXmlSchema = new XmlSchema();
oXmlSchemas.Add(oXmlSchema);
var oXMLSchemaExporter = new XmlSchemaExporter(oXmlSchemas);
oXMLSchemaExporter.ExportTypeMapping(oXmlTypeMapping);
return oXmlSchema;
}
private static dynamic GetObjFormfileWithoutValidation(string filepath, Type type)
{
XmlReader oXmlReader = null;
oXmlReader = XmlReader.Create(filepath);
object obj = null;
try
{
XmlSerializer oXmlSerializer = new XmlSerializer(type);
obj = oXmlSerializer.Deserialize(oXmlReader);
}
finally
{
oXmlReader.Close();
}
return obj;
}
private static void Saveobjecttofile(object objectToSave, string filepath)
{
try
{
System.Xml.Serialization.XmlSerializer oXmlSerializer = new System.Xml.Serialization.XmlSerializer(objectToSave.GetType());
using (System.Xml.XmlTextWriter oXmlTextWriter = new System.Xml.XmlTextWriter(filepath, System.Text.Encoding.UTF8))
{
oXmlTextWriter.Indentation = 2;
oXmlTextWriter.Formatting = System.Xml.Formatting.Indented;
oXmlSerializer.Serialize(oXmlTextWriter, objectToSave);
oXmlTextWriter.Flush();
oXmlTextWriter.Close();
}
}
catch (Exception)
{ throw; }
}
}
[XmlType("Items")]
public class Items
{
[XmlAttribute("key")]
public string key { get; set; }
[XmlText()]
public string value { get; set; }
}
[Serializable, XmlRoot("MyObject")]
public class MyObject
{
[XmlElement("MyObjectType", IsNullable = true)]
public string MyObjectType { get; set; }
[XmlElement("Items")]
public List<Items> Items;
public string this[string key]
{
get
{
return null != Items.Find(x => x.key == key) ? Items.Find(x => x.key == key).value : null;
}
set
{
if (Items == null) Items = new List<Items>();
if (null != Items.Find(x => x.key == key))
{
Items.Find(x => x.key == key).value = value;
}
else
{
Items.Add(new Items { key = key, value = value });
}
}
}
}
}
Exception details:
System.Xml.Schema.XmlSchemaException
Message:There is an error in XML document (3, 10).
Inner Exception message:The 'key' attribute is not declared.
StackTrace:
at system.Xml.Schema.XmlSchemaValidator.SendValidationEvent(XmlSchemaValidationException e, XmlSeverityType severity)
at System.Xml.Schema.XmlSchemaValidator.SendValidationEvent(String code, String arg)
at System.Xml.Schema.XmlSchemaValidator.ValidateAttribute(String lName, String ns, XmlValueGetter attributeValueGetter, String attributeStringValue, XmlSchemaInfo schemaInfo)
at System.Xml.Schema.XmlSchemaValidator.ValidateAttribute(String localName, String namespaceUri, XmlValueGetter attributeValue, XmlSchemaInfo schemaInfo)
at System.Xml.XsdValidatingReader.ValidateAttributes()
at System.Xml.XsdValidatingReader.ProcessElementEvent()
at System.Xml.XsdValidatingReader.ProcessReaderEvent()
at System.Xml.XsdValidatingReader.Read()
at System.Xml.XmlReader.MoveToContent()
at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderMyObject.Read3_MyObject(Boolean isNullable, Boolean checkType)
at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderMyObject.Read4_MyObject()
Demo fiddle here.
Your problem is here:
private static XmlSchema GetSchemaFromType(Type type)
{
var oSoapReflectionImporter = new SoapReflectionImporter();
SoapReflectionImporter is designed to generate a schema for a c# type that has been marked with SOAP attributes. Such a schema can be used to generate an XmlSerializer customized to use such attributes, as shown in How to: Serialize an Object as a SOAP-Encoded XML Stream:
XmlTypeMapping myTypeMapping = new SoapReflectionImporter().ImportTypeMapping(type);
XmlSerializer mySerializer = new XmlSerializer(myTypeMapping);
You, however, are not using SOAP attributes. You are using regular XmlSerializer attributes, as can be see e.g. in your Items class:
[XmlType("Items")]
public class Items
{
[XmlAttribute("key")]
public string key { get; set; }
[XmlText()]
public string value { get; set; }
}
Thus you should be using XmlReflectionImporter instead:
private static XmlSchema GetSchemaFromType(Type type)
{
var oReflectionImporter = new XmlReflectionImporter();
var oXmlTypeMapping = oReflectionImporter.ImportTypeMapping(type);
var oXmlSchemas = new XmlSchemas();
var oXmlSchema = new XmlSchema();
oXmlSchemas.Add(oXmlSchema);
var oXMLSchemaExporter = new XmlSchemaExporter(oXmlSchemas);
oXMLSchemaExporter.ExportTypeMapping(oXmlTypeMapping);
return oXmlSchema;
}
Related: How do I programmatically generate an xml schema from a type?
Fixed sample demo here.
I want to serialize a class to XML that has a field of type List{List{String}} or String[][] or List{String[]}. My class was serializing and deserializing fine before I added the nested collection field, but it is throwing an InvalidOperationException when serializing or deserializing now.
I don't really care if I have to use arrays or lists for this specific instance, but it would be nice to know a general solution that can be used for any nested collection situation.
Currently my field is declared like this:
[XmlElement("foo")]
public List<String[]> foo;
This has worked fine for me on single level lists and arrays in the past.
Here is the full class:
[XmlRoot("ColumnUpdaterPrefs")]
public class ColumnUpdaterPrefs : Prefs {
public ColumnUpdaterPrefs() : base() {
defaultHeaders = new List<String[]>();
}
[XmlAttribute("autoFill")]
public Boolean autoFill = true;
[XmlAttribute("allowErrors")]
public Boolean allowErrors;
[XmlAttribute("allowZeroes")]
public Boolean allowZeroes;
[XmlElement("defaultHeaders")]
public List<String[]> defaultHeaders;
[XmlElement("defaultKey")]
public String defaultKey;
public override Object Clone() {
return new ColumnUpdaterPrefs() {
autoFill = this.autoFill,
allowErrors = this.allowErrors,
allowZeroes = this.allowZeroes,
defaultHeaders = this.defaultHeaders,
defaultKey = this.defaultKey
};
}
}
And its base class:
[Serializable]
public abstract class Prefs : ICloneable {
[XmlAttribute("name")]
public String name;
public Prefs(String name = null) {
this.name = name;
}
public String Serialize() {
var xs = new XmlSerializer(this.GetType()); //InvalidOperationException occurs here
using (var sw = new StringWriter()) {
xs.Serialize(sw, this);
var result = sw.ToString();
return result;
}
}
public static TPrefs Deserialize<TPrefs>(String xml)
where TPrefs : Prefs {
var xs = new XmlSerializer(typeof(TPrefs)); //InvalidOperationException occurs here
using (var sr = new StringReader(xml)) {
var result = (TPrefs)(xs.Deserialize(sr));
return result;
}
}
public void Write(ApplicationSettingsBase settings, Boolean save = false, String name = null) {
if (settings == null) throw new ArgumentNullException("settings");
if (name == null) name = this.name;
settings[name] = Serialize();
if (save) settings.Save();
}
public static TPrefs Read<TPrefs>(ApplicationSettingsBase settings, String name)
where TPrefs : Prefs {
if (settings == null) throw new ArgumentNullException("settings");
return Deserialize<TPrefs>((String)settings[name]);
}
public static TPrefs ReadOrDefault<TPrefs>(ApplicationSettingsBase settings, String name)
where TPrefs : Prefs, new() {
try { return Read<TPrefs>(settings, name); }
catch { return new TPrefs() { name = name }; }
}
public abstract Object Clone();
}
Here are the exception details:
A first chance exception of type 'System.InvalidOperationException' occurred in System.Xml.dll
Additional information: Unable to generate a temporary class (result=1).
error CS0030: Cannot convert type 'System.Collections.Generic.List' to 'string[]'
error CS0029: Cannot implicitly convert type 'string[]' to 'System.Collections.Generic.List'
Is there a simple way to do this without creating a custom collection class?
thanks for the downvote, but it turns out the issue was just a matter of using the wrong attributes; something which could've been suggested based on my original code sample.
[XmlArray("outerList")]
[XmlArrayItem("innerList", typeof(List<String>))]
public List<List<String>> foo;
I have an interface with exposes a property called Pages:
public interface INameSet
{
IQueryable<string> Names { get; }
}
I have this class which implements the interface and must also be parsed from a JSON object:
[DataContract(Name = "surveyPageSet")]
public class SurveyPage : INameSet
{
[DataMember(Name = "names")]
public List<string> SurveyNames { get; set; }
public IQueryable<string> Names
{
get
{
//Returns SurveyNames after some filtration logic
}
}
}
My problem is that when I pass in this object:
{
"names": ["testname"]
}
The JSON interpreter is trying to deserialize it to match the Names property instead of the SurveyNames property. I know this happens because when removing the implementation of the interface and changing SurveyNames to Names it populates the property fine. Is there any way to get it to serialize to the correct property or do I need to create a translator class that will generate the proper concretion of the INameSet interface?
EDIT: This is with the built-in serializer. If there is a solution with Newtonsoft/JSON.NET that would be fine with me.
JavaScriptSerializer doesn't allow for remapping of names out of the box, so don't use it.
Instead, use Json.NET or DataContractJsonSerializer. In fact, both should already work given the data contract attributes you have applied.
For instance, using Json.NET, if I do:
var page1 = JsonConvert.DeserializeObject<SurveyPage>(json);
Debug.Assert(page1.SurveyNames != null && page1.SurveyNames.SequenceEqual(new string [] { "testname" }));
Then there is no assert. Similarly there is no assert if I do:
var page2 = DataContractJsonSerializerHelper.GetObject<SurveyPage>(json);
Debug.Assert(page2.SurveyNames != null && page2.SurveyNames.SequenceEqual(new string[] { "testname" }));
using the helper class:
public static class DataContractJsonSerializerHelper
{
private static MemoryStream GenerateStreamFromString(string value)
{
return new MemoryStream(Encoding.Unicode.GetBytes(value ?? ""));
}
public static string GetJson<T>(T obj, DataContractJsonSerializer serializer)
{
using (var memory = new MemoryStream())
{
serializer.WriteObject(memory, obj);
memory.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(memory))
{
return reader.ReadToEnd();
}
}
}
public static string GetJson<T>(T obj) where T : class
{
DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
return GetJson(obj, serializer);
}
public static T GetObject<T>(string json) where T : class
{
DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(T));
return GetObject<T>(json, serializer);
}
public static T GetObject<T>(string json, DataContractJsonSerializer serializer)
{
T obj = default(T);
using (var stream = GenerateStreamFromString(json))
{
obj = (T)serializer.ReadObject(stream);
}
return obj;
}
}
Update
If you really want to continue to use JavaScriptConverter, you can write your own JavaScriptConverter and deserialize each field manually. But it's a bother and I wouldn't recommend it.
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());
}
}
}