Skip to content

Commit

Permalink
Added ReflectionHelper.Get(Method/Property/Field)Info methods (Subnau…
Browse files Browse the repository at this point in the history
…ticaNitrox#1630)

These methods can get a type from a lambda that calls a method or accesses a field/property. Supports both instance and static members.
  • Loading branch information
Measurity authored Oct 20, 2021
1 parent ce3673d commit 23aca6e
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 9 deletions.
105 changes: 96 additions & 9 deletions NitroxModel/Helper/ReflectionHelper.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace NitroxModel.Helper
{
/// <summary>
/// Utility class for reflection API.
/// </summary>
/// <remarks>
/// This class should be used when requiring <see cref="MethodInfo" /> or <see cref="MemberInfo" /> like information from code. This will ensure that compilation only succeeds
/// when reflection is used properly.
/// </remarks>
public static class ReflectionHelper
{
// Public calls are useful for reflected, inaccessible objects.
Expand Down Expand Up @@ -33,7 +41,7 @@ public static object ReflectionGet<T>(this T o, string fieldName, bool isPublic

public static object ReflectionGet(this object o, FieldInfo fieldInfo)
{
Validate.NotNull(fieldInfo, $"Field cannot be null!");
Validate.NotNull(fieldInfo, "Field cannot be null!");
return fieldInfo.GetValue(o);
}

Expand Down Expand Up @@ -63,7 +71,7 @@ public static void ReflectionSet<T>(this T o, string fieldName, object value, bo

public static void ReflectionSet(this object o, FieldInfo fieldInfo, object value)
{
Validate.NotNull(fieldInfo, $"Field cannot be null!");
Validate.NotNull(fieldInfo, "Field cannot be null!");
fieldInfo.SetValue(o, value);
}

Expand All @@ -81,6 +89,90 @@ public static MethodInfo GetMethod<T>(string methodName, bool isPublic = false,
return GetMethod(typeof(T), methodName, isPublic, isStatic, types);
}

public static FieldInfo GetField<T>(string fieldName, bool isPublic = false, bool isStatic = false)
{
return GetField(typeof(T), fieldName, isPublic, isStatic);
}

/// <summary>
/// Given a lambda expression that calls a method, returns the method info.
/// If method has parameters then anything can be supplied, the actual method won't be called.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="expression">The expression.</param>
/// <returns></returns>
public static MethodInfo GetMethodInfo(Expression<Action> expression)
{
return GetMethodInfo((LambdaExpression)expression);
}

/// <summary>
/// Given a lambda expression that calls a method, returns the method info.
/// If method has parameters then anything can be supplied, the actual method won't be called.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="expression">The expression.</param>
/// <returns></returns>
public static MethodInfo GetMethodInfo<T>(Expression<Action<T>> expression)
{
return GetMethodInfo((LambdaExpression)expression);
}

/// <summary>
/// Given a lambda expression that calls a method, returns the method info.
/// If method has parameters then anything can be supplied, the actual method won't be called.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="expression">The expression.</param>
/// <returns></returns>
public static MethodInfo GetMethodInfo<T, TResult>(Expression<Func<T, TResult>> expression)
{
return GetMethodInfo((LambdaExpression)expression);
}

public static FieldInfo GetFieldInfo<T>(Expression<Func<T>> expression)
{
return (FieldInfo)GetMemberInfo(expression);
}

public static FieldInfo GetFieldInfo<TClass>(Expression<Func<TClass, object>> expression)
{
return (FieldInfo)GetMemberInfo(expression);
}

public static PropertyInfo GetPropertyInfo<T>(Expression<Func<T>> expression)
{
return (PropertyInfo)GetMemberInfo(expression);
}

public static PropertyInfo GetPropertyInfo<TClass>(Expression<Func<TClass, object>> expression)
{
return (PropertyInfo)GetMemberInfo(expression);
}

private static MethodInfo GetMethodInfo(LambdaExpression expression)
{
MethodCallExpression outermostExpression = expression.Body as MethodCallExpression;
if (outermostExpression == null)
{
throw new ArgumentException("Invalid Expression. Expression should consist of a method call only.");
}
return outermostExpression.Method;
}

private static MemberInfo GetMemberInfo(LambdaExpression expression)
{
MemberExpression currentExpression = expression.Body as MemberExpression;
// If expression contains a cast (e.g. (object)value), then operand is the member access.
currentExpression ??= (expression.Body as UnaryExpression)?.Operand as MemberExpression;
if (currentExpression == null)
{
throw new ArgumentException("Invalid Expression. Expression should consist of a field or property access only.");
}
return currentExpression.Member;
}

private static MethodInfo GetMethod(this Type t, string methodName, bool isPublic = false, bool isStatic = false, params Type[] types)
{
MethodInfo methodInfo;
Expand All @@ -99,11 +191,6 @@ private static MethodInfo GetMethod(this Type t, string methodName, bool isPubli
return methodInfo;
}

public static FieldInfo GetField<T>(string fieldName, bool isPublic = false, bool isStatic = false)
{
return GetField(typeof(T), fieldName, isPublic, isStatic);
}

private static FieldInfo GetField(this Type t, string fieldName, bool isPublic = false, bool isStatic = false)
{
BindingFlags bindingFlags = GetBindingFlagsFromMethodQualifiers(isPublic, isStatic);
Expand Down Expand Up @@ -135,10 +222,10 @@ private static void ValidateStatic(object o, bool isStatic)
{
throw new ArgumentException("Object can't be null when isStatic is false!");
}
else if (o != null && isStatic)
if (o != null && isStatic)
{
throw new ArgumentException("Object must be be null when isStatic is true!");
}
}
}
}
}
103 changes: 103 additions & 0 deletions NitroxTest/Model/ReflectionHelperTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using System.Diagnostics;
using System.Reflection;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NitroxModel.Helper;

namespace NitroxTest.Model
{
[TestClass]
public class ReflectionHelperTest
{
[TestMethod]
public void GetMethodInfo()
{
// Get static method.
MethodInfo staticMethod = ReflectionHelper.GetMethodInfo(() => AbusedClass.StaticMethodReturnsInt());
staticMethod.Should().NotBeNull();
staticMethod.ReturnType.Should().Be<int>();
staticMethod.Name.Should().BeEquivalentTo(nameof(AbusedClass.StaticMethodReturnsInt));
staticMethod.Invoke(null, Array.Empty<object>());
// Extra check for method with parameters, just to be safe.
staticMethod = ReflectionHelper.GetMethodInfo(() => AbusedClass.StaticMethodHasParams("", null));
staticMethod.Should().NotBeNull();
staticMethod.ReturnType.Should().Be<string>();
staticMethod.Name.Should().BeEquivalentTo(nameof(AbusedClass.StaticMethodHasParams));
staticMethod.GetParameters().Should().OnlyHaveUniqueItems();
staticMethod.GetParameters()[0].Name.Should().BeEquivalentTo("myValue");
staticMethod.GetParameters()[0].ParameterType.Should().Be<string>();
staticMethod.GetParameters()[1].ParameterType.Should().Be<Process>();
staticMethod.Invoke(null, new[] { "hello, reflection", (object)null }).Should().BeEquivalentTo("hello, reflection");

// Get instance method.
MethodInfo instanceMethod = ReflectionHelper.GetMethodInfo<AbusedClass>(t => t.Method());
instanceMethod.Should().NotBeNull();
instanceMethod.ReturnType.Should().Be<int>();
instanceMethod.Name.Should().BeEquivalentTo(nameof(AbusedClass.Method));
}

[TestMethod]
public void GetFieldInfo()
{
// Get static field.
FieldInfo staticField = ReflectionHelper.GetFieldInfo(() => AbusedClass.StaticField);
staticField.Name.Should().BeEquivalentTo(nameof(AbusedClass.StaticField));
staticField.FieldType.Should().Be<int>();
// Get instance field.
FieldInfo instanceField = ReflectionHelper.GetFieldInfo<AbusedClass>(t => t.InstanceField);
instanceField.Name.Should().BeEquivalentTo(nameof(AbusedClass.InstanceField));
instanceField.FieldType.Should().Be<int>();
}

[TestMethod]
public void GetPropertyInfo()
{
// Get static property.
PropertyInfo staticProperty = ReflectionHelper.GetPropertyInfo(() => AbusedClass.StaticProperty);
staticProperty.Name.Should().BeEquivalentTo(nameof(AbusedClass.StaticProperty));
staticProperty.PropertyType.Should().Be<int>();
// Get instance property.
PropertyInfo instanceProperty = ReflectionHelper.GetPropertyInfo<AbusedClass>(t => t.InstanceProperty);
instanceProperty.Name.Should().BeEquivalentTo(nameof(AbusedClass.InstanceProperty));
instanceProperty.PropertyType.Should().Be<int>();
}

[TestMethod]
public void GetMemberInfo()
{
// Get static property.
PropertyInfo staticProperty = ReflectionHelper.GetPropertyInfo(() => AbusedClass.StaticProperty);
staticProperty.Name.Should().BeEquivalentTo(nameof(AbusedClass.StaticProperty));
staticProperty.PropertyType.Should().Be<int>();
// Get instance property.
PropertyInfo instanceProperty = ReflectionHelper.GetPropertyInfo<AbusedClass>(t => t.InstanceProperty);
instanceProperty.Name.Should().BeEquivalentTo(nameof(AbusedClass.InstanceProperty));
instanceProperty.PropertyType.Should().Be<int>();
}

private class AbusedClass
{
public static readonly int StaticReadOnlyField = 1;
public static int StaticField = 2;
public int InstanceField = 3;
public static int StaticProperty { get; set; } = 4;
public int InstanceProperty { get; set; } = 5;

public static int StaticMethodReturnsInt()
{
return 2;
}

public static string StaticMethodHasParams(string myValue, Process process)
{
return myValue;
}

public int Method()
{
return 1;
}
}
}
}
1 change: 1 addition & 0 deletions NitroxTest/NitroxTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<Compile Include="Model\PacketProcessorTest.cs" />
<Compile Include="Model\Packets\PacketsSerializableTest.cs" />
<Compile Include="Model\PriorityQueueTest.cs" />
<Compile Include="Model\ReflectionHelperTest.cs" />
<Compile Include="Model\Test\TestNonActionPacket.cs" />
<Compile Include="Patcher\Patches\BaseGhost_Finish_PatchTest.cs" />
<Compile Include="Patcher\Patches\BuilderPatchTest.cs" />
Expand Down

0 comments on commit 23aca6e

Please sign in to comment.