Skip to content

Commit d7b4b49

Browse files
committed
Support StringEnumAttribute on properties (#4036)
This commit supports attributing a an enum property on a type with StringEnumAttribute to serialize it as a string. Closes #4035 (cherry picked from commit 1bce948) (cherry picked from commit 09fbaad)
1 parent 2b4d598 commit d7b4b49

File tree

6 files changed

+118
-26
lines changed

6 files changed

+118
-26
lines changed

src/Elasticsearch.Net/Utf8Json/IJsonProperty.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
// SOFTWARE.
2323
#endregion
2424

25+
using System;
26+
2527
namespace Elasticsearch.Net.Utf8Json
2628
{
2729
internal interface IJsonProperty
@@ -37,20 +39,20 @@ internal interface IJsonProperty
3739

3840
internal class JsonProperty : IJsonProperty
3941
{
40-
public JsonProperty(string name)
41-
{
42-
Name = name;
43-
}
42+
public JsonProperty(string name) => Name = name;
4443

4544
public string Name { get; set; }
4645

47-
public int Order
48-
{
49-
get { return 0; }
50-
}
46+
public int Order => 0;
5147

5248
public bool Ignore { get; set; }
5349

5450
public bool? AllowPrivate { get; set; }
51+
52+
/// <summary>
53+
/// An instance of an <see cref="IJsonFormatter"/> that will be used
54+
/// to serialize/deserialize the property
55+
/// </summary>
56+
public object JsonFormatter { get; set; }
5557
}
5658
}

src/Elasticsearch.Net/Utf8Json/Internal/Emit/MetaMember.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ internal class MetaMember
4444
public PropertyInfo[] InterfacePropertyInfos { get; private set; }
4545
public MethodInfo ShouldSerializeMethodInfo { get; private set; }
4646
public MethodInfo ShouldSerializeTypeMethodInfo { get; private set; }
47+
public object JsonFormatter {get; private set; }
4748

4849
MethodInfo getMethod;
4950
MethodInfo setMethod;
@@ -57,7 +58,7 @@ protected MetaMember(Type type, string name, string memberName, bool isWritable,
5758
this.IsReadable = isReadable;
5859
}
5960

60-
public MetaMember(FieldInfo info, string name, bool allowPrivate)
61+
public MetaMember(FieldInfo info, string name, object jsonFormatter, bool allowPrivate)
6162
{
6263
this.Name = name;
6364
this.MemberName = info.Name;
@@ -67,9 +68,10 @@ public MetaMember(FieldInfo info, string name, bool allowPrivate)
6768
this.IsWritable = allowPrivate || (info.IsPublic && !info.IsInitOnly);
6869
this.ShouldSerializeMethodInfo = GetShouldSerialize(info);
6970
this.ShouldSerializeTypeMethodInfo = info.FieldType.GetTypeInfo().GetShouldSerializeMethod();
71+
this.JsonFormatter = jsonFormatter;
7072
}
7173

72-
public MetaMember(PropertyInfo info, string name, PropertyInfo[] interfaceInfos, bool allowPrivate)
74+
public MetaMember(PropertyInfo info, string name, PropertyInfo[] interfaceInfos, object jsonFormatter, bool allowPrivate)
7375
{
7476
this.getMethod = info.GetGetMethod(true);
7577
this.setMethod = info.GetSetMethod(true);
@@ -83,6 +85,7 @@ public MetaMember(PropertyInfo info, string name, PropertyInfo[] interfaceInfos,
8385
this.IsWritable = (setMethod != null) && (allowPrivate || setMethod.IsPublic) && !setMethod.IsStatic;
8486
this.ShouldSerializeMethodInfo = GetShouldSerialize(info);
8587
this.ShouldSerializeTypeMethodInfo = info.PropertyType.GetTypeInfo().GetShouldSerializeMethod();
88+
this.JsonFormatter = jsonFormatter;
8689
}
8790

8891
static MethodInfo GetShouldSerialize(MemberInfo info)

src/Elasticsearch.Net/Utf8Json/Internal/Emit/MetaType.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private static TAttribute GetCustomAttribute<TAttribute>(PropertyInfo propertyIn
7373
return interfaceProperty != null ? interfaceProperty.GetCustomAttribute<TAttribute>(inherit) : null;
7474
}
7575

76-
public MetaType(Type type, Func<string, string> nameMutator, Func<MemberInfo, IJsonProperty> propertyMapper, bool allowPrivate)
76+
public MetaType(Type type, Func<string, string> nameMutator, Func<MemberInfo, JsonProperty> propertyMapper, bool allowPrivate)
7777
{
7878
var ti = type.GetTypeInfo();
7979
var isClass = ti.IsClass || ti.IsInterface || ti.IsAbstract;
@@ -131,6 +131,8 @@ public MetaType(Type type, Func<string, string> nameMutator, Func<MemberInfo, IJ
131131

132132
var allowPrivateMember = allowPrivate;
133133

134+
object jsonFormatter = null;
135+
134136
if (propertyMapper != null)
135137
{
136138
var property = propertyMapper(item);
@@ -144,12 +146,15 @@ public MetaType(Type type, Func<string, string> nameMutator, Func<MemberInfo, IJ
144146

145147
if (property.AllowPrivate.HasValue)
146148
allowPrivateMember = property.AllowPrivate.Value;
149+
150+
if (property.JsonFormatter != null)
151+
jsonFormatter = property.JsonFormatter;
147152
}
148153
}
149154

150155
var props = interfaceProps != null ? interfaceProps.ToArray() : null;
151156

152-
var member = new MetaMember(item, name, props, allowPrivateMember || dm != null);
157+
var member = new MetaMember(item, name, props, jsonFormatter, allowPrivateMember || dm != null);
153158
if (!member.IsReadable && !member.IsWritable) continue;
154159

155160
if (!stringMembers.ContainsKey(member.Name))
@@ -166,6 +171,7 @@ public MetaType(Type type, Func<string, string> nameMutator, Func<MemberInfo, IJ
166171
if (dataContractPresent && dm == null) continue;
167172
var name = (dm != null && dm.Name != null) ? dm.Name : nameMutator(item.Name);
168173
var allowPrivateMember = allowPrivate;
174+
object jsonFormatter = null;
169175
if (propertyMapper != null)
170176
{
171177
var field = propertyMapper(item);
@@ -179,10 +185,13 @@ public MetaType(Type type, Func<string, string> nameMutator, Func<MemberInfo, IJ
179185

180186
if (field.AllowPrivate.HasValue)
181187
allowPrivateMember = field.AllowPrivate.Value;
188+
189+
if (field.JsonFormatter != null)
190+
jsonFormatter = field.JsonFormatter;
182191
}
183192
}
184193

185-
var member = new MetaMember(item, name, allowPrivateMember || dm != null);
194+
var member = new MetaMember(item, name, jsonFormatter, allowPrivateMember || dm != null);
186195
if (!member.IsReadable && !member.IsWritable) continue;
187196

188197
if (!stringMembers.ContainsKey(member.Name))

src/Elasticsearch.Net/Utf8Json/Resolvers/DynamicObjectResolver.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ internal static class DynamicObjectResolver
6666
/// <summary>AllowPrivate:True, ExcludeNull:True, NameMutate:SnakeCase</summary>
6767
public static readonly IJsonFormatterResolver AllowPrivateExcludeNullSnakeCase = DynamicObjectResolverAllowPrivateTrueExcludeNullTrueNameMutateSnakeCase.Instance;
6868

69-
public static IJsonFormatterResolver Create(Func<MemberInfo, IJsonProperty> propertyMapper, Lazy<Func<string, string>> mutator, bool excludeNull)
69+
public static IJsonFormatterResolver Create(Func<MemberInfo, JsonProperty> propertyMapper, Lazy<Func<string, string>> mutator, bool excludeNull)
7070
{
7171
return new CustomDynamicObjectResolver(propertyMapper, mutator, excludeNull);
7272
}
@@ -79,7 +79,7 @@ internal sealed class CustomDynamicObjectResolver
7979
, ISave
8080
#endif
8181
{
82-
private readonly Func<MemberInfo, IJsonProperty> _propertyMapper;
82+
private readonly Func<MemberInfo, JsonProperty> _propertyMapper;
8383
private readonly Lazy<Func<string, string>> _mutator;
8484
private readonly bool _excludeNull;
8585
private readonly ThreadsafeTypeKeyHashTable<object> _formatters = new ThreadsafeTypeKeyHashTable<object>();
@@ -94,7 +94,7 @@ static CustomDynamicObjectResolver()
9494
assembly = new DynamicAssembly(ModuleName);
9595
}
9696

97-
public CustomDynamicObjectResolver(Func<MemberInfo, IJsonProperty> propertyMapper, Lazy<Func<string, string>> mutator, bool excludeNull)
97+
public CustomDynamicObjectResolver(Func<MemberInfo, JsonProperty> propertyMapper, Lazy<Func<string, string>> mutator, bool excludeNull)
9898
{
9999
_propertyMapper = propertyMapper;
100100
_mutator = mutator;
@@ -588,7 +588,7 @@ internal static class DynamicObjectTypeBuilder
588588
{typeof(string)},
589589
};
590590

591-
public static object BuildFormatterToAssembly<T>(DynamicAssembly assembly, IJsonFormatterResolver selfResolver, Func<string, string> mutator, Func<MemberInfo, IJsonProperty> propertyMapper, bool excludeNull)
591+
public static object BuildFormatterToAssembly<T>(DynamicAssembly assembly, IJsonFormatterResolver selfResolver, Func<string, string> mutator, Func<MemberInfo, JsonProperty> propertyMapper, bool excludeNull)
592592
{
593593
var ti = typeof(T).GetTypeInfo();
594594

@@ -620,7 +620,7 @@ public static object BuildFormatterToAssembly<T>(DynamicAssembly assembly, IJson
620620
return (IJsonFormatter<T>)Activator.CreateInstance(formatterTypeInfo.AsType());
621621
}
622622

623-
public static object BuildFormatterToDynamicMethod<T>(IJsonFormatterResolver selfResolver, Func<string,string> mutator, Func<MemberInfo, IJsonProperty> propertyMapper, bool excludeNull, bool allowPrivate)
623+
public static object BuildFormatterToDynamicMethod<T>(IJsonFormatterResolver selfResolver, Func<string,string> mutator, Func<MemberInfo, JsonProperty> propertyMapper, bool excludeNull, bool allowPrivate)
624624
{
625625
var ti = typeof(T).GetTypeInfo();
626626

@@ -645,7 +645,7 @@ public static object BuildFormatterToDynamicMethod<T>(IJsonFormatterResolver sel
645645
}
646646
}
647647

648-
static TypeInfo BuildType(DynamicAssembly assembly, Type type, Func<string, string> mutator, Func<MemberInfo, IJsonProperty> propertyMapper, bool excludeNull)
648+
static TypeInfo BuildType(DynamicAssembly assembly, Type type, Func<string, string> mutator, Func<MemberInfo, JsonProperty> propertyMapper, bool excludeNull)
649649
{
650650
if (ignoreTypes.Contains(type)) return null;
651651

@@ -708,7 +708,7 @@ static TypeInfo BuildType(DynamicAssembly assembly, Type type, Func<string, stri
708708
return typeBuilder.CreateTypeInfo();
709709
}
710710

711-
public static object BuildAnonymousFormatter(Type type, Func<string, string> nameMutator, Func<MemberInfo, IJsonProperty> propertyMapper, bool excludeNull, bool allowPrivate, bool isException)
711+
public static object BuildAnonymousFormatter(Type type, Func<string, string> nameMutator, Func<MemberInfo, JsonProperty> propertyMapper, bool excludeNull, bool allowPrivate, bool isException)
712712
{
713713
if (ignoreTypes.Contains(type)) return false;
714714

@@ -778,6 +778,10 @@ public static object BuildAnonymousFormatter(Type type, Func<string, string> nam
778778
var formatter = Activator.CreateInstance(formatterType, attr.Attribute.Arguments);
779779
serializeCustomFormatters.Add(formatter);
780780
}
781+
else if (item.JsonFormatter != null)
782+
{
783+
serializeCustomFormatters.Add(item.JsonFormatter);
784+
}
781785
else
782786
{
783787
serializeCustomFormatters.Add(null);
@@ -800,6 +804,10 @@ public static object BuildAnonymousFormatter(Type type, Func<string, string> nam
800804
var formatter = Activator.CreateInstance(formatterType, attr.Attribute.Arguments);
801805
deserializeCustomFormatters.Add(formatter);
802806
}
807+
else if (item.JsonFormatter != null)
808+
{
809+
deserializeCustomFormatters.Add(item.JsonFormatter);
810+
}
803811
else
804812
{
805813
deserializeCustomFormatters.Add(null);

src/Nest/CommonAbstractions/SerializationBehavior/JsonFormatters/NestFormatterResolver.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public IJsonFormatter<T> GetFormatter<T>() =>
8282
return _finalFormatter.GetFormatter<T>();
8383
});
8484

85-
private IJsonProperty GetMapping(MemberInfo member)
85+
private JsonProperty GetMapping(MemberInfo member)
8686
{
8787
// TODO: Skip calling this method for NEST and Elasticsearch.Net types, at the type level
8888
if (!_settings.PropertyMappings.TryGetValue(member, out var propertyMapping))
@@ -101,8 +101,43 @@ private IJsonProperty GetMapping(MemberInfo member)
101101
if (propertyMapping != null || serializerMapping != null)
102102
property.AllowPrivate = true;
103103

104+
if (member.GetCustomAttribute<StringEnumAttribute>() != null)
105+
CreateEnumFormatterForProperty(member, property);
106+
104107
return property;
105108
}
109+
110+
private static void CreateEnumFormatterForType(Type type, JsonProperty property)
111+
{
112+
if (type.IsEnum)
113+
property.JsonFormatter = typeof(EnumFormatter<>).MakeGenericType(type).CreateInstance(true);
114+
else if (type.GetTypeInfo().IsNullable())
115+
{
116+
var underlyingType = Nullable.GetUnderlyingType(type);
117+
if (underlyingType.IsEnum)
118+
{
119+
var innerFormatter = typeof(EnumFormatter<>).MakeGenericType(underlyingType).CreateInstance(true);
120+
property.JsonFormatter = typeof(StaticNullableFormatter<>).MakeGenericType(underlyingType).CreateInstance(innerFormatter);
121+
}
122+
}
123+
}
124+
125+
private static void CreateEnumFormatterForProperty(MemberInfo member, JsonProperty property)
126+
{
127+
switch (member)
128+
{
129+
case PropertyInfo propertyInfo:
130+
{
131+
CreateEnumFormatterForType(propertyInfo.PropertyType, property);
132+
break;
133+
}
134+
case FieldInfo fieldInfo:
135+
{
136+
CreateEnumFormatterForType(fieldInfo.FieldType, property);
137+
break;
138+
}
139+
}
140+
}
106141
}
107142
}
108143
}

src/Tests/Tests/CodeStandards/Serialization/Enums.cs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,13 @@ public void EnumsWithEnumMembersShouldBeMarkedWithStringEnumAttribute()
4444
[U]
4545
public void CanSerializeEnumsWithMultipleMembersMappedToSameValue()
4646
{
47-
var document = new EnumDocument
47+
var document = new EnumSameValuesDocument
4848
{
4949
Int = HttpStatusCode.Moved,
5050
String = AnotherEnum.Value1
5151
};
5252

5353
var client = new ElasticClient();
54-
5554
var json = client.RequestResponseSerializer.SerializeToString(document);
5655

5756
// "Value2" will be written for both "Value1" and "Value2" because the underlying integer value
@@ -62,15 +61,51 @@ public void CanSerializeEnumsWithMultipleMembersMappedToSameValue()
6261
// is not overwritten i.e. "Value1" will be written for both "Value1" and "Value2"
6362
json.Should().Be("{\"int\":301,\"string\":\"Value2\"}");
6463

65-
EnumDocument deserializedDocument;
64+
EnumSameValuesDocument deserializedDocument;
65+
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)))
66+
deserializedDocument = client.RequestResponseSerializer.Deserialize<EnumSameValuesDocument>(stream);
67+
68+
deserializedDocument.Int.Should().Be(document.Int);
69+
deserializedDocument.String.Should().Be(document.String);
70+
}
71+
72+
[U]
73+
public void CanSerializeEnumPropertiesWithStringEnumAttribute()
74+
{
75+
var httpStatusCode = HttpStatusCode.OK;
76+
var document = new StringEnumDocument
77+
{
78+
Int = httpStatusCode,
79+
String = httpStatusCode,
80+
NullableString = httpStatusCode
81+
};
82+
83+
var client = new ElasticClient();
84+
var json = client.RequestResponseSerializer.SerializeToString(document);
85+
86+
json.Should().Be("{\"int\":200,\"string\":\"OK\",\"nullableString\":\"OK\"}");
87+
88+
StringEnumDocument deserializedDocument;
6689
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)))
67-
deserializedDocument = client.RequestResponseSerializer.Deserialize<EnumDocument>(stream);
90+
deserializedDocument = client.RequestResponseSerializer.Deserialize<StringEnumDocument>(stream);
6891

6992
deserializedDocument.Int.Should().Be(document.Int);
7093
deserializedDocument.String.Should().Be(document.String);
94+
deserializedDocument.NullableString.Should().Be(document.NullableString);
95+
}
96+
97+
private class StringEnumDocument
98+
{
99+
public HttpStatusCode Int { get;set;}
100+
101+
[StringEnum]
102+
public HttpStatusCode String { get;set;}
103+
104+
[StringEnum]
105+
public HttpStatusCode? NullableString { get;set;}
71106
}
72107

73-
private class EnumDocument
108+
private class EnumSameValuesDocument
74109
{
75110
public HttpStatusCode Int { get;set;}
76111

0 commit comments

Comments
 (0)