|
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
#nullable enable
#if NETSTANDARD2_1_0_OR_GREATER || NET6_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
using System.Reflection;
namespace OpenTelemetry.Instrumentation;
/// <summary>
/// PropertyFetcher fetches a property from an object.
/// </summary>
/// <typeparam name="T">The type of the property being fetched.</typeparam>
internal sealed class PropertyFetcher<T>
{
#if NET6_0_OR_GREATER
private const string TrimCompatibilityMessage = "PropertyFetcher is used to access properties on objects dynamically by design and cannot be made trim compatible.";
#endif
private readonly string propertyName;
private PropertyFetch? innerFetcher;
/// <summary>
/// Initializes a new instance of the <see cref="PropertyFetcher{T}"/> class.
/// </summary>
/// <param name="propertyName">Property name to fetch.</param>
public PropertyFetcher(string propertyName)
{
this.propertyName = propertyName;
}
public int NumberOfInnerFetchers => this.innerFetcher == null
? 0
: 1 + this.innerFetcher.NumberOfInnerFetchers;
/// <summary>
/// Try to fetch the property from the object.
/// </summary>
/// <param name="obj">Object to be fetched.</param>
/// <param name="value">Fetched value.</param>
/// <returns><see langword= "true"/> if the property was fetched.</returns>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode(TrimCompatibilityMessage)]
#endif
public bool TryFetch(
#if NETSTANDARD2_1_0_OR_GREATER || NET6_0_OR_GREATER
[NotNullWhen(true)]
#endif
object? obj,
out T? value)
{
var innerFetcher = this.innerFetcher;
if (innerFetcher is null)
{
return TryFetchRare(obj, this.propertyName, ref this.innerFetcher, out value);
}
return innerFetcher.TryFetch(obj, out value);
}
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode(TrimCompatibilityMessage)]
#endif
private static bool TryFetchRare(object? obj, string propertyName, ref PropertyFetch? destination, out T? value)
{
if (obj is null)
{
value = default;
return false;
}
var fetcher = PropertyFetch.Create(obj.GetType().GetTypeInfo(), propertyName);
if (fetcher is null)
{
value = default;
return false;
}
destination = fetcher;
return fetcher.TryFetch(obj, out value);
}
// see https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/DiagnosticSourceEventSource.cs
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode(TrimCompatibilityMessage)]
#endif
private abstract class PropertyFetch
{
public abstract int NumberOfInnerFetchers { get; }
public static PropertyFetch? Create(TypeInfo type, string propertyName)
{
var property = type.DeclaredProperties.FirstOrDefault(p => string.Equals(p.Name, propertyName, StringComparison.OrdinalIgnoreCase)) ?? type.GetProperty(propertyName);
return CreateFetcherForProperty(property);
static PropertyFetch? CreateFetcherForProperty(PropertyInfo? propertyInfo)
{
if (propertyInfo == null || !typeof(T).IsAssignableFrom(propertyInfo.PropertyType))
{
// returns null and wait for a valid payload to arrive.
return null;
}
var declaringType = propertyInfo.DeclaringType;
if (declaringType!.IsValueType)
{
throw new NotSupportedException(
$"Type: {declaringType.FullName} is a value type. PropertyFetcher can only operate on reference payload types.");
}
if (declaringType == typeof(object))
{
// TODO: REMOVE this if branch when .NET 7 is out of support.
// This branch is never executed and is only needed for .NET 7 AOT-compiler at trimming stage; i.e.,
// this is not needed in .NET 8, because the compiler is improved and call into MakeGenericMethod will be AOT-compatible.
// It is used to force the AOT compiler to create an instantiation of the method with a reference type.
// The code for that instantiation can then be reused at runtime to create instantiation over any other reference.
return CreateInstantiated<object>(propertyInfo);
}
else
{
return DynamicInstantiationHelper(declaringType, propertyInfo);
}
// Separated as a local function to be able to target the suppression to just this call.
// IL3050 was generated here because of the call to MakeGenericType, which is problematic in AOT if one of the type parameters is a value type;
// because the compiler might need to generate code specific to that type.
// If the type parameter is a reference type, there will be no problem; because the generated code can be shared among all reference type instantiations.
#if NET6_0_OR_GREATER
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "The code guarantees that all the generic parameters are reference types.")]
#endif
static PropertyFetch? DynamicInstantiationHelper(Type declaringType, PropertyInfo propertyInfo)
{
return (PropertyFetch?)typeof(PropertyFetch)
.GetMethod(nameof(CreateInstantiated), BindingFlags.NonPublic | BindingFlags.Static)!
.MakeGenericMethod(declaringType) // This is validated in the earlier call chain to be a reference type.
.Invoke(null, new object[] { propertyInfo })!;
}
}
}
public abstract bool TryFetch(
#if NETSTANDARD2_1_0_OR_GREATER || NET6_0_OR_GREATER
[NotNullWhen(true)]
#endif
object? obj,
out T? value);
// Goal: make PropertyFetcher AOT-compatible.
// AOT compiler can't guarantee correctness when call into MakeGenericType or MakeGenericMethod
// if one of the generic parameters is a value type (reference types are OK.)
// For PropertyFetcher, the decision was made to only support reference type payloads, i.e.:
// the object from which to get the property value MUST be a reference type.
// Create generics with the declared object type as a generic parameter is OK, but we need the return type
// of the property to be a value type (on top of reference types.)
// Normally, we would have a helper class like `PropertyFetchInstantiated` that takes 2 generic parameters,
// the declared object type, and the type of the property value.
// But that would mean calling MakeGenericType, with value type parameters which AOT won't support.
//
// As a workaround, Generic instantiation was split into:
// 1. The object type comes from the PropertyFetcher generic parameter.
// Compiler supports it even if it is a value type; the type is known statically during compilation
// since PropertyFetcher is used with it.
// 2. Then, the declared object type is passed as a generic parameter to a generic method on PropertyFetcher<T> (or nested type.)
// Therefore, calling into MakeGenericMethod will only require specifying one parameter - the declared object type.
// The declared object type is guaranteed to be a reference type (throw on value type.) Thus, MakeGenericMethod is AOT compatible.
private static PropertyFetch CreateInstantiated<TDeclaredObject>(PropertyInfo propertyInfo)
where TDeclaredObject : class
=> new PropertyFetchInstantiated<TDeclaredObject>(propertyInfo);
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode(TrimCompatibilityMessage)]
#endif
private sealed class PropertyFetchInstantiated<TDeclaredObject> : PropertyFetch
where TDeclaredObject : class
{
private readonly string propertyName;
private readonly Func<TDeclaredObject, T> propertyFetch;
private PropertyFetch? innerFetcher;
public PropertyFetchInstantiated(PropertyInfo property)
{
this.propertyName = property.Name;
this.propertyFetch = (Func<TDeclaredObject, T>)property.GetMethod!.CreateDelegate(typeof(Func<TDeclaredObject, T>));
}
public override int NumberOfInnerFetchers => this.innerFetcher == null
? 0
: 1 + this.innerFetcher.NumberOfInnerFetchers;
public override bool TryFetch(
#if NETSTANDARD2_1_0_OR_GREATER || NET6_0_OR_GREATER
[NotNullWhen(true)]
#endif
object? obj,
out T? value)
{
if (obj is TDeclaredObject o)
{
value = this.propertyFetch(o);
return true;
}
var innerFetcher = this.innerFetcher;
if (innerFetcher is null)
{
return TryFetchRare(obj, this.propertyName, ref this.innerFetcher, out value);
}
return innerFetcher.TryFetch(obj, out value);
}
}
}
}
|