Add the ability to serialize enums values (#1898)

* Add the ablility to serialize enums values

* unit test
This commit is contained in:
Cheick Keita
2022-05-04 14:20:23 -07:00
committed by GitHub
parent b2399c4571
commit c07a908cf4
5 changed files with 325 additions and 290 deletions

View File

@ -1,6 +1,9 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
namespace Microsoft.OneFuzz.Service; namespace Microsoft.OneFuzz.Service;
[SerializeValue]
public enum ErrorCode { public enum ErrorCode {
INVALID_REQUEST = 450, INVALID_REQUEST = 450,
INVALID_PERMISSION = 451, INVALID_PERMISSION = 451,

View File

@ -268,7 +268,7 @@ namespace ApiService.TestHooks {
var s = await req.ReadAsStringAsync(); var s = await req.ReadAsStringAsync();
var markTasks = JsonSerializer.Deserialize<MarkTasks>(s!, EntityConverter.GetJsonSerializerOptions()); var markTasks = JsonSerializer.Deserialize<MarkTasks>(s!, EntityConverter.GetJsonSerializerOptions());
await _nodeOps.MarkTasksStoppedEarly(markTasks.node, markTasks.error); await _nodeOps.MarkTasksStoppedEarly(markTasks!.node, markTasks.error);
var resp = req.CreateResponse(HttpStatusCode.OK); var resp = req.CreateResponse(HttpStatusCode.OK);
return resp; return resp;

View File

@ -8,7 +8,7 @@ using System.Text.Json.Serialization;
namespace Microsoft.OneFuzz.Service.OneFuzzLib.Orm; namespace Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
public sealed class CustomEnumConverterFactory : JsonConverterFactory { public sealed class CustomEnumConverterFactory : JsonConverterFactory {
public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum; public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum && (typeToConvert.GetCustomAttribute<SerializeValueAttribute>() == null);
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) {
object[]? knownValues = null; object[]? knownValues = null;

View File

@ -1,289 +1,294 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Azure; using Azure;
using Azure.Data.Tables; using Azure.Data.Tables;
namespace Microsoft.OneFuzz.Service.OneFuzzLib.Orm; namespace Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
public abstract record EntityBase { public abstract record EntityBase {
[JsonIgnore] public ETag? ETag { get; set; } [JsonIgnore] public ETag? ETag { get; set; }
public DateTimeOffset? TimeStamp { 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 // 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 // Produce "good-quality-table-key" based on a DateTimeOffset timestamp
public static string NewSortedKey => $"{DateTimeOffset.MaxValue.Ticks - DateTimeOffset.UtcNow.Ticks}"; public static string NewSortedKey => $"{DateTimeOffset.MaxValue.Ticks - DateTimeOffset.UtcNow.Ticks}";
} }
public abstract record StatefulEntityBase<T>([property: JsonIgnore] T State) : EntityBase() where T : Enum; public abstract record StatefulEntityBase<T>([property: JsonIgnore] T State) : EntityBase() where T : Enum;
/// Indicates that the enum cases should no be renamed
[AttributeUsage(AttributeTargets.Enum)] /// Indicates that the enum cases should no be renamed
public class SkipRename : Attribute { } [AttributeUsage(AttributeTargets.Enum)]
public class RowKeyAttribute : Attribute { } public class SerializeValueAttribute : Attribute { }
public class PartitionKeyAttribute : Attribute { }
public class TypeDiscrimnatorAttribute : Attribute { /// Indicates that the enum cases should no be renamed
public string FieldName { get; } [AttributeUsage(AttributeTargets.Enum)]
// the type of a function that takes the value of fieldName as an input and return the type public class SkipRename : Attribute { }
public Type ConverterType { get; } public class RowKeyAttribute : Attribute { }
public class PartitionKeyAttribute : Attribute { }
public TypeDiscrimnatorAttribute(string fieldName, Type converterType) { public class TypeDiscrimnatorAttribute : Attribute {
if (!converterType.IsAssignableTo(typeof(ITypeProvider))) { public string FieldName { get; }
throw new ArgumentException($"the provided type needs to implement ITypeProvider"); // the type of a function that takes the value of fieldName as an input and return the type
} public Type ConverterType { get; }
FieldName = fieldName; public TypeDiscrimnatorAttribute(string fieldName, Type converterType) {
ConverterType = converterType; if (!converterType.IsAssignableTo(typeof(ITypeProvider))) {
} throw new ArgumentException($"the provided type needs to implement ITypeProvider");
} }
public interface ITypeProvider { FieldName = fieldName;
Type GetTypeInfo(object input); ConverterType = converterType;
} }
}
public enum EntityPropertyKind {
PartitionKey, public interface ITypeProvider {
RowKey, Type GetTypeInfo(object input);
Column }
}
public record EntityProperty(string name, string columnName, Type type, EntityPropertyKind kind, (TypeDiscrimnatorAttribute, ITypeProvider)? discriminator); public enum EntityPropertyKind {
public record EntityInfo(Type type, ILookup<string, EntityProperty> properties, Func<object?[], object> constructor); PartitionKey,
RowKey,
class OnefuzzNamingPolicy : JsonNamingPolicy { Column
public override string ConvertName(string name) { }
return CaseConverter.PascalToSnake(name); public record EntityProperty(string name, string columnName, Type type, EntityPropertyKind kind, (TypeDiscrimnatorAttribute, ITypeProvider)? discriminator);
} public record EntityInfo(Type type, ILookup<string, EntityProperty> properties, Func<object?[], object> constructor);
}
public class EntityConverter { class OnefuzzNamingPolicy : JsonNamingPolicy {
private readonly JsonSerializerOptions _options; public override string ConvertName(string name) {
return CaseConverter.PascalToSnake(name);
private readonly ConcurrentDictionary<Type, EntityInfo> _cache; }
}
private readonly ETag _emptyETag = new ETag(); public class EntityConverter {
private readonly JsonSerializerOptions _options;
public EntityConverter() {
_options = GetJsonSerializerOptions(); private readonly ConcurrentDictionary<Type, EntityInfo> _cache;
_cache = new ConcurrentDictionary<Type, EntityInfo>();
private readonly ETag _emptyETag = new ETag();
}
public EntityConverter() {
_options = GetJsonSerializerOptions();
public static JsonSerializerOptions GetJsonSerializerOptions() { _cache = new ConcurrentDictionary<Type, EntityInfo>();
var options = new JsonSerializerOptions() {
PropertyNamingPolicy = new OnefuzzNamingPolicy(), }
};
options.Converters.Add(new CustomEnumConverterFactory());
options.Converters.Add(new PolymorphicConverterFactory()); public static JsonSerializerOptions GetJsonSerializerOptions() {
return options; var options = new JsonSerializerOptions() {
} PropertyNamingPolicy = new OnefuzzNamingPolicy(),
};
internal static Func<object?[], object> BuildConstructerFrom(ConstructorInfo constructorInfo) { options.Converters.Add(new CustomEnumConverterFactory());
var constructorParameters = Expression.Parameter(typeof(object?[])); options.Converters.Add(new PolymorphicConverterFactory());
return options;
var parameterExpressions = }
constructorInfo.GetParameters().Select((parameterInfo, i) => {
var ithIndex = Expression.Constant(i); internal static Func<object?[], object> BuildConstructerFrom(ConstructorInfo constructorInfo) {
var ithParameter = Expression.ArrayIndex(constructorParameters, ithIndex); var constructorParameters = Expression.Parameter(typeof(object?[]));
var unboxedIthParameter = Expression.Convert(ithParameter, parameterInfo.ParameterType);
return unboxedIthParameter; var parameterExpressions =
constructorInfo.GetParameters().Select((parameterInfo, i) => {
}).ToArray(); var ithIndex = Expression.Constant(i);
var ithParameter = Expression.ArrayIndex(constructorParameters, ithIndex);
NewExpression constructorCall = Expression.New(constructorInfo, parameterExpressions); var unboxedIthParameter = Expression.Convert(ithParameter, parameterInfo.ParameterType);
return unboxedIthParameter;
Func<object?[], object> ctor = Expression.Lambda<Func<object?[], object>>(constructorCall, constructorParameters).Compile();
return ctor; }).ToArray();
}
NewExpression constructorCall = Expression.New(constructorInfo, parameterExpressions);
private IEnumerable<EntityProperty> GetEntityProperties<T>(ParameterInfo parameterInfo) {
var name = parameterInfo.Name.EnsureNotNull($"Invalid paramter {parameterInfo}"); Func<object?[], object> ctor = Expression.Lambda<Func<object?[], object>>(constructorCall, constructorParameters).Compile();
var parameterType = parameterInfo.ParameterType.EnsureNotNull($"Invalid paramter {parameterInfo}"); return ctor;
var isRowkey = parameterInfo.GetCustomAttribute(typeof(RowKeyAttribute)) != null; }
var isPartitionkey = parameterInfo.GetCustomAttribute(typeof(PartitionKeyAttribute)) != null;
private IEnumerable<EntityProperty> GetEntityProperties<T>(ParameterInfo parameterInfo) {
var discriminatorAttribute = typeof(T).GetProperty(name)?.GetCustomAttribute<TypeDiscrimnatorAttribute>(); var name = parameterInfo.Name.EnsureNotNull($"Invalid paramter {parameterInfo}");
var parameterType = parameterInfo.ParameterType.EnsureNotNull($"Invalid paramter {parameterInfo}");
(TypeDiscrimnatorAttribute, ITypeProvider)? discriminator = null; var isRowkey = parameterInfo.GetCustomAttribute(typeof(RowKeyAttribute)) != null;
if (discriminatorAttribute != null) { var isPartitionkey = parameterInfo.GetCustomAttribute(typeof(PartitionKeyAttribute)) != null;
var t = (ITypeProvider)(discriminatorAttribute.ConverterType.GetConstructor(new Type[] { })?.Invoke(null) ?? throw new Exception("unable to retrive the type provider"));
discriminator = (discriminatorAttribute, t); var discriminatorAttribute = typeof(T).GetProperty(name)?.GetCustomAttribute<TypeDiscrimnatorAttribute>();
}
(TypeDiscrimnatorAttribute, ITypeProvider)? discriminator = null;
if (discriminatorAttribute != null) {
if (isPartitionkey) { var t = (ITypeProvider)(discriminatorAttribute.ConverterType.GetConstructor(new Type[] { })?.Invoke(null) ?? throw new Exception("unable to retrive the type provider"));
yield return new EntityProperty(name, "PartitionKey", parameterType, EntityPropertyKind.PartitionKey, discriminator); discriminator = (discriminatorAttribute, t);
} }
if (isRowkey) {
yield return new EntityProperty(name, "RowKey", parameterType, EntityPropertyKind.RowKey, discriminator); if (isPartitionkey) {
} yield return new EntityProperty(name, "PartitionKey", parameterType, EntityPropertyKind.PartitionKey, discriminator);
}
if (!isPartitionkey && !isRowkey) {
var columnName = typeof(T).GetProperty(name)?.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name ?? CaseConverter.PascalToSnake(name); if (isRowkey) {
yield return new EntityProperty(name, columnName, parameterType, EntityPropertyKind.Column, discriminator); yield return new EntityProperty(name, "RowKey", parameterType, EntityPropertyKind.RowKey, discriminator);
} }
}
if (!isPartitionkey && !isRowkey) {
var columnName = typeof(T).GetProperty(name)?.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name ?? CaseConverter.PascalToSnake(name);
private EntityInfo GetEntityInfo<T>() { yield return new EntityProperty(name, columnName, parameterType, EntityPropertyKind.Column, discriminator);
return _cache.GetOrAdd(typeof(T), type => { }
var constructor = type.GetConstructors()[0]; }
var parameterInfos = constructor.GetParameters();
var parameters =
parameterInfos.SelectMany(GetEntityProperties<T>).ToArray(); private EntityInfo GetEntityInfo<T>() {
return _cache.GetOrAdd(typeof(T), type => {
return new EntityInfo(typeof(T), parameters.ToLookup(x => x.name), BuildConstructerFrom(constructor)); var constructor = type.GetConstructors()[0];
}); var parameterInfos = constructor.GetParameters();
} var parameters =
parameterInfos.SelectMany(GetEntityProperties<T>).ToArray();
public string ToJsonString<T>(T typedEntity) {
var serialized = JsonSerializer.Serialize(typedEntity, _options); return new EntityInfo(typeof(T), parameters.ToLookup(x => x.name), BuildConstructerFrom(constructor));
return serialized; });
} }
public TableEntity ToTableEntity<T>(T typedEntity) where T : EntityBase { public string ToJsonString<T>(T typedEntity) {
if (typedEntity == null) { var serialized = JsonSerializer.Serialize(typedEntity, _options);
throw new NullReferenceException(); return serialized;
} }
var type = typeof(T)!;
if (type is null) { public TableEntity ToTableEntity<T>(T typedEntity) where T : EntityBase {
throw new NullReferenceException(); if (typedEntity == null) {
} throw new NullReferenceException();
var tableEntity = new TableEntity(); }
var entityInfo = GetEntityInfo<T>(); var type = typeof(T)!;
foreach (var prop in entityInfo.properties.SelectMany(x => x)) { if (type is null) {
//var prop = kvp.First(); throw new NullReferenceException();
var value = entityInfo.type.GetProperty(prop.name)?.GetValue(typedEntity); }
if (prop.kind == EntityPropertyKind.PartitionKey || prop.kind == EntityPropertyKind.RowKey) { var tableEntity = new TableEntity();
tableEntity.Add(prop.columnName, value?.ToString()); var entityInfo = GetEntityInfo<T>();
} else if (prop.type == typeof(Guid) || prop.type == typeof(Guid?)) { foreach (var prop in entityInfo.properties.SelectMany(x => x)) {
tableEntity.Add(prop.columnName, value?.ToString()); //var prop = kvp.First();
} else if (prop.type == typeof(bool) var value = entityInfo.type.GetProperty(prop.name)?.GetValue(typedEntity);
|| prop.type == typeof(bool?) if (prop.kind == EntityPropertyKind.PartitionKey || prop.kind == EntityPropertyKind.RowKey) {
|| prop.type == typeof(string) tableEntity.Add(prop.columnName, value?.ToString());
|| prop.type == typeof(DateTime) } else if (prop.type == typeof(Guid) || prop.type == typeof(Guid?)) {
|| prop.type == typeof(DateTime?) tableEntity.Add(prop.columnName, value?.ToString());
|| prop.type == typeof(DateTimeOffset) } else if (prop.type == typeof(bool)
|| prop.type == typeof(DateTimeOffset?) || prop.type == typeof(bool?)
|| prop.type == typeof(int) || prop.type == typeof(string)
|| prop.type == typeof(int?) || prop.type == typeof(DateTime)
|| prop.type == typeof(Int64) || prop.type == typeof(DateTime?)
|| prop.type == typeof(Int64?) || prop.type == typeof(DateTimeOffset)
|| prop.type == typeof(double) || prop.type == typeof(DateTimeOffset?)
|| prop.type == typeof(double?) || prop.type == typeof(int)
|| prop.type == typeof(int?)
) { || prop.type == typeof(Int64)
tableEntity.Add(prop.columnName, value); || prop.type == typeof(Int64?)
} else if (prop.type.IsEnum) { || prop.type == typeof(double)
var values = || prop.type == typeof(double?)
(value?.ToString()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(CaseConverter.PascalToSnake)).EnsureNotNull($"Unable to read enum data {value}"); ) {
tableEntity.Add(prop.columnName, value);
tableEntity.Add(prop.columnName, string.Join(",", values)); } else if (prop.type.IsEnum) {
} else { var values =
var serialized = JsonSerializer.Serialize(value, _options); (value?.ToString()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
tableEntity.Add(prop.columnName, serialized.Trim('"')); .Select(CaseConverter.PascalToSnake)).EnsureNotNull($"Unable to read enum data {value}");
}
tableEntity.Add(prop.columnName, string.Join(",", values));
} } else {
var serialized = JsonSerializer.Serialize(value, _options);
if (typedEntity.ETag.HasValue) { tableEntity.Add(prop.columnName, serialized.Trim('"'));
tableEntity.ETag = typedEntity.ETag.Value; }
}
}
return tableEntity;
} if (typedEntity.ETag.HasValue) {
tableEntity.ETag = typedEntity.ETag.Value;
}
private object? GetFieldValue(EntityInfo info, string name, TableEntity entity) {
var ef = info.properties[name].First(); return tableEntity;
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)) private object? GetFieldValue(EntityInfo info, string name, TableEntity entity) {
return Guid.Parse(entity.GetString(ef.kind.ToString())); var ef = info.properties[name].First();
else if (ef.type == typeof(int)) if (ef.kind == EntityPropertyKind.PartitionKey || ef.kind == EntityPropertyKind.RowKey) {
return int.Parse(entity.GetString(ef.kind.ToString())); if (ef.type == typeof(string))
else { return entity.GetString(ef.kind.ToString());
throw new Exception("invalid "); 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()));
var fieldName = ef.columnName; else {
var obj = entity[fieldName]; throw new Exception("invalid ");
if (obj == null) { }
return null; }
}
var objType = obj.GetType(); var fieldName = ef.columnName;
var obj = entity[fieldName];
if (ef.type == typeof(string)) { if (obj == null) {
return entity.GetString(fieldName); return null;
} else if (ef.type == typeof(bool) || ef.type == typeof(bool?)) { }
return entity.GetBoolean(fieldName); var objType = obj.GetType();
} else if (ef.type == typeof(DateTimeOffset) || ef.type == typeof(DateTimeOffset?)) {
return entity.GetDateTimeOffset(fieldName); if (ef.type == typeof(string)) {
} else if (ef.type == typeof(DateTime) || ef.type == typeof(DateTime?)) { return entity.GetString(fieldName);
return entity.GetDateTime(fieldName); } else if (ef.type == typeof(bool) || ef.type == typeof(bool?)) {
} else if (ef.type == typeof(double) || ef.type == typeof(double?)) { return entity.GetBoolean(fieldName);
return entity.GetDouble(fieldName); } else if (ef.type == typeof(DateTimeOffset) || ef.type == typeof(DateTimeOffset?)) {
} else if (ef.type == typeof(Guid) || ef.type == typeof(Guid?)) { return entity.GetDateTimeOffset(fieldName);
return (object?)Guid.Parse(entity.GetString(fieldName)); } else if (ef.type == typeof(DateTime) || ef.type == typeof(DateTime?)) {
} else if (ef.type == typeof(int) || ef.type == typeof(short) || ef.type == typeof(int?) || ef.type == typeof(short?)) { return entity.GetDateTime(fieldName);
return entity.GetInt32(fieldName); } else if (ef.type == typeof(double) || ef.type == typeof(double?)) {
} else if (ef.type == typeof(long) || ef.type == typeof(long?)) { return entity.GetDouble(fieldName);
return entity.GetInt64(fieldName); } else if (ef.type == typeof(Guid) || ef.type == typeof(Guid?)) {
} else if (ef.type.IsEnum) { return (object?)Guid.Parse(entity.GetString(fieldName));
var stringValues = } else if (ef.type == typeof(int) || ef.type == typeof(short) || ef.type == typeof(int?) || ef.type == typeof(short?)) {
entity.GetString(fieldName).Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) return entity.GetInt32(fieldName);
.Select(CaseConverter.SnakeToPascal); } else if (ef.type == typeof(long) || ef.type == typeof(long?)) {
return entity.GetInt64(fieldName);
return Enum.Parse(ef.type, string.Join(",", stringValues)); } else if (ef.type.IsEnum) {
} else { var stringValues =
var outputType = ef.type; entity.GetString(fieldName).Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
if (ef.discriminator != null) { .Select(CaseConverter.SnakeToPascal);
var (attr, typeProvider) = ef.discriminator.Value;
var v = GetFieldValue(info, attr.FieldName, entity) ?? throw new Exception($"No value for {attr.FieldName}"); return Enum.Parse(ef.type, string.Join(",", stringValues));
outputType = typeProvider.GetTypeInfo(v); } else {
} var outputType = ef.type;
if (ef.discriminator != null) {
var (attr, typeProvider) = ef.discriminator.Value;
if (objType == typeof(string)) { var v = GetFieldValue(info, attr.FieldName, entity) ?? throw new Exception($"No value for {attr.FieldName}");
var value = entity.GetString(fieldName); outputType = typeProvider.GetTypeInfo(v);
if (value.StartsWith('[') || value.StartsWith('{') || value == "null") { }
return JsonSerializer.Deserialize(value, outputType, options: _options);
} else {
return JsonSerializer.Deserialize($"\"{value}\"", outputType, options: _options); if (objType == typeof(string)) {
} var value = entity.GetString(fieldName);
} else { if (value.StartsWith('[') || value.StartsWith('{') || value == "null") {
var value = entity.GetString(fieldName); return JsonSerializer.Deserialize(value, outputType, options: _options);
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<T>(TableEntity entity) where T : EntityBase { }
var entityInfo = GetEntityInfo<T>(); }
var parameters = }
entityInfo.properties.Select(grouping => GetFieldValue(entityInfo, grouping.Key, entity)).ToArray();
try {
var entityRecord = (T)entityInfo.constructor.Invoke(parameters); public T ToRecord<T>(TableEntity entity) where T : EntityBase {
if (entity.ETag != _emptyETag) { var entityInfo = GetEntityInfo<T>();
entityRecord.ETag = entity.ETag; var parameters =
} entityInfo.properties.Select(grouping => GetFieldValue(entityInfo, grouping.Key, entity)).ToArray();
entityRecord.TimeStamp = entity.Timestamp; try {
return entityRecord; var entityRecord = (T)entityInfo.constructor.Invoke(parameters);
if (entity.ETag != _emptyETag) {
} catch (Exception ex) { entityRecord.ETag = entity.ETag;
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}"); 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}");
}
}
}

View File

@ -14,6 +14,7 @@ namespace Tests {
public String? TheName { get; set; } public String? TheName { get; set; }
public TestEnum TheEnum { get; set; } public TestEnum TheEnum { get; set; }
public TestFlagEnum TheFlag { get; set; } public TestFlagEnum TheFlag { get; set; }
public TestEnumValue TheEnumValue { get; set; }
} }
enum TestEnum { enum TestEnum {
@ -27,6 +28,12 @@ namespace Tests {
FlagTwo = 2, FlagTwo = 2,
} }
[SerializeValue]
enum TestEnumValue {
One = 1,
Two = 2
}
record Entity1( record Entity1(
[PartitionKey] Guid Id, [PartitionKey] Guid Id,
[RowKey] string TheName, [RowKey] string TheName,
@ -60,7 +67,8 @@ namespace Tests {
new TestObject { new TestObject {
TheName = "testobject", TheName = "testobject",
TheEnum = TestEnum.TheTwo, TheEnum = TestEnum.TheTwo,
TheFlag = TestFlagEnum.FlagOne | TestFlagEnum.FlagTwo TheFlag = TestFlagEnum.FlagOne | TestFlagEnum.FlagTwo,
TheEnumValue = TestEnumValue.Two
}, },
null, null,
new Uri(uriString), new Uri(uriString),
@ -90,6 +98,7 @@ namespace Tests {
Assert.Equal(fromTableEntity.TheObject.TheEnum, entity1.TheObject.TheEnum); Assert.Equal(fromTableEntity.TheObject.TheEnum, entity1.TheObject.TheEnum);
Assert.Equal(fromTableEntity.TheObject.TheFlag, entity1.TheObject.TheFlag); Assert.Equal(fromTableEntity.TheObject.TheFlag, entity1.TheObject.TheFlag);
Assert.Equal(fromTableEntity.TheObject.TheName, entity1.TheObject.TheName); Assert.Equal(fromTableEntity.TheObject.TheName, entity1.TheObject.TheName);
Assert.Equal(fromTableEntity.TheObject.TheEnumValue, entity1.TheObject.TheEnumValue);
} }
@ -108,7 +117,8 @@ namespace Tests {
new TestObject { new TestObject {
TheName = "testobject", TheName = "testobject",
TheEnum = TestEnum.TheTwo, TheEnum = TestEnum.TheTwo,
TheFlag = TestFlagEnum.FlagOne | TestFlagEnum.FlagTwo TheFlag = TestFlagEnum.FlagOne | TestFlagEnum.FlagTwo,
TheEnumValue = TestEnumValue.One
}, },
null, null,
new Uri(uriString), new Uri(uriString),
@ -134,11 +144,12 @@ namespace Tests {
json.TryGetPropertyValue("the_name", out var theName); json.TryGetPropertyValue("the_name", out var theName);
json.TryGetPropertyValue("the_enum", out var theEnum); json.TryGetPropertyValue("the_enum", out var theEnum);
json.TryGetPropertyValue("the_flag", out var theFlag); json.TryGetPropertyValue("the_flag", out var theFlag);
json.TryGetPropertyValue("the_enum_value", out var theEnumValue);
Assert.Equal(entity1.TheObject.TheName, theName?.GetValue<string>()); Assert.Equal(entity1.TheObject.TheName, theName?.GetValue<string>());
Assert.Equal("the_two", theEnum?.GetValue<string>()); Assert.Equal("the_two", theEnum?.GetValue<string>());
Assert.Equal("flag_one,flag_two", theFlag?.GetValue<string>()); Assert.Equal("flag_one,flag_two", theFlag?.GetValue<string>());
Assert.Equal((int)TestEnumValue.One, theEnumValue?.GetValue<int>());
} }
[Fact] [Fact]
@ -303,5 +314,21 @@ namespace Tests {
Assert.Equal(expected.Container.ContainerName, tableEntity.GetString("container")); 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<TestEnumObject>(serialized, EntityConverter.GetJsonSerializerOptions());
Assert.Equal(expectedObject, actual);
}
} }
} }