Table of Contents

About XML IO

The XML IO design pattern in HiNc Framework is based on IMakeXmlSource interface and XFactory class. This pattern provides a standardized way to serialize and deserialize objects to and from XML format.

Don't serialize the runtime member object like Func<TResult> or Action either cache object. The runtime objects can be optionally sent by the res part on the XFactory Registration or set by the other host or dependent object. If it is set by the other object, then there is nothing can do to it in the XML IO procedure.

Core Components

IMakeXmlSource Interface

The IMakeXmlSource interface defines the contract for objects that can be serialized to XML format. It contains a single method MakeXmlSource.

XFactory

XFactory is an instance class with a process-wide Default singleton (XFactory.Default). The instance form exists for test isolation and parallel pipelines that need disjoint generator registries; the static Gen<T> / GenByChild<T> / GenByFile<T> entry points always read from Default.

Each instance owns its own Generators dictionary (XML element name → generator delegate). Types add themselves via a Reg(factory) call (see below).

Explicit Registration via Reg(XFactory factory = null)

Every class implementing IMakeXmlSource exposes a public static Reg method:

public static void Reg(XFactory factory = null)
{
    factory ??= XFactory.Default;
    factory.Generators.TryAdd(XName, (xml, baseDirectory, relFile, progress, res)
        => new MyClass(xml, baseDirectory, relFile, progress));
}

Key properties:

  • Explicit. Callers see registration happen — no hidden side effect from accessing a static member or constructing a type.
  • Idempotent. Uses TryAdd, so the same Reg may be invoked any number of times from any number of boot paths.
  • Composable. Custom factory instances are supported via the optional factory parameter; default usage (MyClass.Reg();) populates XFactory.Default.

For example, see BallApt:

/// <summary>
/// Registers this type's deserializer with the given <see cref="XFactory"/>
/// (or <see cref="XFactory.Default"/> when <paramref name="factory"/> is
/// <c>null</c>). Idempotent.
/// </summary>
public static void Reg(XFactory factory = null)
{
    factory ??= XFactory.Default;
    factory.Generators.TryAdd(XName, (xml,baseDirectory, relFile, progress, res) => new BallApt(xml));
}

Composite types chain Reg(factory) on dependents

When a class deserializes child elements via XFactory.Gen<T> / XFactory.GenByChild<T>, its Reg(factory) must chain Reg(factory) on each concrete child type so the whole dependency graph is reachable from a single root call:

public static void Reg(XFactory factory = null)
{
    factory ??= XFactory.Default;

    DependentA.Reg(factory);
    DependentB.Reg(factory);

    factory.Generators.TryAdd(XName, (xml, baseDirectory, relFile, progress, res)
        => new MyComposite(xml, baseDirectory, relFile, progress));
}

For polymorphic deserialization (GenByChild<IInterface>), the composite must chain every concrete implementation that may appear in the XML. The largest composite, SoftNcRunner, chains roughly 130 dependents (every dependency, initializer, segmenter, syntax, and semantic the NC pipeline may deserialize).

Multi-name registration (legacy aliases)

When the XML payload may carry an old element name for backward compatibility, register the current XName first and group legacy aliases under a //legacy aliases comment:

public static void Reg(XFactory factory = null)
{
    factory ??= XFactory.Default;

    XFactory.XGeneratorDelegate gen = (xml, baseDirectory, relFile, progress, res)
        => new MachiningProject(xml, baseDirectory, progress);

    factory.Generators.TryAdd(XName, gen);

    //legacy aliases
    factory.Generators.TryAdd("MachiningCourse", gen);
    factory.Generators.TryAdd("MillingCourse", gen);
}

IProgress Threading

The IProgress<object> parameter is threaded through the entire deserialization chain. When a class constructor calls XFactory to deserialize child objects, it passes the same progress instance:

public MyClass(XElement src, string baseDirectory, string relFile,
    IProgress<object> progress)
{
    Child = XFactory.GenByChild<IChild>(
        src.Element(nameof(Child)), subBaseDirectory, progress);
}

Parsing errors are reported to the caller-provided IProgress<object> handler.

Boot path

An application's entry point (web service Program.cs, WPF App.xaml.cs, test fixture, etc.) must call the appropriate top-level Reg() once at startup, before any project XML is deserialized. For the simulation pipeline this is:

LocalProjectService.Reg();

LocalProjectService.Reg() chains MachiningProject.Reg(), which in turn chains every type the simulation pipeline may deserialize. After this single call returns, XFactory.Default.Generators carries the full deserialization graph.

Implementation Patterns

Simple Value Objects

See BallApt implementation:

