Skip to content

Add ability to proxy the reading and writing of models #50024

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,13 @@ public partial class ModelReaderWriterOptions
public ModelReaderWriterOptions(string format) { }
public string Format { get { throw null; } }
public static System.ClientModel.Primitives.ModelReaderWriterOptions Json { get { throw null; } }
public object? ProxiedModel { get { throw null; } }
public static System.ClientModel.Primitives.ModelReaderWriterOptions Xml { get { throw null; } }
public void AddProxy<T>(System.ClientModel.Primitives.IPersistableModel<T> proxy) { }
public System.ClientModel.Primitives.IJsonModel<T> ResolveProxy<T>(System.ClientModel.Primitives.IJsonModel<T> model) { throw null; }
public System.ClientModel.Primitives.IPersistableModel<T> ResolveProxy<T>(System.ClientModel.Primitives.IPersistableModel<T> model) { throw null; }
public bool TryGetProxy<T>([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.ClientModel.Primitives.IJsonModel<T>? proxy) { throw null; }
public bool TryGetProxy<T>([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.ClientModel.Primitives.IPersistableModel<T>? proxy) { throw null; }
}
public abstract partial class ModelReaderWriterTypeBuilder
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,13 @@ public partial class ModelReaderWriterOptions
public ModelReaderWriterOptions(string format) { }
public string Format { get { throw null; } }
public static System.ClientModel.Primitives.ModelReaderWriterOptions Json { get { throw null; } }
public object? ProxiedModel { get { throw null; } }
public static System.ClientModel.Primitives.ModelReaderWriterOptions Xml { get { throw null; } }
public void AddProxy<T>(System.ClientModel.Primitives.IPersistableModel<T> proxy) { }
public System.ClientModel.Primitives.IJsonModel<T> ResolveProxy<T>(System.ClientModel.Primitives.IJsonModel<T> model) { throw null; }
public System.ClientModel.Primitives.IPersistableModel<T> ResolveProxy<T>(System.ClientModel.Primitives.IPersistableModel<T> model) { throw null; }
public bool TryGetProxy<T>([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.ClientModel.Primitives.IJsonModel<T>? proxy) { throw null; }
public bool TryGetProxy<T>([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.ClientModel.Primitives.IPersistableModel<T>? proxy) { throw null; }
}
public abstract partial class ModelReaderWriterTypeBuilder
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,13 @@ public partial class ModelReaderWriterOptions
public ModelReaderWriterOptions(string format) { }
public string Format { get { throw null; } }
public static System.ClientModel.Primitives.ModelReaderWriterOptions Json { get { throw null; } }
public object? ProxiedModel { get { throw null; } }
public static System.ClientModel.Primitives.ModelReaderWriterOptions Xml { get { throw null; } }
public void AddProxy<T>(System.ClientModel.Primitives.IPersistableModel<T> proxy) { }
public System.ClientModel.Primitives.IJsonModel<T> ResolveProxy<T>(System.ClientModel.Primitives.IJsonModel<T> model) { throw null; }
public System.ClientModel.Primitives.IPersistableModel<T> ResolveProxy<T>(System.ClientModel.Primitives.IPersistableModel<T> model) { throw null; }
public bool TryGetProxy<T>(out System.ClientModel.Primitives.IJsonModel<T>? proxy) { throw null; }
public bool TryGetProxy<T>(out System.ClientModel.Primitives.IPersistableModel<T>? proxy) { throw null; }
}
public abstract partial class ModelReaderWriterTypeBuilder
{
Expand Down
46 changes: 45 additions & 1 deletion sdk/core/System.ClientModel/samples/ModelReaderWriter.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,48 @@ string json = @"{
""z"": 3
}";
OutputModel? model = JsonSerializer.Deserialize<OutputModel>(json, options);
```
```

## Using a Proxy

In more advanced scenarios a library user might want to override the behavior of how a model is read or written.
In this case you can implement your own class which implements the same interface, either `IPersistableModel<T>` or `IJsonModel<T>`, and register it with the `ModelReaderWriterOptions`.
Unlike the `PersistableModelProxy` attribute the proxy is only valid for a single instance of `ModelReaderWriterOptions` giving you more flexibility to turn it on or off.

Using a proxy with the following definition

```C# Snippet:Readme_Read_Proxy_ClassStub
public class OutputModelProxy : IJsonModel<OutputModel>
```

The example below shows how to read JSON to create a strongly-typed model instance using a proxy.

```C# Snippet:Readme_Read_Proxy
string json = @"{
""x"": 1,
""y"": 2,
""z"": 3
}";

ModelReaderWriterOptions options = new ModelReaderWriterOptions("W");
options.AddProxy(new OutputModelProxy());

OutputModel? model = ModelReaderWriter.Read<OutputModel>(BinaryData.FromString(json), options);
```

Using a proxy with the following definition

```C# Snippet:Readme_Write_Proxy_ClassStub
public class InputModelProxy : IJsonModel<InputModel>
```

The example below shows how to write a persitable model using a proxy to `BinaryData`

```C# Snippet:Readme_Write_Proxy
InputModel model = new InputModel();

ModelReaderWriterOptions options = new ModelReaderWriterOptions("W");
options.AddProxy(new InputModelProxy());

BinaryData data = ModelReaderWriter.Write(model, options);
```
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private static void ReadJsonCollection(
}
else if (itemInstance is IJsonModel<object> iJsonModel)
{
jsonModel = iJsonModel;
jsonModel = options.ResolveProxy(iJsonModel);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private static void WriteJson(object model, Utf8JsonWriter writer, ModelReaderWr
}
else if (model is IJsonModel<object> jsonModel)
{
jsonModel.Write(writer, options);
options.ResolveProxy(jsonModel).Write(writer, options);
}
else if (model is IEnumerable enumerable)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ namespace System.ClientModel.Primitives;
public class JsonModelConverter : JsonConverter<IJsonModel<object>>
#pragma warning restore AZC0014 // Avoid using banned types in public API
{
private ModelReaderWriterOptions _options;
private ModelReaderWriterContext? _context;
private readonly ModelReaderWriterOptions _options;
private readonly ModelReaderWriterContext? _context;

/// <summary>
/// Initializes a new instance of <see cref="JsonModelConverter"/> with a default options of <see cref="ModelReaderWriterOptions.Json"/>.
Expand Down Expand Up @@ -87,7 +87,10 @@ public override IJsonModel<object> Read(ref Utf8JsonReader reader, Type typeToCo
return context.GetTypeBuilder(typeToConvert).CreateObject() as IJsonModel<object>;
}

IJsonModel<object>? iJsonModel = _context is null ? NonAotCompatActivate() : AotCompatActivate();
if (!_options.TryGetProxy(typeToConvert, out IJsonModel<object>? iJsonModel))
{
iJsonModel = _context is null ? NonAotCompatActivate() : AotCompatActivate();
}

if (iJsonModel is null)
{
Expand All @@ -101,6 +104,6 @@ public override IJsonModel<object> Read(ref Utf8JsonReader reader, Type typeToCo
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);
_options.ResolveProxy(value).Write(writer, _options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;

namespace System.ClientModel.Primitives;

Expand Down Expand Up @@ -32,9 +33,7 @@ public static BinaryData Write<T>(T model, ModelReaderWriterOptions? options = d
throw new ArgumentNullException(nameof(model));
}

options ??= ModelReaderWriterOptions.Json;

return WritePersistable(model, options);
return WritePersistable(model, GetOptions(options));
}

/// <summary>
Expand All @@ -53,13 +52,11 @@ public static BinaryData Write(object model, ModelReaderWriterOptions? options =
throw new ArgumentNullException(nameof(model));
}

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 IPersistableModel<object> iModel)
{
return WritePersistable(iModel, options);
return WritePersistable(iModel, GetOptions(options));
}
else
{
Expand Down Expand Up @@ -89,7 +86,7 @@ public static BinaryData Write<T>(T model, ModelReaderWriterOptions options, Mod
throw new ArgumentNullException(nameof(model));
}

return WritePersistableOrEnumerable(model, options, context);
return WritePersistableOrEnumerable(model, GetOptions(options), context);
}

/// <summary>
Expand All @@ -114,7 +111,7 @@ public static BinaryData Write(object model, ModelReaderWriterOptions options, M
throw new ArgumentNullException(nameof(model));
}

return WritePersistableOrEnumerable(model, options, context);
return WritePersistableOrEnumerable(model, GetOptions(options), context);
}

private static BinaryData WritePersistableOrEnumerable<T>(T model, ModelReaderWriterOptions options, ModelReaderWriterContext context)
Expand Down Expand Up @@ -154,7 +151,7 @@ private static BinaryData WritePersistable<T>(IPersistableModel<T> model, ModelR
}
else
{
return model.Write(options);
return options.ResolveProxy(model).Write(options);
}
}

Expand All @@ -173,7 +170,7 @@ private static BinaryData WritePersistable<T>(IPersistableModel<T> model, ModelR
ModelReaderWriterOptions? options = default)
where T : IPersistableModel<T>
{
return ReadInternal<T>(data, options ??= ModelReaderWriterOptions.Json, s_reflectionContext.Value);
return ReadInternal<T>(data, GetOptions(options), s_reflectionContext.Value);
}

/// <summary>
Expand All @@ -199,7 +196,7 @@ private static BinaryData WritePersistable<T>(IPersistableModel<T> model, ModelR
throw new ArgumentNullException(nameof(context));
}

return ReadInternal<T>(data, options, context);
return ReadInternal<T>(data, GetOptions(options), context);
}

