Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions src/Elasticsearch.Net/Utf8Json/IJsonProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
// SOFTWARE.
#endregion

using System;

namespace Elasticsearch.Net.Utf8Json
{
internal interface IJsonProperty
Expand All @@ -37,20 +39,20 @@ internal interface IJsonProperty

internal class JsonProperty : IJsonProperty
{
public JsonProperty(string name)
{
Name = name;
}
public JsonProperty(string name) => Name = name;

public string Name { get; set; }

public int Order
{
get { return 0; }
}
public int Order => 0;

public bool Ignore { get; set; }

public bool? AllowPrivate { get; set; }

/// <summary>
/// An instance of an <see cref="IJsonFormatter"/> that will be used
/// to serialize/deserialize the property
/// </summary>
public object JsonFormatter { get; set; }
}
}
7 changes: 5 additions & 2 deletions src/Elasticsearch.Net/Utf8Json/Internal/Emit/MetaMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ internal class MetaMember
public PropertyInfo[] InterfacePropertyInfos { get; private set; }
public MethodInfo ShouldSerializeMethodInfo { get; private set; }
public MethodInfo ShouldSerializeTypeMethodInfo { get; private set; }
public object JsonFormatter {get; private set; }

MethodInfo getMethod;
MethodInfo setMethod;
Expand All @@ -57,7 +58,7 @@ protected MetaMember(Type type, string name, string memberName, bool isWritable,
this.IsReadable = isReadable;
}

public MetaMember(FieldInfo info, string name, bool allowPrivate)
public MetaMember(FieldInfo info, string name, object jsonFormatter, bool allowPrivate)
{
this.Name = name;
this.MemberName = info.Name;
Expand All @@ -67,9 +68,10 @@ public MetaMember(FieldInfo info, string name, bool allowPrivate)
this.IsWritable = allowPrivate || (info.IsPublic && !info.IsInitOnly);
this.ShouldSerializeMethodInfo = GetShouldSerialize(info);
this.ShouldSerializeTypeMethodInfo = info.FieldType.GetTypeInfo().GetShouldSerializeMethod();
this.JsonFormatter = jsonFormatter;
}

public MetaMember(PropertyInfo info, string name, PropertyInfo[] interfaceInfos, bool allowPrivate)
public MetaMember(PropertyInfo info, string name, PropertyInfo[] interfaceInfos, object jsonFormatter, bool allowPrivate)
{
this.getMethod = info.GetGetMethod(true);
this.setMethod = info.GetSetMethod(true);
Expand All @@ -83,6 +85,7 @@ public MetaMember(PropertyInfo info, string name, PropertyInfo[] interfaceInfos,
this.IsWritable = (setMethod != null) && (allowPrivate || setMethod.IsPublic) && !setMethod.IsStatic;
this.ShouldSerializeMethodInfo = GetShouldSerialize(info);
this.ShouldSerializeTypeMethodInfo = info.PropertyType.GetTypeInfo().GetShouldSerializeMethod();
this.JsonFormatter = jsonFormatter;
}

static MethodInfo GetShouldSerialize(MemberInfo info)
Expand Down
15 changes: 12 additions & 3 deletions src/Elasticsearch.Net/Utf8Json/Internal/Emit/MetaType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ private static TAttribute GetCustomAttribute<TAttribute>(PropertyInfo propertyIn
return interfaceProperty != null ? interfaceProperty.GetCustomAttribute<TAttribute>(inherit) : null;
}

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

var allowPrivateMember = allowPrivate;

object jsonFormatter = null;

if (propertyMapper != null)
{
var property = propertyMapper(item);
Expand All @@ -144,12 +146,15 @@ public MetaType(Type type, Func<string, string> nameMutator, Func<MemberInfo, IJ

if (property.AllowPrivate.HasValue)
allowPrivateMember = property.AllowPrivate.Value;

if (property.JsonFormatter != null)
jsonFormatter = property.JsonFormatter;
}
}

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

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

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

if (field.AllowPrivate.HasValue)
allowPrivateMember = field.AllowPrivate.Value;

if (field.JsonFormatter != null)
jsonFormatter = field.JsonFormatter;
}
}

var member = new MetaMember(item, name, allowPrivateMember || dm != null);
var member = new MetaMember(item, name, jsonFormatter, allowPrivateMember || dm != null);
if (!member.IsReadable && !member.IsWritable) continue;

if (!stringMembers.ContainsKey(member.Name))
Expand Down
22 changes: 15 additions & 7 deletions src/Elasticsearch.Net/Utf8Json/Resolvers/DynamicObjectResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ internal static class DynamicObjectResolver
/// <summary>AllowPrivate:True, ExcludeNull:True, NameMutate:SnakeCase</summary>
public static readonly IJsonFormatterResolver AllowPrivateExcludeNullSnakeCase = DynamicObjectResolverAllowPrivateTrueExcludeNullTrueNameMutateSnakeCase.Instance;

public static IJsonFormatterResolver Create(Func<MemberInfo, IJsonProperty> propertyMapper, Lazy<Func<string, string>> mutator, bool excludeNull)
public static IJsonFormatterResolver Create(Func<MemberInfo, JsonProperty> propertyMapper, Lazy<Func<string, string>> mutator, bool excludeNull)
{
return new CustomDynamicObjectResolver(propertyMapper, mutator, excludeNull);
}
Expand All @@ -79,7 +79,7 @@ internal sealed class CustomDynamicObjectResolver
, ISave
#endif
{
private readonly Func<MemberInfo, IJsonProperty> _propertyMapper;
private readonly Func<MemberInfo, JsonProperty> _propertyMapper;
private readonly Lazy<Func<string, string>> _mutator;
private readonly bool _excludeNull;
private readonly ThreadsafeTypeKeyHashTable<object> _formatters = new ThreadsafeTypeKeyHashTable<object>();
Expand All @@ -94,7 +94,7 @@ static CustomDynamicObjectResolver()
assembly = new DynamicAssembly(ModuleName);
}

