diff --git a/packages/http-client-csharp/generator/docs/xml-proposal.md b/packages/http-client-csharp/generator/docs/xml-proposal.md new file mode 100644 index 00000000000..999c9656265 --- /dev/null +++ b/packages/http-client-csharp/generator/docs/xml-proposal.md @@ -0,0 +1,684 @@ +# XML Serialization Support for Generated Models + +## Table of Contents + +1. [Motivation](#motivation) +2. [Design Overview](#design-overview) +3. [Sample TypeSpec](#sample-typespec) +4. [High Level Implementation Additions](#high-level-implementation-additions) +5. [Generated Code](#generated-code) +6. [Usage Examples](#usage-examples) + +## Motivation + +Some Azure services use XML as their wire format instead of JSON. Currently, the C# generator (MTG) only supports JSON serialization for models via the `IJsonModel` and `IPersistableModel` interfaces. To support services that use XML payloads, we need to add XML serialization capabilities to generated models. + +The goal is to enable models to be serialized to and deserialized from XML using the existing `ModelReaderWriter` infrastructure, allowing seamless round-tripping of models in XML format. + +## Design Overview + +### Approach + +The XML serialization support leverages the `IPersistableModel` infrastructure: + +1. **Format Support**: Models implement `IPersistableModel` to handle the `"X"` format for XML +2. **XML Methods**: Provide XML-specific serialization and deserialization methods using `XmlWriter` and `XElement` + +## Sample TypeSpec + +The following TypeSpec defines a simple PetStore service that uses XML for request/response payloads: + +```tsp +import "@typespec/rest"; +import "@typespec/http"; +import "@typespec/xml"; + +using TypeSpec.Http; + +@service(#{ title: "Pet Store" }) +@doc("Test for xml") +namespace PetStore; + +model Dog { + id: string; + name: string; + breed?: string; +} + +model Address { + city: string; + street?: string; + zipCode?: string; +} + +model PetDetails { + id: string; + ownerName: string; + petName: string; + address: Address; +} + +@post +@route("/dogs") +op uploadDog(@header contentType: "application/xml", @body body: Dog): NoContentResponse; + +@get +@route("/petDetails/{id}") +op getPetDetails(@path id: string, @header accept: "application/xml"): PetDetails; +``` + +## High Level Implementation Additions + +### XML Serialization Methods + +Two new methods handle XML writing: + +```csharp +// Private method that writes the root element wrapper +private void Write(XmlWriter writer, ModelReaderWriterOptions options, string nameHint = null) +{ + writer.WriteStartElement(nameHint ?? "Dog"); + XmlModelWriteCore(writer, options); + writer.WriteEndElement(); +} + +// Protected virtual method for writing inner content - extensibility point for derived types +protected virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options) +{ + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (format != "X") + { + throw new FormatException($"The model {nameof(Dog)} does not support writing '{format}' format."); + } + writer.WriteStartElement("id"); + writer.WriteValue(Id); + writer.WriteEndElement(); + writer.WriteStartElement("name"); + writer.WriteValue(Name); + writer.WriteEndElement(); + if (Optional.IsDefined(Breed)) + { + writer.WriteStartElement("breed"); + writer.WriteValue(Breed); + writer.WriteEndElement(); + } +} +``` + +### XML Deserialization Method + +A new overload of the `Deserialize` method handles XML: + +```csharp +internal static Dog DeserializeDog(XElement element, ModelReaderWriterOptions options) +{ + if (element == null) + { + return null; + } + + string id = default; + string name = default; + string breed = default; + + foreach (var child in element.Elements()) + { + var localName = child.Name.LocalName; + if (localName == "id") + { + id = (string)child; + continue; + } + if (localName == "name") + { + name = (string)child; + continue; + } + if (localName == "breed") + { + breed = (string)child; + continue; + } + } + + return new Dog(id, name, breed, additionalBinaryDataProperties: default); +} +``` + +### Azure Branded Implementation + +For Azure branded libraries, models that support XML will implement Azure Core's `IXmlSerializable`: + +```csharp +public partial class Dog : IXmlSerializable, IPersistableModel +{ + // ... + void IXmlSerializable.Write(XmlWriter writer, string nameHint) => Write(writer, ModelSerializationExtensions.WireOptions, nameHint); + // ... +} +``` + +## Generated Code + +This section shows the complete generated serialization code for each model in the PetStore library. + +### Dog.Serialization.cs + +
+Click to expand + +```csharp +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using System.Xml.Linq; + +namespace PetStore +{ + /// The Dog. + public partial class Dog : IPersistableModel + { + /// Initializes a new instance of for deserialization. + internal Dog() + { + } + + private void Write(XmlWriter writer, ModelReaderWriterOptions options, string nameHint = null) + { + writer.WriteStartElement(nameHint ?? "Dog"); + XmlModelWriteCore(writer, options); + writer.WriteEndElement(); + } + + /// Writes the XML content of this model without the root element wrapper. + /// The XML writer. + /// The client options for reading and writing models. + protected virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (format != "X") + { + throw new FormatException($"The model {nameof(Dog)} does not support writing '{format}' format."); + } + writer.WriteStartElement("id"); + writer.WriteValue(Id); + writer.WriteEndElement(); + writer.WriteStartElement("name"); + writer.WriteValue(Name); + writer.WriteEndElement(); + if (Optional.IsDefined(Breed)) + { + writer.WriteStartElement("breed"); + writer.WriteValue(Breed); + writer.WriteEndElement(); + } + } + + internal static Dog DeserializeDog(XElement element, ModelReaderWriterOptions options) + { + if (element == null) + { + return null; + } + + string id = default; + string name = default; + string breed = default; + + foreach (var child in element.Elements()) + { + var localName = child.Name.LocalName; + if (localName == "id") + { + id = (string)child; + continue; + } + if (localName == "name") + { + name = (string)child; + continue; + } + if (localName == "breed") + { + breed = (string)child; + continue; + } + } + + return new Dog(id, name, breed, additionalBinaryDataProperties: default); + } + + /// The client options for reading and writing models. + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); + + /// The client options for reading and writing models. + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "X": + using (MemoryStream stream = new MemoryStream(256)) + { + using (XmlWriter writer = XmlWriter.Create(stream)) + { + Write(writer, options); + } + return new BinaryData(stream.ToArray()); + } + default: + throw new FormatException($"The model {nameof(Dog)} does not support writing '{format}' format."); + } + } + + /// The data to parse. + /// The client options for reading and writing models. + Dog IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options); + + /// The data to parse. + /// The client options for reading and writing models. + protected virtual Dog PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "X": + using (var dataStream = data.ToStream()) + { + return DeserializeDog(XElement.Load(dataStream, LoadOptions.None), options); + } + default: + throw new FormatException($"The model {nameof(Dog)} does not support reading '{format}' format."); + } + } + + /// The client options for reading and writing models. + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "X"; + + /// The to serialize into . + public static implicit operator BinaryContent(Dog dog) + { + if (dog == null) + { + return null; + } + return BinaryContent.Create(dog, ModelSerializationExtensions.WireOptions); + } + } +} +``` + +
+ +### Address.Serialization.cs + +
+Click to expand + +```csharp +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using System.Xml.Linq; + +namespace PetStore +{ + /// The Address. + public partial class Address : IPersistableModel
+ { + /// Initializes a new instance of for deserialization. + internal Address() + { + } + + private void Write(XmlWriter writer, ModelReaderWriterOptions options, string nameHint = null) + { + writer.WriteStartElement(nameHint ?? "Address"); + XmlModelWriteCore(writer, options); + writer.WriteEndElement(); + } + + /// Writes the XML content of this model without the root element wrapper. + /// The XML writer. + /// The client options for reading and writing models. + protected virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel
)this).GetFormatFromOptions(options) : options.Format; + if (format != "X") + { + throw new FormatException($"The model {nameof(Address)} does not support writing '{format}' format."); + } + writer.WriteStartElement("city"); + writer.WriteValue(City); + writer.WriteEndElement(); + if (Optional.IsDefined(Street)) + { + writer.WriteStartElement("street"); + writer.WriteValue(Street); + writer.WriteEndElement(); + } + if (Optional.IsDefined(ZipCode)) + { + writer.WriteStartElement("zipCode"); + writer.WriteValue(ZipCode); + writer.WriteEndElement(); + } + } + + internal static Address DeserializeAddress(XElement element, ModelReaderWriterOptions options) + { + if (element == null) + { + return null; + } + + string city = default; + string street = default; + string zipCode = default; + + foreach (var child in element.Elements()) + { + var localName = child.Name.LocalName; + if (localName == "city") + { + city = (string)child; + continue; + } + if (localName == "street") + { + street = (string)child; + continue; + } + if (localName == "zipCode") + { + zipCode = (string)child; + continue; + } + } + + return new Address(city, street, zipCode, additionalBinaryDataProperties: default); + } + + /// The client options for reading and writing models. + BinaryData IPersistableModel
.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); + + /// The client options for reading and writing models. + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel
)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "X": + using (MemoryStream stream = new MemoryStream(256)) + { + using (XmlWriter writer = XmlWriter.Create(stream)) + { + Write(writer, options); + } + return new BinaryData(stream.ToArray()); + } + default: + throw new FormatException($"The model {nameof(Address)} does not support writing '{format}' format."); + } + } + + /// The data to parse. + /// The client options for reading and writing models. + Address IPersistableModel
.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options); + + /// The data to parse. + /// The client options for reading and writing models. + protected virtual Address PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel
)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "X": + using (var dataStream = data.ToStream()) + { + return DeserializeAddress(XElement.Load(dataStream, LoadOptions.None), options); + } + default: + throw new FormatException($"The model {nameof(Address)} does not support reading '{format}' format."); + } + } + + /// The client options for reading and writing models. + string IPersistableModel
.GetFormatFromOptions(ModelReaderWriterOptions options) => "X"; + } +} +``` + +
+ +### PetDetails.Serialization.cs + +
+Click to expand + +```csharp +// + +#nullable disable + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using System.Xml.Linq; + +namespace PetStore +{ + /// The PetDetails. + public partial class PetDetails : IPersistableModel + { + /// Initializes a new instance of for deserialization. + internal PetDetails() + { + } + + private void Write(XmlWriter writer, ModelReaderWriterOptions options, string nameHint = null) + { + writer.WriteStartElement(nameHint ?? "PetDetails"); + XmlModelWriteCore(writer, options); + writer.WriteEndElement(); + } + + /// Writes the XML content of this model without the root element wrapper. + /// The XML writer. + /// The client options for reading and writing models. + protected virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (format != "X") + { + throw new FormatException($"The model {nameof(PetDetails)} does not support writing '{format}' format."); + } + writer.WriteStartElement("id"); + writer.WriteValue(Id); + writer.WriteEndElement(); + writer.WriteStartElement("ownerName"); + writer.WriteValue(OwnerName); + writer.WriteEndElement(); + writer.WriteStartElement("petName"); + writer.WriteValue(PetName); + writer.WriteEndElement(); + writer.WriteObjectValue(Address, options, "address"); + } + + internal static PetDetails DeserializePetDetails(XElement element, ModelReaderWriterOptions options) + { + if (element == null) + { + return null; + } + + string id = default; + string ownerName = default; + string petName = default; + Address address = default; + + foreach (var child in element.Elements()) + { + var localName = child.Name.LocalName; + if (localName == "id") + { + id = (string)child; + continue; + } + if (localName == "ownerName") + { + ownerName = (string)child; + continue; + } + if (localName == "petName") + { + petName = (string)child; + continue; + } + if (localName == "address") + { + address = Address.DeserializeAddress(child, options); + continue; + } + } + + return new PetDetails(id, ownerName, petName, address, additionalBinaryDataProperties: default); + } + + /// The client options for reading and writing models. + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); + + /// The client options for reading and writing models. + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "X": + using (MemoryStream stream = new MemoryStream(256)) + { + using (XmlWriter writer = XmlWriter.Create(stream)) + { + Write(writer, options); + } + return new BinaryData(stream.ToArray()); + } + default: + throw new FormatException($"The model {nameof(PetDetails)} does not support writing '{format}' format."); + } + } + + /// The data to parse. + /// The client options for reading and writing models. + PetDetails IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options); + + /// The data to parse. + /// The client options for reading and writing models. + protected virtual PetDetails PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "X": + using (var dataStream = data.ToStream()) + { + return DeserializePetDetails(XElement.Load(dataStream, LoadOptions.None), options); + } + default: + throw new FormatException($"The model {nameof(PetDetails)} does not support reading '{format}' format."); + } + } + + /// The client options for reading and writing models. + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "X"; + + /// The to deserialize the from. + public static explicit operator PetDetails(ClientResult result) + { + PipelineResponse response = result.GetRawResponse(); + using (var dataStream = response.Content.ToStream()) + { + return DeserializePetDetails(XElement.Load(dataStream, LoadOptions.None), ModelSerializationExtensions.WireOptions); + } + } + } +} +``` + +
+ +## Usage Examples + +### Round-Tripping a Model with ModelReaderWriter + +The `ModelReaderWriter` class provides a simple API for serializing and deserializing models. With XML support, users can easily round-trip models in XML format: + +#### Serializing to XML + +```csharp +// Create a Dog model instance +var dog = new Dog("dog-123", "Buddy", "Golden Retriever"); + +// Serialize to XML using ModelReaderWriter +var xmlOptions = new ModelReaderWriterOptions("X"); +BinaryData xmlData = ModelReaderWriter.Write(dog, xmlOptions); + +// Get the XML string +string xml = xmlData.ToString(); +// Output: dog-123BuddyGolden Retriever +``` + +#### Deserializing from XML + +```csharp +// XML data received from a service +string xml = @"dog-456MaxLabrador"; +BinaryData xmlData = BinaryData.FromString(xml); + +// Deserialize from XML using ModelReaderWriter +var xmlOptions = new ModelReaderWriterOptions("X"); +Dog dog = ModelReaderWriter.Read(xmlData, xmlOptions); + +Console.WriteLine($"Id: {dog.Id}, Name: {dog.Name}, Breed: {dog.Breed}"); +// Output: Id: dog-456, Name: Max, Breed: Labrador +``` + +#### Complete Round-Trip Example + +```csharp +using System.ClientModel.Primitives; + +// Create original model +var original = new Dog("dog-789", "Charlie", "Beagle"); + +// Define XML format options +var xmlOptions = new ModelReaderWriterOptions("X"); + +// Serialize to XML +BinaryData xmlData = ModelReaderWriter.Write(original, xmlOptions); +Console.WriteLine($"Serialized XML: {xmlData}"); + +// Deserialize back to model +Dog roundTripped = ModelReaderWriter.Read(xmlData, xmlOptions); + +// Verify the round-trip +Console.WriteLine($"Original: Id={original.Id}, Name={original.Name}, Breed={original.Breed}"); +Console.WriteLine($"Round-tripped: Id={roundTripped.Id}, Name={roundTripped.Name}, Breed={roundTripped.Breed}"); +```