using System;
using System.Reflection;
using System.Runtime.Caching;
namespace StardewModdingAPI.Framework.Reflection
{
/// Provides helper methods for accessing inaccessible code.
/// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimize performance without unnecessary memory usage).
internal class Reflector
{
/*********
** Fields
*********/
/// The cached fields and methods found via reflection.
private readonly MemoryCache Cache = new(typeof(Reflector).FullName!);
/// The sliding cache expiration time.
private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5);
/*********
** Public methods
*********/
/****
** Fields
****/
/// Get a instance field.
/// The field type.
/// The object which has the field.
/// The field name.
/// Whether to throw an exception if the field isn't found. Due to limitations with nullable reference types, setting this to false will still mark the value non-nullable.
/// Returns the field wrapper, or null if is false and the field doesn't exist.
/// The target field doesn't exist, and is true.
public IReflectedField GetField(object obj, string name, bool required = true)
{
// validate
if (obj == null)
throw new ArgumentNullException(nameof(obj), "Can't get a instance field from a null object.");
// get field from hierarchy
IReflectedField? field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (required && field == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance field.");
return field!;
}
/// Get a static field.
/// The field type.
/// The type which has the field.
/// The field name.
/// Whether to throw an exception if the field isn't found. Due to limitations with nullable reference types, setting this to false will still mark the value non-nullable.
/// Returns the field wrapper, or null if is false and the field doesn't exist.
/// The target field doesn't exist, and is true.
public IReflectedField GetField(Type type, string name, bool required = true)
{
// get field from hierarchy
IReflectedField? field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
if (required && field == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static field.");
return field!;
}
/****
** Properties
****/
/// Get a instance property.
/// The property type.
/// The object which has the property.
/// The property name.
/// Whether to throw an exception if the property isn't found. Due to limitations with nullable reference types, setting this to false will still mark the value non-nullable.
/// Returns the property wrapper, or null if is false and the property doesn't exist.
/// The target property doesn't exist, and is true.
public IReflectedProperty GetProperty(object obj, string name, bool required = true)
{
// validate
if (obj == null)
throw new ArgumentNullException(nameof(obj), "Can't get a instance property from a null object.");
// get property from hierarchy
IReflectedProperty? property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (required && property == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance property.");
return property!;
}
/// Get a static property.
/// The property type.
/// The type which has the property.
/// The property name.
/// Whether to throw an exception if the property isn't found. Due to limitations with nullable reference types, setting this to false will still mark the value non-nullable.
/// Returns the property wrapper, or null if is false and the property doesn't exist.
/// The target property doesn't exist, and is true.
public IReflectedProperty GetProperty(Type type, string name, bool required = true)
{
// get field from hierarchy
IReflectedProperty? property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (required && property == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static property.");
return property!;
}
/****
** Methods
****/
/// Get a instance method.
/// The object which has the method.
/// The method name.
/// Whether to throw an exception if the method isn't found. Due to limitations with nullable reference types, setting this to false will still mark the value non-nullable.
/// Returns the method wrapper, or null if is false and the method doesn't exist.
/// The target method doesn't exist, and is true.
public IReflectedMethod GetMethod(object obj, string name, bool required = true)
{
// validate
if (obj == null)
throw new ArgumentNullException(nameof(obj), "Can't get a instance method from a null object.");
// get method from hierarchy
IReflectedMethod? method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (required && method == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method.");
return method!;
}
/// Get a static method.
/// The type which has the method.
/// The method name.
/// Whether to throw an exception if the method isn't found. Due to limitations with nullable reference types, setting this to false will still mark the value non-nullable.
/// Returns the method wrapper, or null if is false and the method doesn't exist.
/// The target method doesn't exist, and is true.
public IReflectedMethod GetMethod(Type type, string name, bool required = true)
{
// get method from hierarchy
IReflectedMethod? method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (required && method == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method.");
return method!;
}
/*********
** Private methods
*********/
/// Get a field from the type hierarchy.
/// The expected field type.
/// The type which has the field.
/// The object which has the field, or null for a static field.
/// The field name.
/// The reflection binding which flags which indicates what type of field to find.
private IReflectedField? GetFieldFromHierarchy(Type type, object? obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
FieldInfo? field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () =>
{
for (Type? curType = type; curType != null; curType = curType.BaseType)
{
FieldInfo? fieldInfo = curType.GetField(name, bindingFlags);
if (fieldInfo != null)
{
type = curType;
return fieldInfo;
}
}
return null;
});
return field != null
? new ReflectedField(type, obj, field, isStatic)
: null;
}
/// Get a property from the type hierarchy.
/// The expected property type.
/// The type which has the property.
/// The object which has the property, or null for a static property.
/// The property name.
/// The reflection binding which flags which indicates what type of property to find.
private IReflectedProperty? GetPropertyFromHierarchy(Type type, object? obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
PropertyInfo? property = this.GetCached($"property::{isStatic}::{type.FullName}::{name}", () =>
{
for (Type? curType = type; curType != null; curType = curType.BaseType)
{
PropertyInfo? propertyInfo = curType.GetProperty(name, bindingFlags);
if (propertyInfo != null)
{
type = curType;
return propertyInfo;
}
}
return null;
});
return property != null
? new ReflectedProperty(type, obj, property, isStatic)
: null;
}
/// Get a method from the type hierarchy.
/// The type which has the method.
/// The object which has the method, or null for a static method.
/// The method name.
/// The reflection binding which flags which indicates what type of method to find.
private IReflectedMethod? GetMethodFromHierarchy(Type type, object? obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
MethodInfo? method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () =>
{
for (Type? curType = type; curType != null; curType = curType.BaseType)
{
MethodInfo? methodInfo = curType.GetMethod(name, bindingFlags);
if (methodInfo != null)
{
type = curType;
return methodInfo;
}
}
return null;
});
return method != null
? new ReflectedMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static))
: null;
}
/// Get a method or field through the cache.
/// The expected type.
/// The cache key.
/// Fetches a new value to cache.
private TMemberInfo? GetCached(string key, Func fetch)
where TMemberInfo : MemberInfo
{
// get from cache
if (this.Cache.Contains(key))
{
CacheEntry entry = (CacheEntry)this.Cache[key];
return entry.IsValid
? (TMemberInfo)entry.MemberInfo
: default;
}
// fetch & cache new value
TMemberInfo? result = fetch();
CacheEntry cacheEntry = new(result);
this.Cache.Add(key, cacheEntry, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry });
return result;
}
}
}