Kylo Ren
Kylo Ren

Reputation: 8813

C# XML deserialization set default values

I've an XML in my application, like below XML

<Default>       
     <Port>7252</Port>
     <FileLocation>D:/test</FileLocation>
</Default>
<Files>
    <File>
        <Type>Send</Type>
            <FileName>xyz</FileName>            
            <Port>7252</Port>
            <FileLocation>c:/test</FileLocation>
    </File>
    <File>
        <Type>Send</Type>
            <FileName>abc</FileName>            
            <Port></Port>
            <FileLocation></FileLocation>
    </File>
</Files>

while deserialization I want to make sure if File element doesn't have any value, it will get picked from Default element. Is there already any classes/ way to do so, or do I need to write a custom logic for this in my program?

PS: I'm a bit flexible to change names/design in the XML as long as it's in the same proportion.

Upvotes: 0

Views: 1271

Answers (3)

Alexander Petrov
Alexander Petrov

Reputation: 14231

I can suggest the following approach.

Create xml schema, which will set default values for certain elements.

<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="Files">
    <xs:complexType>
      <xs:sequence>
        <xs:element maxOccurs="unbounded" name="File">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="Type"         type="xs:string" />
              <xs:element name="FileName"     type="xs:string" />
              <xs:element name="Port"         type="xs:int"    default="7252" />
              <xs:element name="FileLocation" type="xs:string" default="D:/test" />
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

Your xml data file should look like this:

<?xml version="1.0" encoding="utf-8"?>
<Files>
  <File>
    <Type>Send</Type>
    <FileName>xyz</FileName>
    <Port>7252</Port>
    <FileLocation>c:/test</FileLocation>
  </File>
  <File>
    <Type>Send</Type>
    <FileName>abc</FileName>
    <Port></Port>
    <FileLocation></FileLocation>
  </File>
</Files>

So are your classes for deserialization:

public class Files
{
    [XmlElement("File")]
    public File[] File { get; set; }
}

public class File
{
    public string Type { get; set; }
    public string FileName { get; set; }
    public int Port { get; set; }
    public string FileLocation { get; set; }
}

Now, it is enough to add a xml schema during deserialization to retrieve default values.

Files files;

var settings = new XmlReaderSettings();
settings.Schemas.Add("", "test.xsd");
settings.ValidationType = ValidationType.Schema;

var xs = new XmlSerializer(typeof(Files));
using (var reader = XmlReader.Create("test.xml", settings))
{
    files = (Files)xs.Deserialize(reader);
}

Upvotes: 0

rene
rene

Reputation: 42424

There isn't magic in the XmlSerializer or the supporting attributes around it to achieve what you want. You could venture into creating your own overload of an XmlReader that stores the Default node when read and the rewrite the values for any File node on reading. If your file is larger I would look in to that solution. However, you said the file is small so I guess it is small enough to be transformed first, before being read by the XmlSerializer.

The following XSLT sheet achieves that for your given xml (when wrapped in a Root node).

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
   <!-- copy template -->
   <xsl:template match="@*|node()">
     <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
     </xsl:copy>
   </xsl:template>
   <!-- handle nodes that should have defaults -->
   <xsl:template match="File/*">
      <xsl:choose>
        <!-- there is a value -->
        <xsl:when test="string-length(.)>0">
           <xsl:copy-of select="."/>
        </xsl:when>
        <xsl:otherwise>
           <!-- no value, look up an default -->
           <xsl:variable name="def" select="name()"/>
           <xsl:copy-of select="/Root/Default/*[name(.) = $def]/."/>
        </xsl:otherwise>
      </xsl:choose>
   </xsl:template>

 </xsl:stylesheet>

The following code applies the above style over the input XML with the XslCompiledTransform and the hands its result to the XmlSerializer:

   // create the Xsl Transformation
   var xct = new XslCompiledTransform();
   // use any another Stream if needed
   // xsl holds the XSLT stylesheet
   xct.Load(XmlReader.Create(new StringReader(xsl)));
   Root result;
   // we stream the result in memory
   using(var ms = new MemoryStream())
   {
      // write it
      using(var xw = XmlWriter.Create(ms))
      {
         // transform input XML  
         // the string xml holds the test XML input, replace with a stream
         xct.Transform(XmlReader.Create(new StringReader(xml)), xw);
      }
      Encoding.UTF8.GetString(ms.ToArray()).Dump(); // linqpad testing
      // now we're ready to deserialize
      var xs = new XmlSerializer(typeof(Root));
      ms.Position = 0;
      result= (Root) xs.Deserialize(ms);
      result.Dump(); // Linqpad testing
   }

What basically happens is that instead of your input XML the following XML is fed into the serializer:

<?xml version="1.0" encoding="utf-8"?>
<Root>
  <Default>       
     <Port>7252</Port>
     <FileLocation>D:/test</FileLocation>
  </Default>
  <Files>
    <File>
        <Type>Send</Type>
        <FileName>xyz</FileName>            
        <Port>7252</Port>
        <FileLocation>c:/test</FileLocation>
    </File>
    <File>
        <Type>Send</Type>
        <FileName>abc</FileName>            
        <Port>7252</Port>
        <FileLocation>D:/test</FileLocation>
    </File>
  </Files>
</Root>

For completeness here are the serialization types:

public class Root
{
    public File Default;
    public List<File> Files;
}

public class File
{
   public int Port;
   public string FileName;
   public string FileLocation;
}

Upvotes: 1

jdweng
jdweng

Reputation: 34421

Try this :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace ConsoleApplication1
{
    class Program
    {
        const string FILENAME = @"c:\temp\test.xml";
        static void Main(string[] args)
        {
            XDocument doc = XDocument.Load(FILENAME);
            XElement _default = doc.Descendants("Default").FirstOrDefault();

            int defaultPort = (int)_default.Element("Port");
            string defaultFileLocation = (string)_default.Element("FileLocation");

            var files = doc.Descendants("File").Select(x => new {
                type = (string)x.Element("Type"),
                fileName = (string)x.Element("FileName"),
                port = (string)x.Element("Port") == "" ? defaultPort : (int)x.Element("Port"),
                fileLocation = (string)x.Element("FileLocation") == "" ? defaultFileLocation : (string)x.Element("FileLocation"),
            }).ToList();
        }
    }
}

Upvotes: 1

Related Questions