-
-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix property metadata retrieval in case of redefined properties (#347)
- Loading branch information
Showing
6 changed files
with
345 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
namespace Serilog.Exceptions.Reflection | ||
{ | ||
using System; | ||
|
||
/// <summary> | ||
/// Contains metadata information about a type | ||
/// useful for destructuring process. | ||
/// </summary> | ||
internal class ReflectionInfo | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="ReflectionInfo"/> class. | ||
/// </summary> | ||
/// <param name="properties">All properties for a type.</param> | ||
/// <param name="propertiesExceptBaseOnes">All properties except of <see cref="Exception"/> properties which are handled separately.</param> | ||
public ReflectionInfo( | ||
ReflectionPropertyInfo[] properties, | ||
ReflectionPropertyInfo[] propertiesExceptBaseOnes) | ||
{ | ||
this.Properties = properties; | ||
this.PropertiesExceptBaseOnes = propertiesExceptBaseOnes; | ||
} | ||
|
||
/// <summary> | ||
/// Gets information about all properties to be destructured. | ||
/// </summary> | ||
public ReflectionPropertyInfo[] Properties { get; } | ||
|
||
/// <summary> | ||
/// Gets information about properties other than base exception properties | ||
/// which are handled separately. | ||
/// </summary> | ||
public ReflectionPropertyInfo[] PropertiesExceptBaseOnes { get; } | ||
} | ||
} |
122 changes: 122 additions & 0 deletions
122
Source/Serilog.Exceptions/Reflection/ReflectionInfoExtractor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
namespace Serilog.Exceptions.Reflection | ||
{ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Linq.Expressions; | ||
using System.Reflection; | ||
|
||
/// <summary> | ||
/// Utility class that analyzes type using reflection and provides | ||
/// information about properties to be used in destructuring process. | ||
/// </summary> | ||
internal class ReflectionInfoExtractor | ||
{ | ||
private readonly object lockObj = new(); | ||
private readonly Dictionary<Type, ReflectionInfo> reflectionInfoCache = new(); | ||
private readonly IList<PropertyInfo> baseExceptionPropertiesForDestructuring; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="ReflectionInfoExtractor"/> class. | ||
/// </summary> | ||
public ReflectionInfoExtractor() => this.baseExceptionPropertiesForDestructuring = GetExceptionPropertiesForDestructuring(typeof(Exception)); | ||
|
||
/// <summary> | ||
/// Gets reflection info for relevant properties of <paramref name="valueType"/>"/> | ||
/// from cache or by generating it if they are not yet present in cache. | ||
/// </summary> | ||
/// <param name="valueType">The type for which properties are to be analyzed.</param> | ||
/// <returns>The reflection info for relevant properties of <paramref name="valueType"/>.</returns> | ||
public ReflectionInfo GetOrCreateReflectionInfo(Type valueType) | ||
{ | ||
lock (this.lockObj) | ||
{ | ||
if (!this.reflectionInfoCache.TryGetValue(valueType, out var reflectionInfo)) | ||
{ | ||
reflectionInfo = this.GenerateReflectionInfoForType(valueType); | ||
this.reflectionInfoCache.Add(valueType, reflectionInfo); | ||
} | ||
|
||
return reflectionInfo; | ||
} | ||
} | ||
|
||
private static Func<object, object> GenerateFastGetterForProperty(Type type, PropertyInfo property) | ||
{ | ||
var objParam = Expression.Parameter(typeof(object), "num"); | ||
var typedObj = Expression.Convert(objParam, type); | ||
var memberExpression = Expression.Property(typedObj, property); | ||
var typedResult = Expression.Convert(memberExpression, typeof(object)); | ||
var resultLambda = Expression.Lambda<Func<object, object>>(typedResult, objParam); | ||
return resultLambda.Compile(); | ||
} | ||
|
||
private static List<PropertyInfo> GetExceptionPropertiesForDestructuring(Type valueType) | ||
{ | ||
var allProperties = valueType | ||
.GetProperties(BindingFlags.Public | BindingFlags.Instance) | ||
.Where(x => x.CanRead && x.GetIndexParameters().Length == 0) | ||
.ToList(); | ||
|
||
return allProperties; | ||
} | ||
|
||
private static void MarkRedefinedPropertiesWithFullName(ReflectionPropertyInfo[] propertyInfos) | ||
{ | ||
// First, prepare a dictionary of properties grouped by name | ||
var groupedByName = new Dictionary<string, List<ReflectionPropertyInfo>>(); | ||
foreach (var propertyInfo in propertyInfos) | ||
{ | ||
if (groupedByName.ContainsKey(propertyInfo.Name)) | ||
{ | ||
groupedByName[propertyInfo.Name].Add(propertyInfo); | ||
} | ||
else | ||
{ | ||
groupedByName[propertyInfo.Name] = new List<ReflectionPropertyInfo> { propertyInfo }; | ||
} | ||
} | ||
|
||
// Then, fix groups that have more than one property in it | ||
// It means that there is a name uniqueness conflict which needs to be resolved | ||
foreach (var nameAndProperties in groupedByName) | ||
{ | ||
var properties = nameAndProperties.Value; | ||
if (properties.Count > 1) | ||
{ | ||
FixGroupOfPropertiesWithConflictingNames(properties); | ||
} | ||
} | ||
} | ||
|
||
private static void FixGroupOfPropertiesWithConflictingNames(List<ReflectionPropertyInfo> properties) | ||
{ | ||
// Very simplistic approach, just check each pair separately. | ||
// The implementation has O(N^2) complexity but in practice | ||
// N will be extremely rarely other than 2. | ||
foreach (var property in properties) | ||
{ | ||
foreach (var otherProperty in properties) | ||
{ | ||
property.MarkNameWithTypeNameIfRedefinesThatProperty(otherProperty); | ||
} | ||
} | ||
} | ||
|
||
private ReflectionInfo GenerateReflectionInfoForType(Type valueType) | ||
{ | ||
var properties = GetExceptionPropertiesForDestructuring(valueType); | ||
var propertyInfos = properties | ||
.Select(p => new ReflectionPropertyInfo(p.Name, p.DeclaringType, GenerateFastGetterForProperty(valueType, p))) | ||
.ToArray(); | ||
|
||
MarkRedefinedPropertiesWithFullName(propertyInfos); | ||
|
||
var propertiesInfosExceptBaseOnes = propertyInfos | ||
.Where(p => this.baseExceptionPropertiesForDestructuring.All(bp => bp.Name != p.Name)) | ||
.ToArray(); | ||
|
||
return new ReflectionInfo(propertyInfos, propertiesInfosExceptBaseOnes); | ||
} | ||
} | ||
} |
Oops, something went wrong.