Tallan's Technology Blog

Tallan's Top Technologists Share Their Thoughts on Today's Technology Challenges

Xsd.exe, Arrays, and “Specified”

Dan Field

Microsoft’s XSD utility provides an excellent way to generate classes from schema definition files, but has a few quirks that can make using the generated classes a bit tougher. In particular, it serializes repeating structures as arrays rather than using the generic List class, and it serializes any element with a “minOccurs = ‘0’” with a separate “Specified” property (which must be set to true if the member is to be serialzied back to XML).

We frequently use serialization techniques here, and while there are some utilities out there that offer similar post processing, many of them are not free and/or difficult to package with a build (include an executable in the project? not ideal). In light of that, I wrote a PowerShell script (below) that can be included in source control and utilized in a post-build event. For example,

"$(TargetFrameworkSDKToolsDirectory)xsd.exe" /c "$(ProjectDir)ImportedPartCanonical.xsd" "$(ProjectDir)ProjectCanonical.xsd" /n:Tallan.BT.PipelineComponents

powershell.exe -ExecutionPolicy Unrestricted -file "$(solutiondir)\PowerShellScripts\PostProcessXsdExe.ps1" ProjectCanonical.cs "$(SolutionDir)Tallan.BT.PipelineComponents\SerializedClasses\ProjectCanonical.cs"

The /c flag on xsd.exe specifies you want to generate classes from the XSD files (multiple files if there are imports involved). The /n flag is in lieu of putting the classes in the global namespace. The -ExecutionPolicy flag will allow this execution of powershell to run an external script (so if another dev doesn’t have their execution policy set machine wide it will still work). And the script itself does the following:

  1. If the property uses a “Specified” property, it will now automatically set “Specified” to true anytime you set that property.
  2. If the property is an array, it will be changed to a generic List

For example:

public decimal NumericElement
{
  get
  {
    return this.numericElementField;
  }
  set
  {
    this.numericElementFieldSpecified = true; // script adds this line, so dev won't forget to set every time the property is set.
    this.numericElementField = value;
  }
}

...
private System.Collections.Generic.List<RepeatingElement> repeatingElementField; // had been a private RepeatingElement[] repeatingElementField

/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute("Repeatingelement")]
public System.Collections.Generic.List<RepeatingElement> RepeatingElement
...

Here’s the script, which can be modified as you see fit:

# Author: Dan Field (dan.field@tallan.com)
# posted on blog.tallan.com/2016/03/10/xsd-exe-arrays-and-specified
# Purpose: fix the 'specified' attribute and convert arrays to list from XSD.exe generated classes

[CmdletBinding()]
Param(
    [Parameter(Mandatory=$true,Position=1)]
    [string]$inputFile,
    [Parameter(Mandatory=$true,Position=2)]
    [string]$outputFile,
    [switch]$DeleteInputFile
)

# much faster than using Get-Content and/or Out-File/Set-Content
$writer = [System.IO.StreamWriter] $outputFile
$reader = [System.IO.StreamReader] $inputFile

# used to track Specified properties
$setterDict = @{}

while (($line = $reader.ReadLine()) -ne $null)
{
    $thisStart = $line.IndexOf("this.") # will be used for
    $brackets = $line.IndexOf("[]") # indicates an array that will be converted to a Generic List

    # assume that any private field that contains "Specified" needs to be grabbed
    if (($line.IndexOf("private") -gt -1) -and ($line.IndexOf("Specified") -gt -1))
    {
        # get the field name
        $varName = $line.Split("{' ',';'}", [System.StringSplitOptions]::RemoveEmptyEntries)[-1]
        # use field name as a key, minus the ending "Specified" portion, e.g. fieldNameSpecified -> fieldName
        # the value in the dictionary will be added to setters on the main property, e.g. "this.fieldNameSpecified = true;"
        $setterDict.Add($varName.Substring(0, $varName.IndexOf("Specified")), "this." + $varName + " = true;")
        # output the line as is
        $writer.WriteLine($line)
    }
    # find property setters that aren't for the *Specified properties
    elseif (($thisStart -gt -1) -and ($line.IndexOf(" = value") -gt -1) -and ($line.IndexOf("Specified") -lt 0))
    {
        # get the field name
        $thisStart += 5
        $varName = $line.Substring($thisStart, $line.IndexOf(' ', $thisStart) - $thisStart)
        # see if there's a "Specified" property for this one
        if ($setterDict.ContainsKey($varName) -eq $true)
        {
            # set the Specified property whenever this property is set
            $writer.WriteLine((' ' * ($thisStart - 5)) + $setterDict[$varName])
        }
        # output the line itself
        $writer.WriteLine($line)
    }
    elseif ($brackets -gt 0) # change to List<T>
    {
        $lineParts = $line.Split(' ')
        foreach ($linePart in $lineParts)
        {
            if ($linePart.Contains("[]") -eq $true)
            {
                $writer.Write("System.Collections.Generic.List<" + $linePart.Replace("[]", "> "))
            }
            else
            {
                $writer.Write($linePart + " ")
            }
        }
        $writer.WriteLine();
    }
    else # just output the original line
    {
        $writer.WriteLine($line)
    }
}

if ($DeleteInputFile -eq $true)
{
    Remove-Item $inputFile
}    

# Make sure the file gets fully written and clean up handles
$writer.Flush();
$writer.Dispose();
$reader.Dispose();

No comments

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>