Skip to content

Commit

Permalink
Add model converter for Stj and Mrw integration (#45445)
Browse files Browse the repository at this point in the history
* Add model converter for Stj and Mrw integration

* pr feedback

* update api

* update to pass read through

* pr fb

* validate pretty print settings are honored in mrw stj integration
validate content changes such as naming policy are not

* pr fb
  • Loading branch information
m-nash authored Aug 12, 2024
1 parent 5d46384 commit 80b5a9c
Show file tree
Hide file tree
Showing 10 changed files with 11,550 additions and 9 deletions.
2 changes: 2 additions & 0 deletions sdk/core/System.ClientModel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- Added `JsonModelConverter` to allow integration with System.Text.Json.

### Breaking Changes

### Bugs Fixed
Expand Down
9 changes: 9 additions & 0 deletions sdk/core/System.ClientModel/api/System.ClientModel.net6.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,15 @@ public partial interface IPersistableModel<out T>
string GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options);
System.BinaryData Write(System.ClientModel.Primitives.ModelReaderWriterOptions options);
}
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("The constructors of the type being deserialized are dynamically accessed and may be trimmed.")]
public partial class JsonModelConverter : System.Text.Json.Serialization.JsonConverter<System.ClientModel.Primitives.IJsonModel<object>>
{
public JsonModelConverter() { }
public JsonModelConverter(System.ClientModel.Primitives.ModelReaderWriterOptions options) { }
public override bool CanConvert(System.Type typeToConvert) { throw null; }
public override System.ClientModel.Primitives.IJsonModel<object> Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; }
public override void Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.IJsonModel<object> value, System.Text.Json.JsonSerializerOptions options) { }
}
public static partial class ModelReaderWriter
{
public static object? Read(System.BinaryData data, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] System.Type returnType, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ public partial interface IPersistableModel<out T>
string GetFormatFromOptions(System.ClientModel.Primitives.ModelReaderWriterOptions options);
System.BinaryData Write(System.ClientModel.Primitives.ModelReaderWriterOptions options);
}
public partial class JsonModelConverter : System.Text.Json.Serialization.JsonConverter<System.ClientModel.Primitives.IJsonModel<object>>
{
public JsonModelConverter() { }
public JsonModelConverter(System.ClientModel.Primitives.ModelReaderWriterOptions options) { }
public override bool CanConvert(System.Type typeToConvert) { throw null; }
public override System.ClientModel.Primitives.IJsonModel<object> Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) { throw null; }
public override void Write(System.Text.Json.Utf8JsonWriter writer, System.ClientModel.Primitives.IJsonModel<object> value, System.Text.Json.JsonSerializerOptions options) { }
}
public static partial class ModelReaderWriter
{
public static object? Read(System.BinaryData data, System.Type returnType, System.ClientModel.Primitives.ModelReaderWriterOptions? options = null) { throw null; }
Expand Down
32 changes: 32 additions & 0 deletions sdk/core/System.ClientModel/samples/ModelReaderWriter.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,35 @@ string json = @"{
}";
OutputModel? model = ModelReaderWriter.Read<OutputModel>(BinaryData.FromString(json));
```

## Read and Write with System.Text.Json

Client library users can use any model that implements `IJsonModel<T>` with `JsonSerializer` by using the `JsonModelConverter` provided. Add this converter to your `JsonSerializerOptions` and `JsonSerializer` will use the logic defined by `IJsonModel<T>`.

The example below shows how to serialize an `IJsonModel<T>` with `JsonSerializer`

```C# Snippet:Readme_Stj_Write_Sample
JsonSerializerOptions options = new JsonSerializerOptions()
{
Converters = { new JsonModelConverter() }
};

InputModel model = new InputModel();
string data = JsonSerializer.Serialize(model);
```

The example below shows how to deserialize an `IJsonModel<T>` with `JsonSerializer`

```C# Snippet:Readme_Stj_Read_Sample
JsonSerializerOptions options = new JsonSerializerOptions()
{
Converters = { new JsonModelConverter() }
};

