// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
-
using Xunit;
namespace System.Text.Json.Serialization.Tests
{
- public static class ContinuationTests
+ public static partial class ContinuationTests
{
- // To hit all possible continuation positions inside the tested object,
- // the outer-class padding needs to be between 5 and 116 bytes long.
-
- // {"S":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","C":{"A":null,"B":"Hello","C":42,"D":null,"E":3.14E+17,"F":null,"G":true,"H":null,"I":[42,17],"J":{"A":null,"B":7}}}
- // |<------------------------------------------------128 byte buffer------------------------------------------------------------->|
- // {"S":"xxxxx","C":{"A":"Hello","B":null,"C":42,"D":null,"E":3.14E+17,"F":null,"G":true,"H":null,"I":[42,17],"J":{"A":null,"B":7}}}
-
- private const int MinPaddingLength = 5;
- private const int MaxPaddingLength = 116;
-
- private static IEnumerable<int> ContinuationPaddingLengths
- => Enumerable.Range(MinPaddingLength, MaxPaddingLength - MinPaddingLength + 1);
+ private static readonly (Func<string, string>, Func<string, int>, int)[] s_payloadTweaks = new (Func<string, string>, Func<string, int>, int)[]
+ {
+ (payload => payload, payload => -1, 0),
+ (payload => payload.Replace("null", "nullX"), payload => payload.IndexOf("nullX"), "null".Length),
+ (payload => payload.Replace("true", "trueX"), payload => payload.IndexOf("trueX"), "true".Length),
+ (payload => payload.Replace("false", "falseX"), payload => payload.IndexOf("falseX"), "false".Length),
+ (payload => payload.Replace("E+17", "E+-17"), payload => payload.IndexOf("E+-17"), "E+".Length)
+ };
+
+ private static IEnumerable<(ITestObject, INestedObject)> TestObjects
+ => new (ITestObject, INestedObject)[]
+ {
+ (new TestClass<NestedClass>(), new NestedClass()),
+ (new TestClass<NestedValueType>(), new NestedValueType()),
+ (new TestValueType<NestedClass>(), new NestedClass()),
+ (new TestValueType<NestedValueType>(), new NestedValueType()),
+ (new TestClass<NestedClassWithParamCtor>(), new NestedClassWithParamCtor(null)),
+ (new DictionaryTestClass<NestedClass>(), new NestedClass()),
+ };
private static IEnumerable<bool> IgnoreNullValues
=> new[] { true, false };
- [Theory]
- [MemberData(nameof(TestData))]
- [ActiveIssue("https://github.com/dotnet/runtime/issues/42677", platforms: TestPlatforms.Windows, runtimes: TestRuntimes.Mono)]
- public static async Task ContinuationShouldWorkAtAnyPosition_Class_Class(int paddingLength, bool ignoreNullValues)
+ private static IEnumerable<bool> WriteIndented
+ => new[] { true, false };
+
+ private static IEnumerable<object[]> TestData(bool enumeratePayloadTweaks)
{
- var stream = new MemoryStream();
+ // The serialized json gets padded with leading ' ' chars. The length of the
+ // incrementing paddings, leads to continuations at every position of the payload.
+ // The complete strings (padding + payload) are then passed to the test method.
+
+ // <------min-padding------>[{--payload--}] min-padding = buffer - payload + 1
+ // <-----------2^n byte buffer----------->
+ // <-------------max-padding------------>[{--payload--}] max-padding = buffer - 1
+
+ foreach ((ITestObject TestObject, INestedObject Nested) in TestObjects.Take(enumeratePayloadTweaks ? 1 : TestObjects.Count()))
{
- var obj = new Outer<TestClass<NestedClass>>
+ Type testObjectType = TestObject.GetType();
+ TestObject.Initialize(Nested);
+
+ foreach (bool writeIndented in WriteIndented)
{
- S = new string('x', paddingLength),
- C = new()
+ string payload = JsonSerializer.Serialize(TestObject, testObjectType, new JsonSerializerOptions { WriteIndented = writeIndented });
+
+ foreach ((Func<string, string> Tweak, Func<string, int> Position, int Offset) tweak in enumeratePayloadTweaks ? s_payloadTweaks.Skip(1) : s_payloadTweaks.Take(1))
{
- A = "Hello",
- B = null,
- C = 42,
- D = null,
- E = 3.14e+17f,
- F = null,
- G = true,
- H = null,
- I = new int[] {42, 17},
- J = new()
+ string tweaked = tweak.Tweak(payload);
+
+ // Wrap the payload inside an array to have something to read before/after.
+ tweaked = '[' + tweaked + ']';
+ Type arrayType = Type.GetType(testObjectType.FullName + "[]");
+
+ (int Line, int Col) failurePosition = GetExpectedFailure(tweaked, tweak.Position(tweaked), tweak.Offset);
+
+ // Determine the DefaultBufferSize that is required to contain the complete json.
+ int bufferSize = 16;
+ while (tweaked.Length > bufferSize)
{
- A = null,
- B = 7,
+ bufferSize *= 2;
+ }
+ int minPaddingLength = bufferSize - tweaked.Length + 1;
+ int maxPaddingLength = bufferSize - 1;
+
+ foreach (int length in Enumerable.Range(minPaddingLength, maxPaddingLength - minPaddingLength + 1))
+ {
+ (int Line, int Col) paddedFailurePosition = failurePosition;
+ if (failurePosition != default && failurePosition.Line == 0)
+ paddedFailurePosition = (failurePosition.Line, failurePosition.Col + length);
+
+ foreach (bool ignoreNull in IgnoreNullValues)
+ {
+ yield return new object[]
+ {
+ new string(' ', length) + tweaked,
+ bufferSize,
+ arrayType,
+ ignoreNull,
+ paddedFailurePosition
+ };
+ }
}
}
- };
- await JsonSerializer.SerializeAsync(stream, obj, new JsonSerializerOptions { Converters = { new OuterConverter<TestClass<NestedClass>>() } });
+ }
}
- stream.Position = 0;
+ static (int Line, int Col) GetExpectedFailure(string payload, int position, int offset)
{
- var readOptions = new JsonSerializerOptions
+ if (position < 0)
+ return default;
+
+ position += offset;
+ ReadOnlySpan<byte> utf8 = Encoding.UTF8.GetBytes(payload);
+ utf8 = utf8.Slice(0, position);
+ int positionInLine = position;
+ int lastNewLine;
+ int newLineCount = 0;
+ while ((lastNewLine = utf8.LastIndexOf((byte)'\n')) >= 0)
{
- DefaultBufferSize = 128,
- IgnoreNullValues = ignoreNullValues,
- };
+ if (newLineCount == 0)
+ positionInLine -= lastNewLine + 1;
+ newLineCount++;
+ utf8 = utf8.Slice(0, lastNewLine);
+ }
- Outer<TestClass<NestedClass>> obj = await JsonSerializer.DeserializeAsync<Outer<TestClass<NestedClass>>>(stream, readOptions);
-
- Assert.Equal(new string('x', paddingLength), obj.S);
- Assert.Equal("Hello", obj.C.A);
- Assert.Null(obj.C.B);
- Assert.Equal(42, obj.C.C);
- Assert.Null(obj.C.D);
- Assert.Equal(3.14e17f, obj.C.E);
- Assert.Null(obj.C.F);
- Assert.True(obj.C.G);
- Assert.Null(obj.C.H);
- Assert.Collection(obj.C.I, v => Assert.Equal(42, v), v => Assert.Equal(17, v));
- Assert.NotNull(obj.C.J);
- Assert.Null(obj.C.J.A);
- Assert.Equal(7, obj.C.J.B);
+ return (newLineCount, positionInLine);
}
}
[Theory]
- [MemberData(nameof(TestData))]
+ [MemberData(nameof(TestData), /* enumeratePayloadTweaks: */ false)]
[ActiveIssue("https://github.com/dotnet/runtime/issues/42677", platforms: TestPlatforms.Windows, runtimes: TestRuntimes.Mono)]
- public static async Task ContinuationShouldWorkAtAnyPosition_Class_ValueType(int paddingLength, bool ignoreNullValues)
+ public static async Task ShouldWorkAtAnyPosition_Stream(
+ string json,
+ int bufferSize,
+ Type type,
+ bool ignoreNullValues,
+ (int Line, int Column) expectedFailure)
{
- var stream = new MemoryStream();
- {
- var obj = new Outer<TestClass<NestedValueType>>
- {
- S = new string('x', paddingLength),
- C = new()
- {
- A = "Hello",
- B = null,
- C = 42,
- D = null,
- E = 3.14e+17f,
- F = null,
- G = true,
- H = null,
- I = new int[] { 42, 17 },
- J = new()
- {
- A = null,
- B = 7,
- }
- }
- };
- await JsonSerializer.SerializeAsync(stream, obj, new JsonSerializerOptions { Converters = { new OuterConverter<TestClass<NestedClass>>() } });
- }
-
- stream.Position = 0;
+ var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
{
var readOptions = new JsonSerializerOptions
{
- DefaultBufferSize = 128,
+ DefaultBufferSize = bufferSize,
IgnoreNullValues = ignoreNullValues,
};
- Outer<TestClass<NestedValueType>> obj = await JsonSerializer.DeserializeAsync<Outer<TestClass<NestedValueType>>>(stream, readOptions);
-
- Assert.Equal(new string('x', paddingLength), obj.S);
- Assert.Equal("Hello", obj.C.A);
- Assert.Null(obj.C.B);
- Assert.Equal(42, obj.C.C);
- Assert.Null(obj.C.D);
- Assert.Equal(3.14e17f, obj.C.E);
- Assert.Null(obj.C.F);
- Assert.True(obj.C.G);
- Assert.Null(obj.C.H);
- Assert.Collection(obj.C.I, v => Assert.Equal(42, v), v => Assert.Equal(17, v));
- Assert.Null(obj.C.J.A);
- Assert.Equal(7, obj.C.J.B);
+ var array = (ITestObject[])await JsonSerializer.DeserializeAsync(stream, type, readOptions);
+
+ Assert.NotNull(array);
+ Assert.Equal(1, array.Length);
+ array[0].Verify();
}
+ Assert.Equal(default, expectedFailure);
}
[Theory]
- [MemberData(nameof(TestData))]
+ [MemberData(nameof(TestData), /* enumeratePayloadTweaks: */ true)]
[ActiveIssue("https://github.com/dotnet/runtime/issues/42677", platforms: TestPlatforms.Windows, runtimes: TestRuntimes.Mono)]
- public static async Task ContinuationShouldWorkAtAnyPosition_ValueType_Class(int paddingLength, bool ignoreNullValues)
+ public static async Task InvalidJsonShouldFailAtAnyPosition_Stream(
+ string json,
+ int bufferSize,
+ Type type,
+ bool ignoreNullValues,
+ (int Line, int Column) expectedFailure)
{
- var stream = new MemoryStream();
+ if (expectedFailure == default)
{
- var obj = new Outer<TestValueType<NestedClass>>
- {
- S = new string('x', paddingLength),
- C = new()
- {
- A = "Hello",
- B = null,
- C = 42,
- D = null,
- E = 3.14e+17f,
- F = null,
- G = true,
- H = null,
- I = new int[] { 42, 17 },
- J = new()
- {
- A = null,
- B = 7,
- }
- }
- };
- await JsonSerializer.SerializeAsync(stream, obj, new JsonSerializerOptions { Converters = { new OuterConverter<TestClass<NestedClass>>() } });
+ // The tweak didn't find something to tweak in the payload
+ return;
}
- stream.Position = 0;
+ var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
{
var readOptions = new JsonSerializerOptions
{
- DefaultBufferSize = 128,
+ DefaultBufferSize = bufferSize,
IgnoreNullValues = ignoreNullValues,
};
- Outer<TestValueType<NestedClass>> obj = await JsonSerializer.DeserializeAsync<Outer<TestValueType<NestedClass>>>(stream, readOptions);
-
- Assert.Equal(new string('x', paddingLength), obj.S);
- Assert.Equal("Hello", obj.C.A);
- Assert.Null(obj.C.B);
- Assert.Equal(42, obj.C.C);
- Assert.Null(obj.C.D);
- Assert.Equal(3.14e17f, obj.C.E);
- Assert.Null(obj.C.F);
- Assert.True(obj.C.G);
- Assert.Null(obj.C.H);
- Assert.Collection(obj.C.I, v => Assert.Equal(42, v), v => Assert.Equal(17, v));
- Assert.NotNull(obj.C.J);
- Assert.Null(obj.C.J.A);
- Assert.Equal(7, obj.C.J.B);
+ JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await JsonSerializer.DeserializeAsync(stream, type, readOptions));
+ Assert.Equal(expectedFailure.Line, ex.LineNumber);
+ Assert.Equal(expectedFailure.Column, ex.BytePositionInLine);
}
}
[Theory]
- [MemberData(nameof(TestData))]
+ [MemberData(nameof(TestData), /* enumeratePayloadTweaks: */ false)]
[ActiveIssue("https://github.com/dotnet/runtime/issues/42677", platforms: TestPlatforms.Windows, runtimes: TestRuntimes.Mono)]
- public static async Task ContinuationShouldWorkAtAnyPosition_ValueType_ValueType(int paddingLength, bool ignoreNullValues)
+ public static void ShouldWorkAtAnyPosition_Sequence(
+ string json,
+ int bufferSize,
+ Type type,
+ bool ignoreNullValues,
+ (int Line, int Column) expectedFailure)
{
- var stream = new MemoryStream();
- {
- var obj = new Outer<TestValueType<NestedValueType>>
- {
- S = new string('x', paddingLength),
- C = new()
- {
- A = "Hello",
- B = null,
- C = 42,
- D = null,
- E = 3.14e+17f,
- F = null,
- G = true,
- H = null,
- I = new int[] { 42, 17 },
- J = new()
- {
- A = null,
- B = 7,
- }
- }
- };
- await JsonSerializer.SerializeAsync(stream, obj, new JsonSerializerOptions { Converters = { new OuterConverter<TestClass<NestedClass>>() } });
- }
+ var readOptions = new JsonSerializerOptions { IgnoreNullValues = ignoreNullValues, };
- stream.Position = 0;
- {
- var readOptions = new JsonSerializerOptions
- {
- DefaultBufferSize = 128,
- IgnoreNullValues = ignoreNullValues,
- };
+ var chunk = new Chunk(json, bufferSize);
+ var sequence = new ReadOnlySequence<byte>(chunk, 0, chunk.Next, chunk.Next.Memory.Length);
- Outer<TestValueType<NestedValueType>> obj = await JsonSerializer.DeserializeAsync<Outer<TestValueType<NestedValueType>>>(stream, readOptions);
-
- Assert.Equal(new string('x', paddingLength), obj.S);
- Assert.Equal("Hello", obj.C.A);
- Assert.Null(obj.C.B);
- Assert.Equal(42, obj.C.C);
- Assert.Null(obj.C.D);
- Assert.Equal(3.14e17f, obj.C.E);
- Assert.Null(obj.C.F);
- Assert.True(obj.C.G);
- Assert.Null(obj.C.H);
- Assert.Collection(obj.C.I, v => Assert.Equal(42, v), v => Assert.Equal(17, v));
- Assert.Null(obj.C.J.A);
- Assert.Equal(7, obj.C.J.B);
- }
+ var reader = new Utf8JsonReader(sequence);
+ var array = (ITestObject[])JsonSerializer.Deserialize(ref reader, type, readOptions);
+
+ Assert.NotNull(array);
+ Assert.Equal(1, array.Length);
+ array[0].Verify();
+ Assert.Equal(default, expectedFailure);
}
[Theory]
- [MemberData(nameof(TestData))]
+ [MemberData(nameof(TestData), /* enumeratePayloadTweaks: */ true)]
[ActiveIssue("https://github.com/dotnet/runtime/issues/42677", platforms: TestPlatforms.Windows, runtimes: TestRuntimes.Mono)]
- public static async Task ContinuationShouldWorkAtAnyPosition_ClassWithParamCtor_Class(int paddingLength, bool ignoreNullValues)
+ public static void InvalidJsonShouldFailAtAnyPosition_Sequence(
+ string json,
+ int bufferSize,
+ Type type,
+ bool ignoreNullValues,
+ (int Line, int Column) expectedFailure)
{
- var stream = new MemoryStream();
+ if (expectedFailure == default)
{
- var obj = new Outer<TestClassWithParamCtor<NestedClassWithParamCtor>>
- {
- S = new string('x', paddingLength),
- C = new(null, 42, null, 3.14e+17f, null, true, null, new int[] { 42, 17 })
- {
- A = "Hello",
- J = new(null)
- {
- B = 7,
- },
- },
- };
- await JsonSerializer.SerializeAsync(stream, obj, new JsonSerializerOptions { Converters = { new OuterConverter<TestClass<NestedClass>>() } });
+ // The tweak didn't find something to tweak in the payload
+ return;
}
- stream.Position = 0;
- {
- var readOptions = new JsonSerializerOptions
- {
- DefaultBufferSize = 128,
- IgnoreNullValues = ignoreNullValues,
- };
+ var readOptions = new JsonSerializerOptions { IgnoreNullValues = ignoreNullValues, };
- Outer<TestClassWithParamCtor<NestedClassWithParamCtor>> obj = await JsonSerializer.DeserializeAsync<Outer<TestClassWithParamCtor<NestedClassWithParamCtor>>>(stream, readOptions);
-
- Assert.Equal(new string('x', paddingLength), obj.S);
- Assert.Equal("Hello", obj.C.A);
- Assert.Null(obj.C.B);
- Assert.Equal(42, obj.C.C);
- Assert.Null(obj.C.D);
- Assert.Equal(3.14e17f, obj.C.E);
- Assert.Null(obj.C.F);
- Assert.True(obj.C.G);
- Assert.Null(obj.C.H);
- Assert.Collection(obj.C.I, v => Assert.Equal(42, v), v => Assert.Equal(17, v));
- Assert.Null(obj.C.J.A);
- Assert.Equal(7, obj.C.J.B);
- }
+ var chunk = new Chunk(json, bufferSize);
+ var sequence = new ReadOnlySequence<byte>(chunk, 0, chunk.Next, chunk.Next.Memory.Length);
+
+ JsonException ex = Assert.Throws<JsonException>(() =>
+ {
+ var reader = new Utf8JsonReader(sequence);
+ JsonSerializer.Deserialize(ref reader, type, readOptions);
+ });
+ Assert.Equal(expectedFailure.Line, ex.LineNumber);
+ Assert.Equal(expectedFailure.Column, ex.BytePositionInLine);
}
- private static IEnumerable<object[]> TestData()
+ private class Chunk : ReadOnlySequenceSegment<byte>
{
- foreach (int length in ContinuationPaddingLengths)
+ public Chunk(string json, int firstSegmentLength)
{
- foreach (bool ignore in IgnoreNullValues)
+ Memory<byte> bytes = Encoding.UTF8.GetBytes(json);
+ Memory = bytes.Slice(0, firstSegmentLength);
+ RunningIndex = 0;
+ Next = new Chunk()
{
- yield return new object[] { length, ignore };
- }
+ Memory = bytes.Slice(firstSegmentLength),
+ RunningIndex = firstSegmentLength,
+ Next = null,
+ };
}
+ private Chunk()
+ { }
+ }
+
+ private interface ITestObject
+ {
+ void Initialize(INestedObject nested);
+ void Verify();
}
- private class Outer<TTest>
+ private interface INestedObject
{
- public string S { get; set; }
- public TTest C { get; set; }
+ void Initialize();
+ void Verify();
}
- private class TestClass<TNested>
+ private class TestClass<TNested> : ITestObject where TNested : INestedObject
{
public string A { get; set; }
public string B { get; set; }
public int C { get; set; }
public int? D { get; set; }
public float E { get; set; }
- public float? F { get; set; }
public bool G { get; set; }
- public bool? H { get; set; }
public int[] I { get; set; }
public TNested J { get; set; }
- }
- private class TestClassWithParamCtor<TNested> : TestClass<TNested>
- {
- public TestClassWithParamCtor(string b, int c, int? d, float e, float? f, bool g, bool? h, int[] i)
- => (B, C, D, E, F, G, H, I) = (b, c, d, e, f, g, h, i);
+ void ITestObject.Initialize(INestedObject nested)
+ {
+ A = "Hello";
+ B = null;
+ C = 42;
+ D = null;
+ E = 3.14e+17f;
+ G = true;
+ I = new int[] { 42, 17 };
+ nested.Initialize();
+ J = (TNested)nested;
+ }
+
+ void ITestObject.Verify()
+ {
+ Assert.Equal("Hello", A);
+ Assert.Null(B);
+ Assert.Equal(42, C);
+ Assert.Null(D);
+ Assert.Equal(3.14e17f, E);
+ Assert.True(G);
+ Assert.NotNull(I);
+ Assert.True(I.SequenceEqual(new[] { 42, 17 }));
+ Assert.NotNull(J);
+ J.Verify();
+ }
}
- private class TestValueType<TNested>
+ private class TestValueType<TNested> : ITestObject where TNested : INestedObject
{
public string A { get; set; }
public string B { get; set; }
public int C { get; set; }
public int? D { get; set; }
public float E { get; set; }
- public float? F { get; set; }
public bool G { get; set; }
- public bool? H { get; set; }
public int[] I { get; set; }
public TNested J { get; set; }
- }
- private class NestedClass
- {
- public string A { get; set; }
- public int B { get; set; }
- }
+ void ITestObject.Initialize(INestedObject nested)
+ {
+ A = "Hello";
+ B = null;
+ C = 42;
+ D = null;
+ E = 3.14e+17f;
+ G = true;
+ I = new int[] { 42, 17 };
+ nested.Initialize();
+ J = (TNested)nested;
+ }
- private class NestedClassWithParamCtor : NestedClass
- {
- public NestedClassWithParamCtor(string a)
- => A = a;
+ void ITestObject.Verify()
+ {
+ Assert.Equal("Hello", A);
+ Assert.Null(B);
+ Assert.Equal(42, C);
+ Assert.Null(D);
+ Assert.Equal(3.14e17f, E);
+ Assert.True(G);
+ Assert.NotNull(I);
+ Assert.True(I.SequenceEqual(new[] { 42, 17 }));
+ Assert.NotNull(J);
+ J.Verify();
+ }
}
- private struct NestedValueType
+ private class NestedClass : INestedObject
{
public string A { get; set; }
public int B { get; set; }
- }
- // custom converter to ensure that the padding is written in front of the tested object.
- private class OuterConverter<T> : JsonConverter<Outer<T>>
- {
- public override Outer<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- => throw new NotImplementedException();
+ void INestedObject.Initialize()
+ {
+ A = null;
+ B = 7;
+ }
- public override void Write(Utf8JsonWriter writer, Outer<T> value, JsonSerializerOptions options)
+ void INestedObject.Verify()
{
- writer.WriteStartObject();
- writer.WriteString("S", value.S);
- writer.WritePropertyName("C");
- JsonSerializer.Serialize(writer, value.C, typeof(T), options);
- writer.WriteEndObject();
+ Assert.Null(A);
+ Assert.Equal(7, B);
}
}
- // From https://github.com/dotnet/runtime/issues/42070
- [Theory]
- [InlineData("CustomerSearchApi108KB")]
- [InlineData("CustomerSearchApi107KB")]
- [ActiveIssue("https://github.com/dotnet/runtime/issues/42677", platforms: TestPlatforms.Windows, runtimes: TestRuntimes.Mono)]
- public static async Task ContinuationAtNullToken(string resourceName)
+ private struct NestedValueType : INestedObject
{
- using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(SR.GetResourceString(resourceName))))
+ public string A { get; set; }
+ public int B { get; set; }
+
+ void INestedObject.Initialize()
{
- CustomerCollectionResponse response = await JsonSerializer.DeserializeAsync<CustomerCollectionResponse>(stream, new JsonSerializerOptions { IgnoreNullValues = true });
- Assert.Equal(50, response.Customers.Count);
+ A = null;
+ B = 7;
}
- }
- private class CustomerCollectionResponse
- {
- [JsonPropertyName("customers")]
- public List<Customer> Customers { get; set; }
+ void INestedObject.Verify()
+ {
+ Assert.Null(A);
+ Assert.Equal(7, B);
+ }
}
- private class CustomerAddress
+ private class NestedClassWithParamCtor : NestedClass
{
- [JsonPropertyName("first_name")]
- public string FirstName { get; set; }
-
- [JsonPropertyName("address1")]
- public string Address1 { get; set; }
-
- [JsonPropertyName("phone")]
- public string Phone { get; set; }
-
- [JsonPropertyName("city")]
- public string City { get; set; }
-
- [JsonPropertyName("zip")]
- public string Zip { get; set; }
-
- [JsonPropertyName("province")]
- public string Province { get; set; }
-
- [JsonPropertyName("country")]
- public string Country { get; set; }
-
- [JsonPropertyName("last_name")]
- public string LastName { get; set; }
-
- [JsonPropertyName("address2")]
- public string Address2 { get; set; }
-
- [JsonPropertyName("company")]
- public string Company { get; set; }
-
- [JsonPropertyName("latitude")]
- public float? Latitude { get; set; }
-
- [JsonPropertyName("longitude")]
- public float? Longitude { get; set; }
-
- [JsonPropertyName("name")]
- public string Name { get; set; }
-
- [JsonPropertyName("country_code")]
- public string CountryCode { get; set; }
-
- [JsonPropertyName("province_code")]
- public string ProvinceCode { get; set; }
+ public NestedClassWithParamCtor(string a)
+ => A = a;
}
- private class Customer
+ private class DictionaryTestClass<TNested> : ITestObject where TNested : INestedObject
{
- [JsonPropertyName("id")]
- public long Id { get; set; }
-
- [JsonPropertyName("email")]
- public string Email { get; set; }
-
- [JsonPropertyName("accepts_marketing")]
- public bool AcceptsMarketing { get; set; }
-
- [JsonPropertyName("created_at")]
- public DateTimeOffset? CreatedAt { get; set; }
-
- [JsonPropertyName("updated_at")]
- public DateTimeOffset? UpdatedAt { get; set; }
-
- [JsonPropertyName("first_name")]
- public string FirstName { get; set; }
+ public Dictionary<string, TNested> A { get; set; }
- [JsonPropertyName("last_name")]
- public string LastName { get; set; }
-
- [JsonPropertyName("orders_count")]
- public int OrdersCount { get; set; }
-
- [JsonPropertyName("state")]
- public string State { get; set; }
-
- [JsonPropertyName("total_spent")]
- public string TotalSpent { get; set; }
-
- [JsonPropertyName("last_order_id")]
- public long? LastOrderId { get; set; }
-
- [JsonPropertyName("note")]
- public string Note { get; set; }
-
- [JsonPropertyName("verified_email")]
- public bool VerifiedEmail { get; set; }
-
- [JsonPropertyName("multipass_identifier")]
- public string MultipassIdentifier { get; set; }
-
- [JsonPropertyName("tax_exempt")]
- public bool TaxExempt { get; set; }
-
- [JsonPropertyName("tags")]
- public string Tags { get; set; }
-
- [JsonPropertyName("last_order_name")]
- public string LastOrderName { get; set; }
-
- [JsonPropertyName("default_address")]
- public CustomerAddress DefaultAddress { get; set; }
+ void ITestObject.Initialize(INestedObject nested)
+ {
+ nested.Initialize();
+ A = new() { { "a", (TNested)nested }, { "b", (TNested)nested } };
+ }
- [JsonPropertyName("addresses")]
- public IList<CustomerAddress> Addresses { get; set; }
+ void ITestObject.Verify()
+ {
+ Assert.NotNull(A);
+ Assert.Collection(A,
+ kv =>
+ {
+ Assert.Equal("a", kv.Key);
+ kv.Value.Verify();
+ },
+ kv =>
+ {
+ Assert.Equal("b", kv.Key);
+ kv.Value.Verify();
+ });
+ }
}
}
}