/// <summary>
Expand All @@ -219,7 +216,7 @@ private static BinaryData WritePersistable<T>(IPersistableModel<T> model, ModelR
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type returnType,
ModelReaderWriterOptions? options = default)
{
return ReadInternal(data, returnType, options ??= ModelReaderWriterOptions.Json, s_reflectionContext.Value);
return ReadInternal(data, returnType, GetOptions(options), s_reflectionContext.Value);
}

/// <summary>
Expand Down Expand Up @@ -247,7 +244,7 @@ private static BinaryData WritePersistable<T>(IPersistableModel<T> model, ModelR
throw new ArgumentNullException(nameof(context));
}

return ReadInternal(data, returnType, options, context);
return ReadInternal(data, returnType, GetOptions(options), context);
}

private static T? ReadInternal<T>(BinaryData data, ModelReaderWriterOptions options, ModelReaderWriterContext context)
Expand Down Expand Up @@ -277,7 +274,15 @@ private static BinaryData WritePersistable<T>(IPersistableModel<T> model, ModelR
}
else if (returnObj is IPersistableModel<object> persistableModel)
{
return persistableModel.Create(data, options);
if (ShouldWriteAsJson(persistableModel, options, out IJsonModel<object>? jsonModel))
{
Utf8JsonReader reader = new(data);
return options.ResolveProxy(jsonModel).Create(ref reader, options);
}
else
{
return options.ResolveProxy(persistableModel).Create(data, options);
}
}
else
{
Expand Down Expand Up @@ -305,4 +310,16 @@ internal static bool ShouldWriteAsJson<T>(IPersistableModel<T> model, ModelReade
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsJsonFormatRequested<T>(IPersistableModel<T> model, ModelReaderWriterOptions options)
=> options.Format == "J" || (options.Format == "W" && model.GetFormatFromOptions(options) == "J");

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ModelReaderWriterOptions GetOptions(ModelReaderWriterOptions? options)
{
if (options is null)
return ModelReaderWriterOptions.Json;

if (options.IsCoreOwned)
return options;

return options.HasProxies ? new ModelReaderWriterOptions(options) : options;
}
}
Loading
Loading