From 08a8df26abe317d3018a4c9d81d2815ce2fc0db8 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Wed, 12 Mar 2025 17:15:34 -0500 Subject: [PATCH 01/18] add updated design doc --- .../generator/docs/mpfd-design.md | 1222 +++++++++++++++++ 1 file changed, 1222 insertions(+) create mode 100644 packages/http-client-csharp/generator/docs/mpfd-design.md diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md new file mode 100644 index 00000000000..8c30301d870 --- /dev/null +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -0,0 +1,1222 @@ +# Multipart-form Payload Generation Support + +## Table of Contents + +1. [Motivation](#motivation) +2. [System ClientModel Updates](#system-clientmodel-updates) +3. [MultiPartFormDataBinaryContent Internal Helper](#multiPartFormDataBinaryContent-internal-helper-type) +4. [Usage Examples](#usage-examples) + +## Motivation + +TypeSpec support for explicit HTTP parts within a multipart-form request was added as part of [this issue](https://github.com/microsoft/TypeSpec/issues/3046). Currently, MTG does not generate a convenience layer for multipart/form-data requests and users have to rely on custom code or building the requests themselves to use the generated client protocol methods. + +For example, in it's current state, in order to send a request for this sample operation a client user would need to construct the request themselves, relying on custom or BCL type boundary logic: + +```tsp +model Dog { + id: string; + profileImage: bytes; +} + +@post +@route("/dogs") +op uploadDog( + @header contentType: "multipart/form-data", + @body body: Dog, +): NoContentResponse; +``` + +```csharp +PetStoreClient client = new PetStoreClient(); +// use the internal BCL type to create a MultipartFormDataContent +using MultipartFormDataContent multipartContent = new() +{ + // add the id part, including the name of the part and it's value + { new StringContent("123"), "id" } +}; + +// add the file part, including the name of the part and the file name +await using FileStream imageStream = File.OpenRead("C:\\myDog.jpg"); +StreamContent streamContent = new StreamContent(imageStream); +streamContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream"); +multipartContent.Add(streamContent, "dog", "myDog.jpg"); + +// convert the BCL type to BinaryContent +using Stream multipartContentStream = await multipartContent.ReadAsStreamAsync(); +BinaryContent content = BinaryContent.Create(multipartContentStream); +string requestContentType = multipartContent.Headers.ContentType!.ToString(); + +ClientResult response = await client.UploadDogAsync(content, requestContentType); +``` + +This document provides a proposal for a generated convenience layer to remove some of this burden from users. + +## Goals + +- Provide discoverable convenience methods that simplify creating and sending multipart/form-data requests. +- Allow developers to serialize multipart/form-data requests using ModelReaderWriter. + +## System ClientModel Updates + +### File Part Types + +To support generating a convenience layer for file parts described in a TypeSpec request, new convenience model types can be added to the System.ClientModel library, to be consumed by generated clients. These new types can serve as the common types for file parts within a request model. + +```csharp +public partial class MultiPartFileWithOptionalMetadata +{ + public MultiPartFileWithOptionalMetadata(System.BinaryData contents) { } + public MultiPartFileWithOptionalMetadata(System.IO.Stream contents) { } + public System.BinaryData? Contents { get { throw null; } } + public string ContentType { get { throw null; } set { } } + public System.IO.Stream? File { get { throw null; } } + public string? Filename { get { throw null; } set { } } +} + +public partial class MultiPartFileWithRequiredContentType +{ + public MultiPartFileWithRequiredContentType(System.BinaryData contents, string contentType) { } + public MultiPartFileWithRequiredContentType(System.IO.Stream contents, string contentType) { } + public System.BinaryData? Contents { get { throw null; } } + public string ContentType { get { throw null; } } + public System.IO.Stream? File { get { throw null; } } + public string? Filename { get { throw null; } set { } } +} + +public partial class MultiPartFileWithRequiredFilename +{ + public MultiPartFileWithRequiredFilename(System.BinaryData contents, string filename) { } + public MultiPartFileWithRequiredFilename(System.IO.Stream contents, string filename) { } + public System.BinaryData? Contents { get { throw null; } } + public string ContentType { get { throw null; } set { } } + public System.IO.Stream? File { get { throw null; } } + public string Filename { get { throw null; } } +} + +public partial class MultiPartFileWithRequiredMetadata +{ + public MultiPartFileWithRequiredMetadata(System.BinaryData contents, string filename, string contentType) { } + public MultiPartFileWithRequiredMetadata(System.IO.Stream contents, string filename, string contentType) { } + public System.BinaryData? Contents { get { throw null; } } + public string ContentType { get { throw null; } } + public System.IO.Stream? File { get { throw null; } } + public string Filename { get { throw null; } } +} +``` + +### Support Serializing a Model into a Stream using MRW + +To support optimizing the serialization of large file parts within a request, the ModelReaderWriter serialization can be updated to support serializing a model into a stream. To support this, a new interface can be introduced for writing the model to the user supplied stream. + +```csharp +public partial interface IStreamModel : System.ClientModel.Primitives.IPersistableModel +{ + void Write(System.IO.Stream stream, System.ClientModel.Primitives.ModelReaderWriterOptions options); +} + +public static partial class ModelReaderWriter +{ + public static void Write(object model, System.IO.Stream stream, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) { } + public static void Write(T model, System.IO.Stream stream, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) where T : System.ClientModel.Primitives.IStreamModel { } +} +``` + +## MultiPartFormDataBinaryContent Internal Helper Type + +
+MultiPartFormDataBinaryContent.cs + +```c# +internal partial class MultiPartFormDataBinaryContent : BinaryContent +{ + private readonly MultipartFormDataContent _multipartContent; + + private const int BoundaryLength = 70; + private const string BoundaryValues = "0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; + + public MultiPartFormDataBinaryContent() : this(CreateBoundary()) { } + + // CUSTOM: Internal ctor to use in serialization + internal MultiPartFormDataBinaryContent(string boundary) + { + _multipartContent = new MultipartFormDataContent(boundary); + } + + internal string ContentType + { + get + { + Debug.Assert(_multipartContent.Headers.ContentType is not null); + + return _multipartContent.Headers.ContentType!.ToString(); + } + } + + internal HttpContent HttpContent => _multipartContent; + + // CUSTOM: Add filepart to the multipart content. + public void Add(string name, MultiPartFileWithOptionalMetadata file) + { + Argument.AssertNotNull(file, nameof(file)); + + AddFilePart(name, file.File, file.Contents, file.Filename, file.ContentType); + } + + // CUSTOM: Add filepart to the multipart content. + public void Add(string name, MultiPartFileWithRequiredContentType file) + { + Argument.AssertNotNull(file, nameof(file)); + + AddFilePart(name, file.File, file.Contents, file.Filename, file.ContentType); + } + + // CUSTOM: Add filepart to the multipart content. + public void Add(string name, MultiPartFileWithRequiredFilename file) + { + Argument.AssertNotNull(file, nameof(file)); + + AddFilePart(name, file.File, file.Contents, file.Filename, file.ContentType); + } + + // CUSTOM: Add filepart to the multipart content. + public void Add(string name, MultiPartFileWithRequiredMetadata file) + { + Argument.AssertNotNull(file, nameof(file)); + + AddFilePart(name, file.File, file.Contents, file.Filename, file.ContentType); + } + + // CUSTOM: Add IPersistableModel part to the multipart content. + public void Add(string name, IPersistableModel content, string contentType = default) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNullOrEmpty(name, nameof(name)); + + Add(name, ModelReaderWriter.Write(content, ModelSerializationExtensions.WireOptions), contentType: contentType); + } + + // CUSTOM: Add optional content type parameter to the Add method. + public void Add(string name, string content, string contentType = default) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNullOrEmpty(name, nameof(name)); + + StringContent stringContent = new(content); + if (contentType is not null) + { + stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + + Add(stringContent, name); + } + + // CUSTOM: Add optional content type parameter to the Add method. + public void Add(string name, int content, string contentType = default) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNullOrEmpty(name, nameof(name)); + + string value = content.ToString("G", CultureInfo.InvariantCulture); + StringContent stringContent = new(value); + if (contentType is not null) + { + stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + Add(stringContent, name); + } + + // CUSTOM: Add optional content type parameter to the Add method. + public void Add(string name, long content, string contentType = default) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNullOrEmpty(name, nameof(name)); + + string value = content.ToString("G", CultureInfo.InvariantCulture); + StringContent stringContent = new(value); + if (contentType is not null) + { + stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + Add(stringContent, name); + } + + // CUSTOM: Add optional content type parameter to the Add method. + public void Add(string name, float content, string contentType = default) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNullOrEmpty(name, nameof(name)); + + string value = content.ToString("G", CultureInfo.InvariantCulture); + StringContent stringContent = new(value); + if (contentType is not null) + { + stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + Add(stringContent, name); + } + + // CUSTOM: Add optional content type parameter to the Add method. + public void Add(string name, double content, string contentType = default) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNullOrEmpty(name, nameof(name)); + + string value = content.ToString("G", CultureInfo.InvariantCulture); + StringContent stringContent = new(value); + if (contentType is not null) + { + stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + Add(stringContent, name); + } + + // CUSTOM: Add optional content type parameter to the Add method. + public void Add(string name, decimal content, string contentType = default) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNullOrEmpty(name, nameof(name)); + + string value = content.ToString("G", CultureInfo.InvariantCulture); + StringContent stringContent = new(value); + if (contentType is not null) + { + stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + Add(stringContent, name); + } + + // CUSTOM: Add optional content type parameter to the Add method. + public void Add(string name, bool content, string contentType = default) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNullOrEmpty(name, nameof(name)); + + string value = content ? "true" : "false"; + StringContent stringContent = new(value); + if (contentType is not null) + { + stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + Add(stringContent, name); + } + + // CUSTOM: Add optional content type parameter to the Add method. + public void Add(string name, byte[] content, string contentType = default) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNullOrEmpty(name, nameof(name)); + var byteArrayContent = new ByteArrayContent(content); + if (contentType is not null) + { + byteArrayContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + + Add(byteArrayContent, name); + } + + // CUSTOM: Add optional content type parameter to the Add method. + public void Add(string name, BinaryData content, string fileName = default, string contentType = default) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNullOrEmpty(name, nameof(name)); + + ByteArrayContent byteArrayContent = new(content.ToArray()); + if (contentType is not null) + { + byteArrayContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + Add(byteArrayContent, name, fileName); + } + + // CUSTOM: Add helper method to reduce code duplication. + private void AddFilePart(string name, Stream fileStream, BinaryData contents, string filename = default, string contentType = default) + { + Argument.AssertNotNullOrEmpty(name, nameof(name)); + + if (fileStream != null) + { + Add(name, fileStream, filename, contentType); + } + else if (contents != null) + { + Add(name, contents, filename, contentType); + } + else + { + throw new InvalidOperationException("File contents are not set."); + } + } + + // CUSTOM: Make private + private void Add(string name, Stream stream, string fileName = default, string contentType = default) + { + Argument.AssertNotNull(stream, nameof(stream)); + Argument.AssertNotNullOrEmpty(name, nameof(name)); + + StreamContent content = new(stream); + if (contentType is not null) + { + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + Add(content, name, fileName); + } + + private void Add(HttpContent content, string name, string fileName = default) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNull(name, nameof(name)); + + if (fileName is not null) + { + _multipartContent.Add(content, name, fileName); + } + else + { + _multipartContent.Add(content, name); + } + } + + // CUSTOM: Make static & internalize to use in serialization +#if NET6_0_OR_GREATER + internal static string CreateBoundary() => + string.Create(BoundaryLength, 0, (chars, _) => + { + Span random = stackalloc byte[BoundaryLength]; + Random.Shared.NextBytes(random); + + for (int i = 0; i < chars.Length; i++) + { + chars[i] = BoundaryValues[random[i] % BoundaryValues.Length]; + } + }); +#else + private static readonly Random _random = new(); + + internal static string CreateBoundary() + { + Span chars = stackalloc char[BoundaryLength]; + + byte[] random = new byte[BoundaryLength]; + lock (_random) + { + _random.NextBytes(random); + } + + // Instead of `% BoundaryValues.Length` as is used above, use a mask to achieve the same result. + // `% BoundaryValues.Length` is optimized to the equivalent on .NET Core but not on .NET Framework. + const int Mask = 255 >> 2; + Debug.Assert(BoundaryValues.Length - 1 == Mask); + + for (int i = 0; i < chars.Length; i++) + { + chars[i] = BoundaryValues[random[i] & Mask]; + } + + return chars.ToString(); + } +#endif + + public override bool TryComputeLength(out long length) + { + // We can't call the protected method on HttpContent + + if (_multipartContent.Headers.ContentLength is long contentLength) + { + length = contentLength; + return true; + } + + length = 0; + return false; + } + + public override void WriteTo(Stream stream, CancellationToken cancellationToken = default) + { +#if NET5_0_OR_GREATER + _multipartContent.CopyTo(stream, default, cancellationToken); +#else + // TODO: polyfill sync-over-async for netstandard2.0 for Azure clients. + // Tracked by https://github.com/Azure/azure-sdk-for-net/issues/42674 + _multipartContent.CopyToAsync(stream).GetAwaiter().GetResult(); +#endif + } + + public override async Task WriteToAsync(Stream stream, CancellationToken cancellationToken = default) + { +#if NET5_0_OR_GREATER + await _multipartContent.CopyToAsync(stream, cancellationToken).ConfigureAwait(false); +#else + await _multipartContent.CopyToAsync(stream).ConfigureAwait(false); +#endif + } + + public override void Dispose() + { + _multipartContent.Dispose(); + } +} +``` + +
+ +## Usage Examples + +This section covers some common users scenarios for specifying a multipart-form request within TypeSpec. It includes the proposed generated code and example usage. + +### Operation That Contains a Payload with a File Part and a Primitive Type Part + +
+TypeSpec + +```tsp +model Dog { + id: HttpPart; + profileImage: HttpPart; // File is a TypeSpec library model type +} + +@post +@route("/dogs") +op uploadDog( + @header contentType: "multipart/form-data", + @multipartBody body: Dog, +): NoContentResponse; +``` + +#### The same operation can also be expressed using the `@body` decorator and a "bytes" type for the file part + +```tsp +model Dog { + id: string; + profileImage: bytes; +} + +@post +@route("/dogs") +op uploadDog( + @header contentType: "multipart/form-data", + @body body: Dog, +): NoContentResponse; +``` + +
+ +
+Client + +```c# +// Protocol methods + public virtual ClientResult UploadDog(BinaryContent content, string contentType, RequestOptions options = null) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNull(contentType, nameof(contentType)); + + using PipelineMessage message = CreateUploadDogRequest(content, contentType, options); + return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); + } + + public virtual async Task UploadDogAsync(BinaryContent content, string contentType, RequestOptions options = null) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNull(contentType, nameof(contentType)); + + using PipelineMessage message = CreateUploadDogRequest(content, contentType, options); + return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + +// Convenience methods +public virtual async Task UploadDogAsync(Dog body, CancellationToken cancellationToken = default) +{ + Argument.AssertNotNull(body, nameof(body)); + + using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + return await UploadDogAsync(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); +} + +public virtual ClientResult UploadDog(Dog body, CancellationToken cancellationToken = default) +{ + Argument.AssertNotNull(body, nameof(body)); + + using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + return UploadDog(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); +} +``` + +
+ +
+Dog.cs + +```c# +public partial class Dog +{ + public Dog(string id, MultiPartFileWithOptionalMetadata profileImage) + { + Argument.AssertNotNull(id, nameof(id)); + Argument.AssertNotNull(profileImage, nameof(profileImage)); + + Id = id; + ProfileImage = profileImage; + } + + public string Id { get; } + public MultiPartFileWithOptionalMetadata ProfileImage { get; } +} +``` + +
+ +
+Dog.Serialization.cs + +```c# +public partial class Dog : IStreamModel +{ + internal Dog() + { + } + + private string _boundary; + private string Boundary => _boundary ??= MultiPartFormDataBinaryContent.CreateBoundary(); + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "MPFD-ContentType": + return SerializeMultipartContentType(); + case "MPFD": + return SerializeMultipart(); + default: + throw new FormatException($"The model {nameof(Dog)} does not support writing '{options.Format}' format."); + } + } + + void IStreamModel.Write(Stream stream, ModelReaderWriterOptions options) => PersistableStreamModelWriteCore(stream, options); + protected virtual void PersistableStreamModelWriteCore(Stream stream, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "MPFD": + WriteTo(stream); + return; + default: + throw new FormatException($"The model {nameof(Dog)} does not support writing '{options.Format}' format."); + } + } + + 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) + { + default: + throw new FormatException($"The model {nameof(Dog)} does not support reading '{options.Format}' format."); + } + } + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "MPFD"; + + public static implicit operator BinaryContent(Dog dog) + { + if (dog == null) + { + return null; + } + return dog.ToMultipartContent(); + } + + internal MultiPartFormDataBinaryContent ToMultipartContent() + { + MultiPartFormDataBinaryContent content = new(Boundary); + + content.Add("id", Id); + content.Add("profileImage", ProfileImage); + + return content; + } + + private BinaryData SerializeMultipartContentType() + { + using MultiPartFormDataBinaryContent content = new(Boundary); + return BinaryData.FromString(content.ContentType); + } + + private BinaryData SerializeMultipart() + { + using MemoryStream stream = new MemoryStream(); + + WriteTo(stream); + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + return BinaryData.FromStream(stream); + } + + private void WriteTo(Stream stream) + { + using MultiPartFormDataBinaryContent content = ToMultipartContent(); + content.WriteTo(stream); + } +} + +``` + +
+ +
+Convenience Example Usage + +```csharp +PetStoreClient client = new PetStoreClient(); + +await using FileStream imageStream = File.OpenRead("C:\\myDog.jpg"); +Dog dog = new Dog("123", new MultiPartFileWithOptionalMetadata(imageStream)); + +ClientResult response = await client.UploadDogAsync(dog); +``` + +
+ +
+Protocol Example Usage + +```csharp +PetStoreClient client = new PetStoreClient(); + +await using FileStream imageStream = File.OpenRead("C:\\myDog.jpg"); +Dog dog = new Dog("123", new MultiPartFileWithOptionalMetadata(imageStream)); +// get the multipart content type, which includes the boundary +string contentType = ModelReaderWriter.Write(dog, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); + +ClientResult response = await client.UploadDogAsync(dog, contentType); +``` + +
+ +### Operation That Contains a Payload with a File Part, where the file's metadata is required, and a Primitive Type Part + +
+TypeSpec + +```tsp +model Cat { + id: HttpPart; + profileImage: HttpPart; +} + +// filename and contentType are required. File is a TypeSpec library model type +model FileRequiredMetaData extends File { + filename: string; + contentType: string; +} + +@post +@route("/cats") +op uploadCat( + @header contentType: "multipart/form-data", + @multipartBody body: Cat, +): NoContentResponse; +``` + +
+ +
+Client + +```c# +// Protocol methods + public virtual ClientResult UploadCat(BinaryContent content, string contentType, RequestOptions options = null) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNull(contentType, nameof(contentType)); + + using PipelineMessage message = CreateUploadCatRequest(content, contentType, options); + return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); + } + + public virtual async Task UploadCatAsync(BinaryContent content, string contentType, RequestOptions options = null) + { + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNull(contentType, nameof(contentType)); + + using PipelineMessage message = CreateUploadCatRequest(content, contentType, options); + return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); + } + +// Convenience methods +public virtual ClientResult UploadCat(Cat body, CancellationToken cancellationToken = default) +{ + Argument.AssertNotNull(body, nameof(body)); + + using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + return UploadCat(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); +} + +public virtual async Task UploadCatAsync(Cat body, CancellationToken cancellationToken = default) +{ + Argument.AssertNotNull(body, nameof(body)); + + using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + return await UploadCatAsync(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); +} +``` + +
+ +
+Cat.cs + +```c# +public partial class Cat +{ + public Cat(string id, MultiPartFileWithRequiredMetadata profileImage) + { + Argument.AssertNotNull(id, nameof(id)); + Argument.AssertNotNull(profileImage, nameof(profileImage)); + + Id = id; + ProfileImage = profileImage; + } + + public string Id { get; } + public MultiPartFileWithRequiredMetadata ProfileImage { get; } +} +``` + +
+ +
+Cat.Serialization.cs + +```c# +public partial class Cat : IStreamModel +{ + internal Cat() + { + } + + private string _boundary; + private string Boundary => _boundary ??= MultiPartFormDataBinaryContent.CreateBoundary(); + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "MPFD-ContentType": + return SerializeMultipartContentType(); + case "MPFD": + return SerializeMultipart(); + default: + throw new FormatException($"The model {nameof(Cat)} does not support writing '{options.Format}' format."); + } + } + + void IStreamModel.Write(Stream stream, ModelReaderWriterOptions options) => PersistableStreamModelWriteCore(stream, options); + protected virtual void PersistableStreamModelWriteCore(Stream stream, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "MPFD": + WriteTo(stream); + return; + default: + throw new FormatException($"The model {nameof(Cat)} does not support writing '{options.Format}' format."); + } + } + + Cat IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options); + + /// The data to parse. + /// The client options for reading and writing models. + protected virtual Cat PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + default: + throw new FormatException($"The model {nameof(Cat)} does not support reading '{options.Format}' format."); + } + } + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "MPFD"; + + public static implicit operator BinaryContent(Cat cat) + { + if (cat == null) + { + return null; + } + return cat.ToMultipartContent(); + } + + internal MultiPartFormDataBinaryContent ToMultipartContent() + { + MultiPartFormDataBinaryContent content = new(Boundary); + + content.Add("id", Id); + content.Add("profileImage", ProfileImage); + + return content; + } + + private BinaryData SerializeMultipartContentType() + { + using MultiPartFormDataBinaryContent content = new(Boundary); + return BinaryData.FromString(content.ContentType); + } + + private BinaryData SerializeMultipart() + { + using MemoryStream stream = new MemoryStream(); + + WriteTo(stream); + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + return BinaryData.FromStream(stream); + } + + private void WriteTo(Stream stream) + { + using MultiPartFormDataBinaryContent content = ToMultipartContent(); + content.WriteTo(stream); + } +} +``` + +
+ +
+Convenience Example Usage + +```csharp +PetStoreClient client = new PetStoreClient(); + +await using FileStream imageStream = File.OpenRead("C:\\myCat.jpg"); +Cat cat = new Cat("123", new MultiPartFileWithRequiredMetadata(imageStream, "myCat.jpg", "image/jpeg")); + +ClientResult response = await client.UploadCatAsync(cat); +``` + +
+ +
+Protocol Example Usage + +```csharp +PetStoreClient client = new PetStoreClient(); + +await using FileStream imageStream = File.OpenRead("C:\\myDog.jpg"); +Cat cat = new Cat("123", new MultiPartFileWithRequiredMetadata(imageStream, "myCat.jpg", "image/jpeg")); +// get the multipart content type, which includes the boundary +string contentType = ModelReaderWriter.Write(cat, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); + +ClientResult response = await client.UploadCatAsync(cat, contentType); +``` + +
+ +### Operation That Contains a Payload with Primitive Parts, a File Part, and a Model Part + +
+TypeSpec + +```tsp +model Address { + city: string; +} + +model PetDetails { + id: HttpPart; + ownerName: HttpPart; + petName: HttpPart; + address: HttpPart
; + profileImage: HttpPart; +} + +@post +@route("/pet/details") +op uploadPetDetails( + @header contentType: "multipart/form-data", + @body body: PetDetails, +): NoContentResponse; +``` + +#### The same operation can also be expressed using the `@body` decorator and a "bytes" type for the file part + +```tsp +model Address { + city: string; +} + +model PetDetails { + id: string; + ownerName: string; + petName: string; + address: Address; + profileImage: bytes; +} + +@post +@route("/pet/details") +op uploadPetDetails( + @header contentType: "multipart/form-data", + @body body: PetDetails, +): NoContentResponse; +``` + +
+ +
+Client + +```c# +// Protocol methods +public virtual ClientResult UploadPetDetails(BinaryContent content, string contentType, RequestOptions options = null) +{ + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNull(contentType, nameof(contentType)); + + using PipelineMessage message = CreateUploadPetDetailsRequest(content, contentType, options); + return ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); +} + +public virtual async Task UploadPetDetailsAsync(BinaryContent content, string contentType, RequestOptions options = null) +{ + Argument.AssertNotNull(content, nameof(content)); + Argument.AssertNotNull(contentType, nameof(contentType)); + + using PipelineMessage message = CreateUploadPetDetailsRequest(content, contentType, options); + return ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); +} + +// Convenience methods +public virtual ClientResult UploadPetDetails(PetDetails body, CancellationToken cancellationToken = default) +{ + Argument.AssertNotNull(body, nameof(body)); + + using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + return UploadPetDetails(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); +} + +public virtual async Task UploadPetDetailsAsync(PetDetails body, CancellationToken cancellationToken = default) +{ + Argument.AssertNotNull(body, nameof(body)); + + using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + return await UploadPetDetailsAsync(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); +} +``` + +
+ +
+Address.cs + +```c# +public partial class Address +{ + public Address(string city) + { + Argument.AssertNotNull(city, nameof(city)); + + City = city; + } + + public string City { get; } +} +``` + +
+ +
+PetDetails.cs + +```c# +public partial class PetDetails +{ + public PetDetails(string id, string ownerName, string petName, Address address, MultiPartFileWithOptionalMetadata profileImage) + { + Argument.AssertNotNull(id, nameof(id)); + Argument.AssertNotNull(ownerName, nameof(ownerName)); + Argument.AssertNotNull(petName, nameof(petName)); + Argument.AssertNotNull(address, nameof(address)); + Argument.AssertNotNull(profileImage, nameof(profileImage)); + + Id = id; + OwnerName = ownerName; + PetName = petName; + Address = address; + ProfileImage = profileImage; + } + + public string Id { get; } + public string OwnerName { get; } + public string PetName { get; } + public Address Address { get; } + public MultiPartFileWithOptionalMetadata ProfileImage { get; } +} +``` + +
+ +
+PetDetails.Serialization.cs + +```c# +public partial class PetDetails : IStreamModel +{ + internal PetDetails() + { + } + + private string _boundary; + + private string Boundary => _boundary ??= MultiPartFormDataBinaryContent.CreateBoundary(); + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "MPFD-ContentType": + return SerializeMultipartContentType(); + case "MPFD": + return SerializeMultipart(); + default: + throw new FormatException($"The model {nameof(PetDetails)} does not support writing '{options.Format}' format."); + } + } + + void IStreamModel.Write(Stream stream, ModelReaderWriterOptions options) => PersistableModelWithStreamWriteCore(stream, options); + protected virtual void PersistableModelWithStreamWriteCore(Stream stream, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "MPFD": + WriteTo(stream); + return; + default: + throw new FormatException($"The model {nameof(PetDetails)} does not support writing '{options.Format}' format."); + } + } + + 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) + { + default: + throw new FormatException($"The model {nameof(PetDetails)} does not support reading '{options.Format}' format."); + } + } + + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "MPFD"; + + public static implicit operator BinaryContent(PetDetails petDetails) + { + if (petDetails == null) + { + return null; + } + return petDetails.ToMultipartContent(); + } + + internal MultiPartFormDataBinaryContent ToMultipartContent() + { + MultiPartFormDataBinaryContent content = new(Boundary); + content.Add("id", Id); + content.Add("ownerName", OwnerName); + content.Add("petName", PetName); + content.Add("address", Address); + content.Add("profileImage", ProfileImage); + + return content; + } + + private BinaryData SerializeMultipartContentType() + { + using MultiPartFormDataBinaryContent content = new(Boundary); + return BinaryData.FromString(content.ContentType); + } + + private BinaryData SerializeMultipart() + { + using MemoryStream stream = new MemoryStream(); + + WriteTo(stream); + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + return BinaryData.FromStream(stream); + } + + private void WriteTo(Stream stream) + { + using MultiPartFormDataBinaryContent content = ToMultipartContent(); + content.WriteTo(stream); + } +} +``` + +
+ +
+Convenience Example Usage + +```csharp +PetStoreClient client = new PetStoreClient(); + +await using FileStream profileImageStream = File.OpenRead("C:\\winston.jpg"); +PetDetails petDetails = new PetDetails( + "123", + "John Doe", + "Winston", + new Address("123 Main St."), + new MultiPartFileWithOptionalMetadata(profileImageStream)); + +var response = await client.UploadPetDetailsAsync(petDetails); +``` + +
+ +
+Protocol Example Usage + +```csharp +PetStoreClient client = new PetStoreClient(); + +await using FileStream profileImageStream = File.OpenRead("C:\\winston.jpg"); +PetDetails petDetails = new PetDetails( + "123", + "John Doe", + "Winston", + new Address("123 Main St."), + new MultiPartFileWithOptionalMetadata(profileImageStream)); +// get the multipart content type, which includes the boundary +string contentType = ModelReaderWriter.Write(petDetails, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); + +ClientResult response = await client.UploadCatAsync(petDetails, contentType); +``` + +
From 42cc35f57cf628c917d74c3f080f56c88145bf87 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Mon, 17 Mar 2025 11:05:46 -0500 Subject: [PATCH 02/18] temp remove mrw updates --- .../generator/docs/mpfd-design.md | 110 +++--------------- 1 file changed, 19 insertions(+), 91 deletions(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 8c30301d870..0f77258aa1e 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -105,24 +105,9 @@ public partial class MultiPartFileWithRequiredMetadata } ``` -### Support Serializing a Model into a Stream using MRW +## MultiPartFormDataBinaryContent Internal (Shared) Helper Type -To support optimizing the serialization of large file parts within a request, the ModelReaderWriter serialization can be updated to support serializing a model into a stream. To support this, a new interface can be introduced for writing the model to the user supplied stream. - -```csharp -public partial interface IStreamModel : System.ClientModel.Primitives.IPersistableModel -{ - void Write(System.IO.Stream stream, System.ClientModel.Primitives.ModelReaderWriterOptions options); -} - -public static partial class ModelReaderWriter -{ - public static void Write(object model, System.IO.Stream stream, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) { } - public static void Write(T model, System.IO.Stream stream, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) where T : System.ClientModel.Primitives.IStreamModel { } -} -``` - -## MultiPartFormDataBinaryContent Internal Helper Type +This internal type will be generated by the generator for each library to facilitate the creation of multipart/form-data requests.
MultiPartFormDataBinaryContent.cs @@ -570,7 +555,7 @@ public partial class Dog Dog.Serialization.cs ```c# -public partial class Dog : IStreamModel +public partial class Dog : IPersistableModel { internal Dog() { @@ -594,24 +579,9 @@ public partial class Dog : IStreamModel } } - void IStreamModel.Write(Stream stream, ModelReaderWriterOptions options) => PersistableStreamModelWriteCore(stream, options); - protected virtual void PersistableStreamModelWriteCore(Stream stream, ModelReaderWriterOptions options) - { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; - switch (format) - { - case "MPFD": - WriteTo(stream); - return; - default: - throw new FormatException($"The model {nameof(Dog)} does not support writing '{options.Format}' format."); - } - } - 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; @@ -652,20 +622,15 @@ public partial class Dog : IStreamModel private BinaryData SerializeMultipart() { using MemoryStream stream = new MemoryStream(); + using MultiPartFormDataBinaryContent content = ToMultipartContent(); - WriteTo(stream); + content.WriteTo(stream); if (stream.CanSeek) { stream.Seek(0, SeekOrigin.Begin); } return BinaryData.FromStream(stream); } - - private void WriteTo(Stream stream) - { - using MultiPartFormDataBinaryContent content = ToMultipartContent(); - content.WriteTo(stream); - } } ``` @@ -696,8 +661,9 @@ await using FileStream imageStream = File.OpenRead("C:\\myDog.jpg"); Dog dog = new Dog("123", new MultiPartFileWithOptionalMetadata(imageStream)); // get the multipart content type, which includes the boundary string contentType = ModelReaderWriter.Write(dog, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); +using BinaryContent content = dog; -ClientResult response = await client.UploadDogAsync(dog, contentType); + ClientResult response = await client.UploadDogAsync(content, contentType); ```
@@ -798,7 +764,7 @@ public partial class Cat Cat.Serialization.cs ```c# -public partial class Cat : IStreamModel +public partial class Cat : IPersistableModel { internal Cat() { @@ -822,24 +788,8 @@ public partial class Cat : IStreamModel } } - void IStreamModel.Write(Stream stream, ModelReaderWriterOptions options) => PersistableStreamModelWriteCore(stream, options); - protected virtual void PersistableStreamModelWriteCore(Stream stream, ModelReaderWriterOptions options) - { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; - switch (format) - { - case "MPFD": - WriteTo(stream); - return; - default: - throw new FormatException($"The model {nameof(Cat)} does not support writing '{options.Format}' format."); - } - } - Cat IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options); - /// The data to parse. - /// The client options for reading and writing models. protected virtual Cat PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) { string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; @@ -880,8 +830,9 @@ public partial class Cat : IStreamModel private BinaryData SerializeMultipart() { using MemoryStream stream = new MemoryStream(); + using MultiPartFormDataBinaryContent content = ToMultipartContent(); - WriteTo(stream); + content.WriteTo(stream); if (stream.CanSeek) { stream.Seek(0, SeekOrigin.Begin); @@ -889,11 +840,6 @@ public partial class Cat : IStreamModel return BinaryData.FromStream(stream); } - private void WriteTo(Stream stream) - { - using MultiPartFormDataBinaryContent content = ToMultipartContent(); - content.WriteTo(stream); - } } ``` @@ -923,8 +869,9 @@ await using FileStream imageStream = File.OpenRead("C:\\myDog.jpg"); Cat cat = new Cat("123", new MultiPartFileWithRequiredMetadata(imageStream, "myCat.jpg", "image/jpeg")); // get the multipart content type, which includes the boundary string contentType = ModelReaderWriter.Write(cat, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); +using BinaryContent content = cat; -ClientResult response = await client.UploadCatAsync(cat, contentType); + ClientResult response = await client.UploadCatAsync(content, contentType); ``` @@ -951,7 +898,7 @@ model PetDetails { @route("/pet/details") op uploadPetDetails( @header contentType: "multipart/form-data", - @body body: PetDetails, + @multipartBody body: PetDetails, ): NoContentResponse; ``` @@ -1077,7 +1024,7 @@ public partial class PetDetails PetDetails.Serialization.cs ```c# -public partial class PetDetails : IStreamModel +public partial class PetDetails : IPersistableModel { internal PetDetails() { @@ -1102,24 +1049,9 @@ public partial class PetDetails : IStreamModel } } - void IStreamModel.Write(Stream stream, ModelReaderWriterOptions options) => PersistableModelWithStreamWriteCore(stream, options); - protected virtual void PersistableModelWithStreamWriteCore(Stream stream, ModelReaderWriterOptions options) - { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; - switch (format) - { - case "MPFD": - WriteTo(stream); - return; - default: - throw new FormatException($"The model {nameof(PetDetails)} does not support writing '{options.Format}' format."); - } - } 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; @@ -1162,20 +1094,15 @@ public partial class PetDetails : IStreamModel private BinaryData SerializeMultipart() { using MemoryStream stream = new MemoryStream(); + using MultiPartFormDataBinaryContent content = ToMultipartContent(); - WriteTo(stream); + content.WriteTo(stream); if (stream.CanSeek) { stream.Seek(0, SeekOrigin.Begin); } return BinaryData.FromStream(stream); } - - private void WriteTo(Stream stream) - { - using MultiPartFormDataBinaryContent content = ToMultipartContent(); - content.WriteTo(stream); - } } ``` @@ -1215,8 +1142,9 @@ PetDetails petDetails = new PetDetails( new MultiPartFileWithOptionalMetadata(profileImageStream)); // get the multipart content type, which includes the boundary string contentType = ModelReaderWriter.Write(petDetails, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); +using BinaryContent content = petDetails; -ClientResult response = await client.UploadCatAsync(petDetails, contentType); +ClientResult response = await client.UploadPetDetailsAsync(content, contentType); ``` From f69bdb3b5464be27ab96f5a4256be4eb4e13738f Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Mon, 17 Mar 2025 11:11:25 -0500 Subject: [PATCH 03/18] text updates --- packages/http-client-csharp/generator/docs/mpfd-design.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 0f77258aa1e..53237b205d8 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -55,7 +55,6 @@ This document provides a proposal for a generated convenience layer to remove so ## Goals - Provide discoverable convenience methods that simplify creating and sending multipart/form-data requests. -- Allow developers to serialize multipart/form-data requests using ModelReaderWriter. ## System ClientModel Updates @@ -865,7 +864,7 @@ ClientResult response = await client.UploadCatAsync(cat); ```csharp PetStoreClient client = new PetStoreClient(); -await using FileStream imageStream = File.OpenRead("C:\\myDog.jpg"); +await using FileStream imageStream = File.OpenRead("C:\\myCat.jpg"); Cat cat = new Cat("123", new MultiPartFileWithRequiredMetadata(imageStream, "myCat.jpg", "image/jpeg")); // get the multipart content type, which includes the boundary string contentType = ModelReaderWriter.Write(cat, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); From 46cb211241838794e0268273bf90867c3754f146 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Tue, 25 Mar 2025 12:34:00 -0500 Subject: [PATCH 04/18] update design after arch feedback --- .../generator/docs/mpfd-design.md | 401 +++++++++--------- 1 file changed, 208 insertions(+), 193 deletions(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 53237b205d8..909313efd62 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -58,49 +58,22 @@ This document provides a proposal for a generated convenience layer to remove so ## System ClientModel Updates -### File Part Types +### File Part Type -To support generating a convenience layer for file parts described in a TypeSpec request, new convenience model types can be added to the System.ClientModel library, to be consumed by generated clients. These new types can serve as the common types for file parts within a request model. +To support generating a convenience layer for file parts described in a TypeSpec request, new convenience model type can be added to the System.ClientModel library, to be consumed by generated clients. This new type can serve as the common type for file parts within a request. ```csharp -public partial class MultiPartFileWithOptionalMetadata +public partial class FileBinaryContent : System.ClientModel.BinaryContent { - public MultiPartFileWithOptionalMetadata(System.BinaryData contents) { } - public MultiPartFileWithOptionalMetadata(System.IO.Stream contents) { } - public System.BinaryData? Contents { get { throw null; } } + public FileBinaryContent(System.BinaryData data) { } + public FileBinaryContent(System.IO.Stream stream) { } + public FileBinaryContent(string path) { } public string ContentType { get { throw null; } set { } } - public System.IO.Stream? File { get { throw null; } } public string? Filename { get { throw null; } set { } } -} - -public partial class MultiPartFileWithRequiredContentType -{ - public MultiPartFileWithRequiredContentType(System.BinaryData contents, string contentType) { } - public MultiPartFileWithRequiredContentType(System.IO.Stream contents, string contentType) { } - public System.BinaryData? Contents { get { throw null; } } - public string ContentType { get { throw null; } } - public System.IO.Stream? File { get { throw null; } } - public string? Filename { get { throw null; } set { } } -} - -public partial class MultiPartFileWithRequiredFilename -{ - public MultiPartFileWithRequiredFilename(System.BinaryData contents, string filename) { } - public MultiPartFileWithRequiredFilename(System.IO.Stream contents, string filename) { } - public System.BinaryData? Contents { get { throw null; } } - public string ContentType { get { throw null; } set { } } - public System.IO.Stream? File { get { throw null; } } - public string Filename { get { throw null; } } -} - -public partial class MultiPartFileWithRequiredMetadata -{ - public MultiPartFileWithRequiredMetadata(System.BinaryData contents, string filename, string contentType) { } - public MultiPartFileWithRequiredMetadata(System.IO.Stream contents, string filename, string contentType) { } - public System.BinaryData? Contents { get { throw null; } } - public string ContentType { get { throw null; } } - public System.IO.Stream? File { get { throw null; } } - public string Filename { get { throw null; } } + public override void Dispose() { } + public override bool TryComputeLength(out long length) { throw null; } + public override void WriteTo(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } + public override System.Threading.Tasks.Task WriteToAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } ``` @@ -140,35 +113,18 @@ internal partial class MultiPartFormDataBinaryContent : BinaryContent internal HttpContent HttpContent => _multipartContent; // CUSTOM: Add filepart to the multipart content. - public void Add(string name, MultiPartFileWithOptionalMetadata file) - { - Argument.AssertNotNull(file, nameof(file)); - - AddFilePart(name, file.File, file.Contents, file.Filename, file.ContentType); - } - - // CUSTOM: Add filepart to the multipart content. - public void Add(string name, MultiPartFileWithRequiredContentType file) - { - Argument.AssertNotNull(file, nameof(file)); - - AddFilePart(name, file.File, file.Contents, file.Filename, file.ContentType); - } - - // CUSTOM: Add filepart to the multipart content. - public void Add(string name, MultiPartFileWithRequiredFilename file) + public void Add(string name, FileBinaryContent fileContent) { - Argument.AssertNotNull(file, nameof(file)); - - AddFilePart(name, file.File, file.Contents, file.Filename, file.ContentType); - } + Argument.AssertNotNullOrEmpty(name, nameof(name)); + Argument.AssertNotNull(fileContent, nameof(fileContent)); - // CUSTOM: Add filepart to the multipart content. - public void Add(string name, MultiPartFileWithRequiredMetadata file) - { - Argument.AssertNotNull(file, nameof(file)); + HttpContent content = new HttpContentAdapter(fileContent); + if (fileContent.ContentType != null) + { + content.Headers.ContentType = MediaTypeHeaderValue.Parse(fileContent.ContentType); + } - AddFilePart(name, file.File, file.Contents, file.Filename, file.ContentType); + Add(content, name, fileContent.Filename); } // CUSTOM: Add IPersistableModel part to the multipart content. @@ -313,39 +269,6 @@ internal partial class MultiPartFormDataBinaryContent : BinaryContent Add(byteArrayContent, name, fileName); } - // CUSTOM: Add helper method to reduce code duplication. - private void AddFilePart(string name, Stream fileStream, BinaryData contents, string filename = default, string contentType = default) - { - Argument.AssertNotNullOrEmpty(name, nameof(name)); - - if (fileStream != null) - { - Add(name, fileStream, filename, contentType); - } - else if (contents != null) - { - Add(name, contents, filename, contentType); - } - else - { - throw new InvalidOperationException("File contents are not set."); - } - } - - // CUSTOM: Make private - private void Add(string name, Stream stream, string fileName = default, string contentType = default) - { - Argument.AssertNotNull(stream, nameof(stream)); - Argument.AssertNotNullOrEmpty(name, nameof(name)); - - StreamContent content = new(stream); - if (contentType is not null) - { - content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - } - Add(content, name, fileName); - } - private void Add(HttpContent content, string name, string fileName = default) { Argument.AssertNotNull(content, nameof(content)); @@ -420,8 +343,6 @@ internal partial class MultiPartFormDataBinaryContent : BinaryContent #if NET5_0_OR_GREATER _multipartContent.CopyTo(stream, default, cancellationToken); #else - // TODO: polyfill sync-over-async for netstandard2.0 for Azure clients. - // Tracked by https://github.com/Azure/azure-sdk-for-net/issues/42674 _multipartContent.CopyToAsync(stream).GetAwaiter().GetResult(); #endif } @@ -439,6 +360,32 @@ internal partial class MultiPartFormDataBinaryContent : BinaryContent { _multipartContent.Dispose(); } + + private sealed class HttpContentAdapter : HttpContent + { + private readonly BinaryContent _content; + + public HttpContentAdapter(BinaryContent content) + { + Argument.AssertNotNull(content, nameof(content)); + + _content = content; + } + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + => await _content.WriteToAsync(stream).ConfigureAwait(false); + + protected override bool TryComputeLength(out long length) + => _content.TryComputeLength(out length); + +#if NET6_0_OR_GREATER + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) + => await _content!.WriteToAsync(stream, cancellationToken).ConfigureAwait(false); + + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) + => _content.WriteTo(stream, cancellationToken); +#endif + } } ``` @@ -467,22 +414,6 @@ op uploadDog( ): NoContentResponse; ``` -#### The same operation can also be expressed using the `@body` decorator and a "bytes" type for the file part - -```tsp -model Dog { - id: string; - profileImage: bytes; -} - -@post -@route("/dogs") -op uploadDog( - @header contentType: "multipart/form-data", - @body body: Dog, -): NoContentResponse; -``` -
@@ -532,19 +463,51 @@ public virtual ClientResult UploadDog(Dog body, CancellationToken cancellationTo Dog.cs ```c# -public partial class Dog +public partial class Dog : IDisposable { - public Dog(string id, MultiPartFileWithOptionalMetadata profileImage) - { - Argument.AssertNotNull(id, nameof(id)); - Argument.AssertNotNull(profileImage, nameof(profileImage)); + public Dog(string id, string profileImagePath) + { + Argument.AssertNotNull(id, nameof(id)); + Argument.AssertNotNull(profileImagePath, nameof(profileImagePath)); + + Id = id; + ProfileImage = new(profileImagePath); + + } + public Dog(string id, Stream profileImage) + { + Argument.AssertNotNull(id, nameof(id)); + Argument.AssertNotNull(profileImage, nameof(profileImage)); + + Id = id; + ProfileImage = new(profileImage); + } + + public Dog(string id, BinaryData profileImage) + { + Argument.AssertNotNull(id, nameof(id)); + Argument.AssertNotNull(profileImage, nameof(profileImage)); + + Id = id; + ProfileImage = new(profileImage); + } - Id = id; - ProfileImage = profileImage; - } + public Dog(string id, FileBinaryContent profileImage) + { + Argument.AssertNotNull(id, nameof(id)); + Argument.AssertNotNull(profileImage, nameof(profileImage)); + + Id = id; + ProfileImage = profileImage; + } + + public string Id { get; } + public FileBinaryContent ProfileImage { get; } - public string Id { get; } - public MultiPartFileWithOptionalMetadata ProfileImage { get; } + public void Dispose() + { + ProfileImage?.Dispose(); + } } ``` @@ -642,9 +605,7 @@ public partial class Dog : IPersistableModel ```csharp PetStoreClient client = new PetStoreClient(); -await using FileStream imageStream = File.OpenRead("C:\\myDog.jpg"); -Dog dog = new Dog("123", new MultiPartFileWithOptionalMetadata(imageStream)); - +using Dog dog = new Dog("123", "C:\\myDog.jpg"); ClientResult response = await client.UploadDogAsync(dog); ``` @@ -655,14 +616,11 @@ ClientResult response = await client.UploadDogAsync(dog); ```csharp PetStoreClient client = new PetStoreClient(); - -await using FileStream imageStream = File.OpenRead("C:\\myDog.jpg"); -Dog dog = new Dog("123", new MultiPartFileWithOptionalMetadata(imageStream)); +using Dog dog = new Dog("123", "C:\\myDog.jpg"); // get the multipart content type, which includes the boundary string contentType = ModelReaderWriter.Write(dog, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); -using BinaryContent content = dog; - ClientResult response = await client.UploadDogAsync(content, contentType); +ClientResult response = await client.UploadDogAsync(dog, contentType); ```
@@ -741,19 +699,57 @@ public virtual async Task UploadCatAsync(Cat body, CancellationTok Cat.cs ```c# -public partial class Cat +public partial class Cat : IDisposable { - public Cat(string id, MultiPartFileWithRequiredMetadata profileImage) + public Cat(string id, string filename, string contentType, string profileImagePath) { - Argument.AssertNotNull(id, nameof(id)); + Argument.AssertNotNullOrEmpty(id, nameof(id)); + Argument.AssertNotNullOrEmpty(filename, nameof(filename)); + Argument.AssertNotNullOrEmpty(contentType, nameof(contentType)); + Argument.AssertNotNullOrEmpty(profileImagePath, nameof(profileImagePath)); + + ProfileImage = new(profileImagePath) + { + ContentType = contentType, + Filename = filename, + }; + + } + public Cat(string id, string filename, string contentType, Stream profileImage) + { + Argument.AssertNotNullOrEmpty(id, nameof(id)); + Argument.AssertNotNullOrEmpty(filename, nameof(filename)); + Argument.AssertNotNullOrEmpty(contentType, nameof(contentType)); Argument.AssertNotNull(profileImage, nameof(profileImage)); - Id = id; - ProfileImage = profileImage; + ProfileImage = new(profileImage) + { + ContentType = contentType, + Filename = filename, + }; + } + + public Cat(string id, string filename, string contentType, BinaryData profileImage) + { + Argument.AssertNotNullOrEmpty(id, nameof(id)); + Argument.AssertNotNullOrEmpty(filename, nameof(filename)); + Argument.AssertNotNullOrEmpty(contentType, nameof(contentType)); + Argument.AssertNotNull(profileImage, nameof(profileImage)); + + ProfileImage = new(profileImage) + { + ContentType = contentType, + Filename = filename, + }; } public string Id { get; } - public MultiPartFileWithRequiredMetadata ProfileImage { get; } + public FileBinaryContent ProfileImage { get; } + + public void Dispose() + { + ProfileImage?.Dispose(); + } } ``` @@ -848,12 +844,10 @@ public partial class Cat : IPersistableModel Convenience Example Usage ```csharp -PetStoreClient client = new PetStoreClient(); - -await using FileStream imageStream = File.OpenRead("C:\\myCat.jpg"); -Cat cat = new Cat("123", new MultiPartFileWithRequiredMetadata(imageStream, "myCat.jpg", "image/jpeg")); + PetStoreClient client = new PetStoreClient(); -ClientResult response = await client.UploadCatAsync(cat); + using Cat cat = new Cat("123", "myCat.jpg", "image/jpeg", "C:\\myCat.jpg"); + ClientResult response = await client.UploadCatAsync(cat); ``` @@ -862,15 +856,12 @@ ClientResult response = await client.UploadCatAsync(cat); Protocol Example Usage ```csharp -PetStoreClient client = new PetStoreClient(); + PetStoreClient client = new PetStoreClient(); -await using FileStream imageStream = File.OpenRead("C:\\myCat.jpg"); -Cat cat = new Cat("123", new MultiPartFileWithRequiredMetadata(imageStream, "myCat.jpg", "image/jpeg")); -// get the multipart content type, which includes the boundary -string contentType = ModelReaderWriter.Write(cat, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); -using BinaryContent content = cat; - - ClientResult response = await client.UploadCatAsync(content, contentType); + using Cat cat = new("123", "myCat.jpg", "image/jpeg", "C:\\myCat.jpg"); + // get the multipart content type, which includes the boundary + string contentType = ModelReaderWriter.Write(cat, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); + ClientResult response = await client.UploadCatAsync(cat, contentType); ``` @@ -901,29 +892,6 @@ op uploadPetDetails( ): NoContentResponse; ``` -#### The same operation can also be expressed using the `@body` decorator and a "bytes" type for the file part - -```tsp -model Address { - city: string; -} - -model PetDetails { - id: string; - ownerName: string; - petName: string; - address: Address; - profileImage: bytes; -} - -@post -@route("/pet/details") -op uploadPetDetails( - @header contentType: "multipart/form-data", - @body body: PetDetails, -): NoContentResponse; -``` -
@@ -992,13 +960,58 @@ public partial class Address PetDetails.cs ```c# -public partial class PetDetails +public partial class PetDetails : IDisposable { - public PetDetails(string id, string ownerName, string petName, Address address, MultiPartFileWithOptionalMetadata profileImage) + public PetDetails(string id, string ownerName, string petName, Address address, string profileImagePath) { - Argument.AssertNotNull(id, nameof(id)); - Argument.AssertNotNull(ownerName, nameof(ownerName)); - Argument.AssertNotNull(petName, nameof(petName)); + Argument.AssertNotNullOrEmpty(id, nameof(id)); + Argument.AssertNotNullOrEmpty(ownerName, nameof(ownerName)); + Argument.AssertNotNullOrEmpty(petName, nameof(petName)); + Argument.AssertNotNull(address, nameof(address)); + Argument.AssertNotNull(profileImagePath, nameof(profileImagePath)); + + Id = id; + OwnerName = ownerName; + PetName = petName; + Address = address; + ProfileImage = new(profileImagePath); + + } + public PetDetails(string id, string ownerName, string petName, Address address, Stream profileImage) + { + Argument.AssertNotNullOrEmpty(id, nameof(id)); + Argument.AssertNotNullOrEmpty(ownerName, nameof(ownerName)); + Argument.AssertNotNullOrEmpty(petName, nameof(petName)); + Argument.AssertNotNull(address, nameof(address)); + Argument.AssertNotNull(profileImage, nameof(profileImage)); + + Id = id; + OwnerName = ownerName; + PetName = petName; + Address = address; + ProfileImage = new(profileImage); + } + + public PetDetails(string id, string ownerName, string petName, Address address, BinaryData profileImage) + { + Argument.AssertNotNullOrEmpty(id, nameof(id)); + Argument.AssertNotNullOrEmpty(ownerName, nameof(ownerName)); + Argument.AssertNotNullOrEmpty(petName, nameof(petName)); + Argument.AssertNotNull(address, nameof(address)); + Argument.AssertNotNull(profileImage, nameof(profileImage)); + + Id = id; + OwnerName = ownerName; + PetName = petName; + Address = address; + ProfileImage = new(profileImage); + } + + public PetDetails(string id, string ownerName, string petName, Address address, FileBinaryContent profileImage) + { + Argument.AssertNotNullOrEmpty(id, nameof(id)); + Argument.AssertNotNullOrEmpty(ownerName, nameof(ownerName)); + Argument.AssertNotNullOrEmpty(petName, nameof(petName)); Argument.AssertNotNull(address, nameof(address)); Argument.AssertNotNull(profileImage, nameof(profileImage)); @@ -1013,7 +1026,12 @@ public partial class PetDetails public string OwnerName { get; } public string PetName { get; } public Address Address { get; } - public MultiPartFileWithOptionalMetadata ProfileImage { get; } + public FileBinaryContent ProfileImage { get; } + + public void Dispose() + { + ProfileImage?.Dispose(); + } } ``` @@ -1113,13 +1131,12 @@ public partial class PetDetails : IPersistableModel ```csharp PetStoreClient client = new PetStoreClient(); -await using FileStream profileImageStream = File.OpenRead("C:\\winston.jpg"); -PetDetails petDetails = new PetDetails( +using PetDetails petDetails = new PetDetails( "123", "John Doe", "Winston", new Address("123 Main St."), - new MultiPartFileWithOptionalMetadata(profileImageStream)); + "C:\\winston.jpg"); var response = await client.UploadPetDetailsAsync(petDetails); ``` @@ -1132,18 +1149,16 @@ var response = await client.UploadPetDetailsAsync(petDetails); ```csharp PetStoreClient client = new PetStoreClient(); -await using FileStream profileImageStream = File.OpenRead("C:\\winston.jpg"); -PetDetails petDetails = new PetDetails( - "123", - "John Doe", - "Winston", - new Address("123 Main St."), - new MultiPartFileWithOptionalMetadata(profileImageStream)); +using PetDetails petDetails = new PetDetails( + "123", + "John Doe", + "Winston", + new Address("123 Main St."), + "C:\\winston.jpg"); + // get the multipart content type, which includes the boundary string contentType = ModelReaderWriter.Write(petDetails, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); -using BinaryContent content = petDetails; - -ClientResult response = await client.UploadPetDetailsAsync(content, contentType); +ClientResult response = await client.UploadCatAsync(petDetails, contentType); ```
From d1114155aa9e4dc7651b91c228ea90c375b67fc7 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Mon, 31 Mar 2025 11:06:28 -0500 Subject: [PATCH 05/18] add considerations section --- .../generator/docs/mpfd-design.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 909313efd62..6fb82a31941 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -6,6 +6,7 @@ 2. [System ClientModel Updates](#system-clientmodel-updates) 3. [MultiPartFormDataBinaryContent Internal Helper](#multiPartFormDataBinaryContent-internal-helper-type) 4. [Usage Examples](#usage-examples) +5. [Considerations for Advanced Scenarios](#considerations-for-advanced-scenarios) ## Motivation @@ -1162,3 +1163,34 @@ ClientResult response = await client.UploadCatAsync(petDetails, contentType); ``` + +## Considerations for Advanced Scenarios + +The introduction of the file part type described in [_File Part Type_](#file-part-type) has some limitations when used in more advanced scenarios +that should be considered. + +Consider the following scenario: + +```tsp +// filename and contentType are required +model FileRequiredMetaData extends File { + filename: string; + contentType: string; +} + +model Snake { + name: HttpPart; + pictures: HttpPart[]; +} + +@post +@route("/snakes") +op uploadSnake( + @header contentType: "multipart/form-data", + @multipartBody body: Snake, +): NoContentResponse; +``` + +The `pictures` property is a list of file parts where each file has required metadata (filename + content type are required). Since the [`FileBinaryContent`](#file-part-type), by default, has these metadata properties as _optional_, the generated model for `Snake` would need a way to describe the metadata as being required for reach file in `pictures`. Otherwise, the following effect of not doing so should be considered: + +- If the constructor overloads for `Snake` accept a list of streams, file paths, or BinaryData then consumers of the `uploadSnake` operation would not be made aware of this metadata requirement for the `pictures` property until the `uploadSnake` operation returns a service failure. From ac93374891e3f18f2d0d35548ee513c40e021a97 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Mon, 31 Mar 2025 14:12:13 -0500 Subject: [PATCH 06/18] add section for exposing scm type --- .../generator/docs/mpfd-design.md | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 6fb82a31941..bffdec0430a 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -6,7 +6,7 @@ 2. [System ClientModel Updates](#system-clientmodel-updates) 3. [MultiPartFormDataBinaryContent Internal Helper](#multiPartFormDataBinaryContent-internal-helper-type) 4. [Usage Examples](#usage-examples) -5. [Considerations for Advanced Scenarios](#considerations-for-advanced-scenarios) +5. [Considerations](#considerations) ## Motivation @@ -1164,7 +1164,9 @@ ClientResult response = await client.UploadCatAsync(petDetails, contentType); -## Considerations for Advanced Scenarios +## Considerations + +### Limitations in Advanced Scenarios The introduction of the file part type described in [_File Part Type_](#file-part-type) has some limitations when used in more advanced scenarios that should be considered. @@ -1194,3 +1196,29 @@ op uploadSnake( The `pictures` property is a list of file parts where each file has required metadata (filename + content type are required). Since the [`FileBinaryContent`](#file-part-type), by default, has these metadata properties as _optional_, the generated model for `Snake` would need a way to describe the metadata as being required for reach file in `pictures`. Otherwise, the following effect of not doing so should be considered: - If the constructor overloads for `Snake` accept a list of streams, file paths, or BinaryData then consumers of the `uploadSnake` operation would not be made aware of this metadata requirement for the `pictures` property until the `uploadSnake` operation returns a service failure. + +### Protocol Method Usage with Public MPFD Type + +The usage of the generated protocol methods can be improved if we consider exposing the [`MultiPartFormDataBinaryContent`](#multiPartFormDataBinaryContent-internal-helper-type) type as a public SCM type. Consider the [dog example](#operation-that-contains-a-payload-with-a-file-part-and-a-primitive-type-part) outlined earlier: + +```csharp +PetStoreClient client = new PetStoreClient(); +using Dog dog = new Dog("123", "C:\\myDog.jpg"); +// get the multipart content type, which includes the boundary +string contentType = ModelReaderWriter.Write(dog, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); +// dog is implicitly converted to BinaryContent +ClientResult response = await client.UploadDogAsync(dog, contentType); +``` + +If the `MultiPartFormDataBinaryContent` type was made public, advanced users can construct this same request as: + +```csharp +PetStoreClient client = new PetStoreClient(); + +await using FileStream profileImage = File.OpenRead("C:\\myDog.jpg"); +using MultiPartFormDataBinaryContent content = new(); +content.Add("id", "123"); +content.Add("profileImage", profileImage, fileName: "profileImage.jpg", contentType: "application/octet-stream"); + +ClientResult response = await client.UploadDogAsync(content, content.ContentType); +``` From 24a938a7d7d043313050f9e68d1a92c7aabf2de4 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Mon, 31 Mar 2025 14:13:49 -0500 Subject: [PATCH 07/18] improve language --- packages/http-client-csharp/generator/docs/mpfd-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index bffdec0430a..8aad4a01e79 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -1199,7 +1199,7 @@ The `pictures` property is a list of file parts where each file has required met ### Protocol Method Usage with Public MPFD Type -The usage of the generated protocol methods can be improved if we consider exposing the [`MultiPartFormDataBinaryContent`](#multiPartFormDataBinaryContent-internal-helper-type) type as a public SCM type. Consider the [dog example](#operation-that-contains-a-payload-with-a-file-part-and-a-primitive-type-part) outlined earlier: +The usage of the generated protocol methods can be improved if we consider exposing the [`MultiPartFormDataBinaryContent`](#multiPartFormDataBinaryContent-internal-helper-type) type as a public SCM type. Consider the protocol method usage for the [dog example](#operation-that-contains-a-payload-with-a-file-part-and-a-primitive-type-part) outlined earlier: ```csharp PetStoreClient client = new PetStoreClient(); From 464ae8bdb5287216c28dac38ccf88cea4b702272 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Mon, 31 Mar 2025 14:28:43 -0500 Subject: [PATCH 08/18] more language improvements --- .../generator/docs/mpfd-design.md | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 8aad4a01e79..3a1f392dac7 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -1193,13 +1193,33 @@ op uploadSnake( ): NoContentResponse; ``` -The `pictures` property is a list of file parts where each file has required metadata (filename + content type are required). Since the [`FileBinaryContent`](#file-part-type), by default, has these metadata properties as _optional_, the generated model for `Snake` would need a way to describe the metadata as being required for reach file in `pictures`. Otherwise, the following effect of not doing so should be considered: +The pictures property is a collection of file parts where each file has required metadata (filename + content type are required). In the generated code, this creates a mismatch between the TypeSpec definition (where metadata is required) and the FileBinaryContent class (where Filename and ContentType properties are optional). -- If the constructor overloads for `Snake` accept a list of streams, file paths, or BinaryData then consumers of the `uploadSnake` operation would not be made aware of this metadata requirement for the `pictures` property until the `uploadSnake` operation returns a service failure. +Specifically: + +1. The generated Snake model would have constructors that accept collections of `Stream`, `string` (file paths), or `BinaryData` for the pictures property. +2. Each item would be converted to a `FileBinaryContent` object with default (null) metadata properties. +3. The TypeSpec contract requires `filename` and `contentType` for each file, but the C# API doesn't enforce this requirement. +4. The service could reject the request, but users would only discover this at runtime when the upload fails. + +For example, the generated constructor might look like: + +```csharp +public Snake(string name, IEnumerable pictures) +{ + Name = name; + Pictures = pictures.Select(pic => new FileBinaryContent(pic)).ToArray(); + // FileBinaryContent objects created without required metadata. +} +``` ### Protocol Method Usage with Public MPFD Type -The usage of the generated protocol methods can be improved if we consider exposing the [`MultiPartFormDataBinaryContent`](#multiPartFormDataBinaryContent-internal-helper-type) type as a public SCM type. Consider the protocol method usage for the [dog example](#operation-that-contains-a-payload-with-a-file-part-and-a-primitive-type-part) outlined earlier: +Making the `MultiPartFormDataBinaryContent` type public in SCM would significantly enhance flexibility for advanced scenarios while maintaining the simplicity of the convenience layer for common use cases. + +#### Example: Current Protocol Usage vs. Public MPFD Type + +Consider the protocol method usage for the [dog example](#operation-that-contains-a-payload-with-a-file-part-and-a-primitive-type-part) outlined earlier: ```csharp PetStoreClient client = new PetStoreClient(); @@ -1210,7 +1230,7 @@ string contentType = ModelReaderWriter.Write(dog, new ModelReaderWriterOptions(" ClientResult response = await client.UploadDogAsync(dog, contentType); ``` -If the `MultiPartFormDataBinaryContent` type was made public, advanced users can construct this same request as: +With a public `MultiPartFormDataBinaryContent` type, users could directly construct and customize the request: ```csharp PetStoreClient client = new PetStoreClient(); From 3cd0cd4f1db74c7ab2e55381e50d3841b74053b9 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Mon, 31 Mar 2025 14:33:34 -0500 Subject: [PATCH 09/18] fix typo --- packages/http-client-csharp/generator/docs/mpfd-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 3a1f392dac7..412f4a33a0e 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -1238,7 +1238,7 @@ PetStoreClient client = new PetStoreClient(); await using FileStream profileImage = File.OpenRead("C:\\myDog.jpg"); using MultiPartFormDataBinaryContent content = new(); content.Add("id", "123"); -content.Add("profileImage", profileImage, fileName: "profileImage.jpg", contentType: "application/octet-stream"); +content.Add("profileImage", profileImage); ClientResult response = await client.UploadDogAsync(content, content.ContentType); ``` From cfd7919da07c5e7e8ae80d712aa2b3f1af9759f0 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Mon, 2 Jun 2025 12:03:55 -0500 Subject: [PATCH 10/18] apply feedback from latest arch review --- .../generator/docs/mpfd-design.md | 599 +++--------------- 1 file changed, 98 insertions(+), 501 deletions(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 412f4a33a0e..666c9b81bda 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -4,9 +4,7 @@ 1. [Motivation](#motivation) 2. [System ClientModel Updates](#system-clientmodel-updates) -3. [MultiPartFormDataBinaryContent Internal Helper](#multiPartFormDataBinaryContent-internal-helper-type) -4. [Usage Examples](#usage-examples) -5. [Considerations](#considerations) +3. [Usage Examples](#usage-examples) ## Motivation @@ -59,17 +57,45 @@ This document provides a proposal for a generated convenience layer to remove so ## System ClientModel Updates +### BinaryContent APIs + +The BinaryContent class is being extended with multipart/form-data capabilities to provide a streamlined API for building requests for clients that need to send multipart payloads. These additions eliminate the need for manual boundary management and complex multipart construction while maintaining full control over content types and part metadata. + +```c# +public abstract partial class BinaryContent : System.IDisposable +{ + // Add ContentType property + public virtual string? ContentType { get { throw null; } set { } } + + // Add APIs for creating MPFD parts and payload. + public static System.ClientModel.BinaryContent CreateMultipartFormDataContent(System.Collections.Generic.IEnumerable parts) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataContent(string boundary, System.Collections.Generic.IEnumerable parts) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, System.BinaryData content) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, bool content) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, byte[] content) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, System.ClientModel.FileBinaryContent content) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, decimal content) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, double content) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, int content) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, long content) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, System.IO.Stream stream) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, float content) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, string content) { throw null; } + public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, T model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } +} +``` + ### File Part Type To support generating a convenience layer for file parts described in a TypeSpec request, new convenience model type can be added to the System.ClientModel library, to be consumed by generated clients. This new type can serve as the common type for file parts within a request. ```csharp -public partial class FileBinaryContent : System.ClientModel.BinaryContent +public sealed partial class FileBinaryContent : System.ClientModel.BinaryContent { public FileBinaryContent(System.BinaryData data) { } public FileBinaryContent(System.IO.Stream stream) { } public FileBinaryContent(string path) { } - public string ContentType { get { throw null; } set { } } + public override string? ContentType { get { throw null; } set { } } public string? Filename { get { throw null; } set { } } public override void Dispose() { } public override bool TryComputeLength(out long length) { throw null; } @@ -78,318 +104,6 @@ public partial class FileBinaryContent : System.ClientModel.BinaryContent } ``` -## MultiPartFormDataBinaryContent Internal (Shared) Helper Type - -This internal type will be generated by the generator for each library to facilitate the creation of multipart/form-data requests. - -
-MultiPartFormDataBinaryContent.cs - -```c# -internal partial class MultiPartFormDataBinaryContent : BinaryContent -{ - private readonly MultipartFormDataContent _multipartContent; - - private const int BoundaryLength = 70; - private const string BoundaryValues = "0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; - - public MultiPartFormDataBinaryContent() : this(CreateBoundary()) { } - - // CUSTOM: Internal ctor to use in serialization - internal MultiPartFormDataBinaryContent(string boundary) - { - _multipartContent = new MultipartFormDataContent(boundary); - } - - internal string ContentType - { - get - { - Debug.Assert(_multipartContent.Headers.ContentType is not null); - - return _multipartContent.Headers.ContentType!.ToString(); - } - } - - internal HttpContent HttpContent => _multipartContent; - - // CUSTOM: Add filepart to the multipart content. - public void Add(string name, FileBinaryContent fileContent) - { - Argument.AssertNotNullOrEmpty(name, nameof(name)); - Argument.AssertNotNull(fileContent, nameof(fileContent)); - - HttpContent content = new HttpContentAdapter(fileContent); - if (fileContent.ContentType != null) - { - content.Headers.ContentType = MediaTypeHeaderValue.Parse(fileContent.ContentType); - } - - Add(content, name, fileContent.Filename); - } - - // CUSTOM: Add IPersistableModel part to the multipart content. - public void Add(string name, IPersistableModel content, string contentType = default) - { - Argument.AssertNotNull(content, nameof(content)); - Argument.AssertNotNullOrEmpty(name, nameof(name)); - - Add(name, ModelReaderWriter.Write(content, ModelSerializationExtensions.WireOptions), contentType: contentType); - } - - // CUSTOM: Add optional content type parameter to the Add method. - public void Add(string name, string content, string contentType = default) - { - Argument.AssertNotNull(content, nameof(content)); - Argument.AssertNotNullOrEmpty(name, nameof(name)); - - StringContent stringContent = new(content); - if (contentType is not null) - { - stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - } - - Add(stringContent, name); - } - - // CUSTOM: Add optional content type parameter to the Add method. - public void Add(string name, int content, string contentType = default) - { - Argument.AssertNotNull(content, nameof(content)); - Argument.AssertNotNullOrEmpty(name, nameof(name)); - - string value = content.ToString("G", CultureInfo.InvariantCulture); - StringContent stringContent = new(value); - if (contentType is not null) - { - stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - } - Add(stringContent, name); - } - - // CUSTOM: Add optional content type parameter to the Add method. - public void Add(string name, long content, string contentType = default) - { - Argument.AssertNotNull(content, nameof(content)); - Argument.AssertNotNullOrEmpty(name, nameof(name)); - - string value = content.ToString("G", CultureInfo.InvariantCulture); - StringContent stringContent = new(value); - if (contentType is not null) - { - stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - } - Add(stringContent, name); - } - - // CUSTOM: Add optional content type parameter to the Add method. - public void Add(string name, float content, string contentType = default) - { - Argument.AssertNotNull(content, nameof(content)); - Argument.AssertNotNullOrEmpty(name, nameof(name)); - - string value = content.ToString("G", CultureInfo.InvariantCulture); - StringContent stringContent = new(value); - if (contentType is not null) - { - stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - } - Add(stringContent, name); - } - - // CUSTOM: Add optional content type parameter to the Add method. - public void Add(string name, double content, string contentType = default) - { - Argument.AssertNotNull(content, nameof(content)); - Argument.AssertNotNullOrEmpty(name, nameof(name)); - - string value = content.ToString("G", CultureInfo.InvariantCulture); - StringContent stringContent = new(value); - if (contentType is not null) - { - stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - } - Add(stringContent, name); - } - - // CUSTOM: Add optional content type parameter to the Add method. - public void Add(string name, decimal content, string contentType = default) - { - Argument.AssertNotNull(content, nameof(content)); - Argument.AssertNotNullOrEmpty(name, nameof(name)); - - string value = content.ToString("G", CultureInfo.InvariantCulture); - StringContent stringContent = new(value); - if (contentType is not null) - { - stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - } - Add(stringContent, name); - } - - // CUSTOM: Add optional content type parameter to the Add method. - public void Add(string name, bool content, string contentType = default) - { - Argument.AssertNotNull(content, nameof(content)); - Argument.AssertNotNullOrEmpty(name, nameof(name)); - - string value = content ? "true" : "false"; - StringContent stringContent = new(value); - if (contentType is not null) - { - stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - } - Add(stringContent, name); - } - - // CUSTOM: Add optional content type parameter to the Add method. - public void Add(string name, byte[] content, string contentType = default) - { - Argument.AssertNotNull(content, nameof(content)); - Argument.AssertNotNullOrEmpty(name, nameof(name)); - var byteArrayContent = new ByteArrayContent(content); - if (contentType is not null) - { - byteArrayContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - } - - Add(byteArrayContent, name); - } - - // CUSTOM: Add optional content type parameter to the Add method. - public void Add(string name, BinaryData content, string fileName = default, string contentType = default) - { - Argument.AssertNotNull(content, nameof(content)); - Argument.AssertNotNullOrEmpty(name, nameof(name)); - - ByteArrayContent byteArrayContent = new(content.ToArray()); - if (contentType is not null) - { - byteArrayContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); - } - Add(byteArrayContent, name, fileName); - } - - private void Add(HttpContent content, string name, string fileName = default) - { - Argument.AssertNotNull(content, nameof(content)); - Argument.AssertNotNull(name, nameof(name)); - - if (fileName is not null) - { - _multipartContent.Add(content, name, fileName); - } - else - { - _multipartContent.Add(content, name); - } - } - - // CUSTOM: Make static & internalize to use in serialization -#if NET6_0_OR_GREATER - internal static string CreateBoundary() => - string.Create(BoundaryLength, 0, (chars, _) => - { - Span random = stackalloc byte[BoundaryLength]; - Random.Shared.NextBytes(random); - - for (int i = 0; i < chars.Length; i++) - { - chars[i] = BoundaryValues[random[i] % BoundaryValues.Length]; - } - }); -#else - private static readonly Random _random = new(); - - internal static string CreateBoundary() - { - Span chars = stackalloc char[BoundaryLength]; - - byte[] random = new byte[BoundaryLength]; - lock (_random) - { - _random.NextBytes(random); - } - - // Instead of `% BoundaryValues.Length` as is used above, use a mask to achieve the same result. - // `% BoundaryValues.Length` is optimized to the equivalent on .NET Core but not on .NET Framework. - const int Mask = 255 >> 2; - Debug.Assert(BoundaryValues.Length - 1 == Mask); - - for (int i = 0; i < chars.Length; i++) - { - chars[i] = BoundaryValues[random[i] & Mask]; - } - - return chars.ToString(); - } -#endif - - public override bool TryComputeLength(out long length) - { - // We can't call the protected method on HttpContent - - if (_multipartContent.Headers.ContentLength is long contentLength) - { - length = contentLength; - return true; - } - - length = 0; - return false; - } - - public override void WriteTo(Stream stream, CancellationToken cancellationToken = default) - { -#if NET5_0_OR_GREATER - _multipartContent.CopyTo(stream, default, cancellationToken); -#else - _multipartContent.CopyToAsync(stream).GetAwaiter().GetResult(); -#endif - } - - public override async Task WriteToAsync(Stream stream, CancellationToken cancellationToken = default) - { -#if NET5_0_OR_GREATER - await _multipartContent.CopyToAsync(stream, cancellationToken).ConfigureAwait(false); -#else - await _multipartContent.CopyToAsync(stream).ConfigureAwait(false); -#endif - } - - public override void Dispose() - { - _multipartContent.Dispose(); - } - - private sealed class HttpContentAdapter : HttpContent - { - private readonly BinaryContent _content; - - public HttpContentAdapter(BinaryContent content) - { - Argument.AssertNotNull(content, nameof(content)); - - _content = content; - } - - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) - => await _content.WriteToAsync(stream).ConfigureAwait(false); - - protected override bool TryComputeLength(out long length) - => _content.TryComputeLength(out length); - -#if NET6_0_OR_GREATER - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) - => await _content!.WriteToAsync(stream, cancellationToken).ConfigureAwait(false); - - protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) - => _content.WriteTo(stream, cancellationToken); -#endif - } -} -``` -
## Usage Examples @@ -445,7 +159,7 @@ public virtual async Task UploadDogAsync(Dog body, CancellationTok { Argument.AssertNotNull(body, nameof(body)); - using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + using BinaryContent content = body.ToMultipartContent(); return await UploadDogAsync(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); } @@ -453,7 +167,7 @@ public virtual ClientResult UploadDog(Dog body, CancellationToken cancellationTo { Argument.AssertNotNull(body, nameof(body)); - using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + using BinaryContent content = body.ToMultipartContent(); return UploadDog(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); } ``` @@ -464,7 +178,7 @@ public virtual ClientResult UploadDog(Dog body, CancellationToken cancellationTo Dog.cs ```c# -public partial class Dog : IDisposable +public partial class Dog { public Dog(string id, string profileImagePath) { @@ -504,11 +218,6 @@ public partial class Dog : IDisposable public string Id { get; } public FileBinaryContent ProfileImage { get; } - - public void Dispose() - { - ProfileImage?.Dispose(); - } } ``` @@ -525,7 +234,6 @@ public partial class Dog : IPersistableModel } private string _boundary; - private string Boundary => _boundary ??= MultiPartFormDataBinaryContent.CreateBoundary(); BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) @@ -533,8 +241,6 @@ public partial class Dog : IPersistableModel string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; switch (format) { - case "MPFD-ContentType": - return SerializeMultipartContentType(); case "MPFD": return SerializeMultipart(); default: @@ -563,29 +269,23 @@ public partial class Dog : IPersistableModel { return null; } - return dog.ToMultipartContent(); - } - internal MultiPartFormDataBinaryContent ToMultipartContent() - { - MultiPartFormDataBinaryContent content = new(Boundary); - - content.Add("id", Id); - content.Add("profileImage", ProfileImage); - - return content; + return BinaryContent.Create(dog, ModelSerializationExtensions.WireOptions); } - private BinaryData SerializeMultipartContentType() + internal BinaryContent ToMultipartContent() { - using MultiPartFormDataBinaryContent content = new(Boundary); - return BinaryData.FromString(content.ContentType); + List parts = []; + parts.Add(BinaryContent.CreateMultipartFormDataPart("id", Id)); + parts.Add(BinaryContent.CreateMultipartFormDataPart("profileImage", ProfileImage)); + + return BinaryContent.CreateMultipartFormDataContent(parts); } private BinaryData SerializeMultipart() { using MemoryStream stream = new MemoryStream(); - using MultiPartFormDataBinaryContent content = ToMultipartContent(); + using BinaryContent content = ToMultipartContent(); content.WriteTo(stream); if (stream.CanSeek) @@ -606,7 +306,7 @@ public partial class Dog : IPersistableModel ```csharp PetStoreClient client = new PetStoreClient(); -using Dog dog = new Dog("123", "C:\\myDog.jpg"); +Dog dog = new Dog("123", "C:\\myDog.jpg"); ClientResult response = await client.UploadDogAsync(dog); ``` @@ -616,12 +316,14 @@ ClientResult response = await client.UploadDogAsync(dog); Protocol Example Usage ```csharp -PetStoreClient client = new PetStoreClient(); -using Dog dog = new Dog("123", "C:\\myDog.jpg"); -// get the multipart content type, which includes the boundary -string contentType = ModelReaderWriter.Write(dog, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); + PetStoreClient client = new PetStoreClient(); + + List parts = []; + parts.Add(BinaryContent.CreateMultipartFormDataPart("id", "123")); + parts.Add(BinaryContent.CreateMultipartFormDataPart("profileImage", new FileBinaryContent("C:\\myDog.jpg"))); -ClientResult response = await client.UploadDogAsync(dog, contentType); + using BinaryContent content = BinaryContent.CreateMultipartFormDataContent(parts); + ClientResult response = await client.UploadDogAsync(content, content.ContentType); ``` @@ -681,7 +383,7 @@ public virtual ClientResult UploadCat(Cat body, CancellationToken cancellationTo { Argument.AssertNotNull(body, nameof(body)); - using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + using BinaryContent content = body.ToMultipartContent(); return UploadCat(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); } @@ -689,7 +391,7 @@ public virtual async Task UploadCatAsync(Cat body, CancellationTok { Argument.AssertNotNull(body, nameof(body)); - using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + using BinaryContent content = body.ToMultipartContent(); return await UploadCatAsync(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); } ``` @@ -700,7 +402,7 @@ public virtual async Task UploadCatAsync(Cat body, CancellationTok Cat.cs ```c# -public partial class Cat : IDisposable +public partial class Cat { public Cat(string id, string filename, string contentType, string profileImagePath) { @@ -746,11 +448,6 @@ public partial class Cat : IDisposable public string Id { get; } public FileBinaryContent ProfileImage { get; } - - public void Dispose() - { - ProfileImage?.Dispose(); - } } ``` @@ -766,17 +463,12 @@ public partial class Cat : IPersistableModel { } - private string _boundary; - private string Boundary => _boundary ??= MultiPartFormDataBinaryContent.CreateBoundary(); - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) { string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; switch (format) { - case "MPFD-ContentType": - return SerializeMultipartContentType(); case "MPFD": return SerializeMultipart(); default: @@ -804,29 +496,23 @@ public partial class Cat : IPersistableModel { return null; } - return cat.ToMultipartContent(); - } - internal MultiPartFormDataBinaryContent ToMultipartContent() - { - MultiPartFormDataBinaryContent content = new(Boundary); - - content.Add("id", Id); - content.Add("profileImage", ProfileImage); - - return content; + return BinaryContent.Create(cat, ModelSerializationExtensions.WireOptions); } - private BinaryData SerializeMultipartContentType() + internal BinaryContent ToMultipartContent() { - using MultiPartFormDataBinaryContent content = new(Boundary); - return BinaryData.FromString(content.ContentType); + List parts = []; + parts.Add(BinaryContent.CreateMultipartFormDataPart("id", Id)); + parts.Add(BinaryContent.CreateMultipartFormDataPart("profileImage", ProfileImage)); + + return BinaryContent.CreateMultipartFormDataContent(parts); } private BinaryData SerializeMultipart() { using MemoryStream stream = new MemoryStream(); - using MultiPartFormDataBinaryContent content = ToMultipartContent(); + using BinaryContent content = ToMultipartContent(); content.WriteTo(stream); if (stream.CanSeek) @@ -835,7 +521,6 @@ public partial class Cat : IPersistableModel } return BinaryData.FromStream(stream); } - } ``` @@ -847,7 +532,7 @@ public partial class Cat : IPersistableModel ```csharp PetStoreClient client = new PetStoreClient(); - using Cat cat = new Cat("123", "myCat.jpg", "image/jpeg", "C:\\myCat.jpg"); + Cat cat = new Cat("123", "myCat.jpg", "image/jpeg", "C:\\myCat.jpg"); ClientResult response = await client.UploadCatAsync(cat); ``` @@ -859,10 +544,18 @@ public partial class Cat : IPersistableModel ```csharp PetStoreClient client = new PetStoreClient(); - using Cat cat = new("123", "myCat.jpg", "image/jpeg", "C:\\myCat.jpg"); - // get the multipart content type, which includes the boundary - string contentType = ModelReaderWriter.Write(cat, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); - ClientResult response = await client.UploadCatAsync(cat, contentType); + List parts = []; + parts.Add(BinaryContent.CreateMultipartFormDataPart("id", "123")); + parts.Add(BinaryContent.CreateMultipartFormDataPart( + "profileImage", + new FileBinaryContent("C:\\myCat.jpg") + { + ContentType = "image/jpeg", + Filename = "myCat.jpg" + })); + + using BinaryContent content = BinaryContent.CreateMultipartFormDataContent(parts); + ClientResult response = await client.UploadCatAsync(content, content.ContentType); ``` @@ -923,7 +616,7 @@ public virtual ClientResult UploadPetDetails(PetDetails body, CancellationToken { Argument.AssertNotNull(body, nameof(body)); - using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + using BinaryContent content = body.ToMultipartContent(); return UploadPetDetails(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); } @@ -931,7 +624,7 @@ public virtual async Task UploadPetDetailsAsync(PetDetails body, C { Argument.AssertNotNull(body, nameof(body)); - using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + using BinaryContent content = body.ToMultipartContent(); return await UploadPetDetailsAsync(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); } ``` @@ -961,7 +654,7 @@ public partial class Address PetDetails.cs ```c# -public partial class PetDetails : IDisposable +public partial class PetDetails { public PetDetails(string id, string ownerName, string petName, Address address, string profileImagePath) { @@ -1028,11 +721,6 @@ public partial class PetDetails : IDisposable public string PetName { get; } public Address Address { get; } public FileBinaryContent ProfileImage { get; } - - public void Dispose() - { - ProfileImage?.Dispose(); - } } ``` @@ -1048,18 +736,12 @@ public partial class PetDetails : IPersistableModel { } - private string _boundary; - - private string Boundary => _boundary ??= MultiPartFormDataBinaryContent.CreateBoundary(); - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) { string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; switch (format) { - case "MPFD-ContentType": - return SerializeMultipartContentType(); case "MPFD": return SerializeMultipart(); default: @@ -1088,31 +770,26 @@ public partial class PetDetails : IPersistableModel { return null; } - return petDetails.ToMultipartContent(); - } - internal MultiPartFormDataBinaryContent ToMultipartContent() - { - MultiPartFormDataBinaryContent content = new(Boundary); - content.Add("id", Id); - content.Add("ownerName", OwnerName); - content.Add("petName", PetName); - content.Add("address", Address); - content.Add("profileImage", ProfileImage); - - return content; + return BinaryContent.Create(petDetails, ModelSerializationExtensions.WireOptions); } - private BinaryData SerializeMultipartContentType() + internal BinaryContent ToMultipartContent() { - using MultiPartFormDataBinaryContent content = new(Boundary); - return BinaryData.FromString(content.ContentType); + List parts = []; + parts.Add(BinaryContent.CreateMultipartFormDataPart("id", Id)); + parts.Add(BinaryContent.CreateMultipartFormDataPart("ownerName", OwnerName)); + parts.Add(BinaryContent.CreateMultipartFormDataPart("petName", PetName)); + parts.Add(BinaryContent.CreateMultipartFormDataPart("address", Address)); + parts.Add(BinaryContent.CreateMultipartFormDataPart("profileImage", ProfileImage)); + + return BinaryContent.CreateMultipartFormDataContent(parts); } private BinaryData SerializeMultipart() { using MemoryStream stream = new MemoryStream(); - using MultiPartFormDataBinaryContent content = ToMultipartContent(); + using BinaryContent content = ToMultipartContent(); content.WriteTo(stream); if (stream.CanSeek) @@ -1132,7 +809,7 @@ public partial class PetDetails : IPersistableModel ```csharp PetStoreClient client = new PetStoreClient(); -using PetDetails petDetails = new PetDetails( +PetDetails petDetails = new PetDetails( "123", "John Doe", "Winston", @@ -1148,97 +825,17 @@ var response = await client.UploadPetDetailsAsync(petDetails); Protocol Example Usage ```csharp -PetStoreClient client = new PetStoreClient(); + PetStoreClient client = new PetStoreClient(); -using PetDetails petDetails = new PetDetails( - "123", - "John Doe", - "Winston", - new Address("123 Main St."), - "C:\\winston.jpg"); + List parts = []; + parts.Add(BinaryContent.CreateMultipartFormDataPart("id", "123")); + parts.Add(BinaryContent.CreateMultipartFormDataPart("ownerName", "John Doe")); + parts.Add(BinaryContent.CreateMultipartFormDataPart("petName", "Winston")); + parts.Add(BinaryContent.CreateMultipartFormDataPart("address", new Address("123 Main St."))); + parts.Add(BinaryContent.CreateMultipartFormDataPart("profileImage", new FileBinaryContent("C:\\winston.jpg"))); -// get the multipart content type, which includes the boundary -string contentType = ModelReaderWriter.Write(petDetails, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); -ClientResult response = await client.UploadCatAsync(petDetails, contentType); + using BinaryContent content = BinaryContent.CreateMultipartFormDataContent(parts); + var response = await client.UploadPetDetailsAsync(content, content.ContentType); ``` - -## Considerations - -### Limitations in Advanced Scenarios - -The introduction of the file part type described in [_File Part Type_](#file-part-type) has some limitations when used in more advanced scenarios -that should be considered. - -Consider the following scenario: - -```tsp -// filename and contentType are required -model FileRequiredMetaData extends File { - filename: string; - contentType: string; -} - -model Snake { - name: HttpPart; - pictures: HttpPart[]; -} - -@post -@route("/snakes") -op uploadSnake( - @header contentType: "multipart/form-data", - @multipartBody body: Snake, -): NoContentResponse; -``` - -The pictures property is a collection of file parts where each file has required metadata (filename + content type are required). In the generated code, this creates a mismatch between the TypeSpec definition (where metadata is required) and the FileBinaryContent class (where Filename and ContentType properties are optional). - -Specifically: - -1. The generated Snake model would have constructors that accept collections of `Stream`, `string` (file paths), or `BinaryData` for the pictures property. -2. Each item would be converted to a `FileBinaryContent` object with default (null) metadata properties. -3. The TypeSpec contract requires `filename` and `contentType` for each file, but the C# API doesn't enforce this requirement. -4. The service could reject the request, but users would only discover this at runtime when the upload fails. - -For example, the generated constructor might look like: - -```csharp -public Snake(string name, IEnumerable pictures) -{ - Name = name; - Pictures = pictures.Select(pic => new FileBinaryContent(pic)).ToArray(); - // FileBinaryContent objects created without required metadata. -} -``` - -### Protocol Method Usage with Public MPFD Type - -Making the `MultiPartFormDataBinaryContent` type public in SCM would significantly enhance flexibility for advanced scenarios while maintaining the simplicity of the convenience layer for common use cases. - -#### Example: Current Protocol Usage vs. Public MPFD Type - -Consider the protocol method usage for the [dog example](#operation-that-contains-a-payload-with-a-file-part-and-a-primitive-type-part) outlined earlier: - -```csharp -PetStoreClient client = new PetStoreClient(); -using Dog dog = new Dog("123", "C:\\myDog.jpg"); -// get the multipart content type, which includes the boundary -string contentType = ModelReaderWriter.Write(dog, new ModelReaderWriterOptions("MPFD-ContentType")).ToString(); -// dog is implicitly converted to BinaryContent -ClientResult response = await client.UploadDogAsync(dog, contentType); -``` - -With a public `MultiPartFormDataBinaryContent` type, users could directly construct and customize the request: - -```csharp -PetStoreClient client = new PetStoreClient(); - -await using FileStream profileImage = File.OpenRead("C:\\myDog.jpg"); -using MultiPartFormDataBinaryContent content = new(); -content.Add("id", "123"); -content.Add("profileImage", profileImage); - -ClientResult response = await client.UploadDogAsync(content, content.ContentType); -``` From 22f9156ddafcd15fd6975eaaa8374c53931f8945 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Thu, 5 Jun 2025 10:56:07 -0500 Subject: [PATCH 11/18] add azure.core apis --- .../generator/docs/mpfd-design.md | 90 +++++++++++-------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 666c9b81bda..ae2fe22459e 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -3,7 +3,7 @@ ## Table of Contents 1. [Motivation](#motivation) -2. [System ClientModel Updates](#system-clientmodel-updates) +2. [System ClientModel & Azure.Core Updates](#system-clientmodel-and-azure.core-updates) 3. [Usage Examples](#usage-examples) ## Motivation @@ -49,17 +49,18 @@ string requestContentType = multipartContent.Headers.ContentType!.ToString(); ClientResult response = await client.UploadDogAsync(content, requestContentType); ``` -This document provides a proposal for a generated convenience layer to remove some of this burden from users. +This document provides a proposal for a generated convenience layer to remove some of this burden from users focusing on unbranded clients, +but with the intention to provide support for both unbranded and azure branded libraries. ## Goals -- Provide discoverable convenience methods that simplify creating and sending multipart/form-data requests. +- Provide discoverable convenience methods & APIs that simplify creating and sending multipart/form-data requests. -## System ClientModel Updates +## System ClientModel and Azure.Core Updates -### BinaryContent APIs +The BinaryContent & RequestContent classes are being extended with multipart/form-data capabilities to provide a streamlined API for building requests for clients that need to send multipart payloads. These additions eliminate the need for manual boundary management and complex multipart construction while maintaining full control over content types and part metadata. -The BinaryContent class is being extended with multipart/form-data capabilities to provide a streamlined API for building requests for clients that need to send multipart payloads. These additions eliminate the need for manual boundary management and complex multipart construction while maintaining full control over content types and part metadata. +### System.ClientModel ```c# public abstract partial class BinaryContent : System.IDisposable @@ -78,16 +79,41 @@ public abstract partial class BinaryContent : System.IDisposable public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, double content) { throw null; } public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, int content) { throw null; } public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, long content) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, System.IO.Stream stream) { throw null; } public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, float content) { throw null; } public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, string content) { throw null; } public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, T model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } } ``` +### Azure.Core + +```c# +public abstract partial class RequestContent : System.IDisposable +{ + // Add ContentType property + public virtual string? ContentType { get { throw null; } set { } } + + // Add APIs for creating MPFD parts and payload. + public static Azure.Core.RequestContent CreateMultipartFormDataContent(System.Collections.Generic.IEnumerable parts) { throw null; } + public static Azure.Core.RequestContent CreateMultipartFormDataContent(string boundary, System.Collections.Generic.IEnumerable parts) { throw null; } + public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, Azure.Core.FileRequestContent content) { throw null; } + public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, System.BinaryData content) { throw null; } + public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, bool content) { throw null; } + public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, byte[] content) { throw null; } + public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, decimal content) { throw null; } + public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, double content) { throw null; } + public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, int content) { throw null; } + public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, long content) { throw null; } + public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, float content) { throw null; } + public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, string content) { throw null; } +} +``` + ### File Part Type -To support generating a convenience layer for file parts described in a TypeSpec request, new convenience model type can be added to the System.ClientModel library, to be consumed by generated clients. This new type can serve as the common type for file parts within a request. +To support generating a convenience layer for file parts described in a TypeSpec request, new convenience model type can be added to the System.ClientModel & Azure.Core libraries, to be consumed by generated clients. This new type can serve as the common type for file parts within a request. + +#### System.ClientModel ```csharp public sealed partial class FileBinaryContent : System.ClientModel.BinaryContent @@ -104,6 +130,24 @@ public sealed partial class FileBinaryContent : System.ClientModel.BinaryContent } ``` +#### Azure.Core + + +```csharp +public sealed partial class FileRequestContent : Azure.Core.RequestContent +{ + public FileRequestContent(System.BinaryData data) { } + public FileRequestContent(System.IO.Stream stream) { } + public FileRequestContent(string path) { } + public override string? ContentType { get { throw null; } set { } } + public string? Filename { get { throw null; } set { } } + public override void Dispose() { } + public override bool TryComputeLength(out long length) { throw null; } + public override void WriteTo(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } + public override System.Threading.Tasks.Task WriteToAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } +} +``` + ## Usage Examples @@ -263,16 +307,6 @@ public partial class Dog : IPersistableModel string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "MPFD"; - public static implicit operator BinaryContent(Dog dog) - { - if (dog == null) - { - return null; - } - - return BinaryContent.Create(dog, ModelSerializationExtensions.WireOptions); - } - internal BinaryContent ToMultipartContent() { List parts = []; @@ -490,16 +524,6 @@ public partial class Cat : IPersistableModel string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "MPFD"; - public static implicit operator BinaryContent(Cat cat) - { - if (cat == null) - { - return null; - } - - return BinaryContent.Create(cat, ModelSerializationExtensions.WireOptions); - } - internal BinaryContent ToMultipartContent() { List parts = []; @@ -764,16 +788,6 @@ public partial class PetDetails : IPersistableModel string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "MPFD"; - public static implicit operator BinaryContent(PetDetails petDetails) - { - if (petDetails == null) - { - return null; - } - - return BinaryContent.Create(petDetails, ModelSerializationExtensions.WireOptions); - } - internal BinaryContent ToMultipartContent() { List parts = []; From 92633e05faead4201c7b29fcbcff301b1ef6ca35 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Mon, 9 Jun 2025 10:20:17 -0500 Subject: [PATCH 12/18] fix anchors --- packages/http-client-csharp/generator/docs/mpfd-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index ae2fe22459e..10f2edacea3 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -3,7 +3,7 @@ ## Table of Contents 1. [Motivation](#motivation) -2. [System ClientModel & Azure.Core Updates](#system-clientmodel-and-azure.core-updates) +2. [System ClientModel & Azure.Core Updates](#system-clientmodel-and-azurecore-updates) 3. [Usage Examples](#usage-examples) ## Motivation From 9ac75d76f579f6373b3c23c056f962fce852d0d9 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Thu, 17 Jul 2025 11:21:14 -0500 Subject: [PATCH 13/18] update api based on feedback --- .../generator/docs/mpfd-design.md | 333 +++++------------- 1 file changed, 81 insertions(+), 252 deletions(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 10f2edacea3..3ea52c4cd3c 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -3,7 +3,7 @@ ## Table of Contents 1. [Motivation](#motivation) -2. [System ClientModel & Azure.Core Updates](#system-clientmodel-and-azurecore-updates) +2. [System ClientModel & Azure.Core Updates](#systemclientmodel-updates) 3. [Usage Examples](#usage-examples) ## Motivation @@ -56,73 +56,28 @@ but with the intention to provide support for both unbranded and azure branded l - Provide discoverable convenience methods & APIs that simplify creating and sending multipart/form-data requests. -## System ClientModel and Azure.Core Updates +## System.ClientModel Updates -The BinaryContent & RequestContent classes are being extended with multipart/form-data capabilities to provide a streamlined API for building requests for clients that need to send multipart payloads. These additions eliminate the need for manual boundary management and complex multipart construction while maintaining full control over content types and part metadata. +A new type can be added to facilitate building multipart/form-data requests and provide a streamlined API for clients that need to send multipart payloads. This type eliminates the need for manual boundary management and complex multipart construction while maintaining full control over content types and part metadata. ### System.ClientModel ```c# -public abstract partial class BinaryContent : System.IDisposable +public partial class MultiPartFormDataBinaryContent : System.ClientModel.BinaryContent { - // Add ContentType property - public virtual string? ContentType { get { throw null; } set { } } - - // Add APIs for creating MPFD parts and payload. - public static System.ClientModel.BinaryContent CreateMultipartFormDataContent(System.Collections.Generic.IEnumerable parts) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataContent(string boundary, System.Collections.Generic.IEnumerable parts) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, System.BinaryData content) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, bool content) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, byte[] content) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, System.ClientModel.FileBinaryContent content) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, decimal content) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, double content) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, int content) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, long content) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, float content) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, string content) { throw null; } - public static System.ClientModel.BinaryContent CreateMultipartFormDataPart(string name, T model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) where T : System.ClientModel.Primitives.IPersistableModel { throw null; } -} -``` - -### Azure.Core - -```c# -public abstract partial class RequestContent : System.IDisposable -{ - // Add ContentType property - public virtual string? ContentType { get { throw null; } set { } } - - // Add APIs for creating MPFD parts and payload. - public static Azure.Core.RequestContent CreateMultipartFormDataContent(System.Collections.Generic.IEnumerable parts) { throw null; } - public static Azure.Core.RequestContent CreateMultipartFormDataContent(string boundary, System.Collections.Generic.IEnumerable parts) { throw null; } - public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, Azure.Core.FileRequestContent content) { throw null; } - public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, System.BinaryData content) { throw null; } - public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, bool content) { throw null; } - public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, byte[] content) { throw null; } - public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, decimal content) { throw null; } - public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, double content) { throw null; } - public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, int content) { throw null; } - public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, long content) { throw null; } - public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, float content) { throw null; } - public static Azure.Core.RequestContent CreateMultipartFormDataPart(string name, string content) { throw null; } -} -``` - -### File Part Type - -To support generating a convenience layer for file parts described in a TypeSpec request, new convenience model type can be added to the System.ClientModel & Azure.Core libraries, to be consumed by generated clients. This new type can serve as the common type for file parts within a request. - -#### System.ClientModel - -```csharp -public sealed partial class FileBinaryContent : System.ClientModel.BinaryContent -{ - public FileBinaryContent(System.BinaryData data) { } - public FileBinaryContent(System.IO.Stream stream) { } - public FileBinaryContent(string path) { } - public override string? ContentType { get { throw null; } set { } } - public string? Filename { get { throw null; } set { } } + public MultiPartFormDataBinaryContent() { } + public MultiPartFormDataBinaryContent(string boundary) { } + public void Add(string name, System.BinaryData content, string? mediaType = null) { } + public void Add(string name, bool content, string? mediaType = null) { } + public void Add(string name, byte[] content, string? mediaType = null) { } + public void Add(string name, System.ClientModel.FileBinaryContent fileContent) { } + public void Add(string name, decimal content, string? mediaType = null) { } + public void Add(string name, double content, string? mediaType = null) { } + public void Add(string name, int content, string? mediaType = null) { } + public void Add(string name, long content, string? mediaType = null) { } + public void Add(string name, float content, string? mediaType = null) { } + public void Add(string name, string content, string? mediaType = null) { } + public void Add(string name, System.ClientModel.Primitives.IPersistableModel model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null, System.ClientModel.Primitives.ModelReaderWriterContext? context = null, string? mediaType = null) { } public override void Dispose() { } public override bool TryComputeLength(out long length) { throw null; } public override void WriteTo(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } @@ -130,16 +85,18 @@ public sealed partial class FileBinaryContent : System.ClientModel.BinaryContent } ``` -#### Azure.Core +### File Part Type +To support generating a convenience layer for file parts in multipart/form-data requests, a new type can be added to the System.ClientModel library for use by generated clients. This type serves as the common representation for file parts within multipart requests. + +#### System.ClientModel ```csharp -public sealed partial class FileRequestContent : Azure.Core.RequestContent +public partial class FileBinaryContent : System.ClientModel.BinaryContent { - public FileRequestContent(System.BinaryData data) { } - public FileRequestContent(System.IO.Stream stream) { } - public FileRequestContent(string path) { } - public override string? ContentType { get { throw null; } set { } } + public FileBinaryContent(System.BinaryData data, string? mediaType = null) { } + public FileBinaryContent(System.IO.Stream stream, string? mediaType = null) { } + public FileBinaryContent(string path, string? mediaType = null) { } public string? Filename { get { throw null; } set { } } public override void Dispose() { } public override bool TryComputeLength(out long length) { throw null; } @@ -203,16 +160,16 @@ public virtual async Task UploadDogAsync(Dog body, CancellationTok { Argument.AssertNotNull(body, nameof(body)); - using BinaryContent content = body.ToMultipartContent(); - return await UploadDogAsync(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); + using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + return await UploadDogAsync(content, content.MediaType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); } public virtual ClientResult UploadDog(Dog body, CancellationToken cancellationToken = default) { Argument.AssertNotNull(body, nameof(body)); - using BinaryContent content = body.ToMultipartContent(); - return UploadDog(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); + using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + return UploadDog(content, content.MediaType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); } ``` @@ -271,62 +228,23 @@ public partial class Dog Dog.Serialization.cs ```c# -public partial class Dog : IPersistableModel +public partial class Dog { internal Dog() { } - private string _boundary; - - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); - protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) - { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; - switch (format) - { - case "MPFD": - return SerializeMultipart(); - default: - throw new FormatException($"The model {nameof(Dog)} does not support writing '{options.Format}' format."); - } - } - - Dog IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options); - - - protected virtual Dog PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) - { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; - switch (format) - { - default: - throw new FormatException($"The model {nameof(Dog)} does not support reading '{options.Format}' format."); - } - } - - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "MPFD"; - - internal BinaryContent ToMultipartContent() - { - List parts = []; - parts.Add(BinaryContent.CreateMultipartFormDataPart("id", Id)); - parts.Add(BinaryContent.CreateMultipartFormDataPart("profileImage", ProfileImage)); - - return BinaryContent.CreateMultipartFormDataContent(parts); - } - - private BinaryData SerializeMultipart() + public partial class Dog { - using MemoryStream stream = new MemoryStream(); - using BinaryContent content = ToMultipartContent(); - content.WriteTo(stream); - if (stream.CanSeek) + internal MultiPartFormDataBinaryContent ToMultipartContent() { - stream.Seek(0, SeekOrigin.Begin); + MultiPartFormDataBinaryContent content = new(); + content.Add("id", Id); + content.Add("profileImage", ProfileImage); + + return content; } - return BinaryData.FromStream(stream); } } @@ -352,12 +270,11 @@ ClientResult response = await client.UploadDogAsync(dog); ```csharp PetStoreClient client = new PetStoreClient(); - List parts = []; - parts.Add(BinaryContent.CreateMultipartFormDataPart("id", "123")); - parts.Add(BinaryContent.CreateMultipartFormDataPart("profileImage", new FileBinaryContent("C:\\myDog.jpg"))); + using MultiPartFormDataBinaryContent content = new(); + content.Add("id", "123"); + content.Add("profileImage", new FileBinaryContent("C:\\myDog.jpg")); - using BinaryContent content = BinaryContent.CreateMultipartFormDataContent(parts); - ClientResult response = await client.UploadDogAsync(content, content.ContentType); + ClientResult response = await client.UploadDogAsync(content, content.MediaType); ``` @@ -417,16 +334,16 @@ public virtual ClientResult UploadCat(Cat body, CancellationToken cancellationTo { Argument.AssertNotNull(body, nameof(body)); - using BinaryContent content = body.ToMultipartContent(); - return UploadCat(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); + using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + return UploadCat(content, content.MediaType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); } public virtual async Task UploadCatAsync(Cat body, CancellationToken cancellationToken = default) { Argument.AssertNotNull(body, nameof(body)); - using BinaryContent content = body.ToMultipartContent(); - return await UploadCatAsync(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); + using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + return await UploadCatAsync(content, content.MediaType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); } ``` @@ -445,9 +362,8 @@ public partial class Cat Argument.AssertNotNullOrEmpty(contentType, nameof(contentType)); Argument.AssertNotNullOrEmpty(profileImagePath, nameof(profileImagePath)); - ProfileImage = new(profileImagePath) + ProfileImage = new(profileImagePath, contentType) { - ContentType = contentType, Filename = filename, }; @@ -459,9 +375,8 @@ public partial class Cat Argument.AssertNotNullOrEmpty(contentType, nameof(contentType)); Argument.AssertNotNull(profileImage, nameof(profileImage)); - ProfileImage = new(profileImage) + ProfileImage = new(profileImage, contentType) { - ContentType = contentType, Filename = filename, }; } @@ -473,9 +388,8 @@ public partial class Cat Argument.AssertNotNullOrEmpty(contentType, nameof(contentType)); Argument.AssertNotNull(profileImage, nameof(profileImage)); - ProfileImage = new(profileImage) + ProfileImage = new(profileImage, contentType) { - ContentType = contentType, Filename = filename, }; } @@ -491,59 +405,19 @@ public partial class Cat Cat.Serialization.cs ```c# -public partial class Cat : IPersistableModel +public partial class Cat { internal Cat() { } - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); - protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) - { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; - switch (format) - { - case "MPFD": - return SerializeMultipart(); - default: - throw new FormatException($"The model {nameof(Cat)} does not support writing '{options.Format}' format."); - } - } - - Cat IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options); - - protected virtual Cat PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) - { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; - switch (format) - { - default: - throw new FormatException($"The model {nameof(Cat)} does not support reading '{options.Format}' format."); - } - } - - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "MPFD"; - - internal BinaryContent ToMultipartContent() - { - List parts = []; - parts.Add(BinaryContent.CreateMultipartFormDataPart("id", Id)); - parts.Add(BinaryContent.CreateMultipartFormDataPart("profileImage", ProfileImage)); - - return BinaryContent.CreateMultipartFormDataContent(parts); - } - - private BinaryData SerializeMultipart() + internal MultiPartFormDataBinaryContent ToMultipartContent() { - using MemoryStream stream = new MemoryStream(); - using BinaryContent content = ToMultipartContent(); + MultiPartFormDataBinaryContent content = new(); + content.Add("id", Id); + content.Add("profileImage", ProfileImage); - content.WriteTo(stream); - if (stream.CanSeek) - { - stream.Seek(0, SeekOrigin.Begin); - } - return BinaryData.FromStream(stream); + return content; } } ``` @@ -568,18 +442,15 @@ public partial class Cat : IPersistableModel ```csharp PetStoreClient client = new PetStoreClient(); - List parts = []; - parts.Add(BinaryContent.CreateMultipartFormDataPart("id", "123")); - parts.Add(BinaryContent.CreateMultipartFormDataPart( - "profileImage", - new FileBinaryContent("C:\\myCat.jpg") + using MultiPartFormDataBinaryContent content = new(); + content.Add("id", "123"); + content.Add("profileImage", + new FileBinaryContent("C:\\myCat.jpg", "image/jpeg") { - ContentType = "image/jpeg", Filename = "myCat.jpg" - })); + }); - using BinaryContent content = BinaryContent.CreateMultipartFormDataContent(parts); - ClientResult response = await client.UploadCatAsync(content, content.ContentType); + ClientResult response = await client.UploadCatAsync(content, content.MediaType); ``` @@ -640,16 +511,16 @@ public virtual ClientResult UploadPetDetails(PetDetails body, CancellationToken { Argument.AssertNotNull(body, nameof(body)); - using BinaryContent content = body.ToMultipartContent(); - return UploadPetDetails(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); + using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + return UploadPetDetails(content, content.MediaType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); } public virtual async Task UploadPetDetailsAsync(PetDetails body, CancellationToken cancellationToken = default) { Argument.AssertNotNull(body, nameof(body)); - using BinaryContent content = body.ToMultipartContent(); - return await UploadPetDetailsAsync(content, content.ContentType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); + using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + return await UploadPetDetailsAsync(content, content.MediaType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); } ``` @@ -754,63 +625,22 @@ public partial class PetDetails PetDetails.Serialization.cs ```c# -public partial class PetDetails : IPersistableModel +public partial class PetDetails { internal PetDetails() { } - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) => PersistableModelWriteCore(options); - protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) + internal MultiPartFormDataBinaryContent ToMultipartContent() { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; - switch (format) - { - case "MPFD": - return SerializeMultipart(); - default: - throw new FormatException($"The model {nameof(PetDetails)} does not support writing '{options.Format}' format."); - } - } - - - PetDetails IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) => PersistableModelCreateCore(data, options); - - protected virtual PetDetails PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) - { - string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; - switch (format) - { - default: - throw new FormatException($"The model {nameof(PetDetails)} does not support reading '{options.Format}' format."); - } - } - - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "MPFD"; - - internal BinaryContent ToMultipartContent() - { - List parts = []; - parts.Add(BinaryContent.CreateMultipartFormDataPart("id", Id)); - parts.Add(BinaryContent.CreateMultipartFormDataPart("ownerName", OwnerName)); - parts.Add(BinaryContent.CreateMultipartFormDataPart("petName", PetName)); - parts.Add(BinaryContent.CreateMultipartFormDataPart("address", Address)); - parts.Add(BinaryContent.CreateMultipartFormDataPart("profileImage", ProfileImage)); - - return BinaryContent.CreateMultipartFormDataContent(parts); - } - - private BinaryData SerializeMultipart() - { - using MemoryStream stream = new MemoryStream(); - using BinaryContent content = ToMultipartContent(); - - content.WriteTo(stream); - if (stream.CanSeek) - { - stream.Seek(0, SeekOrigin.Begin); - } - return BinaryData.FromStream(stream); + MultiPartFormDataBinaryContent content = new MultiPartFormDataBinaryContent(); + content.Add("id", Id); + content.Add("ownerName", OwnerName); + content.Add("petName", PetName); + content.Add("address", Address, ModelSerializationExtensions.WireOptions, new PetStoreContext()); + content.Add("profileImage", ProfileImage); + + return content; } } ``` @@ -841,15 +671,14 @@ var response = await client.UploadPetDetailsAsync(petDetails); ```csharp PetStoreClient client = new PetStoreClient(); - List parts = []; - parts.Add(BinaryContent.CreateMultipartFormDataPart("id", "123")); - parts.Add(BinaryContent.CreateMultipartFormDataPart("ownerName", "John Doe")); - parts.Add(BinaryContent.CreateMultipartFormDataPart("petName", "Winston")); - parts.Add(BinaryContent.CreateMultipartFormDataPart("address", new Address("123 Main St."))); - parts.Add(BinaryContent.CreateMultipartFormDataPart("profileImage", new FileBinaryContent("C:\\winston.jpg"))); +using MultiPartFormDataBinaryContent content = new(); +content.Add("id", "123"); +content.Add("ownerName", "John Doe"); +content.Add("petName", "Winston"); +content.Add("address", new Address("123 Main St.")); +content.Add("profileImage", new FileBinaryContent("C:\\winston.jpg")); - using BinaryContent content = BinaryContent.CreateMultipartFormDataContent(parts); - var response = await client.UploadPetDetailsAsync(content, content.ContentType); +var response = await client.UploadPetDetailsAsync(content, content.MediaType); ``` From 42498dc42f84f321b3833b8f7be97bdb46c7cdc2 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Thu, 17 Jul 2025 11:24:37 -0500 Subject: [PATCH 14/18] formatting --- packages/http-client-csharp/generator/docs/mpfd-design.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 3ea52c4cd3c..3a733d661d1 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -3,7 +3,7 @@ ## Table of Contents 1. [Motivation](#motivation) -2. [System ClientModel & Azure.Core Updates](#systemclientmodel-updates) +2. [System.ClientModel Updates](#systemclientmodel-updates) 3. [Usage Examples](#usage-examples) ## Motivation @@ -60,8 +60,6 @@ but with the intention to provide support for both unbranded and azure branded l A new type can be added to facilitate building multipart/form-data requests and provide a streamlined API for clients that need to send multipart payloads. This type eliminates the need for manual boundary management and complex multipart construction while maintaining full control over content types and part metadata. -### System.ClientModel - ```c# public partial class MultiPartFormDataBinaryContent : System.ClientModel.BinaryContent { @@ -89,8 +87,6 @@ public partial class MultiPartFormDataBinaryContent : System.ClientModel.BinaryC To support generating a convenience layer for file parts in multipart/form-data requests, a new type can be added to the System.ClientModel library for use by generated clients. This type serves as the common representation for file parts within multipart requests. -#### System.ClientModel - ```csharp public partial class FileBinaryContent : System.ClientModel.BinaryContent { From 7927233461f350451e021cae3b069691f3570859 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Tue, 29 Jul 2025 16:42:27 -0500 Subject: [PATCH 15/18] pr feedback --- .../generator/docs/mpfd-design.md | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 3a733d661d1..c193862f058 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -61,20 +61,21 @@ but with the intention to provide support for both unbranded and azure branded l A new type can be added to facilitate building multipart/form-data requests and provide a streamlined API for clients that need to send multipart payloads. This type eliminates the need for manual boundary management and complex multipart construction while maintaining full control over content types and part metadata. ```c# -public partial class MultiPartFormDataBinaryContent : System.ClientModel.BinaryContent +public partial class MultiPartFormContent : System.ClientModel.BinaryContent { - public MultiPartFormDataBinaryContent() { } - public MultiPartFormDataBinaryContent(string boundary) { } - public void Add(string name, System.BinaryData content, string? mediaType = null) { } - public void Add(string name, bool content, string? mediaType = null) { } - public void Add(string name, byte[] content, string? mediaType = null) { } + public MultiPartFormContent() { } + public MultiPartFormContent(string boundary) { } + public void Add(string name, System.BinaryData content) { } + public void Add(string name, bool content, string? mediaType = "text/plain") { } + public void Add(string name, byte[] content, string? mediaType = "application/octet-stream") { } public void Add(string name, System.ClientModel.FileBinaryContent fileContent) { } - public void Add(string name, decimal content, string? mediaType = null) { } - public void Add(string name, double content, string? mediaType = null) { } - public void Add(string name, int content, string? mediaType = null) { } - public void Add(string name, long content, string? mediaType = null) { } - public void Add(string name, float content, string? mediaType = null) { } - public void Add(string name, string content, string? mediaType = null) { } + public void Add(string name, decimal content, string? mediaType = "text/plain") { } + public void Add(string name, double content, string? mediaType = "text/plain") { } + public void Add(string name, int content, string? mediaType = "text/plain") { } + public void Add(string name, long content, string? mediaType = "text/plain") { } + public void Add(string name, float content, string? mediaType = "text/plain") { } + public void Add(string name, string content, string? mediaType = "text/plain") { } + public void Add(string name, System.ClientModel.Primitives.IPersistableModel model) { } public void Add(string name, System.ClientModel.Primitives.IPersistableModel model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null, System.ClientModel.Primitives.ModelReaderWriterContext? context = null, string? mediaType = null) { } public override void Dispose() { } public override bool TryComputeLength(out long length) { throw null; } @@ -156,7 +157,7 @@ public virtual async Task UploadDogAsync(Dog body, CancellationTok { Argument.AssertNotNull(body, nameof(body)); - using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + using MultiPartFormContent content = body.ToMultipartContent(); return await UploadDogAsync(content, content.MediaType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); } @@ -164,7 +165,7 @@ public virtual ClientResult UploadDog(Dog body, CancellationToken cancellationTo { Argument.AssertNotNull(body, nameof(body)); - using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + using MultiPartFormContent content = body.ToMultipartContent(); return UploadDog(content, content.MediaType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); } ``` @@ -233,9 +234,9 @@ public partial class Dog public partial class Dog { - internal MultiPartFormDataBinaryContent ToMultipartContent() + internal MultiPartFormContent ToMultipartContent() { - MultiPartFormDataBinaryContent content = new(); + MultiPartFormContent content = new(); content.Add("id", Id); content.Add("profileImage", ProfileImage); @@ -266,7 +267,7 @@ ClientResult response = await client.UploadDogAsync(dog); ```csharp PetStoreClient client = new PetStoreClient(); - using MultiPartFormDataBinaryContent content = new(); + using MultiPartFormContent content = new(); content.Add("id", "123"); content.Add("profileImage", new FileBinaryContent("C:\\myDog.jpg")); @@ -330,7 +331,7 @@ public virtual ClientResult UploadCat(Cat body, CancellationToken cancellationTo { Argument.AssertNotNull(body, nameof(body)); - using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + using MultiPartFormContent content = body.ToMultipartContent(); return UploadCat(content, content.MediaType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); } @@ -338,7 +339,7 @@ public virtual async Task UploadCatAsync(Cat body, CancellationTok { Argument.AssertNotNull(body, nameof(body)); - using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + using MultiPartFormContent content = body.ToMultipartContent(); return await UploadCatAsync(content, content.MediaType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); } ``` @@ -407,9 +408,9 @@ public partial class Cat { } - internal MultiPartFormDataBinaryContent ToMultipartContent() + internal MultiPartFormContent ToMultipartContent() { - MultiPartFormDataBinaryContent content = new(); + MultiPartFormContent content = new(); content.Add("id", Id); content.Add("profileImage", ProfileImage); @@ -438,7 +439,7 @@ public partial class Cat ```csharp PetStoreClient client = new PetStoreClient(); - using MultiPartFormDataBinaryContent content = new(); + using MultiPartFormContent content = new(); content.Add("id", "123"); content.Add("profileImage", new FileBinaryContent("C:\\myCat.jpg", "image/jpeg") @@ -507,7 +508,7 @@ public virtual ClientResult UploadPetDetails(PetDetails body, CancellationToken { Argument.AssertNotNull(body, nameof(body)); - using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + using MultiPartFormContent content = body.ToMultipartContent(); return UploadPetDetails(content, content.MediaType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null); } @@ -515,7 +516,7 @@ public virtual async Task UploadPetDetailsAsync(PetDetails body, C { Argument.AssertNotNull(body, nameof(body)); - using MultiPartFormDataBinaryContent content = body.ToMultipartContent(); + using MultiPartFormContent content = body.ToMultipartContent(); return await UploadPetDetailsAsync(content, content.MediaType, cancellationToken.CanBeCanceled ? new RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); } ``` @@ -627,9 +628,9 @@ public partial class PetDetails { } - internal MultiPartFormDataBinaryContent ToMultipartContent() + internal MultiPartFormContent ToMultipartContent() { - MultiPartFormDataBinaryContent content = new MultiPartFormDataBinaryContent(); + MultiPartFormContent content = new MultiPartFormContent(); content.Add("id", Id); content.Add("ownerName", OwnerName); content.Add("petName", PetName); @@ -667,7 +668,7 @@ var response = await client.UploadPetDetailsAsync(petDetails); ```csharp PetStoreClient client = new PetStoreClient(); -using MultiPartFormDataBinaryContent content = new(); +using MultiPartFormContent content = new(); content.Add("id", "123"); content.Add("ownerName", "John Doe"); content.Add("petName", "Winston"); From 3783eeb3dfa1c670264074998610b20edfaa55a1 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Wed, 30 Jul 2025 18:00:45 -0500 Subject: [PATCH 16/18] add default value to mediatype --- packages/http-client-csharp/generator/docs/mpfd-design.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index c193862f058..35617349c20 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -91,9 +91,9 @@ To support generating a convenience layer for file parts in multipart/form-data ```csharp public partial class FileBinaryContent : System.ClientModel.BinaryContent { - public FileBinaryContent(System.BinaryData data, string? mediaType = null) { } - public FileBinaryContent(System.IO.Stream stream, string? mediaType = null) { } - public FileBinaryContent(string path, string? mediaType = null) { } + public FileBinaryContent(System.BinaryData data, string? mediaType = "application/octet-stream") { } + public FileBinaryContent(System.IO.Stream stream, string? mediaType = "application/octet-stream") { } + public FileBinaryContent(string path, string? mediaType = "application/octet-stream") { } public string? Filename { get { throw null; } set { } } public override void Dispose() { } public override bool TryComputeLength(out long length) { throw null; } From b4a844f9f1aad2479e1e5475ca8a55440853c3f2 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Wed, 30 Jul 2025 18:07:18 -0500 Subject: [PATCH 17/18] default models mediatype to json --- packages/http-client-csharp/generator/docs/mpfd-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index 35617349c20..b5cf3d7a9ff 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -76,7 +76,7 @@ public partial class MultiPartFormContent : System.ClientModel.BinaryContent public void Add(string name, float content, string? mediaType = "text/plain") { } public void Add(string name, string content, string? mediaType = "text/plain") { } public void Add(string name, System.ClientModel.Primitives.IPersistableModel model) { } - public void Add(string name, System.ClientModel.Primitives.IPersistableModel model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null, System.ClientModel.Primitives.ModelReaderWriterContext? context = null, string? mediaType = null) { } + public void Add(string name, System.ClientModel.Primitives.IPersistableModel model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null, System.ClientModel.Primitives.ModelReaderWriterContext? context = null, string? mediaType = "application/json") { } public override void Dispose() { } public override bool TryComputeLength(out long length) { throw null; } public override void WriteTo(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } From 28b8c5cccbda625094dcaba49b418de66a8282c9 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Tue, 5 May 2026 17:49:31 -0500 Subject: [PATCH 18/18] update doc with last feedback --- .../generator/docs/mpfd-design.md | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/http-client-csharp/generator/docs/mpfd-design.md b/packages/http-client-csharp/generator/docs/mpfd-design.md index b5cf3d7a9ff..603ca54c6bc 100644 --- a/packages/http-client-csharp/generator/docs/mpfd-design.md +++ b/packages/http-client-csharp/generator/docs/mpfd-design.md @@ -3,7 +3,7 @@ ## Table of Contents 1. [Motivation](#motivation) -2. [System.ClientModel Updates](#systemclientmodel-updates) +2. [Core Updates](#core-updates) 3. [Usage Examples](#usage-examples) ## Motivation @@ -56,7 +56,9 @@ but with the intention to provide support for both unbranded and azure branded l - Provide discoverable convenience methods & APIs that simplify creating and sending multipart/form-data requests. -## System.ClientModel Updates +## Core Updates + +### System.ClientModel A new type can be added to facilitate building multipart/form-data requests and provide a streamlined API for clients that need to send multipart payloads. This type eliminates the need for manual boundary management and complex multipart construction while maintaining full control over content types and part metadata. @@ -66,17 +68,16 @@ public partial class MultiPartFormContent : System.ClientModel.BinaryContent public MultiPartFormContent() { } public MultiPartFormContent(string boundary) { } public void Add(string name, System.BinaryData content) { } - public void Add(string name, bool content, string? mediaType = "text/plain") { } public void Add(string name, byte[] content, string? mediaType = "application/octet-stream") { } public void Add(string name, System.ClientModel.FileBinaryContent fileContent) { } - public void Add(string name, decimal content, string? mediaType = "text/plain") { } - public void Add(string name, double content, string? mediaType = "text/plain") { } - public void Add(string name, int content, string? mediaType = "text/plain") { } - public void Add(string name, long content, string? mediaType = "text/plain") { } - public void Add(string name, float content, string? mediaType = "text/plain") { } - public void Add(string name, string content, string? mediaType = "text/plain") { } + public void Add(string name, decimal content, string? mediaType = "application/json") { } + public void Add(string name, double content, string? mediaType = "application/json") { } + public void Add(string name, int content, string? mediaType = "application/json") { } + public void Add(string name, long content, string? mediaType = "application/json") { } + public void Add(string name, float content, string? mediaType = "application/json") { } + public void Add(string name, string content, string? mediaType = "application/json") { } public void Add(string name, System.ClientModel.Primitives.IPersistableModel model) { } - public void Add(string name, System.ClientModel.Primitives.IPersistableModel model, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null, System.ClientModel.Primitives.ModelReaderWriterContext? context = null, string? mediaType = "application/json") { } + public void Add(string name, System.ClientModel.Primitives.IPersistableModel model, System.ClientModel.Primitives.ModelReaderWriterContext? context = null, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null, string? mediaType = "application/json") { } public override void Dispose() { } public override bool TryComputeLength(out long length) { throw null; } public override void WriteTo(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } @@ -102,7 +103,13 @@ public partial class FileBinaryContent : System.ClientModel.BinaryContent } ``` - +### Azure.Core + +A new factory overload on `RequestContent` is required so that Azure-branded clients can easily adapt a `MultiPartFormContent` into the `RequestContent` type when invoking the generated protocol methods directly. + +```csharp +public static Azure.Core.RequestContent Create(System.ClientModel.BinaryContent content) { throw null; } +``` ## Usage Examples @@ -239,7 +246,7 @@ public partial class Dog MultiPartFormContent content = new(); content.Add("id", Id); content.Add("profileImage", ProfileImage); - + return content; } }