From c07a908cf4c508a3d6e6a7eebca0954bde8f991b Mon Sep 17 00:00:00 2001 From: Cheick Keita Date: Wed, 4 May 2022 14:20:23 -0700 Subject: [PATCH] Add the ability to serialize enums values (#1898) * Add the ablility to serialize enums values * unit test --- .../ApiService/OneFuzzTypes/Enums.cs | 3 + .../TestHooks/NodeOperationsTestHooks.cs | 2 +- .../onefuzzlib/orm/CustomConverterFactory.cs | 2 +- .../onefuzzlib/orm/EntityConverter.cs | 575 +++++++++--------- src/ApiService/Tests/OrmTest.cs | 33 +- 5 files changed, 325 insertions(+), 290 deletions(-) diff --git a/src/ApiService/ApiService/OneFuzzTypes/Enums.cs b/src/ApiService/ApiService/OneFuzzTypes/Enums.cs index 0a4408026..116b7a02b 100644 --- a/src/ApiService/ApiService/OneFuzzTypes/Enums.cs +++ b/src/ApiService/ApiService/OneFuzzTypes/Enums.cs @@ -1,6 +1,9 @@ using System.Collections.Concurrent; +using Microsoft.OneFuzz.Service.OneFuzzLib.Orm; namespace Microsoft.OneFuzz.Service; + +[SerializeValue] public enum ErrorCode { INVALID_REQUEST = 450, INVALID_PERMISSION = 451, diff --git a/src/ApiService/ApiService/TestHooks/NodeOperationsTestHooks.cs b/src/ApiService/ApiService/TestHooks/NodeOperationsTestHooks.cs index 8a5b43c73..be1e54405 100644 --- a/src/ApiService/ApiService/TestHooks/NodeOperationsTestHooks.cs +++ b/src/ApiService/ApiService/TestHooks/NodeOperationsTestHooks.cs @@ -268,7 +268,7 @@ namespace ApiService.TestHooks { var s = await req.ReadAsStringAsync(); var markTasks = JsonSerializer.Deserialize(s!, EntityConverter.GetJsonSerializerOptions()); - await _nodeOps.MarkTasksStoppedEarly(markTasks.node, markTasks.error); + await _nodeOps.MarkTasksStoppedEarly(markTasks!.node, markTasks.error); var resp = req.CreateResponse(HttpStatusCode.OK); return resp; diff --git a/src/ApiService/ApiService/onefuzzlib/orm/CustomConverterFactory.cs b/src/ApiService/ApiService/onefuzzlib/orm/CustomConverterFactory.cs index 90505d4c6..fcfcaf016 100644 --- a/src/ApiService/ApiService/onefuzzlib/orm/CustomConverterFactory.cs +++ b/src/ApiService/ApiService/onefuzzlib/orm/CustomConverterFactory.cs @@ -8,7 +8,7 @@ using System.Text.Json.Serialization; namespace Microsoft.OneFuzz.Service.OneFuzzLib.Orm; public sealed class CustomEnumConverterFactory : JsonConverterFactory { - public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum; + public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum && (typeToConvert.GetCustomAttribute() == null); public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { object[]? knownValues = null; diff --git a/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs b/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs index f657d8ab3..04d5e5550 100644 --- a/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs +++ b/src/ApiService/ApiService/onefuzzlib/orm/EntityConverter.cs @@ -1,289 +1,294 @@ -using System.Collections.Concurrent; -using System.Linq.Expressions; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; -using Azure; -using Azure.Data.Tables; - -namespace Microsoft.OneFuzz.Service.OneFuzzLib.Orm; - -public abstract record EntityBase { - [JsonIgnore] public ETag? ETag { get; set; } +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure; +using Azure.Data.Tables; + +namespace Microsoft.OneFuzz.Service.OneFuzzLib.Orm; + +public abstract record EntityBase { + [JsonIgnore] public ETag? ETag { get; set; } public DateTimeOffset? TimeStamp { get; set; } // https://docs.microsoft.com/en-us/rest/api/storageservices/designing-a-scalable-partitioning-strategy-for-azure-table-storage#yyy // Produce "good-quality-table-key" based on a DateTimeOffset timestamp - public static string NewSortedKey => $"{DateTimeOffset.MaxValue.Ticks - DateTimeOffset.UtcNow.Ticks}"; -} - -public abstract record StatefulEntityBase([property: JsonIgnore] T State) : EntityBase() where T : Enum; - -/// Indicates that the enum cases should no be renamed -[AttributeUsage(AttributeTargets.Enum)] -public class SkipRename : Attribute { } -public class RowKeyAttribute : Attribute { } -public class PartitionKeyAttribute : Attribute { } -public class TypeDiscrimnatorAttribute : Attribute { - public string FieldName { get; } - // the type of a function that takes the value of fieldName as an input and return the type - public Type ConverterType { get; } - - public TypeDiscrimnatorAttribute(string fieldName, Type converterType) { - if (!converterType.IsAssignableTo(typeof(ITypeProvider))) { - throw new ArgumentException($"the provided type needs to implement ITypeProvider"); - } - - FieldName = fieldName; - ConverterType = converterType; - } -} - -public interface ITypeProvider { - Type GetTypeInfo(object input); -} - -public enum EntityPropertyKind { - PartitionKey, - RowKey, - Column -} -public record EntityProperty(string name, string columnName, Type type, EntityPropertyKind kind, (TypeDiscrimnatorAttribute, ITypeProvider)? discriminator); -public record EntityInfo(Type type, ILookup properties, Func constructor); - -class OnefuzzNamingPolicy : JsonNamingPolicy { - public override string ConvertName(string name) { - return CaseConverter.PascalToSnake(name); - } -} -public class EntityConverter { - private readonly JsonSerializerOptions _options; - - private readonly ConcurrentDictionary _cache; - - private readonly ETag _emptyETag = new ETag(); - - public EntityConverter() { - _options = GetJsonSerializerOptions(); - _cache = new ConcurrentDictionary(); - - } - - - public static JsonSerializerOptions GetJsonSerializerOptions() { - var options = new JsonSerializerOptions() { - PropertyNamingPolicy = new OnefuzzNamingPolicy(), - }; - options.Converters.Add(new CustomEnumConverterFactory()); - options.Converters.Add(new PolymorphicConverterFactory()); - return options; - } - - internal static Func BuildConstructerFrom(ConstructorInfo constructorInfo) { - var constructorParameters = Expression.Parameter(typeof(object?[])); - - var parameterExpressions = - constructorInfo.GetParameters().Select((parameterInfo, i) => { - var ithIndex = Expression.Constant(i); - var ithParameter = Expression.ArrayIndex(constructorParameters, ithIndex); - var unboxedIthParameter = Expression.Convert(ithParameter, parameterInfo.ParameterType); - return unboxedIthParameter; - - }).ToArray(); - - NewExpression constructorCall = Expression.New(constructorInfo, parameterExpressions); - - Func ctor = Expression.Lambda>(constructorCall, constructorParameters).Compile(); - return ctor; - } - - private IEnumerable GetEntityProperties(ParameterInfo parameterInfo) { - var name = parameterInfo.Name.EnsureNotNull($"Invalid paramter {parameterInfo}"); - var parameterType = parameterInfo.ParameterType.EnsureNotNull($"Invalid paramter {parameterInfo}"); - var isRowkey = parameterInfo.GetCustomAttribute(typeof(RowKeyAttribute)) != null; - var isPartitionkey = parameterInfo.GetCustomAttribute(typeof(PartitionKeyAttribute)) != null; - - var discriminatorAttribute = typeof(T).GetProperty(name)?.GetCustomAttribute(); - - (TypeDiscrimnatorAttribute, ITypeProvider)? discriminator = null; - if (discriminatorAttribute != null) { - var t = (ITypeProvider)(discriminatorAttribute.ConverterType.GetConstructor(new Type[] { })?.Invoke(null) ?? throw new Exception("unable to retrive the type provider")); - discriminator = (discriminatorAttribute, t); - } - - - if (isPartitionkey) { - yield return new EntityProperty(name, "PartitionKey", parameterType, EntityPropertyKind.PartitionKey, discriminator); - } - - if (isRowkey) { - yield return new EntityProperty(name, "RowKey", parameterType, EntityPropertyKind.RowKey, discriminator); - } - - if (!isPartitionkey && !isRowkey) { - var columnName = typeof(T).GetProperty(name)?.GetCustomAttribute()?.Name ?? CaseConverter.PascalToSnake(name); - yield return new EntityProperty(name, columnName, parameterType, EntityPropertyKind.Column, discriminator); - } - } - - - private EntityInfo GetEntityInfo() { - return _cache.GetOrAdd(typeof(T), type => { - var constructor = type.GetConstructors()[0]; - var parameterInfos = constructor.GetParameters(); - var parameters = - parameterInfos.SelectMany(GetEntityProperties).ToArray(); - - return new EntityInfo(typeof(T), parameters.ToLookup(x => x.name), BuildConstructerFrom(constructor)); - }); - } - - public string ToJsonString(T typedEntity) { - var serialized = JsonSerializer.Serialize(typedEntity, _options); - return serialized; - } - - public TableEntity ToTableEntity(T typedEntity) where T : EntityBase { - if (typedEntity == null) { - throw new NullReferenceException(); - } - var type = typeof(T)!; - if (type is null) { - throw new NullReferenceException(); - } - var tableEntity = new TableEntity(); - var entityInfo = GetEntityInfo(); - foreach (var prop in entityInfo.properties.SelectMany(x => x)) { - //var prop = kvp.First(); - var value = entityInfo.type.GetProperty(prop.name)?.GetValue(typedEntity); - if (prop.kind == EntityPropertyKind.PartitionKey || prop.kind == EntityPropertyKind.RowKey) { - tableEntity.Add(prop.columnName, value?.ToString()); - } else if (prop.type == typeof(Guid) || prop.type == typeof(Guid?)) { - tableEntity.Add(prop.columnName, value?.ToString()); - } else if (prop.type == typeof(bool) - || prop.type == typeof(bool?) - || prop.type == typeof(string) - || prop.type == typeof(DateTime) - || prop.type == typeof(DateTime?) - || prop.type == typeof(DateTimeOffset) - || prop.type == typeof(DateTimeOffset?) - || prop.type == typeof(int) - || prop.type == typeof(int?) - || prop.type == typeof(Int64) - || prop.type == typeof(Int64?) - || prop.type == typeof(double) - || prop.type == typeof(double?) - - ) { - tableEntity.Add(prop.columnName, value); - } else if (prop.type.IsEnum) { - var values = - (value?.ToString()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Select(CaseConverter.PascalToSnake)).EnsureNotNull($"Unable to read enum data {value}"); - - tableEntity.Add(prop.columnName, string.Join(",", values)); - } else { - var serialized = JsonSerializer.Serialize(value, _options); - tableEntity.Add(prop.columnName, serialized.Trim('"')); - } - - } - - if (typedEntity.ETag.HasValue) { - tableEntity.ETag = typedEntity.ETag.Value; - } - - return tableEntity; - } - - - private object? GetFieldValue(EntityInfo info, string name, TableEntity entity) { - var ef = info.properties[name].First(); - if (ef.kind == EntityPropertyKind.PartitionKey || ef.kind == EntityPropertyKind.RowKey) { - if (ef.type == typeof(string)) - return entity.GetString(ef.kind.ToString()); - else if (ef.type == typeof(Guid)) - return Guid.Parse(entity.GetString(ef.kind.ToString())); - else if (ef.type == typeof(int)) - return int.Parse(entity.GetString(ef.kind.ToString())); - else { - throw new Exception("invalid "); - } - } - - var fieldName = ef.columnName; - var obj = entity[fieldName]; - if (obj == null) { - return null; - } - var objType = obj.GetType(); - - if (ef.type == typeof(string)) { - return entity.GetString(fieldName); - } else if (ef.type == typeof(bool) || ef.type == typeof(bool?)) { - return entity.GetBoolean(fieldName); - } else if (ef.type == typeof(DateTimeOffset) || ef.type == typeof(DateTimeOffset?)) { - return entity.GetDateTimeOffset(fieldName); - } else if (ef.type == typeof(DateTime) || ef.type == typeof(DateTime?)) { - return entity.GetDateTime(fieldName); - } else if (ef.type == typeof(double) || ef.type == typeof(double?)) { - return entity.GetDouble(fieldName); - } else if (ef.type == typeof(Guid) || ef.type == typeof(Guid?)) { - return (object?)Guid.Parse(entity.GetString(fieldName)); - } else if (ef.type == typeof(int) || ef.type == typeof(short) || ef.type == typeof(int?) || ef.type == typeof(short?)) { - return entity.GetInt32(fieldName); - } else if (ef.type == typeof(long) || ef.type == typeof(long?)) { - return entity.GetInt64(fieldName); - } else if (ef.type.IsEnum) { - var stringValues = - entity.GetString(fieldName).Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Select(CaseConverter.SnakeToPascal); - - return Enum.Parse(ef.type, string.Join(",", stringValues)); - } else { - var outputType = ef.type; - if (ef.discriminator != null) { - var (attr, typeProvider) = ef.discriminator.Value; - var v = GetFieldValue(info, attr.FieldName, entity) ?? throw new Exception($"No value for {attr.FieldName}"); - outputType = typeProvider.GetTypeInfo(v); - } - - - if (objType == typeof(string)) { - var value = entity.GetString(fieldName); - if (value.StartsWith('[') || value.StartsWith('{') || value == "null") { - return JsonSerializer.Deserialize(value, outputType, options: _options); - } else { - return JsonSerializer.Deserialize($"\"{value}\"", outputType, options: _options); - } - } else { - var value = entity.GetString(fieldName); - return JsonSerializer.Deserialize(value, outputType, options: _options); - } - } - } - - - public T ToRecord(TableEntity entity) where T : EntityBase { - var entityInfo = GetEntityInfo(); - var parameters = - entityInfo.properties.Select(grouping => GetFieldValue(entityInfo, grouping.Key, entity)).ToArray(); - try { - var entityRecord = (T)entityInfo.constructor.Invoke(parameters); - if (entity.ETag != _emptyETag) { - entityRecord.ETag = entity.ETag; - } - entityRecord.TimeStamp = entity.Timestamp; - return entityRecord; - - } catch (Exception ex) { - var stringParam = string.Join(", ", parameters); - throw new Exception($"Could not initialize object of type {typeof(T)} with the following parameters: {stringParam} constructor {entityInfo.constructor} : {ex}"); - } - - } - -} - - - + public static string NewSortedKey => $"{DateTimeOffset.MaxValue.Ticks - DateTimeOffset.UtcNow.Ticks}"; +} + +public abstract record StatefulEntityBase([property: JsonIgnore] T State) : EntityBase() where T : Enum; + + +/// Indicates that the enum cases should no be renamed +[AttributeUsage(AttributeTargets.Enum)] +public class SerializeValueAttribute : Attribute { } + +/// Indicates that the enum cases should no be renamed +[AttributeUsage(AttributeTargets.Enum)] +public class SkipRename : Attribute { } +public class RowKeyAttribute : Attribute { } +public class PartitionKeyAttribute : Attribute { } +public class TypeDiscrimnatorAttribute : Attribute { + public string FieldName { get; } + // the type of a function that takes the value of fieldName as an input and return the type + public Type ConverterType { get; } + + public TypeDiscrimnatorAttribute(string fieldName, Type converterType) { + if (!converterType.IsAssignableTo(typeof(ITypeProvider))) { + throw new ArgumentException($"the provided type needs to implement ITypeProvider"); + } + + FieldName = fieldName; + ConverterType = converterType; + } +} + +public interface ITypeProvider { + Type GetTypeInfo(object input); +} + +public enum EntityPropertyKind { + PartitionKey, + RowKey, + Column +} +public record EntityProperty(string name, string columnName, Type type, EntityPropertyKind kind, (TypeDiscrimnatorAttribute, ITypeProvider)? discriminator); +public record EntityInfo(Type type, ILookup properties, Func constructor); + +class OnefuzzNamingPolicy : JsonNamingPolicy { + public override string ConvertName(string name) { + return CaseConverter.PascalToSnake(name); + } +} +public class EntityConverter { + private readonly JsonSerializerOptions _options; + + private readonly ConcurrentDictionary _cache; + + private readonly ETag _emptyETag = new ETag(); + + public EntityConverter() { + _options = GetJsonSerializerOptions(); + _cache = new ConcurrentDictionary(); + + } + + + public static JsonSerializerOptions GetJsonSerializerOptions() { + var options = new JsonSerializerOptions() { + PropertyNamingPolicy = new OnefuzzNamingPolicy(), + }; + options.Converters.Add(new CustomEnumConverterFactory()); + options.Converters.Add(new PolymorphicConverterFactory()); + return options; + } + + internal static Func BuildConstructerFrom(ConstructorInfo constructorInfo) { + var constructorParameters = Expression.Parameter(typeof(object?[])); + + var parameterExpressions = + constructorInfo.GetParameters().Select((parameterInfo, i) => { + var ithIndex = Expression.Constant(i); + var ithParameter = Expression.ArrayIndex(constructorParameters, ithIndex); + var unboxedIthParameter = Expression.Convert(ithParameter, parameterInfo.ParameterType); + return unboxedIthParameter; + + }).ToArray(); + + NewExpression constructorCall = Expression.New(constructorInfo, parameterExpressions); + + Func ctor = Expression.Lambda>(constructorCall, constructorParameters).Compile(); + return ctor; + } + + private IEnumerable GetEntityProperties(ParameterInfo parameterInfo) { + var name = parameterInfo.Name.EnsureNotNull($"Invalid paramter {parameterInfo}"); + var parameterType = parameterInfo.ParameterType.EnsureNotNull($"Invalid paramter {parameterInfo}"); + var isRowkey = parameterInfo.GetCustomAttribute(typeof(RowKeyAttribute)) != null; + var isPartitionkey = parameterInfo.GetCustomAttribute(typeof(PartitionKeyAttribute)) != null; + + var discriminatorAttribute = typeof(T).GetProperty(name)?.GetCustomAttribute(); + + (TypeDiscrimnatorAttribute, ITypeProvider)? discriminator = null; + if (discriminatorAttribute != null) { + var t = (ITypeProvider)(discriminatorAttribute.ConverterType.GetConstructor(new Type[] { })?.Invoke(null) ?? throw new Exception("unable to retrive the type provider")); + discriminator = (discriminatorAttribute, t); + } + + + if (isPartitionkey) { + yield return new EntityProperty(name, "PartitionKey", parameterType, EntityPropertyKind.PartitionKey, discriminator); + } + + if (isRowkey) { + yield return new EntityProperty(name, "RowKey", parameterType, EntityPropertyKind.RowKey, discriminator); + } + + if (!isPartitionkey && !isRowkey) { + var columnName = typeof(T).GetProperty(name)?.GetCustomAttribute()?.Name ?? CaseConverter.PascalToSnake(name); + yield return new EntityProperty(name, columnName, parameterType, EntityPropertyKind.Column, discriminator); + } + } + + + private EntityInfo GetEntityInfo() { + return _cache.GetOrAdd(typeof(T), type => { + var constructor = type.GetConstructors()[0]; + var parameterInfos = constructor.GetParameters(); + var parameters = + parameterInfos.SelectMany(GetEntityProperties).ToArray(); + + return new EntityInfo(typeof(T), parameters.ToLookup(x => x.name), BuildConstructerFrom(constructor)); + }); + } + + public string ToJsonString(T typedEntity) { + var serialized = JsonSerializer.Serialize(typedEntity, _options); + return serialized; + } + + public TableEntity ToTableEntity(T typedEntity) where T : EntityBase { + if (typedEntity == null) { + throw new NullReferenceException(); + } + var type = typeof(T)!; + if (type is null) { + throw new NullReferenceException(); + } + var tableEntity = new TableEntity(); + var entityInfo = GetEntityInfo(); + foreach (var prop in entityInfo.properties.SelectMany(x => x)) { + //var prop = kvp.First(); + var value = entityInfo.type.GetProperty(prop.name)?.GetValue(typedEntity); + if (prop.kind == EntityPropertyKind.PartitionKey || prop.kind == EntityPropertyKind.RowKey) { + tableEntity.Add(prop.columnName, value?.ToString()); + } else if (prop.type == typeof(Guid) || prop.type == typeof(Guid?)) { + tableEntity.Add(prop.columnName, value?.ToString()); + } else if (prop.type == typeof(bool) + || prop.type == typeof(bool?) + || prop.type == typeof(string) + || prop.type == typeof(DateTime) + || prop.type == typeof(DateTime?) + || prop.type == typeof(DateTimeOffset) + || prop.type == typeof(DateTimeOffset?) + || prop.type == typeof(int) + || prop.type == typeof(int?) + || prop.type == typeof(Int64) + || prop.type == typeof(Int64?) + || prop.type == typeof(double) + || prop.type == typeof(double?) + + ) { + tableEntity.Add(prop.columnName, value); + } else if (prop.type.IsEnum) { + var values = + (value?.ToString()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(CaseConverter.PascalToSnake)).EnsureNotNull($"Unable to read enum data {value}"); + + tableEntity.Add(prop.columnName, string.Join(",", values)); + } else { + var serialized = JsonSerializer.Serialize(value, _options); + tableEntity.Add(prop.columnName, serialized.Trim('"')); + } + + } + + if (typedEntity.ETag.HasValue) { + tableEntity.ETag = typedEntity.ETag.Value; + } + + return tableEntity; + } + + + private object? GetFieldValue(EntityInfo info, string name, TableEntity entity) { + var ef = info.properties[name].First(); + if (ef.kind == EntityPropertyKind.PartitionKey || ef.kind == EntityPropertyKind.RowKey) { + if (ef.type == typeof(string)) + return entity.GetString(ef.kind.ToString()); + else if (ef.type == typeof(Guid)) + return Guid.Parse(entity.GetString(ef.kind.ToString())); + else if (ef.type == typeof(int)) + return int.Parse(entity.GetString(ef.kind.ToString())); + else { + throw new Exception("invalid "); + } + } + + var fieldName = ef.columnName; + var obj = entity[fieldName]; + if (obj == null) { + return null; + } + var objType = obj.GetType(); + + if (ef.type == typeof(string)) { + return entity.GetString(fieldName); + } else if (ef.type == typeof(bool) || ef.type == typeof(bool?)) { + return entity.GetBoolean(fieldName); + } else if (ef.type == typeof(DateTimeOffset) || ef.type == typeof(DateTimeOffset?)) { + return entity.GetDateTimeOffset(fieldName); + } else if (ef.type == typeof(DateTime) || ef.type == typeof(DateTime?)) { + return entity.GetDateTime(fieldName); + } else if (ef.type == typeof(double) || ef.type == typeof(double?)) { + return entity.GetDouble(fieldName); + } else if (ef.type == typeof(Guid) || ef.type == typeof(Guid?)) { + return (object?)Guid.Parse(entity.GetString(fieldName)); + } else if (ef.type == typeof(int) || ef.type == typeof(short) || ef.type == typeof(int?) || ef.type == typeof(short?)) { + return entity.GetInt32(fieldName); + } else if (ef.type == typeof(long) || ef.type == typeof(long?)) { + return entity.GetInt64(fieldName); + } else if (ef.type.IsEnum) { + var stringValues = + entity.GetString(fieldName).Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(CaseConverter.SnakeToPascal); + + return Enum.Parse(ef.type, string.Join(",", stringValues)); + } else { + var outputType = ef.type; + if (ef.discriminator != null) { + var (attr, typeProvider) = ef.discriminator.Value; + var v = GetFieldValue(info, attr.FieldName, entity) ?? throw new Exception($"No value for {attr.FieldName}"); + outputType = typeProvider.GetTypeInfo(v); + } + + + if (objType == typeof(string)) { + var value = entity.GetString(fieldName); + if (value.StartsWith('[') || value.StartsWith('{') || value == "null") { + return JsonSerializer.Deserialize(value, outputType, options: _options); + } else { + return JsonSerializer.Deserialize($"\"{value}\"", outputType, options: _options); + } + } else { + var value = entity.GetString(fieldName); + return JsonSerializer.Deserialize(value, outputType, options: _options); + } + } + } + + + public T ToRecord(TableEntity entity) where T : EntityBase { + var entityInfo = GetEntityInfo(); + var parameters = + entityInfo.properties.Select(grouping => GetFieldValue(entityInfo, grouping.Key, entity)).ToArray(); + try { + var entityRecord = (T)entityInfo.constructor.Invoke(parameters); + if (entity.ETag != _emptyETag) { + entityRecord.ETag = entity.ETag; + } + entityRecord.TimeStamp = entity.Timestamp; + return entityRecord; + + } catch (Exception ex) { + var stringParam = string.Join(", ", parameters); + throw new Exception($"Could not initialize object of type {typeof(T)} with the following parameters: {stringParam} constructor {entityInfo.constructor} : {ex}"); + } + + } + +} + + + diff --git a/src/ApiService/Tests/OrmTest.cs b/src/ApiService/Tests/OrmTest.cs index 54d8fd0e4..c1d271de9 100644 --- a/src/ApiService/Tests/OrmTest.cs +++ b/src/ApiService/Tests/OrmTest.cs @@ -14,6 +14,7 @@ namespace Tests { public String? TheName { get; set; } public TestEnum TheEnum { get; set; } public TestFlagEnum TheFlag { get; set; } + public TestEnumValue TheEnumValue { get; set; } } enum TestEnum { @@ -27,6 +28,12 @@ namespace Tests { FlagTwo = 2, } + [SerializeValue] + enum TestEnumValue { + One = 1, + Two = 2 + } + record Entity1( [PartitionKey] Guid Id, [RowKey] string TheName, @@ -60,7 +67,8 @@ namespace Tests { new TestObject { TheName = "testobject", TheEnum = TestEnum.TheTwo, - TheFlag = TestFlagEnum.FlagOne | TestFlagEnum.FlagTwo + TheFlag = TestFlagEnum.FlagOne | TestFlagEnum.FlagTwo, + TheEnumValue = TestEnumValue.Two }, null, new Uri(uriString), @@ -90,6 +98,7 @@ namespace Tests { Assert.Equal(fromTableEntity.TheObject.TheEnum, entity1.TheObject.TheEnum); Assert.Equal(fromTableEntity.TheObject.TheFlag, entity1.TheObject.TheFlag); Assert.Equal(fromTableEntity.TheObject.TheName, entity1.TheObject.TheName); + Assert.Equal(fromTableEntity.TheObject.TheEnumValue, entity1.TheObject.TheEnumValue); } @@ -108,7 +117,8 @@ namespace Tests { new TestObject { TheName = "testobject", TheEnum = TestEnum.TheTwo, - TheFlag = TestFlagEnum.FlagOne | TestFlagEnum.FlagTwo + TheFlag = TestFlagEnum.FlagOne | TestFlagEnum.FlagTwo, + TheEnumValue = TestEnumValue.One }, null, new Uri(uriString), @@ -134,11 +144,12 @@ namespace Tests { json.TryGetPropertyValue("the_name", out var theName); json.TryGetPropertyValue("the_enum", out var theEnum); json.TryGetPropertyValue("the_flag", out var theFlag); + json.TryGetPropertyValue("the_enum_value", out var theEnumValue); Assert.Equal(entity1.TheObject.TheName, theName?.GetValue()); Assert.Equal("the_two", theEnum?.GetValue()); Assert.Equal("flag_one,flag_two", theFlag?.GetValue()); - + Assert.Equal((int)TestEnumValue.One, theEnumValue?.GetValue()); } [Fact] @@ -303,5 +314,21 @@ namespace Tests { Assert.Equal(expected.Container.ContainerName, tableEntity.GetString("container")); } + + record TestEnumObject(TestEnumValue TheEnumValue); + + [Fact] + public void TestSerializeEnumValue() { + var expectedObject = new TestEnumObject( + TheEnumValue: TestEnumValue.One + ); + + var serialized = JsonSerializer.Serialize(expectedObject, EntityConverter.GetJsonSerializerOptions()); + var json = JsonDocument.Parse(serialized); + Assert.Equal((int)expectedObject.TheEnumValue, json.RootElement.GetProperty("the_enum_value").GetInt32()); + var actual = JsonSerializer.Deserialize(serialized, EntityConverter.GetJsonSerializerOptions()); + Assert.Equal(expectedObject, actual); + } + } }