AyCode.Core/AyCode.Core.Tests/JsonExtensionTests.cs

1541 lines
49 KiB
C#

using System.Runtime.Serialization;
using AyCode.Core.Enums;
using AyCode.Core.Extensions;
using AyCode.Core.Interfaces;
using AyCode.Core.Loggers;
using AyCode.Core.Serializers.Jsons;
using Newtonsoft.Json;
using AyCode.Core.Tests.TestModels;
namespace AyCode.Core.Tests;
[TestClass]
public sealed class JsonExtensionTests
{
[TestInitialize]
public void TestInit()
{
TestDataFactory.ResetIdCounter();
}
#region Deep Hierarchy Tests (5 Levels)
[TestMethod]
public void DeepHierarchy_5Levels_MergePreservesAllReferences()
{
// Arrange: Create 5-level deep hierarchy
var order = TestDataFactory.CreateOrder(itemCount: 2, palletsPerItem: 2, measurementsPerPallet: 2, pointsPerMeasurement: 3);
// Store original references at all levels
var originalItem = order.Items[0];
var originalPallet = order.Items[0].Pallets[0];
var originalMeasurement = order.Items[0].Pallets[0].Measurements[0];
var originalPoint = order.Items[0].Pallets[0].Measurements[0].Points[0];
var updateJson = $@"{{
""Id"": {order.Id},
""OrderNumber"": ""ORD-UPDATED"",
""Items"": [{{
""Id"": {originalItem.Id},
""ProductName"": ""Updated-Product"",
""Pallets"": [{{
""Id"": {originalPallet.Id},
""PalletCode"": ""PLT-UPDATED"",
""Measurements"": [{{
""Id"": {originalMeasurement.Id},
""Name"": ""Measurement-UPDATED"",
""Points"": [{{
""Id"": {originalPoint.Id},
""Label"": ""Point-UPDATED"",
""Value"": 999.99
}}]
}}]
}}]
}}]
}}";
// Act
updateJson.JsonTo(order);
// Assert: All references preserved
Assert.AreSame(originalItem, order.Items[0], "Level 2: Item reference must be preserved");
Assert.AreSame(originalPallet, order.Items[0].Pallets[0], "Level 3: Pallet reference must be preserved");
Assert.AreSame(originalMeasurement, order.Items[0].Pallets[0].Measurements[0], "Level 4: Measurement reference must be preserved");
Assert.AreSame(originalPoint, order.Items[0].Pallets[0].Measurements[0].Points[0], "Level 5: Point reference must be preserved");
// Assert: Values updated
Assert.AreEqual("ORD-UPDATED", order.OrderNumber);
Assert.AreEqual("Updated-Product", order.Items[0].ProductName);
Assert.AreEqual("PLT-UPDATED", order.Items[0].Pallets[0].PalletCode);
Assert.AreEqual("Measurement-UPDATED", order.Items[0].Pallets[0].Measurements[0].Name);
Assert.AreEqual("Point-UPDATED", order.Items[0].Pallets[0].Measurements[0].Points[0].Label);
Assert.AreEqual(999.99, order.Items[0].Pallets[0].Measurements[0].Points[0].Value);
}
[TestMethod]
public void DeepHierarchy_5Levels_InsertAndKeepLogic()
{
// Arrange
var order = TestDataFactory.CreateOrder(itemCount: 2, palletsPerItem: 1, measurementsPerPallet: 1, pointsPerMeasurement: 2);
var originalItemCount = order.Items.Count;
var originalItem2 = order.Items[1];
var existingPointId = order.Items[0].Pallets[0].Measurements[0].Points[0].Id;
var updateJson = $@"{{
""Id"": {order.Id},
""Items"": [{{
""Id"": {order.Items[0].Id},
""Pallets"": [{{
""Id"": {order.Items[0].Pallets[0].Id},
""Measurements"": [{{
""Id"": {order.Items[0].Pallets[0].Measurements[0].Id},
""Points"": [
{{ ""Id"": {existingPointId}, ""Label"": ""Updated-Point"" }},
{{ ""Id"": 9999, ""Label"": ""NEW-Point"", ""Value"": 123.45 }}
]
}}]
}}]
}}]
}}";
// Act
updateJson.JsonTo(order);
// Assert
Assert.AreEqual(originalItemCount, order.Items.Count, "Items count should be preserved (KEEP logic)");
Assert.AreSame(originalItem2, order.Items[1], "Second item reference should be preserved");
Assert.IsTrue(order.Items[0].Pallets[0].Measurements[0].Points.Any(p => p.Id == 9999), "New point should be inserted");
}
#endregion
#region Semantic Reference Tests (IId types with $id/$ref)
[TestMethod]
public void SemanticReference_SharedTag_SerializesWithSemanticId()
{
// Arrange
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var order = TestDataFactory.CreateOrder(itemCount: 2, sharedTag: sharedTag);
// Act
var json = order.ToJson();
Console.WriteLine($"Semantic Reference JSON:\n{json}");
// Assert
Assert.IsTrue(json.Contains("\"$id\""), "Should contain $id for IId types");
Assert.IsTrue(json.Contains("\"$ref\""), "Should contain $ref for shared references");
}
[TestMethod]
public void SemanticReference_DeserializeAndMerge_PreservesSharedReferences()
{
// Arrange
var sharedTag = TestDataFactory.CreateTag("OriginalKey");
var order = new TestOrder
{
Id = 1,
OrderNumber = "ORD-001",
PrimaryTag = sharedTag,
SecondaryTag = sharedTag,
Tags = [sharedTag]
};
var originalTagRef = order.PrimaryTag;
var updateJson = @"{
""Id"": 1,
""OrderNumber"": ""ORD-UPDATED"",
""PrimaryTag"": { ""Id"": 1, ""Name"": ""UpdatedKey"" }
}";
// Act
updateJson.JsonTo(order);
// Assert
Assert.AreEqual("ORD-UPDATED", order.OrderNumber);
Assert.AreEqual("UpdatedKey", order.PrimaryTag?.Name);
Assert.AreSame(originalTagRef, order.SecondaryTag, "SecondaryTag reference should be preserved");
}
#endregion
#region Newtonsoft Reference Tests (Non-IId types)
[TestMethod]
public void NewtonsoftReference_SharedMetadata_SerializesWithNumericId()
{
// Arrange
var sharedMeta = TestDataFactory.CreateMetadata(withChild: true);
var order = TestDataFactory.CreateOrder(itemCount: 2, sharedMetadata: sharedMeta);
// Act
var json = order.ToJson();
Console.WriteLine($"Newtonsoft Reference JSON:\n{json}");
// Assert
Assert.IsTrue(json.Contains("\"$id\""), "Should contain $id");
Assert.IsTrue(json.Contains("\"$ref\""), "Should contain $ref for shared references");
}
[TestMethod]
public void NewtonsoftReference_DeepNestedNonId_HandlesCorrectly()
{
// Arrange
var rootMeta = new MetadataInfo
{
Key = "Root",
Value = "RootValue",
ChildMetadata = new MetadataInfo
{
Key = "Child",
Value = "ChildValue",
ChildMetadata = new MetadataInfo { Key = "GrandChild", Value = "GrandChildValue" }
}
};
var order = new TestOrder
{
Id = 1,
OrderNumber = "ORD-001",
OrderMetadata = rootMeta,
AuditMetadata = rootMeta
};
// Act
var json = order.ToJson();
// Assert
Assert.IsTrue(json.Contains("Root"));
Assert.IsTrue(json.Contains("Child"));
Assert.IsTrue(json.Contains("GrandChild"));
Assert.IsTrue(json.Contains("\"$ref\""), "Should contain $ref for duplicate reference");
}
#endregion
#region Hybrid Reference Tests (Mixed IId and Non-IId)
[TestMethod]
public void HybridReference_MixedTypes_BothRefSystemsWork()
{
// Arrange
var sharedTag = TestDataFactory.CreateTag();
var sharedMeta = TestDataFactory.CreateMetadata();
sharedTag.Description = sharedMeta.Key; // Link them
var order = new TestOrder
{
Id = 1,
OrderNumber = "ORD-001",
PrimaryTag = sharedTag,
SecondaryTag = sharedTag,
OrderMetadata = sharedMeta,
AuditMetadata = sharedMeta,
Tags = [sharedTag],
Items = [new TestOrderItem { Id = 10, ProductName = "A", Tag = sharedTag, ItemMetadata = sharedMeta }]
};
var json = order.ToJson();
// Assert
var refCount = json.Split("\"$ref\"").Length - 1;
Assert.IsTrue(refCount >= 2, $"Should have multiple $ref tokens. Found: {refCount}");
}
#endregion
#region NoMerge Collection Tests
[TestMethod]
public void NoMergeCollection_ReplacesEntireCollection()
{
// Arrange
var order = TestDataFactory.CreateOrder(itemCount: 1);
order.NoMergeItems = [
new TestOrderItem { Id = 100, ProductName = "NoMerge-A" },
new TestOrderItem { Id = 101, ProductName = "NoMerge-B" }
];
var originalRef = order.NoMergeItems;
var updateJson = $@"{{
""Id"": {order.Id},
""NoMergeItems"": [
{{ ""Id"": 200, ""ProductName"": ""NoMerge-NEW"" }}
]
}}";
// Act
order.DeepPopulateWithMerge(updateJson);
// Assert
Assert.AreNotSame(originalRef, order.NoMergeItems);
Assert.AreEqual(1, order.NoMergeItems.Count);
Assert.AreEqual(200, order.NoMergeItems[0].Id);
}
#endregion
#region Non-IId Collection Tests
[TestMethod]
public void NonIdCollection_ReplacesContent()
{
// Arrange
var order = new TestOrder
{
Id = 1,
OrderNumber = "ORD-001",
MetadataList = [
new MetadataInfo { Key = "Old-A" },
new MetadataInfo { Key = "Old-B" }
]
};
var updateJson = @"{
""Id"": 1,
""MetadataList"": [
{ ""Key"": ""New-X"" },
{ ""Key"": ""New-Y"" }
]
}";
// Act
order.DeepPopulateWithMerge(updateJson);
// Assert
Assert.AreEqual(2, order.MetadataList.Count);
Assert.IsTrue(order.MetadataList.Any(m => m.Key == "New-X"));
Assert.IsFalse(order.MetadataList.Any(m => m.Key == "Old-A"));
}
#endregion
#region Guid IId Tests
[TestMethod]
public void GuidId_DeepPopulate_ReferencePreserved()
{
var order = new TestGuidOrder
{
Id = Guid.NewGuid(),
Code = "ORD-001",
Items = [
new TestGuidItem { Id = Guid.NewGuid(), Name = "Apple", Qty = 5 },
new TestGuidItem { Id = Guid.NewGuid(), Name = "Orange", Qty = 3 }
]
};
var originalItemsRef = order.Items;
var originalAppleRef = order.Items[0];
var appleId = order.Items[0].Id;
var json = new {
Id = order.Id,
Code = "ORD-UPDATED",
Items = new[] {
new { Id = appleId, Name = "Apple", Qty = 7 },
new { Id = Guid.NewGuid(), Name = "Banana", Qty = 4 }
}
}.ToJson();
json.JsonTo(order);
// List reference preserved
Assert.AreSame(originalItemsRef, order.Items, "List reference must be preserved");
// Apple reference preserved (updated in-place)
Assert.AreSame(originalAppleRef, order.Items[0], "Apple reference must be preserved");
Assert.AreEqual(7, order.Items[0].Qty, "Apple Qty should be updated");
// Count: Apple (updated) + Orange (kept) + Banana (new)
Assert.AreEqual(3, order.Items.Count, "Should have 3 items: Apple updated, Orange kept, Banana added");
Assert.AreEqual("ORD-UPDATED", order.Code);
}
#endregion
#region Round-Trip Serialization Tests
[TestMethod]
public void RoundTrip_DeepHierarchy_PreservesData()
{
// Arrange
var sharedTag = TestDataFactory.CreateTag();
var sharedMeta = TestDataFactory.CreateMetadata(withChild: true);
var order = TestDataFactory.CreateOrder(itemCount: 2, palletsPerItem: 2, sharedTag: sharedTag, sharedMetadata: sharedMeta);
// Act
var json = order.ToJson();
var deserialized = json.JsonTo<TestOrder>();
// Assert
Assert.IsNotNull(deserialized);
Assert.AreEqual(order.Id, deserialized.Id);
Assert.AreEqual(order.OrderNumber, deserialized.OrderNumber);
Assert.AreEqual(order.Items.Count, deserialized.Items.Count);
}
#endregion
#region Primitive Array Tests (SignalR IdMessage pattern)
[TestMethod]
public void PrimitiveArray_BooleanTrue_RoundTrips()
{
var jsonString = (new[] { true }).ToJson();
var result = jsonString.JsonTo(typeof(bool[])) as bool[];
Assert.IsNotNull(result);
Assert.IsTrue(result[0], "Boolean true should deserialize as true!");
}
[TestMethod]
public void PrimitiveArray_AllTypes_RoundTrip()
{
var testCases = new (Type type, object value)[]
{
(typeof(bool), true),
(typeof(int), 42),
(typeof(long), 123456789L),
(typeof(double), 3.14159),
(typeof(decimal), 99.99m),
(typeof(string), "test"),
(typeof(Guid), Guid.NewGuid()),
(typeof(DateTime), DateTime.UtcNow),
(typeof(TestStatus), TestStatus.Processing)
};
foreach (var (type, value) in testCases)
{
var wrapped = Array.CreateInstance(type, 1);
wrapped.SetValue(value, 0);
var json = wrapped.ToJson();
var result = json.JsonTo(type.MakeArrayType()) as Array;
Assert.IsNotNull(result, $"Failed for {type.Name}");
Assert.AreEqual(value, result.GetValue(0), $"Value mismatch for {type.Name}");
}
}
[TestMethod]
public void IdMessage_MultipleParameters_SimulateSignalR()
{
var @params = new (Type t, object v)[] { (typeof(bool), true), (typeof(string), "filter"), (typeof(int), 100) };
foreach (var (type, value) in @params)
{
var wrapped = Array.CreateInstance(type, 1);
wrapped.SetValue(value, 0);
var json = wrapped.ToJson();
var arr = json.JsonTo(type.MakeArrayType()) as Array;
Assert.AreEqual(value, arr?.GetValue(0));
}
}
#endregion
#region WASM Compatibility Tests
[TestMethod]
public void WasmCompat_AcJsonSerializer_SimpleObject()
{
var item = new TestOrderItem { Id = 1, ProductName = "Test", Quantity = 10, UnitPrice = 99.99m, Status = TestStatus.Processing };
var json = AcJsonSerializer.Serialize(item);
Assert.IsTrue(json.Contains("\"Id\":1"));
Assert.IsTrue(json.Contains("\"ProductName\":\"Test\""));
}
[TestMethod]
public void WasmCompat_AcJsonDeserializer_RoundTrip()
{
var original = new TestOrderItem { Id = 42, ProductName = "WASM Test", Quantity = 5, UnitPrice = 25.50m, Status = TestStatus.Shipped };
var json = AcJsonSerializer.Serialize(original);
var deserialized = AcJsonDeserializer.Deserialize<TestOrderItem>(json);
Assert.IsNotNull(deserialized);
Assert.AreEqual(42, deserialized.Id);
Assert.AreEqual("WASM Test", deserialized.ProductName);
Assert.AreEqual(TestStatus.Shipped, deserialized.Status);
}
[TestMethod]
public void WasmCompat_AllPrimitiveTypes()
{
var testData = TestDataFactory.CreatePrimitiveTestData();
var json = AcJsonSerializer.Serialize(testData);
var deserialized = AcJsonDeserializer.Deserialize<PrimitiveTestClass>(json);
Assert.IsNotNull(deserialized);
Assert.AreEqual(testData.IntValue, deserialized.IntValue);
Assert.AreEqual(testData.LongValue, deserialized.LongValue);
Assert.AreEqual(testData.BoolValue, deserialized.BoolValue);
Assert.AreEqual(testData.StringValue, deserialized.StringValue);
Assert.AreEqual(testData.GuidValue, deserialized.GuidValue);
Assert.AreEqual(testData.EnumValue, deserialized.EnumValue);
}
[TestMethod]
public void WasmCompat_EmptyCollections_HandleCorrectly()
{
var order = new TestOrder { Id = 1, OrderNumber = "EMPTY-TEST", Items = [], Tags = [] };
var json = AcJsonSerializer.Serialize(order);
Assert.IsTrue(json.Contains("\"Items\":[]"), "Empty Items should serialize as []");
Assert.IsTrue(json.Contains("\"Tags\":[]"), "Empty Tags should serialize as []");
var deserialized = AcJsonDeserializer.Deserialize<TestOrder>(json);
Assert.IsNotNull(deserialized?.Items);
Assert.AreEqual(0, deserialized.Items.Count);
Assert.IsNotNull(deserialized?.Tags);
Assert.AreEqual(0, deserialized.Tags.Count);
}
[TestMethod]
public void Serialize_NullCollection_IsOmitted()
{
var order = new TestOrderWithNullableCollections { Id = 1, OrderNumber = "TEST", Items = null, Tags = null };
var json = AcJsonSerializer.Serialize(order);
Assert.IsFalse(json.Contains("\"Items\""), "Null Items should not be serialized");
Assert.IsFalse(json.Contains("\"Tags\""), "Null Tags should not be serialized");
}
[TestMethod]
public void WasmCompat_SharedReferences_IdRefResolution()
{
var sharedTag = new SharedTag { Id = 999, Name = "SharedKey" };
var order = new TestOrder { Id = 1, OrderNumber = "REF-TEST", PrimaryTag = sharedTag, SecondaryTag = sharedTag, Tags = [sharedTag] };
var json = AcJsonSerializer.Serialize(order);
Assert.IsTrue(json.Contains("\"$id\""));
Assert.IsTrue(json.Contains("\"$ref\""));
var nativeSettings = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore
};
var deserialized = JsonConvert.DeserializeObject<TestOrder>(json, nativeSettings);
Assert.IsNotNull(deserialized);
Assert.AreSame(deserialized.PrimaryTag, deserialized.SecondaryTag);
Assert.AreSame(deserialized.PrimaryTag, deserialized.Tags[0]);
}
#endregion
#region Cross-Serializer Compatibility Tests
[TestMethod]
public void CrossSerializer_MixedReferences_CompatibleWithNewtonsoft()
{
// Arrange
var sharedTag = new SharedTag { Id = 100, Name = "SharedKey", CreatedAt = DateTime.UtcNow };
var sharedMeta = new MetadataInfo { Key = "SharedMeta", Value = "MetaValue", ChildMetadata = new MetadataInfo { Key = "Child" } };
var order = new TestOrder
{
Id = 1,
OrderNumber = "ORD-001",
Status = TestStatus.Processing,
PrimaryTag = sharedTag,
SecondaryTag = sharedTag,
OrderMetadata = sharedMeta,
AuditMetadata = sharedMeta,
Tags = [sharedTag],
Items = [new TestOrderItem { Id = 10, ProductName = "Product-A", Tag = sharedTag, ItemMetadata = sharedMeta }]
};
// Act - Serialize with AyCode
var json = order.ToJson();
// Deserialize with native Newtonsoft
var nativeSettings = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore
};
var deserialized = JsonConvert.DeserializeObject<TestOrder>(json, nativeSettings);
// Assert
Assert.IsNotNull(deserialized);
Assert.AreSame(deserialized.PrimaryTag, deserialized.SecondaryTag);
Assert.AreSame(deserialized.OrderMetadata, deserialized.AuditMetadata);
Assert.AreSame(deserialized.PrimaryTag, deserialized.Items[0].Tag);
}
#endregion
#region Populate $ref Handling Tests
[TestMethod]
public void Populate_RefNode_ShouldSetPropertyToReferencedObject()
{
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""SharedTag"" },
""SecondaryTag"": { ""$ref"": ""1"" }
}";
var order = new TestOrder { Id = 1, OrderNumber = "OLD" };
// Act
json.JsonTo(order);
// Assert
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set");
Assert.IsNotNull(order.SecondaryTag, "SecondaryTag should be set from $ref");
Assert.AreEqual(100, order.PrimaryTag.Id);
Assert.AreEqual("SharedTag", order.PrimaryTag.Name);
Assert.AreSame(order.PrimaryTag, order.SecondaryTag,
"SecondaryTag should reference the same object as PrimaryTag via $ref");
}
[TestMethod]
public void Populate_RefNodeInCollection_ShouldSetPropertyToReferencedObject()
{
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""SharedTag"" },
""Tags"": [
{ ""$ref"": ""1"" },
{ ""$id"": ""2"", ""Id"": 200, ""Name"": ""OtherTag"" }
]
}";
var order = new TestOrder { Id = 1, OrderNumber = "OLD", Tags = new List<SharedTag>() };
// Act
json.JsonTo(order);
// Assert
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set");
Assert.AreEqual(2, order.Tags.Count, "Tags should have 2 items");
Assert.AreSame(order.PrimaryTag, order.Tags[0],
"Tags[0] should reference the same object as PrimaryTag via $ref");
Assert.AreEqual(200, order.Tags[1].Id);
Assert.AreNotSame(order.PrimaryTag, order.Tags[1]);
}
[TestMethod]
public void Populate_NestedRefNode_ShouldResolveCorrectly()
{
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
""Items"": [{
""Id"": 10,
""ProductName"": ""Product-A"",
""Tag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""ItemTag"" }
}],
""PrimaryTag"": { ""$ref"": ""1"" }
}";
var order = new TestOrder
{
Id = 1,
OrderNumber = "OLD",
Items = new List<TestOrderItem> { new TestOrderItem { Id = 10, ProductName = "OLD" } }
};
// Act
json.JsonTo(order);
// Assert
Assert.IsNotNull(order.Items[0].Tag, "Item's Tag should be set");
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set from $ref");
Assert.AreSame(order.Items[0].Tag, order.PrimaryTag,
"PrimaryTag should reference the same object as Items[0].Tag via $ref");
}
[TestMethod]
public void Populate_ForwardRef_ShouldResolveDeferredReference()
{
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
""SecondaryTag"": { ""$ref"": ""1"" },
""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""SharedTag"" }
}";
var order = new TestOrder { Id = 1, OrderNumber = "OLD" };
// Act
json.JsonTo(order);
// Assert
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set");
Assert.IsNotNull(order.SecondaryTag, "SecondaryTag should be set from forward $ref");
Assert.AreSame(order.PrimaryTag, order.SecondaryTag,
"Forward $ref should resolve to the same object");
}
[TestMethod]
public void Populate_MultipleRefsToSameId_AllShouldResolveToSameObject()
{
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""SharedTag"" },
""SecondaryTag"": { ""$ref"": ""1"" },
""Tags"": [
{ ""$ref"": ""1"" },
{ ""$ref"": ""1"" },
{ ""$ref"": ""1"" }
]
}";
var order = new TestOrder { Id = 1, OrderNumber = "OLD", Tags = new List<SharedTag>() };
// Act
json.JsonTo(order);
// Assert
Assert.IsNotNull(order.PrimaryTag);
Assert.AreSame(order.PrimaryTag, order.SecondaryTag);
Assert.AreEqual(3, order.Tags.Count);
Assert.AreSame(order.PrimaryTag, order.Tags[0]);
Assert.AreSame(order.PrimaryTag, order.Tags[1]);
Assert.AreSame(order.PrimaryTag, order.Tags[2]);
}
[TestMethod]
public void Populate_DeeplyNestedRef_ShouldResolveAcrossLevels()
{
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
""Items"": [{
""Id"": 10,
""ProductName"": ""Product-A"",
""Tag"": { ""$id"": ""deep1"", ""Id"": 999, ""Name"": ""DeepTag"" }
}],
""PrimaryTag"": { ""$ref"": ""deep1"" }
}";
var order = new TestOrder
{
Id = 1,
Items = new List<TestOrderItem> { new TestOrderItem { Id = 10 } }
};
// Act
json.JsonTo(order);
// Assert
var deepTag = order.Items[0].Tag;
Assert.IsNotNull(deepTag, "Item's Tag should be set");
Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set from deep $ref");
Assert.AreSame(deepTag, order.PrimaryTag,
"Root PrimaryTag should reference the nested Item's Tag via $ref");
}
[TestMethod]
public void Populate_RefInNestedObject_ShouldResolveFromParentContext()
{
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""RootTag"" },
""Items"": [{
""Id"": 10,
""ProductName"": ""Product-A"",
""Tag"": { ""$ref"": ""1"" }
}]
}";
var order = new TestOrder
{
Id = 1,
Items = new List<TestOrderItem> { new TestOrderItem { Id = 10 } }
};
// Act
json.JsonTo(order);
// Assert
Assert.IsNotNull(order.PrimaryTag);
Assert.IsNotNull(order.Items[0].Tag);
Assert.AreSame(order.PrimaryTag, order.Items[0].Tag,
"Nested Tag should reference root PrimaryTag via $ref");
}
[TestMethod]
public void Deserialize_RefOnly_ShouldCreateDeferredAndResolve()
{
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
""SecondaryTag"": { ""$ref"": ""1"" },
""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""Tag"" }
}";
// Act
var order = AcJsonDeserializer.Deserialize<TestOrder>(json);
// Assert
Assert.IsNotNull(order);
Assert.IsNotNull(order.PrimaryTag);
Assert.IsNotNull(order.SecondaryTag);
Assert.AreSame(order.PrimaryTag, order.SecondaryTag);
}
[TestMethod]
public void Deserialize_MultipleIdRefs_ComplexGraph()
{
var json = @"{
""Id"": 1,
""OrderNumber"": ""ORD-001"",
""PrimaryTag"": { ""$id"": ""tag1"", ""Id"": 100, ""Name"": ""Tag1"" },
""SecondaryTag"": { ""$id"": ""tag2"", ""Id"": 200, ""Name"": ""Tag2"" },
""Tags"": [
{ ""$ref"": ""tag1"" },
{ ""$ref"": ""tag2"" },
{ ""$ref"": ""tag1"" }
],
""Items"": [{
""Id"": 10,
""ProductName"": ""Product-A"",
""Tag"": { ""$ref"": ""tag2"" }
}]
}";
// Act
var order = AcJsonDeserializer.Deserialize<TestOrder>(json);
// Assert
Assert.IsNotNull(order);
Assert.AreEqual(3, order.Tags.Count);
Assert.AreSame(order.PrimaryTag, order.Tags[0]);
Assert.AreSame(order.PrimaryTag, order.Tags[2]);
Assert.AreSame(order.SecondaryTag, order.Tags[1]);
Assert.AreSame(order.SecondaryTag, order.Items[0].Tag);
Assert.AreNotSame(order.PrimaryTag, order.SecondaryTag);
}
[TestMethod]
public void Populate_RefWithExistingTarget_ShouldOverwriteWithReference()
{
var json = @"{
""Id"": 1,
""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""NewTag"" },
""SecondaryTag"": { ""$ref"": ""1"" }
}";
var existingTag = new SharedTag { Id = 999, Name = "ExistingTag" };
var order = new TestOrder
{
Id = 1,
SecondaryTag = existingTag
};
// Act
json.JsonTo(order);
// Assert
Assert.IsNotNull(order.PrimaryTag);
Assert.AreSame(order.PrimaryTag, order.SecondaryTag,
"SecondaryTag should be overwritten with $ref reference");
Assert.AreNotSame(existingTag, order.SecondaryTag,
"Original SecondaryTag should be replaced");
}
#endregion
#region AcJsonSerializer Complex Object Tests
[TestMethod]
public void Serialize_ObjectWithDictionaryProperty_SerializesDictionaryCorrectly()
{
var obj = new ExtendedPrimitiveTestClass
{
Id = 1,
Name = "Test",
Counts = new Dictionary<string, int>
{
{ "apple", 5 },
{ "banana", 3 }
}
};
var json = AcJsonSerializer.Serialize(obj);
Assert.IsTrue(json.Contains("\"Counts\":{"));
Assert.IsTrue(json.Contains("\"apple\":5"));
Assert.IsTrue(json.Contains("\"banana\":3"));
}
[TestMethod]
public void Serialize_ObjectWithDateTimeOffsetProperty_SerializesCorrectly()
{
var dto = new DateTimeOffset(2024, 6, 15, 10, 30, 0, TimeSpan.FromHours(2));
var obj = new ExtendedPrimitiveTestClass
{
Id = 1,
Name = "Test",
DateTimeOffsetValue = dto
};
var json = AcJsonSerializer.Serialize(obj);
Assert.IsTrue(json.Contains("\"DateTimeOffsetValue\":\"2024-06-15"));
}
[TestMethod]
public void Serialize_ObjectWithTimeSpanProperty_SerializesCorrectly()
{
var obj = new ExtendedPrimitiveTestClass
{
Id = 1,
Name = "Test",
TimeSpanValue = new TimeSpan(2, 30, 45)
};
var json = AcJsonSerializer.Serialize(obj);
Assert.IsTrue(json.Contains("\"TimeSpanValue\":\"02:30:45\""));
}
[TestMethod]
public void Serialize_ObjectWithNullProperties_SkipsNullProperties()
{
var obj = new ExtendedPrimitiveTestClass
{
Id = 1,
Name = "Test",
NullString = null,
NullObject = null,
Counts = null
};
var json = AcJsonSerializer.Serialize(obj);
Assert.IsFalse(json.Contains("\"NullString\""));
Assert.IsFalse(json.Contains("\"NullObject\""));
Assert.IsFalse(json.Contains("\"Counts\""));
}
[TestMethod]
public void Serialize_ObjectWithUIntProperty_SerializesCorrectly()
{
var obj = new ExtendedPrimitiveTestClass
{
Id = 1,
Name = "Test",
UIntValue = 4000000000
};
var json = AcJsonSerializer.Serialize(obj);
Assert.IsTrue(json.Contains("\"UIntValue\":4000000000"));
}
[TestMethod]
public void Serialize_ObjectWithULongProperty_SerializesCorrectly()
{
var obj = new ExtendedPrimitiveTestClass
{
Id = 1,
Name = "Test",
ULongValue = 18000000000000000000
};
var json = AcJsonSerializer.Serialize(obj);
Assert.IsTrue(json.Contains("\"ULongValue\":18000000000000000000"));
}
[TestMethod]
public void Serialize_ObjectWithSByteProperty_SerializesCorrectly()
{
var obj = new ExtendedPrimitiveTestClass
{
Id = 1,
Name = "Test",
SByteValue = -100
};
var json = AcJsonSerializer.Serialize(obj);
Assert.IsTrue(json.Contains("\"SByteValue\":-100"));
}
[TestMethod]
public void Serialize_ObjectWithCharProperty_SerializesCorrectly()
{
var obj = new ExtendedPrimitiveTestClass
{
Id = 1,
Name = "Test",
CharValue = 'X'
};
var json = AcJsonSerializer.Serialize(obj);
Assert.IsTrue(json.Contains("\"CharValue\":\"X\""));
}
[TestMethod]
public void Serialize_ObjectWithUShortProperty_SerializesCorrectly()
{
var obj = new ExtendedPrimitiveTestClass
{
Id = 1,
Name = "Test",
UShortValue = 60000
};
var json = AcJsonSerializer.Serialize(obj);
Assert.IsTrue(json.Contains("\"UShortValue\":60000"));
}
[TestMethod]
public void Serialize_ArrayWithNullItems_SerializesNullCorrectly()
{
var obj = new ObjectWithNullItems
{
Id = 1,
MixedItems = [1, null, "text", null, 3]
};
var json = AcJsonSerializer.Serialize(obj);
Assert.IsTrue(json.Contains("[1,null,\"text\",null,3]"));
}
[TestMethod]
public void Serialize_DictionaryDirect_SerializesAsObject()
{
var dict = new Dictionary<string, object>
{
{ "name", "Test" },
{ "value", 42 }
};
var json = AcJsonSerializer.Serialize(dict);
Assert.IsTrue(json.StartsWith("{"));
Assert.IsTrue(json.Contains("\"name\":\"Test\""));
Assert.IsTrue(json.Contains("\"value\":42"));
}
[TestMethod]
public void Serialize_ObjectWithGuidProperty_SerializesCorrectly()
{
var guid = Guid.NewGuid();
var obj = new ExtendedPrimitiveTestClass
{
Id = 1,
Name = "Test",
Tag = new SharedTag { Id = 1, Name = "Tag" }
};
// Using existing Tag property with Guid in SharedTag's CreatedAt
var json = AcJsonSerializer.Serialize(obj);
Assert.IsTrue(json.Contains("\"Tag\":{"));
Assert.IsTrue(json.Contains("\"Name\":\"Tag\""));
}
#endregion
#region AcJsonDeserializer Extended Tests
[TestMethod]
public void Deserialize_GenericString_DirectPath()
{
var json = "\"Hello World\"";
var result = AcJsonDeserializer.Deserialize<string>(json);
Assert.AreEqual("Hello World", result);
}
[TestMethod]
public void Deserialize_GenericInt_DirectPath()
{
var json = "42";
var result = AcJsonDeserializer.Deserialize<int>(json);
Assert.AreEqual(42, result);
}
[TestMethod]
public void Deserialize_GenericBool_DirectPath()
{
var json = "true";
var result = AcJsonDeserializer.Deserialize<bool>(json);
Assert.IsTrue(result);
}
[TestMethod]
public void Deserialize_GenericDouble_DirectPath()
{
var json = "3.14159";
var result = AcJsonDeserializer.Deserialize<double>(json);
Assert.AreEqual(3.14159, result, 0.00001);
}
[TestMethod]
public void Deserialize_GenericGuid_DirectPath()
{
var guid = Guid.NewGuid();
var json = $"\"{guid}\"";
var result = AcJsonDeserializer.Deserialize<Guid>(json);
Assert.AreEqual(guid, result);
}
[TestMethod]
public void Deserialize_GenericDateTime_DirectPath()
{
var dt = new DateTime(2024, 12, 25, 12, 30, 45, DateTimeKind.Utc);
var json = $"\"{dt:O}\"";
var result = AcJsonDeserializer.Deserialize<DateTime>(json);
Assert.AreEqual(dt, result);
}
[TestMethod]
public void Deserialize_GenericEnum_DirectPath()
{
var json = "2";
var result = AcJsonDeserializer.Deserialize<TestStatus>(json);
Assert.AreEqual(TestStatus.Processing, result);
}
[TestMethod]
public void Deserialize_GenericDecimal_DirectPath()
{
var json = "123.456";
var result = AcJsonDeserializer.Deserialize<decimal>(json);
Assert.AreEqual(123.456m, result);
}
[TestMethod]
public void Deserialize_GenericFloat_DirectPath()
{
var json = "3.14";
var result = AcJsonDeserializer.Deserialize<float>(json);
Assert.AreEqual(3.14f, result, 0.001f);
}
[TestMethod]
public void Deserialize_GenericLong_DirectPath()
{
var json = "9223372036854775807";
var result = AcJsonDeserializer.Deserialize<long>(json);
Assert.AreEqual(9223372036854775807L, result);
}
[TestMethod]
public void Deserialize_GenericByte_DirectPath()
{
var json = "255";
var result = AcJsonDeserializer.Deserialize<byte>(json);
Assert.AreEqual((byte)255, result);
}
[TestMethod]
public void Deserialize_GenericShort_DirectPath()
{
var json = "32767";
var result = AcJsonDeserializer.Deserialize<short>(json);
Assert.AreEqual((short)32767, result);
}
[TestMethod]
public void Deserialize_GenericUShort_DirectPath()
{
var json = "65535";
var result = AcJsonDeserializer.Deserialize<ushort>(json);
Assert.AreEqual((ushort)65535, result);
}
[TestMethod]
public void Deserialize_GenericUInt_DirectPath()
{
var json = "4294967295";
var result = AcJsonDeserializer.Deserialize<uint>(json);
Assert.AreEqual(4294967295U, result);
}
[TestMethod]
public void Deserialize_GenericULong_DirectPath()
{
var json = "18446744073709551615";
var result = AcJsonDeserializer.Deserialize<ulong>(json);
Assert.AreEqual(18446744073709551615UL, result);
}
[TestMethod]
public void Deserialize_GenericSByte_DirectPath()
{
var json = "-128";
var result = AcJsonDeserializer.Deserialize<sbyte>(json);
Assert.AreEqual((sbyte)-128, result);
}
[TestMethod]
public void Deserialize_GenericChar_DirectPath()
{
var json = "\"A\"";
var result = AcJsonDeserializer.Deserialize<char>(json);
Assert.AreEqual('A', result);
}
[TestMethod]
public void Deserialize_GenericDateTimeOffset_DirectPath()
{
var dto = new DateTimeOffset(2024, 12, 25, 12, 30, 45, TimeSpan.FromHours(2));
var json = $"\"{dto:O}\"";
var result = AcJsonDeserializer.Deserialize<DateTimeOffset>(json);
Assert.AreEqual(dto, result);
}
[TestMethod]
public void Deserialize_GenericTimeSpan_DirectPath()
{
var ts = new TimeSpan(1, 2, 3, 4, 5);
var json = $"\"{ts:c}\"";
var result = AcJsonDeserializer.Deserialize<TimeSpan>(json);
Assert.AreEqual(ts, result);
}
[TestMethod]
public void Deserialize_GenericEnumFromString_DirectPath()
{
var json = "\"Processing\"";
var result = AcJsonDeserializer.Deserialize<TestStatus>(json);
Assert.AreEqual(TestStatus.Processing, result);
}
[TestMethod]
public void Populate_Array_PopulatesList()
{
var json = "[1, 2, 3]";
var list = new List<int>();
AcJsonDeserializer.Populate(json, list);
Assert.AreEqual(3, list.Count);
Assert.AreEqual(1, list[0]);
Assert.AreEqual(3, list[2]);
}
[TestMethod]
public void Populate_ObjectToObject_PopulatesProperties()
{
var json = "{\"Name\": \"Updated\", \"Id\": 99}";
var obj = new SharedTag { Id = 1, Name = "Original" };
AcJsonDeserializer.Populate(json, obj, typeof(SharedTag));
Assert.AreEqual(99, obj.Id);
Assert.AreEqual("Updated", obj.Name);
}
[TestMethod]
public void Deserialize_NullJson_ReturnsDefault()
{
var result = AcJsonDeserializer.Deserialize<TestOrderItem>("null");
Assert.IsNull(result);
}
[TestMethod]
public void Deserialize_EmptyJson_ReturnsDefault()
{
var result = AcJsonDeserializer.Deserialize<TestOrderItem>("");
Assert.IsNull(result);
}
[TestMethod]
public void Deserialize_GenericNullJson_ReturnsDefaultInt()
{
var result = AcJsonDeserializer.Deserialize<int>("null");
Assert.AreEqual(0, result);
}
[TestMethod]
public void Deserialize_RuntimeType_WithDateTimeOffset()
{
var dto = new DateTimeOffset(2024, 6, 15, 10, 30, 0, TimeSpan.FromHours(2));
var json = $"\"{dto:O}\"";
var result = AcJsonDeserializer.Deserialize(json, typeof(DateTimeOffset));
Assert.AreEqual(dto, result);
}
[TestMethod]
public void Deserialize_RuntimeType_WithTimeSpan()
{
var ts = new TimeSpan(2, 30, 45);
var json = $"\"{ts:c}\"";
var result = AcJsonDeserializer.Deserialize(json, typeof(TimeSpan));
Assert.AreEqual(ts, result);
}
[TestMethod]
public void Deserialize_RuntimeType_WithChar()
{
var json = "\"X\"";
var result = AcJsonDeserializer.Deserialize(json, typeof(char));
Assert.AreEqual('X', result);
}
[TestMethod]
public void Deserialize_RuntimeType_WithEnumString()
{
var json = "\"Active\"";
var result = AcJsonDeserializer.Deserialize(json, typeof(TestStatus));
Assert.AreEqual(TestStatus.Active, result);
}
#endregion
#region AcJsonDeserializer Error Handling Tests
[TestMethod]
public void Deserialize_InvalidJson_ThrowsException()
{
var invalidJson = "{ this is not valid json }";
try
{
AcJsonDeserializer.Deserialize<TestOrderItem>(invalidJson);
Assert.Fail("Expected AcJsonDeserializationException");
}
catch (AcJsonDeserializationException)
{
// Expected
}
}
[TestMethod]
public void Deserialize_DoubleQuotedJson_ThrowsException()
{
// This is what double-serialized JSON looks like: a JSON string containing escaped JSON
var doubleQuotedJson = "\"{\\\"Id\\\":1,\\\"Name\\\":\\\"Test\\\"}\"";
try
{
AcJsonDeserializer.Deserialize<TestOrderItem>(doubleQuotedJson);
Assert.Fail("Expected AcJsonDeserializationException for double-serialized JSON");
}
catch (AcJsonDeserializationException ex)
{
Assert.IsTrue(ex.Message.Contains("double-serialized"));
}
}
[TestMethod]
public void Deserialize_ArrayToObject_ThrowsException()
{
// Trying to deserialize an array JSON to a single object
var arrayJson = "[{\"Id\":1},{\"Id\":2}]";
try
{
AcJsonDeserializer.Deserialize<TestOrderItem>(arrayJson);
Assert.Fail("Expected AcJsonDeserializationException");
}
catch (AcJsonDeserializationException ex)
{
Assert.IsTrue(ex.Message.Contains("array") || ex.Message.Contains("collection"));
}
}
[TestMethod]
public void Deserialize_ObjectToArray_ThrowsException()
{
// Trying to deserialize an object JSON to a collection
var objectJson = "{\"Id\":1,\"ProductName\":\"Test\"}";
try
{
AcJsonDeserializer.Deserialize<List<TestOrderItem>>(objectJson);
Assert.Fail("Expected AcJsonDeserializationException");
}
catch (AcJsonDeserializationException ex)
{
Assert.IsTrue(ex.Message.Contains("object") || ex.Message.Contains("collection"));
}
}
[TestMethod]
public void Populate_NullTarget_ThrowsArgumentNullException()
{
var json = "{\"Id\":1}";
TestOrderItem? target = null;
try
{
AcJsonDeserializer.Populate(json, target!);
Assert.Fail("Expected ArgumentNullException");
}
catch (ArgumentNullException)
{
// Expected
}
}
[TestMethod]
public void Populate_InvalidJson_ThrowsException()
{
var target = new TestOrderItem();
var invalidJson = "{ not valid }";
try
{
AcJsonDeserializer.Populate(invalidJson, target);
Assert.Fail("Expected AcJsonDeserializationException");
}
catch (AcJsonDeserializationException)
{
// Expected
}
}
[TestMethod]
public void Populate_ArrayToNonList_ThrowsException()
{
var target = new TestOrderItem();
var arrayJson = "[1,2,3]";
try
{
AcJsonDeserializer.Populate(arrayJson, target);
Assert.Fail("Expected AcJsonDeserializationException");
}
catch (AcJsonDeserializationException)
{
// Expected
}
}
#endregion
#region Edge Case Tests
[TestMethod]
public void Deserialize_SpecialCharactersInStrings_HandledCorrectly()
{
var json = "{\"Id\":1,\"ProductName\":\"Test \\\"quoted\\\" and \\\\backslash\"}";
var result = AcJsonDeserializer.Deserialize<TestOrderItem>(json);
Assert.IsNotNull(result);
Assert.AreEqual("Test \"quoted\" and \\backslash", result.ProductName);
}
[TestMethod]
public void Deserialize_UnicodeCharacters_HandledCorrectly()
{
var json = "{\"Id\":1,\"ProductName\":\"中文日本語한국어🎉\"}";
var result = AcJsonDeserializer.Deserialize<TestOrderItem>(json);
Assert.IsNotNull(result);
Assert.AreEqual("中文日本語한국어🎉", result.ProductName);
}
[TestMethod]
public void Deserialize_LargeNumbers_HandledCorrectly()
{
var json = "{\"Id\":999999999,\"ProductName\":\"Big\",\"Quantity\":2147483647}";
var result = AcJsonDeserializer.Deserialize<TestOrderItem>(json);
Assert.IsNotNull(result);
Assert.AreEqual(999999999, result.Id);
Assert.AreEqual(int.MaxValue, result.Quantity);
}
[TestMethod]
public void Serialize_ThenDeserialize_RoundTripPreservesData()
{
var original = new TestOrderItem
{
Id = 42,
ProductName = "Test with \"quotes\" and \\backslash",
Quantity = 100,
UnitPrice = 99.99m,
Status = TestStatus.Processing
};
var json = original.ToJson();
var restored = AcJsonDeserializer.Deserialize<TestOrderItem>(json);
Assert.IsNotNull(restored);
Assert.AreEqual(original.Id, restored.Id);
Assert.AreEqual(original.ProductName, restored.ProductName);
Assert.AreEqual(original.Quantity, restored.Quantity);
Assert.AreEqual(original.UnitPrice, restored.UnitPrice);
Assert.AreEqual(original.Status, restored.Status);
}
#endregion
#region Task-like JSON Wrapper Tests
[TestMethod]
public void Deserialize_TaskWrappedJson_DirectDeserialization_OnlyGetsRootProperties()
{
// This JSON represents a serialized Task<TestOrderItem> - the actual data is in "Result"
// This happens when someone forgets to await an async method before serializing
var taskWrappedJson = "{\"Result\":{\"Id\":1,\"ProductName\":\"Processed: TestProduct\",\"Quantity\":10,\"UnitPrice\":20,\"TotalPrice\":200},\"Id\":1,\"Status\":5,\"IsCompleted\":true,\"IsCompletedSuccessfully\":true}";
// Direct deserialization to TestOrderItem only gets root-level properties
var result = AcJsonDeserializer.Deserialize<TestOrderItem>(taskWrappedJson);
Assert.IsNotNull(result);
// Id=1 is at root level and matches
Assert.AreEqual(1, result.Id);
// These values are inside "Result" object, not at root - they remain default
Assert.AreEqual(0, result.Quantity, "Quantity should be 0 because it's inside Result, not at root");
Assert.AreEqual(0m, result.UnitPrice, "UnitPrice should be 0 because it's inside Result, not at root");
}
[TestMethod]
public void Deserialize_TaskWrappedJson_UseWrapperClass_ExtractsCorrectly()
{
// This JSON represents a serialized Task<TestOrderItem> - the actual data is in "Result"
var taskWrappedJson = "{\"Result\":{\"Id\":1,\"ProductName\":\"Processed: TestProduct\",\"Quantity\":10,\"UnitPrice\":20,\"TotalPrice\":200},\"Id\":1,\"Status\":5,\"IsCompleted\":true,\"IsCompletedSuccessfully\":true}";
// Proper approach: deserialize to a wrapper type and extract Result
var wrapper = AcJsonDeserializer.Deserialize<TaskResultWrapper<TestOrderItem>>(taskWrappedJson);
Assert.IsNotNull(wrapper);
Assert.IsNotNull(wrapper.Result);
Assert.AreEqual(1, wrapper.Result.Id);
Assert.AreEqual("Processed: TestProduct", wrapper.Result.ProductName);
Assert.AreEqual(10, wrapper.Result.Quantity);
Assert.AreEqual(20m, wrapper.Result.UnitPrice);
Assert.IsTrue(wrapper.IsCompleted);
Assert.IsTrue(wrapper.IsCompletedSuccessfully);
Assert.AreEqual(5, wrapper.Status);
}
/// <summary>
/// Wrapper class to deserialize Task-like JSON structures.
/// This is what you get when you accidentally serialize a Task object instead of awaiting it.
/// </summary>
private class TaskResultWrapper<T>
{
public T? Result { get; set; }
public int Id { get; set; }
public int Status { get; set; }
public bool IsCompleted { get; set; }
public bool IsCompletedSuccessfully { get; set; }
}
#endregion
}