string json = @"{
""x"": 1,
""y"": 2,
""z"": 3
}";
OutputModel? model = JsonSerializer.Deserialize<OutputModel>(json, options);
```
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ namespace System.ClientModel.Primitives;
/// <summary>
/// A generic converter which allows <see cref="JsonSerializer"/> to be able to write and read any models that implement <see cref="IJsonModel{T}"/>.
/// </summary>
/// <remarks>
/// Since <see cref="IJsonModel{T}"/> defines what the serialized shape should look like the <see cref="JsonSerializerOptions"/> are ignored
/// except for those pertaining to indentation formatting.
/// </remarks>
[RequiresUnreferencedCode("The constructors of the type being deserialized are dynamically accessed and may be trimmed.")]
#pragma warning disable AZC0014 // Avoid using banned types in public API
internal class JsonModelConverter : JsonConverter<IJsonModel<object>>
public class JsonModelConverter : JsonConverter<IJsonModel<object>>
#pragma warning restore AZC0014 // Avoid using banned types in public API
{
/// <summary>
/// Gets the <see cref="ModelReaderWriterOptions"/> used to read and write models.
/// </summary>
public ModelReaderWriterOptions Options { get; }
private ModelReaderWriterOptions _options { get; }

/// <summary>
/// Initializes a new instance of <see cref="JsonModelConverter"/> with a default options of <see cref="ModelReaderWriterOptions.Json"/>.
Expand All @@ -32,7 +36,7 @@ public JsonModelConverter()
/// <param name="options">The <see cref="ModelReaderWriterOptions"/> to use.</param>
public JsonModelConverter(ModelReaderWriterOptions options)
{
Options = options;
_options = options;
}

/// <inheritdoc/>
Expand All @@ -46,15 +50,19 @@ public override bool CanConvert(Type typeToConvert)
public override IJsonModel<object> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
#pragma warning restore AZC0014 // Avoid using banned types in public API
{
using JsonDocument document = JsonDocument.ParseValue(ref reader);
return (IJsonModel<object>)ModelReaderWriter.Read(BinaryData.FromString(document.RootElement.GetRawText()), typeToConvert, Options)!;
var iJsonModel = ModelReaderWriter.GetObjectInstance(typeToConvert) as IJsonModel<object>;
if (iJsonModel is null)
{
throw new InvalidOperationException($"Either {typeToConvert.Name} or the PersistableModelProxyAttribute defined needs to implement IJsonModel.");
}
return (IJsonModel<object>)iJsonModel.Create(ref reader, _options);
}

/// <inheritdoc/>
#pragma warning disable AZC0014 // Avoid using banned types in public API
public override void Write(Utf8JsonWriter writer, IJsonModel<object> value, JsonSerializerOptions options)
#pragma warning restore AZC0014 // Avoid using banned types in public API
{
value.Write(writer, Options);
value.Write(writer, _options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ private static IPersistableModel<object> GetInstance([DynamicallyAccessedMembers
return model;
}

private static object GetObjectInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type returnType)
internal static object GetObjectInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type returnType)
{
PersistableModelProxyAttribute? attribute = Attribute.GetCustomAttribute(returnType, typeof(PersistableModelProxyAttribute), false) as PersistableModelProxyAttribute;
Type typeToActivate = attribute is null ? returnType : attribute.ProxyType;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using NUnit.Framework;
using System.IO;
using System.ClientModel.Primitives;
using System.ClientModel.Tests.Client;
using System.ClientModel.Tests.Client.Models.ResourceManager.Resources;
using System.IO;
using System.Text.Json;
using NUnit.Framework;

namespace System.ClientModel.Tests.ModelReaderWriterTests.Models
{
Expand All @@ -17,6 +19,11 @@ internal class ResourceProviderDataTests : ModelJsonTests<ResourceProviderData>
protected override void CompareModels(ResourceProviderData model, ResourceProviderData model2, string format)
{
Assert.AreEqual(model.Id, model2.Id);
Assert.AreEqual(model.Namespace, model2.Namespace);
Assert.AreEqual(model.RegistrationState, model2.RegistrationState);
Assert.AreEqual(model.RegistrationPolicy, model2.RegistrationPolicy);
Assert.AreEqual(model.ProviderAuthorizationConsentState, model2.ProviderAuthorizationConsentState);
Assert.AreEqual(model.ResourceTypes.Count, model2.ResourceTypes.Count);
}

protected override string GetExpectedResult(string format) => WirePayload;
Expand All @@ -26,5 +33,55 @@ protected override void VerifyModel(ResourceProviderData model, string format)
Assert.IsNotNull(model);
Assert.IsNotNull(model.Id);
}

[Test]
public void ValideStjIntegration()
{
var stjOptions = new JsonSerializerOptions
{
Converters = { new JsonModelConverter() }
};

var modelFromStj = JsonSerializer.Deserialize<ResourceProviderData>(WirePayload, stjOptions);
var modelFromMrw = ModelReaderWriter.Read<ResourceProviderData>(BinaryData.FromString(WirePayload));

Assert.IsNotNull(modelFromStj);
Assert.IsNotNull(modelFromMrw);

CompareModels(modelFromStj!, modelFromMrw!, "J");
var stjResult = JsonSerializer.Serialize(modelFromStj, stjOptions);
Assert.AreEqual(WirePayload, stjResult);
}

[Test]
public void ValidatePrettyPrintWithStj()
{
var stjOptions = new JsonSerializerOptions
{
Converters = { new JsonModelConverter() },
WriteIndented = true,
};

var modelFromStj = JsonSerializer.Deserialize<ResourceProviderData>(WirePayload, stjOptions);
var stjResult = JsonSerializer.Serialize(modelFromStj, stjOptions);
Assert.AreEqual(File.ReadAllText(TestData.GetLocation("ResourceProviderData/ResourceProviderData-TwoSpaces.json")).TrimEnd(), stjResult);
}

[Test]
public void ValidateCapitalizationIsIgnored()
{
#if NET8_0_OR_GREATER
var stjOptions = new JsonSerializerOptions
{
Converters = { new JsonModelConverter() },
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.KebabCaseUpper,
};

var modelFromStj = JsonSerializer.Deserialize<ResourceProviderData>(WirePayload, stjOptions);
var stjResult = JsonSerializer.Serialize(modelFromStj, stjOptions);
Assert.AreEqual(File.ReadAllText(TestData.GetLocation("ResourceProviderData/ResourceProviderData-TwoSpaces.json")).TrimEnd(), stjResult);
#endif
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,36 @@ public void Read_Simple()
#endregion
}

public void Stj_Write_Simple()
{
#region Snippet:Readme_Stj_Write_Sample
JsonSerializerOptions options = new JsonSerializerOptions()
{
Converters = { new JsonModelConverter() }
};

InputModel model = new InputModel();
string data = JsonSerializer.Serialize(model);
#endregion
}

public void Stj_Read_Simple()
{
#region Snippet:Readme_Stj_Read_Sample
JsonSerializerOptions options = new JsonSerializerOptions()
{
Converters = { new JsonModelConverter() }
};

string json = @"{
""x"": 1,
""y"": 2,
""z"": 3
}";
OutputModel? model = JsonSerializer.Deserialize<OutputModel>(json, options);
#endregion
}

private class OutputModel : IJsonModel<OutputModel>
{
OutputModel IJsonModel<OutputModel>.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
<None Update="TestData\ModelX\ModelXWireFormat.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestData\ResourceProviderData\ResourceProviderData-TwoSpaces.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestData\ResourceProviderData\ResourceProviderData-Collapsed.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
Expand Down
Loading

0 comments on commit 80b5a9c

Please sign in to comment.