Skip to content

Commit

Permalink
Improve serialization of HttpRequestMessage and HttpResponseMessage, …
Browse files Browse the repository at this point in the history
…and support if preprocessor directives
  • Loading branch information
meziantou committed May 27, 2023
1 parent cb66ccf commit 3bf464e
Show file tree
Hide file tree
Showing 17 changed files with 558 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Meziantou.Framework.HumanReadable;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class HumanReadableDefaultValueAttribute : HumanReadableAttribute
{
/// <summary>
/// Specifies the condition that must be met before a property or field will be ignored.
/// </summary>
public object? DefaultValue { get; }

/// <summary>
/// Initializes a new instance of <see cref="HumanReadableDefaultValueAttribute"/>.
/// </summary>
public HumanReadableDefaultValueAttribute(object? defaultValue)
{
DefaultValue = defaultValue;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Text;

namespace Meziantou.Framework.HumanReadable.Converters;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,20 @@ internal sealed class HttpHeadersConverter : HumanReadableConverter<HttpHeaders>
protected override void WriteValue(HumanReadableTextWriter writer, HttpHeaders? value, HumanReadableSerializerOptions options)
{
Debug.Assert(value != null);

var hasValue = false;
foreach (var header in value)

#if NETSTANDARD2_0 || NETFRAMEWORK
IEnumerable<KeyValuePair<string, IEnumerable<string>>> values = value;
#else
IEnumerable<KeyValuePair<string, HeaderStringValues>> values = value.NonValidated;
#endif

if (options.PropertyOrder != null)
{
values = values.OrderBy(o => o.Key, options.PropertyOrder);
}

foreach (var header in values)
{
if (!hasValue)
{
Expand All @@ -20,7 +31,11 @@ protected override void WriteValue(HumanReadableTextWriter writer, HttpHeaders?

writer.WritePropertyName(header.Key);

#if NETSTANDARD2_0 || NETFRAMEWORK
var valueCount = header.Value.Count();
#else
var valueCount = header.Value.Count;
#endif
if (valueCount == 1)
{
writer.WriteValue(header.Value.First());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Globalization;
using System.Net;

namespace Meziantou.Framework.HumanReadable.Converters;
internal sealed class HttpStatusCodeConverter : HumanReadableConverter<HttpStatusCode>
{
protected override void WriteValue(HumanReadableTextWriter writer, HttpStatusCode value, HumanReadableSerializerOptions options)
{
writer.WriteValue($"{((int)value).ToString(CultureInfo.InvariantCulture)} ({value})");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,33 @@ public enum HumanReadableIgnoreCondition
/// <summary>
/// Property is never ignored during serialization.
/// </summary>
Never = 0,
Never,

/// <summary>
/// Property is always ignored during serialization.
/// </summary>
Always = 1,
Always,

/// <summary>
/// If the value is the default, the property is ignored during serialization.
/// This is applied to both reference and value-type properties and fields.
/// </summary>
WhenWritingDefault = 2,
WhenWritingDefault,

/// <summary>
/// If the value is <see langword="null"/>, the property is ignored during serialization.
/// This is applied only to reference-type or <see cref="Nullable{T}"/> properties and fields.
/// </summary>
WhenWritingNull = 3,
WhenWritingNull,

/// <summary>
/// If the value implements <see cref="System.Collections.IEnumerable"/> and the collection is empty, the property is ignored during serialization.
/// </summary>
/// <remarks>If the value is not a collection, the property is not ignored during serialization.</remarks>
WhenWritingEmptyCollection,

/// <summary>
/// If the value implements <see cref="System.Collections.IEnumerable"/> and the collection is empty, or if the value is the default, the property is ignored during serialization.
/// </summary>
WhenWritingDefaultOrEmptyCollection,
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using System.Reflection;
using System.Collections;
using System.Diagnostics;
using System.Reflection;

namespace Meziantou.Framework.HumanReadable;

[DebuggerDisplay("{DebuggerDisplay}")]
internal sealed class HumanReadableMemberInfo
{
public HumanReadableMemberInfo(Type memberType, Func<object, object?> getValue, HumanReadableIgnoreCondition ignoreCondition, string propertyName, HumanReadableConverter? converter, int order, object? defaultValue)
public HumanReadableMemberInfo(Type memberType, Func<object, object?> getValue, HumanReadableIgnoreCondition ignoreCondition, string propertyName, HumanReadableConverter? converter, int? order, object? defaultValue)
{
MemberType = memberType;
GetValue = getValue;
Expand All @@ -20,10 +23,18 @@ public HumanReadableMemberInfo(Type memberType, Func<object, object?> getValue,
public HumanReadableIgnoreCondition IgnoreCondition { get; }
public string Name { get; }
public HumanReadableConverter? Converter { get; }
public int Order { get; }
public int? Order { get; }
public object? DefaultValue { get; }

public static List<HumanReadableMemberInfo> Get(Type type, HumanReadableSerializerOptions options)
private string DebuggerDisplay
{
get
{
return $"{Name} ({MemberType.FullName})";
}
}

public static HumanReadableMemberInfo[] Get(Type type, HumanReadableSerializerOptions options)
{
var members = new List<HumanReadableMemberInfo>();

Expand All @@ -41,8 +52,14 @@ public static List<HumanReadableMemberInfo> Get(Type type, HumanReadableSerializ
members.Add(data);
}

members.Sort((a, b) => a.Order.CompareTo(b.Order));
return members;
members.Sort((a, b) => (a.Order, b.Order) switch
{
(not null, null) => -1,
(null, not null) => 1,
({ } order1, { } order2) => order1.CompareTo(order2),
_ => options.PropertyOrder == null ? 0 : options.PropertyOrder.Compare(a.Name, b.Name),
});
return members.ToArray();
}

public static HumanReadableMemberInfo? Get(PropertyInfo member, HumanReadableSerializerOptions options)
Expand All @@ -54,14 +71,23 @@ public static List<HumanReadableMemberInfo> Get(Type type, HumanReadableSerializ
if (!hasInclude && !(member.GetGetMethod()?.IsPublic ?? false))
return null;

if (!options.IncludeObsoleteMembers)
{
var obsoleteAttribute = member.GetCustomAttribute<ObsoleteAttribute>();
if (obsoleteAttribute != null)
return null;
}

var ignore = options.GetCustomAttribute<HumanReadableIgnoreAttribute>(member)?.Condition ?? options.DefaultIgnoreCondition;
if (ignore == HumanReadableIgnoreCondition.Always)
return null;

var propertyName = options.GetCustomAttribute<HumanReadablePropertyNameAttribute>(member)?.Name ?? member.Name;
var order = options.GetCustomAttribute<HumanReadablePropertyOrderAttribute>(member)?.Order ?? 0;
var order = options.GetCustomAttribute<HumanReadablePropertyOrderAttribute>(member)?.Order;
var converter = GetConverter(member, member.PropertyType, options);
var defaultValue = GetDefaultValue(ignore, member.PropertyType);

var defaultValueAttribute = options.GetCustomAttribute<HumanReadableDefaultValueAttribute>(member);
var defaultValue = defaultValueAttribute != null ? defaultValueAttribute.DefaultValue : GetDefaultValue(ignore, member.PropertyType);
return new HumanReadableMemberInfo(member.PropertyType, member.GetValue, ignore, propertyName, converter, order, defaultValue);
}

Expand All @@ -76,7 +102,7 @@ public static List<HumanReadableMemberInfo> Get(Type type, HumanReadableSerializ
return null;

var propertyName = options.GetCustomAttribute<HumanReadablePropertyNameAttribute>(member)?.Name ?? member.Name;
var order = options.GetCustomAttribute<HumanReadablePropertyOrderAttribute>(member)?.Order ?? 0;
var order = options.GetCustomAttribute<HumanReadablePropertyOrderAttribute>(member)?.Order;
var converter = GetConverter(member, member.FieldType, options);
var defaultValue = GetDefaultValue(ignore, member.FieldType);
return new HumanReadableMemberInfo(member.FieldType, member.GetValue, ignore, propertyName, converter, order, defaultValue);
Expand All @@ -93,7 +119,7 @@ public static List<HumanReadableMemberInfo> Get(Type type, HumanReadableSerializ

private static object? GetDefaultValue(HumanReadableIgnoreCondition ignoreCondition, Type type)
{
if (ignoreCondition != HumanReadableIgnoreCondition.WhenWritingDefault)
if (ignoreCondition is not HumanReadableIgnoreCondition.WhenWritingDefault and not HumanReadableIgnoreCondition.WhenWritingDefaultOrEmptyCollection)
return null;

if (type.IsValueType)
Expand All @@ -114,8 +140,31 @@ public bool MustIgnore(object? memberValue)
{
HumanReadableIgnoreCondition.WhenWritingDefault => Equals(memberValue, DefaultValue),
HumanReadableIgnoreCondition.WhenWritingNull => memberValue == null,
HumanReadableIgnoreCondition.WhenWritingEmptyCollection => IsEmptyCollection(memberValue),
HumanReadableIgnoreCondition.WhenWritingDefaultOrEmptyCollection => Equals(memberValue, DefaultValue) || IsEmptyCollection(memberValue),
_ => false,
};

static bool IsEmptyCollection(object? value)
{
if (value is IEnumerable enumerable)
{
var enumerator = enumerable.GetEnumerator();
try
{
return !enumerator.MoveNext();
}
finally
{
if (enumerator is IDisposable disposable)
{
disposable.Dispose();
}
}
}

return false;
}
}

[SuppressMessage("Performance", "CA1812", Justification = "The class is instantiated using Activator.CreateInstance")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ public static string Serialize(object? value, Type type, HumanReadableSerializer
return writer.ToString();
}

internal static void Serialize(HumanReadableTextWriter writer, object? value, Type type, HumanReadableSerializerOptions options)
public static void Serialize(HumanReadableTextWriter writer, object? value, Type type, HumanReadableSerializerOptions options)
{
var converter = options.GetConverter(type);
converter.WriteValue(writer, value, options);
}

internal static void Serialize<T>(HumanReadableTextWriter writer, T? value, HumanReadableSerializerOptions options)
public static void Serialize<T>(HumanReadableTextWriter writer, T? value, HumanReadableSerializerOptions options)
{
Serialize(writer, value, value?.GetType() ?? typeof(T), options);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ public sealed record HumanReadableSerializerOptions
{
// Cache
private readonly ConcurrentDictionary<Type, HumanReadableConverter> _convertersCache;
private readonly ConcurrentDictionary<Type, List<HumanReadableMemberInfo>> _memberInfosCache;
private readonly ConcurrentDictionary<Type, HumanReadableMemberInfo[]> _memberInfosCache;

private readonly Dictionary<Type, List<HumanReadableAttribute>> _typeAttributes;
private readonly Dictionary<MemberInfo, List<HumanReadableAttribute>> _memberAttributes;
private bool _includeFields;
private HumanReadableIgnoreCondition _defaultIgnoreCondition;
private IComparer<string>? _propertyOrder;
private bool _includeObsoleteMembers;

public HumanReadableSerializerOptions()
{
Expand Down Expand Up @@ -62,15 +64,36 @@ private HumanReadableSerializerOptions(HumanReadableSerializerOptions? options)
public bool ShowInvisibleCharactersInValues { get; set; }
public IList<HumanReadableConverter> Converters { get; }

public IComparer<string>? PropertyOrder
{
get => _propertyOrder;
set
{
VerifyMutable();
_propertyOrder = value;
}
}

public bool IncludeFields
{
get => _includeFields; set
get => _includeFields;
set
{
VerifyMutable();
_includeFields = value;
}
}

public bool IncludeObsoleteMembers
{
get => _includeObsoleteMembers;
set
{
VerifyMutable();
_includeObsoleteMembers = value;
}
}

public HumanReadableIgnoreCondition DefaultIgnoreCondition
{
get => _defaultIgnoreCondition;
Expand Down Expand Up @@ -114,7 +137,7 @@ public void AddAttribute(PropertyInfo member, HumanReadableAttribute attribute)

private static void AddValue<TKey, TValue>(Dictionary<TKey, List<TValue>> dict, TKey key, TValue value)
where TKey : notnull
where TValue: notnull
where TValue : notnull
{
if (!dict.TryGetValue(key, out var list))
{
Expand Down Expand Up @@ -142,8 +165,10 @@ private static void AddValue<TKey, TValue>(Dictionary<TKey, List<TValue>> dict,
MakeReadOnly();
if (_typeAttributes.TryGetValue(type, out var attributes))
{
foreach (var attribute in attributes)
// Read reverse, so attributes set by the user override the default attributes
for (var i = attributes.Count - 1; i >= 0; i--)
{
var attribute = attributes[i];
if (attribute is T result)
return result;
}
Expand All @@ -157,8 +182,10 @@ private static void AddValue<TKey, TValue>(Dictionary<TKey, List<TValue>> dict,
MakeReadOnly();
if (_memberAttributes.TryGetValue(member, out var attributes))
{
foreach (var attribute in attributes)
// Read reverse, so attributes set by the user override the default attributes
for (var i = attributes.Count - 1; i >= 0; i--)
{
var attribute = attributes[i];
if (attribute is T result)
return result;
}
Expand Down Expand Up @@ -234,7 +261,7 @@ HumanReadableConverter FindConverter(Type type, IList<HumanReadableConverter> co
}
}

internal List<HumanReadableMemberInfo> GetMembers(Type type)
internal HumanReadableMemberInfo[] GetMembers(Type type)
{
#if NET6_0_OR_GREATER
return _memberInfosCache.GetOrAdd(type, static (type, options) => HumanReadableMemberInfo.Get(type, options), this);
Expand Down Expand Up @@ -276,6 +303,7 @@ private sealed class ConverterList : ConfigurationList<HumanReadableConverter>
new HttpContentConverter(),
new HttpMethodConverter(),
new HttpHeadersConverter(),
new HttpStatusCodeConverter(),
new Int16Converter(),
new Int32Converter(),
new Int64Converter(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Description>One-way serializer to a human readable format</Description>
<Version>1.0.8</Version>
<Version>1.0.9</Version>
</PropertyGroup>

<ItemGroup Condition="$(TargetFramework) == 'netstandard2.0'">
Expand Down
Loading

0 comments on commit 3bf464e

Please sign in to comment.