|
1 | 1 | using System; |
| 2 | +using System.Collections.Generic; |
2 | 3 | using System.Linq; |
3 | 4 | using System.Net; |
| 5 | +using System.Security.Cryptography; |
4 | 6 | using System.Text; |
| 7 | +using System.Text.Json; |
5 | 8 | using ManagedCode.Communication.Orleans.Converters; |
6 | 9 | using ManagedCode.Communication.Orleans.Extensions; |
7 | 10 | using ManagedCode.Communication.Orleans.Surrogates; |
8 | 11 | using ManagedCode.Communication.Tests.Orleans.Fixtures; |
9 | 12 | using Microsoft.Extensions.DependencyInjection; |
10 | 13 | using Microsoft.Extensions.Options; |
11 | 14 | using Newtonsoft.Json; |
| 15 | +using Newtonsoft.Json.Serialization; |
12 | 16 | using Orleans.Serialization; |
13 | 17 | using Orleans.Storage; |
14 | 18 | using Orleans.TestingHost; |
@@ -114,6 +118,83 @@ public void StandaloneRegistrationMustBeIdempotentAndPreserveNativeJson() |
114 | 118 | serializer.Deserialize<Result<int>>(serializer.Serialize(Result<int>.Succeed(42))).IsSuccess.ShouldBeTrue(); |
115 | 119 | } |
116 | 120 |
|
| 121 | + private const string NestedJson = "{\"value\":[17,{\"label\":\"雪\",\"decimal\":1.00}],\"enabled\":true}"; |
| 122 | + private const string JsonString = "\"text\""; |
| 123 | + private const string JsonNumber = "1.00"; |
| 124 | + private const string JsonNull = "null"; |
| 125 | + private const string JsonBoolean = "true"; |
| 126 | + private const string ElementKey = "element"; |
| 127 | + private const string InvalidElementJson = "{\"json\":\"{\"}"; |
| 128 | + private const string MissingElementJson = "{}"; |
| 129 | + private const string NullElementJson = "{\"json\":null}"; |
| 130 | + |
| 131 | + [Test] |
| 132 | + [Arguments(NestedJson)] |
| 133 | + [Arguments(JsonString)] |
| 134 | + [Arguments(JsonNumber)] |
| 135 | + [Arguments(JsonNull)] |
| 136 | + [Arguments(JsonBoolean)] |
| 137 | + public void JsonElementsMustRetainTypedAndObjectValuedStorageFingerprints(string json) |
| 138 | + { |
| 139 | + System.Text.Json.JsonElement expected; |
| 140 | + using (var document = JsonDocument.Parse(json)) |
| 141 | + { |
| 142 | + expected = document.RootElement.Clone(); |
| 143 | + } |
| 144 | + var payload = new JsonElementStoredState |
| 145 | + { |
| 146 | + Element = expected, |
| 147 | + Values = new Dictionary<string, object?> { [ElementKey] = expected }, |
| 148 | + Update = Result<object>.Succeed(expected) |
| 149 | + }; |
| 150 | + var serializer = Serializer(); |
| 151 | + var restored = serializer.Deserialize<JsonElementStoredState>(serializer.Serialize(payload))!; |
| 152 | + var dictionaryValue = restored.Values[ElementKey].ShouldBeOfType<JsonElement>(); |
| 153 | + var resultValue = restored.Update.Value.ShouldBeOfType<JsonElement>(); |
| 154 | + restored.Update.IsSuccess.ShouldBeTrue(); |
| 155 | + foreach (var actual in new[] { restored.Element, dictionaryValue, resultValue }) |
| 156 | + { |
| 157 | + actual.ValueKind.ShouldBe(expected.ValueKind); |
| 158 | + Fingerprint(actual).ShouldBe(Fingerprint(expected)); |
| 159 | + actual.GetRawText().ShouldBe(System.Text.Json.JsonSerializer.Serialize(expected)); |
| 160 | + } |
| 161 | + var result = Result<JsonElementStoredState>.Succeed(payload); |
| 162 | + var restoredResult = serializer.Deserialize<Result<JsonElementStoredState>>(serializer.Serialize(result)); |
| 163 | + restoredResult.IsSuccess.ShouldBeTrue(); |
| 164 | + Fingerprint(restoredResult.Value.Element).ShouldBe(Fingerprint(expected)); |
| 165 | + } |
| 166 | + |
| 167 | + [Test] |
| 168 | + [Arguments(InvalidElementJson)] |
| 169 | + [Arguments(MissingElementJson)] |
| 170 | + [Arguments(NullElementJson)] |
| 171 | + public void MalformedStoredJsonElementsMustFailClosed(string json) => |
| 172 | + Should.Throw<JsonSerializationException>(() => Serializer().Deserialize<JsonElement>(new BinaryData(json))); |
| 173 | + |
| 174 | + [Test] |
| 175 | + public void JsonElementRegistrationMustPreserveConfiguredContractsAndRemainIdempotent() |
| 176 | + { |
| 177 | + var configured = new CamelCasePropertyNamesContractResolver(); |
| 178 | + var services = new ServiceCollection(); |
| 179 | + services.Configure<OrleansJsonSerializerOptions>(options => options.JsonSerializerSettings.ContractResolver = configured); |
| 180 | + using var provider = services.AddCommunicationOrleansJsonStorage().AddCommunicationOrleansJsonStorage().BuildServiceProvider(); |
| 181 | + var settings = provider.GetRequiredService<IOptions<OrleansJsonSerializerOptions>>().Value; |
| 182 | + var resolver = settings.JsonSerializerSettings.ContractResolver.ShouldBeOfType<CommunicationJsonElementContractResolver>(); |
| 183 | + resolver.ResolveContract(typeof(StoredState)).ShouldBeSameAs(configured.ResolveContract(typeof(StoredState))); |
| 184 | + settings.JsonSerializerSettings.Converters.OfType<CommunicationResultJsonConverter>().Count().ShouldBe(1); |
| 185 | + } |
| 186 | + |
| 187 | + private static string Fingerprint(JsonElement element) => Convert.ToHexString( |
| 188 | + SHA256.HashData(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(element))); |
| 189 | + |
| 190 | + [global::Orleans.GenerateSerializer] |
| 191 | + internal sealed class JsonElementStoredState |
| 192 | + { |
| 193 | + [global::Orleans.Id(0)] public JsonElement Element { get; init; } |
| 194 | + [global::Orleans.Id(1)] public Dictionary<string, object?> Values { get; init; } = new(); |
| 195 | + [global::Orleans.Id(2)] public Result<object> Update { get; init; } |
| 196 | + } |
| 197 | + |
117 | 198 | [global::Orleans.GenerateSerializer] |
118 | 199 | internal sealed class StoredState |
119 | 200 | { |
|
0 commit comments