From 5e0f976c042cc7ac2539376250c75d1d8adb2843 Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Thu, 6 Mar 2025 11:35:01 -0600 Subject: [PATCH] mpfd apis proposal --- .../src/Convenience/MultiPartFile.cs | 57 ++++ .../src/ModelReaderWriter/CollectionWriter.cs | 2 + .../src/ModelReaderWriter/IStreamModel.cs | 22 ++ .../ModelReaderWriter/JsonCollectionWriter.cs | 8 + .../ModelReaderWriter/ModelReaderWriter.cs | 210 +++++++++++- .../src/ModelReaderWriter/ModelWriterOfT.cs | 14 +- .../Models/ModelWithXmlAndJsonTests.cs | 66 ++++ .../Models/StreamableModelTests.cs | 61 ++++ .../ModelReaderWriterTests/MrwModelTests.cs | 17 +- .../RoundTripStrategy.cs | 132 +++++++- .../Models/ModelWithXmlAndJson.cs | 298 ++++++++++++++++++ .../TestClientModelReaderWriterContext.cs | 8 +- .../System.ClientModel.Tests.Client.csproj | 3 + .../ModelWithXmlAndJson.json | 6 + .../ModelWithXmlAndJson.xml | 6 + .../ModelWithXmlAndJsonWireFormat.xml | 6 + 16 files changed, 902 insertions(+), 14 deletions(-) create mode 100644 sdk/core/System.ClientModel/src/Convenience/MultiPartFile.cs create mode 100644 sdk/core/System.ClientModel/src/ModelReaderWriter/IStreamModel.cs create mode 100644 sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/ModelWithXmlAndJsonTests.cs create mode 100644 sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/StreamableModelTests.cs create mode 100644 sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ModelWithXmlAndJson.cs create mode 100644 sdk/core/System.ClientModel/tests/client/TestData/ModelWithXmlAndJson/ModelWithXmlAndJson.json create mode 100644 sdk/core/System.ClientModel/tests/client/TestData/ModelWithXmlAndJson/ModelWithXmlAndJson.xml create mode 100644 sdk/core/System.ClientModel/tests/client/TestData/ModelWithXmlAndJson/ModelWithXmlAndJsonWireFormat.xml diff --git a/sdk/core/System.ClientModel/src/Convenience/MultiPartFile.cs b/sdk/core/System.ClientModel/src/Convenience/MultiPartFile.cs new file mode 100644 index 000000000000..8847558059c6 --- /dev/null +++ b/sdk/core/System.ClientModel/src/Convenience/MultiPartFile.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Internal; +using System.IO; + +namespace System.ClientModel.Primitives; + +/// +/// A file to be uploaded as part of a multipart request. +/// +public class MultiPartFile +{ + /// + /// Creates a new instance of . + /// + public MultiPartFile(Stream contents, string? filename = default, string? contentType = default) + { + Argument.AssertNotNull(contents, nameof(contents)); + + File = contents; + Filename = filename; + ContentType = contentType ?? "application/octet-stream"; + } + + /// + /// Creates a new instance of . + /// + public MultiPartFile(BinaryData contents, string? filename = default, string? contentType = default) + { + Argument.AssertNotNull(contents, nameof(contents)); + + Contents = contents; + Filename = filename; + ContentType = contentType ?? "application/octet-stream"; + } + + /// + /// The file stream to be uploaded as part of a multipart request. + /// + public Stream? File { get; } + + /// + /// The file contents to be uploaded as part of a multipart request. + /// + public BinaryData? Contents { get; } + + /// + /// The name of the file to be uploaded as part of a multipart request. + /// + public string? Filename { get; } + + /// + /// The content type of the file to be uploaded as part of a multipart request. + /// + public string ContentType { get; } +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/CollectionWriter.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/CollectionWriter.cs index 43c17ba4f8cb..360e7edaff8f 100644 --- a/sdk/core/System.ClientModel/src/ModelReaderWriter/CollectionWriter.cs +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/CollectionWriter.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Collections; +using System.IO; namespace System.ClientModel.Primitives; @@ -72,4 +73,5 @@ private static object GetFirstObject(IEnumerable enumerable) } internal abstract BinaryData Write(IEnumerable enumerable, ModelReaderWriterOptions options); + internal abstract void WriteTo(IEnumerable enumerable, Stream stream, ModelReaderWriterOptions options); } diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/IStreamModel.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/IStreamModel.cs new file mode 100644 index 000000000000..d013d55a7047 --- /dev/null +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/IStreamModel.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.IO; + +namespace System.ClientModel.Primitives; + +/// +/// Allows an object to control its own writing to a . +/// The format is determined by the implementer. +/// +/// The type the model can be converted into. +public interface IStreamModel : IPersistableModel +{ + /// + /// Writes the model into the provided . + /// + /// The to write the model into. + /// The to use. + /// If the model does not support the requested . + void Write(Stream stream, ModelReaderWriterOptions options); +} diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonCollectionWriter.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonCollectionWriter.cs index 48a75c6472af..634b75af6393 100644 --- a/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonCollectionWriter.cs +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/JsonCollectionWriter.cs @@ -3,6 +3,7 @@ using System.ClientModel.Internal; using System.Collections; +using System.IO; using System.Text.Json; namespace System.ClientModel.Primitives; @@ -18,6 +19,13 @@ internal override BinaryData Write(IEnumerable enumerable, ModelReaderWriterOpti return sequenceWriter.ExtractReader().ToBinaryData(); } + internal override void WriteTo(IEnumerable enumerable, Stream stream, ModelReaderWriterOptions options) + { + using var writer = new Utf8JsonWriter(stream); + WriteEnumerable(enumerable, writer, options); + writer.Flush(); + } + private static void WriteJson(object model, Utf8JsonWriter writer, ModelReaderWriterOptions options) { if (model is IJsonModel jsonModel) diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/ModelReaderWriter.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/ModelReaderWriter.cs index 92a51804eac9..36bcc24f1d16 100644 --- a/sdk/core/System.ClientModel/src/ModelReaderWriter/ModelReaderWriter.cs +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/ModelReaderWriter.cs @@ -4,6 +4,7 @@ using System.ClientModel.Internal; using System.Collections; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Runtime.CompilerServices; namespace System.ClientModel.Primitives; @@ -67,6 +68,65 @@ public static BinaryData Write(object model, ModelReaderWriterOptions? options = } } + /// + /// Writes the model into the provided . + /// + /// + /// + /// + /// + /// + public static void Write(T model, Stream stream, ModelReaderWriterOptions? options = default) + where T : IStreamModel + { + if (model is null) + { + throw new ArgumentNullException(nameof(model)); + } + + if (stream is null) + { + throw new ArgumentNullException(nameof(stream)); + } + + options ??= ModelReaderWriterOptions.Json; + + WriteStreamModel(model, stream, options); + } + + /// + /// Writes the model into the provided . + /// + /// + /// + /// + /// + /// + public static void Write(object model, Stream stream, ModelReaderWriterOptions? options = default) + { + if (model is null) + { + throw new ArgumentNullException(nameof(model)); + } + if (stream is null) + { + throw new ArgumentNullException(nameof(stream)); + } + + options ??= ModelReaderWriterOptions.Json; + + //temp blocking this for symetry of functionality on read/write with no context. + //will be allowed after https://github.com/Azure/azure-sdk-for-net/issues/48294 + if (model is IStreamModel iModel) + { + WriteStreamModel(iModel, stream, options); + } + else + { + throw new InvalidOperationException($"{model.GetType().Name} does not implement IStreamModel"); + } + } + /// /// Converts the value of a model into a . /// @@ -111,6 +171,56 @@ public static BinaryData Write(object model, ModelReaderWriterContext context, M return WritePersistableOrEnumerable(model, context, options); } + /// + /// Writes the model into the provided . + /// + /// + /// + /// + /// + /// + public static void Write(T model, Stream stream, ModelReaderWriterContext context, ModelReaderWriterOptions? options = default) + { + if (model is null) + { + throw new ArgumentNullException(nameof(model)); + } + + if (stream is null) + { + throw new ArgumentNullException(nameof(stream)); + } + + options ??= ModelReaderWriterOptions.Json; + + WriteStreamModelOrEnumerable(model, stream, context, options); + } + + /// + /// Writes the model into the provided . + /// + /// + /// + /// + /// + /// + public static void Write(object model, Stream stream, ModelReaderWriterContext context, ModelReaderWriterOptions? options = default) + { + if (model is null) + { + throw new ArgumentNullException(nameof(model)); + } + + if (stream is null) + { + throw new ArgumentNullException(nameof(stream)); + } + + options ??= ModelReaderWriterOptions.Json; + + WriteStreamModelOrEnumerable(model, stream, context, options); + } + private static BinaryData WritePersistableOrEnumerable(T model, ModelReaderWriterContext context, ModelReaderWriterOptions options) { if (model is IPersistableModel iModel) @@ -119,11 +229,9 @@ private static BinaryData WritePersistableOrEnumerable(T model, ModelReaderWr } else { - var enumerable = model as IEnumerable ?? context.GetModelInfoInternal(model!.GetType()).GetEnumerable(model); - if (enumerable is not null) + if (TryWriteEnumerable(model, context, options, out BinaryData? data) && data != null) { - var collectionWriter = CollectionWriter.GetCollectionWriter(enumerable, options); - return collectionWriter.Write(enumerable, options); + return data; } else { @@ -134,17 +242,103 @@ private static BinaryData WritePersistableOrEnumerable(T model, ModelReaderWr private static BinaryData WritePersistable(IPersistableModel model, ModelReaderWriterOptions options) { - if (ShouldWriteAsJson(model, options, out IJsonModel? jsonModel)) + if (TryWriteJson(model, options, out BinaryData? data) && data != null) + { + return data; + } + else { - using (UnsafeBufferSequence.Reader reader = new ModelWriter(jsonModel, options).ExtractReader()) + return model.Write(options); + } + } + + private static void WriteStreamModelOrEnumerable( + T model, + Stream stream, + ModelReaderWriterContext context, + ModelReaderWriterOptions options) + { + if (model is IStreamModel iModel) + { + WriteStreamModel(iModel, stream, options); + return; + } + else + { + if (TryWriteEnumerable(model, context, options, out _, stream)) { - return reader.ToBinaryData(); + return; } + else + { + throw new InvalidOperationException($"{model!.GetType().Name} must implement IEnumerable or IStreamModel"); + } + } + } + + private static void WriteStreamModel(IStreamModel model, Stream stream, ModelReaderWriterOptions options) + { + if (TryWriteJson(model, options, out _, stream)) + { + return; } else { - return model.Write(options); + model.Write(stream, options); } + + return; + } + + private static bool TryWriteJson( + IPersistableModel model, + ModelReaderWriterOptions options, + out BinaryData? data, + Stream? stream = default) + { + data = null; + + if (ShouldWriteAsJson(model, options, out IJsonModel? jsonModel)) + { + var writer = new ModelWriter(jsonModel, options); + if (stream != null) + { + writer.WriteTo(stream); + return true; + } + + using var reader = writer.ExtractReader(); + data = reader.ToBinaryData(); + return true; + } + + return false; + } + + private static bool TryWriteEnumerable( + T model, + ModelReaderWriterContext context, + ModelReaderWriterOptions options, + out BinaryData? data, + Stream? stream = default) + { + data = null; + + var enumerable = model as IEnumerable ?? context.GetModelInfoInternal(model!.GetType()).GetEnumerable(model); + if (enumerable != null) + { + var collectionWriter = CollectionWriter.GetCollectionWriter(enumerable, options); + if (stream != null) + { + collectionWriter.WriteTo(enumerable, stream, options); + return true; + } + + data = collectionWriter.Write(enumerable, options); + return true; + } + + return false; } /// diff --git a/sdk/core/System.ClientModel/src/ModelReaderWriter/ModelWriterOfT.cs b/sdk/core/System.ClientModel/src/ModelReaderWriter/ModelWriterOfT.cs index 7d8d5704cfaa..2018fde93a3a 100644 --- a/sdk/core/System.ClientModel/src/ModelReaderWriter/ModelWriterOfT.cs +++ b/sdk/core/System.ClientModel/src/ModelReaderWriter/ModelWriterOfT.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.ClientModel.Primitives; +using System.IO; using System.Text.Json; namespace System.ClientModel.Internal; @@ -30,8 +31,19 @@ public UnsafeBufferSequence.Reader ExtractReader() { using UnsafeBufferSequence sequenceWriter = new UnsafeBufferSequence(); using var jsonWriter = new Utf8JsonWriter(sequenceWriter); + WriteToInternal(jsonWriter); + return sequenceWriter.ExtractReader(); + } + + public void WriteTo(Stream stream) + { + using var jsonWriter = new Utf8JsonWriter(stream); + WriteToInternal(jsonWriter); + } + + private void WriteToInternal(Utf8JsonWriter jsonWriter) + { _model.Write(jsonWriter, _options); jsonWriter.Flush(); - return sequenceWriter.ExtractReader(); } } diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/ModelWithXmlAndJsonTests.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/ModelWithXmlAndJsonTests.cs new file mode 100644 index 000000000000..cee6d6fe4ce9 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/ModelWithXmlAndJsonTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Tests.Client.ModelReaderWriterTests.Models; +using NUnit.Framework; +using System.IO; +using System.ClientModel.Tests.Client; +using System.ClientModel.Primitives; + +namespace System.ClientModel.Tests.ModelReaderWriterTests.Models +{ + internal class ModelWithXmlAndJsonTests : StreamableModelTests + { + protected override string WirePayload => File.ReadAllText(TestData.GetLocation("ModelWithXmlAndJson/ModelWithXmlAndJson.xml")).TrimEnd(); + + protected override string JsonPayload => File.ReadAllText(TestData.GetLocation("ModelWithXmlAndJson/ModelWithXmlAndJson.json")).TrimEnd(); + protected override ModelReaderWriterContext Context => new TestClientModelReaderWriterContext(); + + protected override void CompareModels(ModelWithXmlAndJson model, ModelWithXmlAndJson model2, string format) + { + Assert.AreEqual(model.Key, model2.Key); + Assert.AreEqual(model.Value, model2.Value); + Assert.AreEqual(model.ReadOnlyProperty, model2.ReadOnlyProperty); + + var rawData1 = GetRawData(model); + Assert.IsNotNull(rawData1); + var rawData2 = GetRawData(model2); + Assert.IsNotNull(rawData2); + + if (format != "W") + { + Assert.AreEqual(rawData1["extra"].ToObjectFromJson(), rawData2["extra"].ToObjectFromJson()); + } + } + + protected override string GetExpectedResult(string format) + { + if (format == "W") + { + return "\uFEFFColorRed" + + "ReadOnly"; + } + + if (format == "J") + { + return "{\"key\":\"Color\",\"value\":\"Red\"" + ",\"readOnlyProperty\":\"ReadOnly\",\"extra\":\"stuff\"}"; + } + throw new InvalidOperationException($"Unknown format used in test {format}"); + } + + protected override void VerifyModel(ModelWithXmlAndJson model, string format) + { + Assert.AreEqual("Color", model.Key); + Assert.AreEqual("Red", model.Value); + Assert.AreEqual("ReadOnly", model.ReadOnlyProperty); + + var rawData = GetRawData(model); + Assert.IsNotNull(rawData); + + if (format != "W") + { + Assert.AreEqual("stuff", rawData["extra"].ToObjectFromJson()); + } + } + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/StreamableModelTests.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/StreamableModelTests.cs new file mode 100644 index 000000000000..703fff1ee8d6 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/Models/StreamableModelTests.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using NUnit.Framework; + +namespace System.ClientModel.Tests.ModelReaderWriterTests.Models +{ + internal abstract class StreamableModelTests : ModelJsonTests where T : IStreamModel, IJsonModel + { + [TestCase("J")] + [TestCase("W")] + public void RoundTripWithStreamableModelReaderWriter(string format) + => RoundTripTest(format, new ModelReaderWriterStreamableStrategy()); + + [TestCase("J")] + [TestCase("W")] + public void RoundTripWithStreamableModelReaderWriterNonGeneric(string format) + => RoundTripTest(format, new ModelReaderWriterStreamableNonGenericStrategy()); + + [TestCase("J")] + [TestCase("W")] + public void RoundTripWithStreamableModelInterface(string format) + => RoundTripTest(format, new ModelInterfaceStreamableStrategy()); + + [TestCase("J")] + [TestCase("W")] + public void RoundTripWithStreamableModelInterfaceNonGeneric(string format) + => RoundTripTest(format, new ModelInterfaceAsObjectStreamableStrategy()); + + [TestCase("J")] + [TestCase("W")] + public void RoundTripWithJsonStreamableInterfaceOfT(string format) + => RoundTripTest(format, new JsonStreamableInterfaceStrategy()); + + [TestCase("J")] + [TestCase("W")] + public void RoundTripWithJsonStreamableInterfaceNonGeneric(string format) + => RoundTripTest(format, new JsonStreamableInterfaceAsObjectStrategy()); + + [TestCase("J")] + [TestCase("W")] + public void RoundTripWithJsonStreamableInterfaceUtf8Reader(string format) + => RoundTripTest(format, new JsonStreamableInterfaceUtf8ReaderStrategy()); + + [TestCase("J")] + [TestCase("W")] + public void RoundTripWithJsonStreamableInterfaceUtf8ReaderNonGeneric(string format) + => RoundTripTest(format, new JsonStreamableInterfaceUtf8ReaderAsObjectStrategy()); + + [TestCase("J")] + [TestCase("W")] + public void RoundTripWithModelReaderWriterStreamable_WithContext(string format) + => RoundTripTest(format, new ModelReaderWriterStreamableStrategy_WithContext(Context)); + + [TestCase("J")] + [TestCase("W")] + public void RoundTripWithModelReaderWriterStreamableNonGeneric_WithContext(string format) + => RoundTripTest(format, new ModelReaderWriterStreamableNonGenericStrategy_WithContext(Context)); + } +} diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/MrwModelTests.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/MrwModelTests.cs index 79263e450c78..a91f983c477f 100644 --- a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/MrwModelTests.cs +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/MrwModelTests.cs @@ -58,7 +58,22 @@ protected override void RoundTripTest(string format, RoundTripStrategy strate T model = (T)strategy.Read(serviceResponse, Instance, options); VerifyModel(model, format); - var data = strategy.Write(model, options); + BinaryData data; + if (strategy.SupportsStreaming) + { + using MemoryStream stream = new MemoryStream(); + strategy.Write(stream, model, options); + if (stream.CanSeek) + { + stream.Position = 0; + } + data = BinaryData.FromStream(stream); + } + else + { + data = strategy.Write(model, options); + } + string roundTrip = data.ToString(); Assert.That(roundTrip, Is.EqualTo(expectedSerializedString)); diff --git a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/RoundTripStrategy.cs b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/RoundTripStrategy.cs index bc39859c5de6..aa3851c03266 100644 --- a/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/RoundTripStrategy.cs +++ b/sdk/core/System.ClientModel/tests/ModelReaderWriterTests/RoundTripStrategy.cs @@ -20,15 +20,16 @@ public RoundTripStrategy(ModelReaderWriterContext? context) public abstract object Read(string payload, object model, ModelReaderWriterOptions options); public abstract BinaryData Write(T model, ModelReaderWriterOptions options); + public virtual void Write(Stream stream, T model, ModelReaderWriterOptions options) + => throw new NotImplementedException(); public abstract bool IsExplicitJsonWrite { get; } public abstract bool IsExplicitJsonRead { get; } + public abstract bool SupportsStreaming { get; } protected BinaryData WriteWithJsonInterface(IJsonModel model, ModelReaderWriterOptions options) { using MemoryStream stream = new MemoryStream(); - using Utf8JsonWriter writer = new Utf8JsonWriter(stream); - model.Write(writer, options); - writer.Flush(); + WriteWithJsonInterface(model, stream, options); if (stream.Position > int.MaxValue) { return BinaryData.FromStream(stream); @@ -38,6 +39,13 @@ protected BinaryData WriteWithJsonInterface(IJsonModel model, ModelReaderW return new BinaryData(stream.GetBuffer().AsMemory(0, (int)stream.Position)); } } + + protected void WriteWithJsonInterface(IJsonModel model, Stream stream, ModelReaderWriterOptions options) + { + using Utf8JsonWriter writer = new Utf8JsonWriter(stream); + model.Write(writer, options); + writer.Flush(); + } } public class ModelReaderWriterStrategy_WithContext : RoundTripStrategy @@ -48,6 +56,7 @@ public ModelReaderWriterStrategy_WithContext(ModelReaderWriterContext context) : public override bool IsExplicitJsonWrite => false; public override bool IsExplicitJsonRead => false; + public override bool SupportsStreaming => false; public override BinaryData Write(T model, ModelReaderWriterOptions options) { @@ -67,6 +76,7 @@ public ModelReaderWriterStrategy() : base(null) public override bool IsExplicitJsonWrite => false; public override bool IsExplicitJsonRead => false; + public override bool SupportsStreaming => false; public override BinaryData Write(T model, ModelReaderWriterOptions options) { @@ -86,6 +96,7 @@ public ModelReaderWriterNonGenericStrategy_WithContext(ModelReaderWriterContext public override bool IsExplicitJsonWrite => false; public override bool IsExplicitJsonRead => false; + public override bool SupportsStreaming => false; public override BinaryData Write(T model, ModelReaderWriterOptions options) { @@ -106,6 +117,7 @@ public ModelReaderWriterNonGenericStrategy() : base(null) public override bool IsExplicitJsonWrite => false; public override bool IsExplicitJsonRead => false; + public override bool SupportsStreaming => false; public override BinaryData Write(T model, ModelReaderWriterOptions options) { @@ -126,6 +138,7 @@ public ModelInterfaceStrategy() : base(null) public override bool IsExplicitJsonWrite => false; public override bool IsExplicitJsonRead => false; + public override bool SupportsStreaming => false; public override BinaryData Write(T model, ModelReaderWriterOptions options) { @@ -146,6 +159,7 @@ public ModelInterfaceAsObjectStrategy() : base(null) public override bool IsExplicitJsonWrite => false; public override bool IsExplicitJsonRead => false; + public override bool SupportsStreaming => false; public override BinaryData Write(T model, ModelReaderWriterOptions options) { @@ -166,6 +180,7 @@ public JsonInterfaceStrategy() : base(null) public override bool IsExplicitJsonWrite => true; public override bool IsExplicitJsonRead => false; + public override bool SupportsStreaming => false; public override BinaryData Write(T model, ModelReaderWriterOptions options) { @@ -186,6 +201,7 @@ public JsonInterfaceAsObjectStrategy() : base(null) public override bool IsExplicitJsonWrite => true; public override bool IsExplicitJsonRead => false; + public override bool SupportsStreaming => false; public override BinaryData Write(T model, ModelReaderWriterOptions options) { @@ -206,6 +222,7 @@ public JsonInterfaceUtf8ReaderStrategy() : base(null) public override bool IsExplicitJsonWrite => true; public override bool IsExplicitJsonRead => true; + public override bool SupportsStreaming => false; public override BinaryData Write(T model, ModelReaderWriterOptions options) { @@ -227,6 +244,7 @@ public JsonInterfaceUtf8ReaderAsObjectStrategy() : base(null) public override bool IsExplicitJsonWrite => true; public override bool IsExplicitJsonRead => true; + public override bool SupportsStreaming => false; public override BinaryData Write(T model, ModelReaderWriterOptions options) { @@ -239,5 +257,113 @@ public override object Read(string payload, object model, ModelReaderWriterOptio return ((IJsonModel)model).Create(ref reader, options); } } + + public class ModelReaderWriterStreamableStrategy_WithContext : ModelReaderWriterStrategy_WithContext + { + public ModelReaderWriterStreamableStrategy_WithContext(ModelReaderWriterContext context) : base(context) + { + } + + public override bool SupportsStreaming => true; + + public override void Write(Stream stream, T model, ModelReaderWriterOptions options) + { + ModelReaderWriter.Write(model, stream, _context!, options); + } + } + + public class ModelReaderWriterStreamableStrategy : ModelReaderWriterStrategy where T : IStreamModel + { + public override bool SupportsStreaming => true; + + public override void Write(Stream stream, T model, ModelReaderWriterOptions options) + { + ModelReaderWriter.Write(model, stream, options); + } + } + + public class ModelReaderWriterStreamableNonGenericStrategy_WithContext : ModelReaderWriterNonGenericStrategy_WithContext + { + public ModelReaderWriterStreamableNonGenericStrategy_WithContext(ModelReaderWriterContext context) : base(context) + { + } + + public override bool SupportsStreaming => true; + + public override void Write(Stream stream, T model, ModelReaderWriterOptions options) + { + ModelReaderWriter.Write((object)model!, stream, _context!, options); + } + } + + public class ModelReaderWriterStreamableNonGenericStrategy : ModelReaderWriterNonGenericStrategy where T : IStreamModel + { + public override bool SupportsStreaming => true; + + public override void Write(Stream stream, T model, ModelReaderWriterOptions options) + { + ModelReaderWriter.Write((object)model, stream, options); + } + } + + public class ModelInterfaceStreamableStrategy : ModelInterfaceStrategy where T : IStreamModel + { + public override bool SupportsStreaming => true; + + public override void Write(Stream stream, T model, ModelReaderWriterOptions options) + { + model.Write(stream, options); + } + } + + public class ModelInterfaceAsObjectStreamableStrategy : ModelInterfaceAsObjectStrategy where T : IStreamModel + { + public override bool SupportsStreaming => true; + + public override void Write(Stream stream, T model, ModelReaderWriterOptions options) + { + ((IStreamModel)model).Write(stream, options); + } + } + + public class JsonStreamableInterfaceStrategy : JsonInterfaceStrategy where T : IStreamModel, IJsonModel + { + public override bool SupportsStreaming => true; + + public override void Write(Stream stream, T model, ModelReaderWriterOptions options) + { + model.Write(stream, options); + } + } + + public class JsonStreamableInterfaceAsObjectStrategy : JsonInterfaceAsObjectStrategy where T : IStreamModel, IJsonModel + { + public override bool SupportsStreaming => true; + + public override void Write(Stream stream, T model, ModelReaderWriterOptions options) + { + WriteWithJsonInterface((IJsonModel)model, stream, options); + } + } + + public class JsonStreamableInterfaceUtf8ReaderStrategy : JsonInterfaceUtf8ReaderStrategy where T : IStreamModel, IJsonModel + { + public override bool SupportsStreaming => true; + + public override void Write(Stream stream, T model, ModelReaderWriterOptions options) + { + model.Write(stream, options); + } + } + + public class JsonStreamableInterfaceUtf8ReaderAsObjectStrategy : JsonInterfaceUtf8ReaderAsObjectStrategy where T : IStreamModel, IJsonModel + { + public override bool SupportsStreaming => true; + + public override void Write(Stream stream, T model, ModelReaderWriterOptions options) + { + WriteWithJsonInterface((IJsonModel)model, stream, options); + } + } } #pragma warning restore SA1402 // File may only contain a single type diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ModelWithXmlAndJson.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ModelWithXmlAndJson.cs new file mode 100644 index 000000000000..cc0a3efd3638 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/Models/ModelWithXmlAndJson.cs @@ -0,0 +1,298 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Xml; +using System.Xml.Linq; +using System.Xml.Serialization; +using ClientModel.Tests.ClientShared; +using ClientModel.Tests.Collections; + +namespace System.ClientModel.Tests.Client.ModelReaderWriterTests.Models +{ + [XmlRoot("Tag")] + public class ModelWithXmlAndJson : IStreamModel, IJsonModel + { + private protected readonly IDictionary _rawData; + internal ModelWithXmlAndJson() + { + _rawData = new Dictionary(); + } + + internal ModelWithXmlAndJson(string? key, string? value, string? readonlyProperty, IDictionary additionalBinaryDataProperties) + { + Argument.AssertNotNull(key, nameof(key)); + Argument.AssertNotNull(value, nameof(value)); + + Key = key; + Value = value; + ReadOnlyProperty = readonlyProperty; + _rawData = additionalBinaryDataProperties; + } + + /// Initializes a new instance of ModelXml for testing. + /// + /// + /// or is null. + public ModelWithXmlAndJson(string? key, string? value, string? readonlyProperty) + { + Argument.AssertNotNull(key, nameof(key)); + Argument.AssertNotNull(value, nameof(value)); + + Key = key; + Value = value; + ReadOnlyProperty = readonlyProperty; + _rawData = new Dictionary(); + } + + /// Gets or sets the key. + [XmlElement("Key")] + public string? Key { get; set; } + /// Gets or sets the value. + [XmlElement("Value")] + public string? Value { get; set; } + /// Gets or sets the value. + [XmlElement("ReadOnlyProperty")] + public string? ReadOnlyProperty { get; } + + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + writer.WriteStartObject(); + JsonModelWriteCore(writer, options); + writer.WriteEndObject(); + } + + protected virtual void JsonModelWriteCore(Utf8JsonWriter writer, ModelReaderWriterOptions options) + { + ModelReaderWriterHelper.ValidateFormat(this, options.Format); + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (options.Format != "J") + { + throw new InvalidOperationException($"Must use 'J' format when calling the {nameof(IJsonModel)} interface"); + } + + writer.WritePropertyName("key"u8); + writer.WriteStringValue(Key); + writer.WritePropertyName("value"u8); + writer.WriteStringValue(Value); + writer.WritePropertyName("readOnlyProperty"u8); + writer.WriteStringValue(ReadOnlyProperty); + if (options.Format != "W" && _rawData != null) + { + foreach (var item in _rawData) + { + writer.WritePropertyName(item.Key); +#if NET6_0_OR_GREATER + writer.WriteRawValue(item.Value); +#else + using (JsonDocument document = JsonDocument.Parse(item.Value)) + { + JsonSerializer.Serialize(writer, document.RootElement); + } +#endif + } + } + } + + ModelWithXmlAndJson IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + => JsonModelCreateCore(ref reader, options); + + protected virtual ModelWithXmlAndJson JsonModelCreateCore(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + { + ModelReaderWriterHelper.ValidateFormat(this, options.Format); + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if (options.Format != "J") + { + throw new InvalidOperationException($"Must use 'J' format when calling the {nameof(IJsonModel)} interface"); + } + + using JsonDocument document = JsonDocument.ParseValue(ref reader); + return DeserializeModelXmlJson(document.RootElement, options); + } + + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + => PersistableModelWriteCore(options); + + protected virtual BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options) + { + if (TryWriteModel(options, out BinaryData? data) && data != null) + { + return data; + } + + throw new InvalidOperationException($"Unable to write the model {nameof(ModelWithXmlAndJson)} using '{options.Format}' format."); + } + + protected virtual void PersistableModelWriteCore(Stream stream, ModelReaderWriterOptions options) + { + if (TryWriteModel(options, out _, stream)) + { + return; + } + + throw new InvalidOperationException($"Unable to write the model {nameof(ModelWithXmlAndJson)} using '{options.Format}' format."); + } + + private bool TryWriteModel(ModelReaderWriterOptions options, out BinaryData? data, Stream? stream = null) + { + ModelReaderWriterHelper.ValidateFormat(this, options.Format); + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + + data = null; + switch (format) + { + case "J": + { + if (stream != null) + { + ModelReaderWriter.Write(this, stream, options); + return true; + } + else + { + data = ModelReaderWriter.Write(this, options); + return true; + } + } + case "X": + { + if (stream != null) + { + using XmlWriter writer = XmlWriter.Create(stream); + SerializeAsXml(writer, options, null); + writer.Flush(); + return true; + } + else + { + using MemoryStream memoryStream = new MemoryStream(); + using XmlWriter writer = XmlWriter.Create(memoryStream); + SerializeAsXml(writer, options, null); + writer.Flush(); + if (memoryStream.Position > int.MaxValue) + { + data = BinaryData.FromStream(memoryStream); + } + else + { + data = new BinaryData(memoryStream.GetBuffer().AsMemory(0, (int)memoryStream.Position)); + } + + return true; + } + } + default: + throw new FormatException($"The model {nameof(ModelWithXmlAndJson)} does not support writing '{options.Format}' format."); + } + } + + ModelWithXmlAndJson IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + => PersistableModelCreateCore(data, options); + + protected virtual ModelWithXmlAndJson PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options) + { + string format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + switch (format) + { + case "J": + using (JsonDocument document = JsonDocument.Parse(data)) + { + return DeserializeModelXmlJson(document.RootElement, options); + } + case "X": + return DeserializeModelXmlJson(XElement.Load(data.ToStream()), options); + default: + throw new FormatException($"The model {nameof(ModelWithXmlAndJson)} does not support reading '{options.Format}' format."); + } + } + + void IStreamModel.Write(Stream stream, ModelReaderWriterOptions options) + => PersistableModelWriteCore(stream, options); + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "X"; + + private void SerializeAsXml(XmlWriter writer, ModelReaderWriterOptions options, string? nameHint) + { + writer.WriteStartElement(nameHint ?? "Tag"); + writer.WriteStartElement("Key"); + writer.WriteValue(Key); + writer.WriteEndElement(); + writer.WriteStartElement("Value"); + writer.WriteValue(Value); + writer.WriteEndElement(); + writer.WriteStartElement("ReadOnlyProperty"); + writer.WriteValue(ReadOnlyProperty); + writer.WriteEndElement(); + if (options.Format != "W" && _rawData != null) + { + foreach (var item in _rawData) + { + writer.WriteStartElement(item.Key); + writer.WriteValue(item.Value); + writer.WriteEndElement(); + } + } + writer.WriteEndElement(); + } + + internal static ModelWithXmlAndJson DeserializeModelXmlJson(XElement element, ModelReaderWriterOptions? options = default) + { + options ??= ModelReaderWriterHelper.WireOptions; + + string? key = default; + string? value = default; + string? readonlyProperty = default; + IDictionary additionalBinaryDataProperties = new Dictionary(); + + if (element.Element("Key") is XElement keyElement) + { + key = (string)keyElement; + } + if (element.Element("Value") is XElement valueElement) + { + value = (string)valueElement; + } + if (element.Element("ReadOnlyProperty") is XElement readonlyPropertyElement) + { + readonlyProperty = (string)readonlyPropertyElement; + } + + return new ModelWithXmlAndJson(key, value, readonlyProperty, additionalBinaryDataProperties); + } + + internal static ModelWithXmlAndJson DeserializeModelXmlJson(JsonElement element, ModelReaderWriterOptions? options = default) + { + options ??= ModelReaderWriterHelper.WireOptions; + + string? key = default; + string? value = default; + string? readOnlyProperty = default; + IDictionary additionalBinaryDataProperties = new Dictionary(); + foreach (var property in element.EnumerateObject()) + { + if (property.NameEquals("key"u8)) + { + key = property.Value.GetString(); + continue; + } + if (property.NameEquals("value"u8)) + { + value = property.Value.GetString(); + continue; + } + if (property.NameEquals("readOnlyProperty"u8)) + { + readOnlyProperty = property.Value.GetString(); + continue; + } + if (options.Format != "W") + { + additionalBinaryDataProperties.Add(property.Name, BinaryData.FromString(property.Value.GetRawText())); + } + } + return new ModelWithXmlAndJson(key, value, readOnlyProperty, additionalBinaryDataProperties); + } + } +} diff --git a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/TestClientModelReaderWriterContext.cs b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/TestClientModelReaderWriterContext.cs index 7f1bb9de491f..4c3a8f56e7bc 100644 --- a/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/TestClientModelReaderWriterContext.cs +++ b/sdk/core/System.ClientModel/tests/client/ModelReaderWriter/TestClientModelReaderWriterContext.cs @@ -5,7 +5,6 @@ using System.ClientModel.Tests.Client.ModelReaderWriterTests.Models; using System.ClientModel.Tests.Client.Models.ResourceManager.Compute; using System.ClientModel.Tests.Client.Models.ResourceManager.Resources; -using System.Collections.Generic; namespace System.ClientModel.Tests.ModelReaderWriterTests { @@ -19,6 +18,7 @@ public class TestClientModelReaderWriterContext : ModelReaderWriterContext private ResourceProviderData_Info? _resourceProviderData_Info; private UnknownBaseModel_Info? _unknownBaseModel_Info; private ModelY_Info? _modelY_Info; + private ModelWithXmlAndJson_Info? _modelWithXmlAndJson_Info; public override ModelInfo? GetModelInfo(Type type) { @@ -32,6 +32,7 @@ public class TestClientModelReaderWriterContext : ModelReaderWriterContext Type t when t == typeof(ResourceProviderData) => _resourceProviderData_Info ??= new(), Type t when t == typeof(UnknownBaseModel) => _unknownBaseModel_Info ??= new(), Type t when t == typeof(ModelY) => _modelY_Info ??= new(), + Type t when t == typeof(ModelWithXmlAndJson) => _modelWithXmlAndJson_Info ??= new(), _ => null }; } @@ -75,5 +76,10 @@ private class AvailabilitySetData_Info : ModelInfo { public override object CreateObject() => new AvailabilitySetData(); } + + private class ModelWithXmlAndJson_Info : ModelInfo + { + public override object CreateObject() => new ModelWithXmlAndJson(); + } } } diff --git a/sdk/core/System.ClientModel/tests/client/System.ClientModel.Tests.Client.csproj b/sdk/core/System.ClientModel/tests/client/System.ClientModel.Tests.Client.csproj index 30f59228282d..73fefacad5cb 100644 --- a/sdk/core/System.ClientModel/tests/client/System.ClientModel.Tests.Client.csproj +++ b/sdk/core/System.ClientModel/tests/client/System.ClientModel.Tests.Client.csproj @@ -23,6 +23,9 @@ Always + + Always + Always diff --git a/sdk/core/System.ClientModel/tests/client/TestData/ModelWithXmlAndJson/ModelWithXmlAndJson.json b/sdk/core/System.ClientModel/tests/client/TestData/ModelWithXmlAndJson/ModelWithXmlAndJson.json new file mode 100644 index 000000000000..3d85aa191165 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/TestData/ModelWithXmlAndJson/ModelWithXmlAndJson.json @@ -0,0 +1,6 @@ +{ + "key": "Color", + "value": "Red", + "readOnlyProperty": "ReadOnly", + "extra": "stuff" +} diff --git a/sdk/core/System.ClientModel/tests/client/TestData/ModelWithXmlAndJson/ModelWithXmlAndJson.xml b/sdk/core/System.ClientModel/tests/client/TestData/ModelWithXmlAndJson/ModelWithXmlAndJson.xml new file mode 100644 index 000000000000..1e5973092210 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/TestData/ModelWithXmlAndJson/ModelWithXmlAndJson.xml @@ -0,0 +1,6 @@ + + Color + Red + ReadOnly + Stuff + diff --git a/sdk/core/System.ClientModel/tests/client/TestData/ModelWithXmlAndJson/ModelWithXmlAndJsonWireFormat.xml b/sdk/core/System.ClientModel/tests/client/TestData/ModelWithXmlAndJson/ModelWithXmlAndJsonWireFormat.xml new file mode 100644 index 000000000000..1e5973092210 --- /dev/null +++ b/sdk/core/System.ClientModel/tests/client/TestData/ModelWithXmlAndJson/ModelWithXmlAndJsonWireFormat.xml @@ -0,0 +1,6 @@ + + Color + Red + ReadOnly + Stuff +