|
| 1 | +using System.Reflection; |
| 2 | +using System.Text.Json; |
| 3 | +using Microsoft.CodeAnalysis; |
| 4 | +using Microsoft.CodeAnalysis.CSharp; |
| 5 | +using MetaObjects.Codegen; |
| 6 | +using MetaObjects.Codegen.Generators; |
| 7 | +using MetaObjects.Loader; |
| 8 | +using Xunit; |
| 9 | + |
| 10 | +namespace MetaObjects.Codegen.Tests; |
| 11 | + |
| 12 | +/// <summary> |
| 13 | +/// A value-object member that is null is omitted on the wire via |
| 14 | +/// <c>[JsonIgnore(WhenWritingNull)]</c>, but System.Text.Json throws InvalidOperationException |
| 15 | +/// the first time it builds type info for a type carrying that condition on a NON-nullable value |
| 16 | +/// type (<c>double</c>, <c>DateTimeOffset</c>), so every request that touched the owning entity |
| 17 | +/// answered 500. The condition belongs on the nullable members only. |
| 18 | +/// </summary> |
| 19 | +public class ValueObjectWireIgnoreTests |
| 20 | +{ |
| 21 | + private const string Model = """ |
| 22 | + { "metadata.root": { "package": "acme", "children": [ |
| 23 | + { "object.entity": { "name": "Leg", "children": [ |
| 24 | + { "source.rdb": { "@table": "legs" } }, |
| 25 | + { "field.long": { "name": "id" } }, |
| 26 | + { "field.object": { "name": "telemetry", "@objectRef": "acme::Telemetry", "@storage": "jsonb" } }, |
| 27 | + { "identity.primary": { "@fields": "id" } } |
| 28 | + ]}}, |
| 29 | + { "object.value": { "name": "Telemetry", "children": [ |
| 30 | + { "field.timestamp": { "name": "capturedAt", "@required": true } }, |
| 31 | + { "field.double": { "name": "latitude", "@required": true } }, |
| 32 | + { "field.int": { "name": "tempC" } }, |
| 33 | + { "field.boolean": { "name": "live", "@required": true } }, |
| 34 | + { "field.decimal": { "name": "fee", "@required": true, "@precision": 10, "@scale": 2 } }, |
| 35 | + { "field.uuid": { "name": "token", "@required": true } }, |
| 36 | + { "field.string": { "name": "tags", "isArray": true } }, |
| 37 | + { "field.string": { "name": "note" } } |
| 38 | + ]}} |
| 39 | + ]}} |
| 40 | + """; |
| 41 | + |
| 42 | + [Fact] |
| 43 | + public void A_value_object_with_required_value_type_members_round_trips_and_omits_a_null_optional_member() |
| 44 | + { |
| 45 | + var r = new MetaDataLoader().Load([new InMemoryStringSource(Model, id: "model.json")]); |
| 46 | + Assert.Empty(r.Errors); |
| 47 | + var files = new EntityGenerator().Generate(new GenContext |
| 48 | + { |
| 49 | + Entities = r.Root.Objects(), |
| 50 | + Root = r.Root, |
| 51 | + Config = new GenConfig { OutDir = "/tmp", Namespace = "Acme.Generated" }, |
| 52 | + }); |
| 53 | + var trees = files.Select(f => CSharpSyntaxTree.ParseText(f.Content, new CSharpParseOptions(LanguageVersion.CSharp12), path: f.Path)); |
| 54 | + var comp = CSharpCompilation.Create("vo_wire_ignore_" + Guid.NewGuid().ToString("N"), trees, |
| 55 | + DbContextCompileTests.BuildReferences(), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); |
| 56 | + using var ms = new MemoryStream(); |
| 57 | + var emit = comp.Emit(ms); |
| 58 | + Assert.True(emit.Success, string.Join("\n", emit.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error))); |
| 59 | + var telemetry = Assembly.Load(ms.ToArray()).GetTypes().Single(t => t.Name == "Telemetry"); |
| 60 | + |
| 61 | + const string body = """ |
| 62 | + {"capturedAt":"2026-03-04T05:06:07Z","latitude":51.5,"live":true,"fee":"12.50","token":"3f2504e0-4f89-11d3-9a0c-0305e82c3301"} |
| 63 | + """; |
| 64 | + var value = JsonSerializer.Deserialize(body, telemetry)!; |
| 65 | + Assert.Equal(51.5, telemetry.GetProperty("Latitude")!.GetValue(value)); |
| 66 | + Assert.Equal(DateTimeOffset.Parse("2026-03-04T05:06:07Z"), telemetry.GetProperty("CapturedAt")!.GetValue(value)); |
| 67 | + Assert.Null(telemetry.GetProperty("TempC")!.GetValue(value)); |
| 68 | + |
| 69 | + var wire = JsonDocument.Parse(JsonSerializer.Serialize(value, telemetry)).RootElement; |
| 70 | + Assert.Equal(51.5, wire.GetProperty("latitude").GetDouble()); |
| 71 | + Assert.Equal(DateTimeOffset.Parse("2026-03-04T05:06:07Z"), wire.GetProperty("capturedAt").GetDateTimeOffset()); |
| 72 | + Assert.True(wire.GetProperty("live").GetBoolean()); |
| 73 | + Assert.False(wire.TryGetProperty("tempC", out _)); |
| 74 | + Assert.False(wire.TryGetProperty("note", out _)); |
| 75 | + } |
| 76 | +} |
0 commit comments