/// <summary>
/// Name for XML IO.
/// </summary>
public static string XName => nameof(BallApt);
/// <summary>
/// Ctor.
/// </summary>
/// <param name="src">XML</param>
public BallApt(XElement src)
{
    Diameter_mm = double.Parse(src.Element("D").Value);
    FluteHeight_mm = double.Parse(src.Element("FluteH").Value);
}
/// <inheritdoc/>
public XElement MakeXmlSource(string baseDirectory, string relFile, bool exhibitionOnly) => ToXElement();

/// <inheritdoc/>
public XElement ToXElement()
{
    return new XElement(XName,
        new XElement("D", Diameter_mm),
        new XElement("FluteH", FluteHeight_mm)
        );
}

Complex Data Structures

See SpindleCapability implementation:

/// <summary>
/// Name for XML IO.
/// </summary>
public static string XName => nameof(SpindleCapability);
/// <summary>
/// Initializes a new instance of the <see cref="SpindleCapability"/> class.
/// </summary>
/// <param name="src">The XML element containing spindle data.</param>
/// <param name="baseDirectory">The base directory for resolving relative paths.</param>
/// <param name="res">Additional resolution parameters.</param>
public SpindleCapability(XElement src, string baseDirectory, params object[] res)
{
    this.SetNameNote(src);
    if (src.Element(nameof(EnergyEfficiency)) != null)
        EnergyEfficiency = XmlConvert.ToDouble(
            src.Element(nameof(EnergyEfficiency)).Value);
    src.Element(nameof(WorkingTemperatureUpperBoundary_C))?.SelfInvoke(
        e => WorkingTemperatureUpperBoundary_C = XmlConvert.ToDouble(e.Value));
    src.Element(nameof(GearShiftSpindleSpeed_rpm))?.Value?.SelfInvoke(
        s => GearShiftSpindleSpeed_rpm = string.IsNullOrEmpty(s)
            ? null : XmlConvert.ToDouble(s));
    if (src.Element(nameof(DryRunFrictionPowerCoefficient_mWdrpm)) != null)
        DryRunFrictionPowerCoefficient_mWdrpm = XmlConvert.ToDouble(
            src.Element(nameof(DryRunFrictionPowerCoefficient_mWdrpm)).Value);
    if (src.Element(nameof(DryRunWindagePowerCoefficient_pWdrpm3)) != null)
        DryRunWindagePowerCoefficient_pWdrpm3 = XmlConvert.ToDouble(
            src.Element(nameof(DryRunWindagePowerCoefficient_pWdrpm3)).Value);

    if (src.Element("SpindleSpeedToPowerContours") != null) //for legacy
        WorkableDurationToSpindleSpeedPowerContoursDictionary_min_cycleDs_kW =
            src.Element("SpindleSpeedToPowerContours").Elements("Contour")
            .ToDictionary(
                contourElem =>
                {
                    double r = XmlConvert.ToDouble(contourElem.Attribute("InsistentRatio")?.Value);
                    //600s=10mins
                    return r == 1 ? double.PositiveInfinity : (r * 600);
                },
                contourElem => contourElem.Elements("SpindleSpeedToPower").Select(
                    elem => new Vec2d(
                        XmlConvert.ToDouble(elem.Element("SpindleSpeed-RPM").Value) / 60,
                        XmlConvert.ToDouble(elem.Element("Power-kW").Value)))
                .ToList());
    src.Element("WorkableDurationToSpindleSpeedPowerContoursDictionary")
        ?.SelfInvoke(dicElem =>
        {
            WorkableDurationToSpindleSpeedPowerContoursDictionary_min_cycleDs_kW
            = dicElem.Elements("Contour")
            .ToDictionary(
                contourElem => XmlConvert.ToDouble(
                    contourElem.Attribute("WorkableDuration-min")?.Value),
                contourElem => contourElem.Elements("SpindleSpeedToPower").Select(
                    elem => new Vec2d(
                        XmlConvert.ToDouble(elem.Element("SpindleSpeed-RPM").Value) / 60,
                        XmlConvert.ToDouble(elem.Element("Power-kW").Value)))
                .ToList());
        });

    if (src.Element("SpindleSpeedToTorqueContours") != null) //for legacy
        WorkableDurationToSpindleSpeedTorqueContoursDictionary_min_cycleDs_Nm =
            src.Element("SpindleSpeedToTorqueContours").Elements("Contour")
            .ToDictionary(
                contourElem =>
                {
                    double r = XmlConvert.ToDouble(contourElem.Attribute("InsistentRatio")?.Value);
                    //600s=10mins
                    return r == 1 ? double.PositiveInfinity : (r * 600);
                },
                contourElem => contourElem.Elements("SpindleSpeedToTorque").Select(
                    elem => new Vec2d(
                        XmlConvert.ToDouble(elem.Element("SpindleSpeed-RPM").Value) / 60,
                        XmlConvert.ToDouble(elem.Element("Torque-Nm").Value)))
                .ToList());
    src.Element("WorkableDurationToSpindleSpeedTorqueContoursDictionary")
        ?.SelfInvoke(dicElem =>
        {
            //MessageUtil.WriteLine($"dicElem: {dicElem}");
            WorkableDurationToSpindleSpeedTorqueContoursDictionary_min_cycleDs_Nm =
            dicElem.Elements("Contour").ToDictionary(
                contourElem => XmlConvert.ToDouble(
                    contourElem.Attribute("WorkableDuration-min")?.Value),
                contourElem => contourElem.Elements("SpindleSpeedToTorque").Select(
                    elem => new Vec2d(
                        XmlConvert.ToDouble(elem.Element("SpindleSpeed-RPM").Value) / 60,
                        XmlConvert.ToDouble(elem.Element("Torque-Nm").Value)))
                .ToList());
            //MessageUtil.WriteLine($"keys: {string.Join(',',WorkableDurationToSpindleSpeedTorqueContoursDictionary_min_cycleDs_Nm.Select(e=>e.Key))}");
        });

    //for legacy compatible.
    if (src.Element("SpindleSpeedToPower--RPM-to-kW") != null)
        InfInsistentSpindleSpeedToPower_cycleDs_kW =
            src.Element("SpindleSpeedToPower--RPM-to-kW").Elements()
            .Select(elem => new Vec2d(XmlConvert.ToDouble(elem.Attribute(
                "SpindleSpeed-RPM").Value) / 60,
                XmlConvert.ToDouble(elem.Value))).ToList();
    //for legacy compatible.
    if (src.Element("SpindleSpeedToTorque--RPM-to-Nm") != null)
        InfInsistentSpindleSpeedToTorque_cycleDs_Nm =
            src.Element("SpindleSpeedToTorque--RPM-to-Nm").Elements()
            .Select(elem => new Vec2d(XmlConvert.ToDouble(elem.Attribute(
                "SpindleSpeed-RPM").Value) / 60,
                XmlConvert.ToDouble(elem.Value))).ToList();
}
/// <inheritdoc/>
public XElement MakeXmlSource(string baseDirectory, string relFile, bool exhibitionOnly)
{
    return new XElement(XName,
        this.GetNameNoteXElementList(),
        new XElement(nameof(EnergyEfficiency), EnergyEfficiency),
        new XElement(nameof(GearShiftSpindleSpeed_rpm), GearShiftSpindleSpeed_rpm),
        new XElement(nameof(DryRunFrictionPowerCoefficient_mWdrpm),
            DryRunFrictionPowerCoefficient_mWdrpm),
        new XElement(nameof(DryRunWindagePowerCoefficient_pWdrpm3),
            DryRunWindagePowerCoefficient_pWdrpm3),
        new XElement("WorkableDurationToSpindleSpeedPowerContoursDictionary",
            WorkableDurationToSpindleSpeedPowerContoursDictionary_min_cycleDs_kW.OrderBy(entry => entry.Key)
            .Select(entry => new XElement("Contour",
                new XAttribute("WorkableDuration-min", entry.Key),
                entry.Value.Select(entry
                => new XElement("SpindleSpeedToPower",
                new XElement("SpindleSpeed-RPM", entry.X * 60),
                new XElement("Power-kW", entry.Y)))))
            ),
        new XElement("WorkableDurationToSpindleSpeedTorqueContoursDictionary",
            WorkableDurationToSpindleSpeedTorqueContoursDictionary_min_cycleDs_Nm.OrderBy(entry => entry.Key)
            .Select(entry => new XElement("Contour",
                new XAttribute("WorkableDuration-min", entry.Key),
                entry.Value.Select(entry
                => new XElement("SpindleSpeedToTorque",
                new XElement("SpindleSpeed-RPM", entry.X * 60),
                new XElement("Torque-Nm", entry.Y)))))
            )
        );
}

Best Practices

  1. XName: Always define static XName property matching the class name.
  2. Registration: Expose public static void Reg(XFactory factory = null); first line is factory ??= XFactory.Default; then factory.Generators.TryAdd(XName, …).
  3. Chain dependents: For every concrete type T that the ctor reads via XFactory.Gen<T> / XFactory.GenByChild<T>, add T.Reg(factory); to the chain. For polymorphic GenByChild<IInterface>, chain every implementation that the XML may carry.
  4. Idempotent: Use TryAdd, never Add. The same Reg is called from many boot paths.
  5. Progress Threading: Pass the IProgress<object> parameter through all nested XFactory calls. See Message Management for the rationale.
  6. Legacy Support: Register the canonical XName first, then group aliases under a //legacy aliases comment.
  7. Derived class registration: When a derived class needs its own Reg, mark it public new static void Reg(XFactory factory = null) so the C# compiler does not warn about hiding the base method.