public CustomDynamicObjectResolver(Func<MemberInfo, IJsonProperty> propertyMapper, Lazy<Func<string, string>> mutator, bool excludeNull)
public CustomDynamicObjectResolver(Func<MemberInfo, JsonProperty> propertyMapper, Lazy<Func<string, string>> mutator, bool excludeNull)
{
_propertyMapper = propertyMapper;
_mutator = mutator;
Expand Down Expand Up @@ -588,7 +588,7 @@ internal static class DynamicObjectTypeBuilder
{typeof(string)},
};

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

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

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

Expand All @@ -645,7 +645,7 @@ public static object BuildFormatterToDynamicMethod<T>(IJsonFormatterResolver sel
}
}

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

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

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

Expand Down Expand Up @@ -778,6 +778,10 @@ public static object BuildAnonymousFormatter(Type type, Func<string, string> nam
var formatter = Activator.CreateInstance(formatterType, attr.Attribute.Arguments);
serializeCustomFormatters.Add(formatter);
}
else if (item.JsonFormatter != null)
{
serializeCustomFormatters.Add(item.JsonFormatter);
}
else
{
serializeCustomFormatters.Add(null);
Expand All @@ -800,6 +804,10 @@ public static object BuildAnonymousFormatter(Type type, Func<string, string> nam
var formatter = Activator.CreateInstance(formatterType, attr.Attribute.Arguments);
deserializeCustomFormatters.Add(formatter);
}
else if (item.JsonFormatter != null)
{
deserializeCustomFormatters.Add(item.JsonFormatter);
}
else
{
deserializeCustomFormatters.Add(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public IJsonFormatter<T> GetFormatter<T>() =>
return _finalFormatter.GetFormatter<T>();
});

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

if (member.GetCustomAttribute<StringEnumAttribute>() != null)
CreateEnumFormatterForProperty(member, property);

return property;
}

private static void CreateEnumFormatterForType(Type type, JsonProperty property)
{
if (type.IsEnum)
property.JsonFormatter = typeof(EnumFormatter<>).MakeGenericType(type).CreateInstance(true);
else if (type.GetTypeInfo().IsNullable())
{
var underlyingType = Nullable.GetUnderlyingType(type);
if (underlyingType.IsEnum)
{
var innerFormatter = typeof(EnumFormatter<>).MakeGenericType(underlyingType).CreateInstance(true);
property.JsonFormatter = typeof(StaticNullableFormatter<>).MakeGenericType(underlyingType).CreateInstance(innerFormatter);
}
}
}

private static void CreateEnumFormatterForProperty(MemberInfo member, JsonProperty property)
{
switch (member)
{
case PropertyInfo propertyInfo:
{
CreateEnumFormatterForType(propertyInfo.PropertyType, property);
break;
}
case FieldInfo fieldInfo:
{
CreateEnumFormatterForType(fieldInfo.FieldType, property);
break;
}
}
}
}
}
}
45 changes: 40 additions & 5 deletions src/Tests/Tests/CodeStandards/Serialization/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,13 @@ public void EnumsWithEnumMembersShouldBeMarkedWithStringEnumAttribute()
[U]
public void CanSerializeEnumsWithMultipleMembersMappedToSameValue()
{
var document = new EnumDocument
var document = new EnumSameValuesDocument
{
Int = HttpStatusCode.Moved,
String = AnotherEnum.Value1
};

var client = new ElasticClient();

var json = client.RequestResponseSerializer.SerializeToString(document);

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

EnumDocument deserializedDocument;
EnumSameValuesDocument deserializedDocument;
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)))
deserializedDocument = client.RequestResponseSerializer.Deserialize<EnumSameValuesDocument>(stream);

deserializedDocument.Int.Should().Be(document.Int);
deserializedDocument.String.Should().Be(document.String);
}

[U]
public void CanSerializeEnumPropertiesWithStringEnumAttribute()
{
var httpStatusCode = HttpStatusCode.OK;
var document = new StringEnumDocument
{
Int = httpStatusCode,
String = httpStatusCode,
NullableString = httpStatusCode
};

var client = new ElasticClient();
var json = client.RequestResponseSerializer.SerializeToString(document);

json.Should().Be("{\"int\":200,\"string\":\"OK\",\"nullableString\":\"OK\"}");

StringEnumDocument deserializedDocument;
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)))
deserializedDocument = client.RequestResponseSerializer.Deserialize<EnumDocument>(stream);
deserializedDocument = client.RequestResponseSerializer.Deserialize<StringEnumDocument>(stream);

deserializedDocument.Int.Should().Be(document.Int);
deserializedDocument.String.Should().Be(document.String);
deserializedDocument.NullableString.Should().Be(document.NullableString);
}

private class StringEnumDocument
{
public HttpStatusCode Int { get;set;}

[StringEnum]
public HttpStatusCode String { get;set;}

[StringEnum]
public HttpStatusCode? NullableString { get;set;}
}

private class EnumDocument
private class EnumSameValuesDocument
{
public HttpStatusCode Int { get;set;}

Expand Down