HolisticWare.StackXml 0.0.1-alpha-202409272206

Prefix Reserved
This is a prerelease version of HolisticWare.StackXml.
dotnet add package HolisticWare.StackXml --version 0.0.1-alpha-202409272206                
NuGet\Install-Package HolisticWare.StackXml -Version 0.0.1-alpha-202409272206                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="HolisticWare.StackXml" Version="0.0.1-alpha-202409272206" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add HolisticWare.StackXml --version 0.0.1-alpha-202409272206                
#r "nuget: HolisticWare.StackXml, 0.0.1-alpha-202409272206"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install HolisticWare.StackXml as a Cake Addin
#addin nuget:?package=HolisticWare.StackXml&version=0.0.1-alpha-202409272206&prerelease

// Install HolisticWare.StackXml as a Cake Tool
#tool nuget:?package=HolisticWare.StackXml&version=0.0.1-alpha-202409272206&prerelease                

StackXML

Stack based zero*-allocation XML serializer and deserializer powered by C# 9 source generators.

Why

Premature optimisation 😃

Setup

  • StackXML targets netstandard2.1 which means back to .NET Core 3.0 is supported, but I would recommend .NET 5
  • Add the following to your project to reference the serializer and enable the source generator
<ItemGroup>
    <ProjectReference Include="..\StackXML\StackXML.csproj" />
    <ProjectReference Include="..\StackXML.Generator\StackXML.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
  • The common entrypoint for deserializing is XmlReadBuffer.ReadStatic(ReadOnlySpan<char>)
  • The common entrypoint for serializing is XmlWriteBuffer.SerializeStatic(IXmlSerializable)
    • This method returns a string, to avoid this allocation you will need create your own instance of XmlWriteBuffer and ensure it is disposed safely like SerializeStatic does. The ToSpan method returns the char span containing the serialized text

Features

  • Fully structured XML serialization and deserialization with 0 allocations, apart from the output data structure when deserializing. Serialization uses a pooled buffer from ArrayPool<char>.Shared that is released when the serializer is disposed.
    • XmlReadBuffer handles deserialization
    • XmlWriteBuffer handles serialization
    • XmlCls maps a type to an element
      • Used for the serializer to know what the element name should be
      • Used by the deserializer to map to IXmlSerializable bodies with no explicit name
    • XmlField maps to attributes
    • XmlBody maps to child elements
    • IXmlSerializable (not actually an interface, see quirks) represents a type that can be read from or written to XML
      • Can be manually added as a base, or the source generator will add it automatically to any type that has XML attributes
  • Parsing delimited attributes into typed lists
    • <test list='1,2,3,4,6,7,8,9'>
    • [XmlField("list")] [XmlSplitStr(',')] public List<int> m_list;
    • Using StrReader and StrWriter, see below
  • StrReader and StrWriter classes, for reading and writing (comma usually) delimited strings with 0 allocations.
    • Can be used in a fully structured way by adding StrField attributes to fields on a ref partial struct (not compatible with XmlSplitStr, maybe future consideration)
  • Agnostic logging through LibLog

Quirks

  • Invalid data between elements is ignored
    • <test>anything here is completely missed<testInner/><test/>
  • Spaces between attributes is not required by the deserializer
    • e.g <test one='aa'two='bb'>
  • XmlSerializer must be disposed otherwise the pooled buffer will be leaked.
    • XmlSerializer.SerializeStatic gives of an example of how this should be done in a safe way
  • Data types can only be classes, not structs.
    • All types must inherit from IXmlSerializable (either manually or added by the source generator) which is actually an abstract class and not an interface
    • Using structs would be possible but I don't think its worth the box
  • Types from another assembly can't be used as a field/body. Needs fixing
  • All elements in the data to parse must be defined in the type in one way or another, otherwise an exception will be thrown.
    • The deserializer relies on complete parsing and has no way of skipping elements
  • Comments within a primitive type body will cause the parser to crash (future consideration...)
    • <n>hi<n>
  • Null strings are currently output exactly the same as empty strings... might need changing
  • The source generator emits a parameterless constructor on all XML types that initializes List<T> bodies to an empty list
    • Trying to serialize a null list currently crashes the serializer....
  • When decoding XML text an extra allocation of the input string is required
    • WebUtility.HtmlDecode does not provide an overload taking a span, but the method taking a string turns it into a span anyway.. hmm
    • The decode is avoided where possible
  • Would be nice to be able to use ValueStringBuilder. See https://github.com/dotnet/runtime/issues/25587

Performance

Very simple benchmark, loading a single element and getting the string value of its attribute attribute


BenchmarkDotNet=v0.12.1, OS=Windows 10.0.17134.1845 (1803/April2018Update/Redstone4)
Intel Core i5-6600K CPU 3.50GHz (Skylake), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=5.0.100
  [Host]     : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
  DefaultJob : .NET Core 5.0.0 (CoreC CoreCLRLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT
Method Mean Error StdDev Ratio RatioSD Gen 0 Gen 1 Gen 2 Allocated
ReadBuffer 95.81 ns 0.983 ns 0.872 ns 1.00 0.00 0.0178 - - 56 B
XmlReader 1,866.22 ns 37.250 ns 79.383 ns 19.57 0.87 3.3216 - - 10424 B
XDocument 2,286.97 ns 45.784 ns 124.560 ns 24.48 1.16 3.4313 - - 10776 B
XmlDocument 2,869.48 ns 44.058 ns 39.057 ns 29.96 0.60 3.9196 - - 12328 B
XmlSerializer 10,386.07 ns 152.481 ns 142.631 ns 108.44 1.49 4.7150 - - 14882 B

Example data classes

Simple Attribute

<test attribute='value'/>
[XmlCls("test"))]
public partial class Test
{
    [XmlField("attribute")]
    public string m_attribute;
}

Text body

<test2>
    <name><![CDATA[Hello world]]></name>
</test2>

CData can be configured by setting cdataMode for serializing and deserializing

<test2>
    <name>Hello world</name>
</test2>
[XmlCls("test2"))]
public partial class Test2
{
    [XmlBody("name")]
    public string m_name;
}

Lists

<container>
    <listItem name="hey" age='25'/>
    <listItem name="how" age='2'/>
    <listItem name="are" age='4'/>
    <listItem name="you" age='53'/>
</container>
[XmlCls("listItem"))]
public partial class ListItem
{
    [XmlField("name")]
    public string m_name;
    
    [XmlField("age")]
    public int m_age; // could also be byte, uint etc
}

[XmlCls("container")]
public partial class ListContainer
{
    [XmlBody()]
    public List<ListItem> m_items; // no explicit name, is taken from XmlCls
}

Delimited attributes

<musicTrack id='5' artists='5,6,1,24,535'>
    <n><![CDATA[Awesome music]]></n>
    <tags>cool</tags>
    <tags>awesome</tags>
    <tags>fresh</tags>
</musicTrack>
[XmlCls("musicTrack"))]
public partial class MusicTrack
{
    [XmlField("id")]
    public int m_id;
    
    [XmlBody("n")]
    public string m_name;
    
    [XmlField("artists"), XmlSplitStr(',')]
    public List<int> m_artists;
    
    [XmlBody("tags")]
    public List<string> m_tags;
}
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.1

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.0.1-alpha-202409272206 63 9/27/2024
0.0.1-alpha-202409242242 69 9/24/2024