//
// Copyright (c) 2019 cactuaroid All Rights Reserved
//
//
// Released under the MIT license
// https://github.com/cactuaroid/PrivateObjectExtensions
//
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using System.Reflection;
namespace System
{
///
/// Extension methods for PrivateObject
///
public static class PrivateObjectExtensions
{
private static readonly BindingFlags Static = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.Static;
private static readonly BindingFlags Instance = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.Instance;
///
/// Get from private (and any other) field/property.
/// If the real type of specified object doesn't contain the specified field/property,
/// base types are searched automatically.
///
/// The object to get from
/// The name of the field/property
/// The object got from the field/property
/// 'name' is not found.
/// Arguments contain null.
public static object GetPrivate(this object obj, string name)
{
if (obj == null) { throw new ArgumentNullException("obj"); }
return GetPrivate(obj, name, obj.GetType(), null);
}
///
/// Get from private (and any other) field/property.
/// If the real type of specified object doesn't contain the specified field/property,
/// base types are searched automatically.
///
/// The type of the field/property
/// The object to get from
/// The name of the field/property
/// The object got from the field/property
/// 'name' is not found.
/// Arguments contain null.
public static T GetPrivate(this object obj, string name)
{
if (obj == null) { throw new ArgumentNullException("obj"); }
return (T)GetPrivate(obj, name, obj.GetType(), typeof(T));
}
///
/// Get from private (and any other) field/property with assuming the specified object as specified type.
/// If the specified type doesn't contain the specified field/property,
/// base types are searched automatically.
///
/// The object to get from
/// The name of the field/property
/// The type of 'obj' for seaching member starting from. Real type of 'obj' is ignored.
/// The object got from the field/property
/// 'name' is not found.
/// 'objType' is not assignable from 'obj'.
/// Arguments contain null.
public static object GetPrivate(this object obj, string name, Type objType)
{
return GetPrivate(obj, name, objType, null);
}
///
/// Get from private (and any other) field/property with assuming the specified object as specified type.
/// If the specified type doesn't contain the specified field/property,
/// base types are searched automatically.
///
/// The type of the field/property
/// The object to get from
/// The name of the field/property
/// The type of 'obj' for seaching member starting from. Real type of 'obj' is ignored.
/// The object got from the field/property
/// 'name' is not found.
/// 'objType' is not assignable from 'obj'.
/// Arguments contain null.
public static T GetPrivate(this object obj, string name, Type objType)
{
return (T)GetPrivate(obj, name, objType, typeof(T));
}
private static object GetPrivate(object obj, string name, Type objType, Type memberType)
{
if (obj == null) { throw new ArgumentNullException("obj"); }
if (name == null) { throw new ArgumentNullException("name"); }
if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("name is empty or white-space.", "name"); }
if (objType == null) { throw new ArgumentNullException("objType"); }
if (!objType.IsAssignableFrom(obj.GetType())) { throw new ArgumentException($"{objType} is not assignable from {obj.GetType()}.", "objType"); }
bool memberTypeMatching(Type actualType) => actualType == memberType;
if (TryFindFieldOrPropertyOwnerType(objType, name, memberType, memberTypeMatching, Instance, out var ownerType))
{
return new PrivateObject(obj, new PrivateType(ownerType)).GetFieldOrProperty(name);
}
else if (TryFindFieldOrPropertyOwnerType(objType, name, memberType, memberTypeMatching, Static, out ownerType))
{
return new PrivateType(ownerType).GetStaticFieldOrProperty(name);
}
throw new ArgumentException(((memberType != null) ? memberType + " " : "") + name + " is not found.");
}
///
/// Get from private (and any other) static field/property.
///
/// The type to get from
/// The name of the static field/property
/// The object got from the static field/property
/// 'name' is not found.
/// Arguments contain null.
public static object GetPrivate(this Type type, string name)
{
return GetPrivate(type, name, null);
}
///
/// Get from private (and any other) static field/property.
///
/// The type of the field/property
/// The type to get from
/// The name of the static field/property
/// The object got from the static field/property
/// 'name' is not found.
/// Arguments contain null.
public static T GetPrivate(this Type type, string name)
{
return (T)GetPrivate(type, name, typeof(T));
}
private static object GetPrivate(this Type type, string name, Type memberType)
{
if (type == null) { throw new ArgumentNullException("type"); }
if (name == null) { throw new ArgumentNullException("name"); }
if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("name is empty or white-space.", "name"); }
bool memberTypeMatching(Type actualType) => actualType == memberType;
if (type.ContainsFieldOrProperty(name, memberType, memberTypeMatching, Static))
{
return new PrivateType(type).GetStaticFieldOrProperty(name);
}
throw new ArgumentException(((memberType != null) ? memberType + " " : "") + name + " is not found.");
}
///
/// Set to private (and any other) field/property.
/// If the real type of specified object doesn't contain the specified field/property,
/// base types are searched automatically.
///
/// The object to set to
/// The name of the field/property
/// The value to set for 'name'
/// 'name' is not found.
/// Arguments contain null.
public static void SetPrivate(this object obj, string name, T value)
{
if (obj == null) { throw new ArgumentNullException("obj"); }
SetPrivate(obj, name, value, obj.GetType());
}
///
/// Set to private (and any other) field/property with assuming the specified object as specified type.
/// If the specified type doesn't contain the specified field/property,
/// base types are searched automatically.
///
/// The object to set to
/// The name of the field/property
/// The value to set for 'name'
/// The type of 'obj' for seaching member starting from. Real type of 'obj' is ignored.
/// 'name' is not found.
/// 'objType' is not assignable from 'obj'.
/// Arguments contain null.
public static void SetPrivate(this object obj, string name, T value, Type objType)
{
if (obj == null) { throw new ArgumentNullException("obj"); }
if (name == null) { throw new ArgumentNullException("name"); }
if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("name is empty or white-space.", "name"); }
if (value == null) { throw new ArgumentNullException("value"); }
if (objType == null) { throw new ArgumentNullException("objType"); }
if (!objType.IsAssignableFrom(obj.GetType())) { throw new ArgumentException($"{objType} is not assignable from {obj.GetType()}.", "objType"); }
if (TrySetPrivate(obj, name, value, objType)) { return; }
// retry for the case of getter only property
if (TrySetPrivate(obj, GetBackingFieldName(name), value, objType)) { return; }
throw new ArgumentException($"{typeof(T)} {name} is not found.");
}
private static bool TrySetPrivate(object obj, string name, T value, Type objType)
{
var memberType = typeof(T);
bool memberTypeMatching(Type actualType) => actualType.IsAssignableFrom(memberType);
try
{
if (TryFindFieldOrPropertyOwnerType(objType, name, memberType, memberTypeMatching, Instance, out var ownerType))
{
new PrivateObject(obj, new PrivateType(ownerType)).SetFieldOrProperty(name, value);
return true;
}
else if (TryFindFieldOrPropertyOwnerType(objType, name, memberType, memberTypeMatching, Static, out ownerType))
{
new PrivateType(ownerType).SetStaticFieldOrProperty(name, value);
return true;
}
}
catch(MissingMethodException)
{
// When getter only property name is given, the property is found but fails to set.
return false;
}
return false;
}
///
/// Set to private (and any other) static field/property.
///
/// The type to set to
/// The name of the field/property
/// The value to set for 'name'
/// 'name' is not found.
/// Arguments contain null.
public static void SetPrivate(this Type type, string name, T value)
{
if (type == null) { throw new ArgumentNullException("type"); }
if (name == null) { throw new ArgumentNullException("name"); }
if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("name is empty or white-space.", "name"); }
if (TrySetPrivate(type, name, value)) { return; }
// retry for the case of getter only property
if (TrySetPrivate(type, GetBackingFieldName(name), value)) { return; }
throw new ArgumentException($"{typeof(T)} {name} is not found.");
}
private static bool TrySetPrivate(this Type type, string name, T value)
{
var memberType = typeof(T);
bool memberTypeMatching(Type actualType) => actualType.IsAssignableFrom(memberType);
try
{
if (type.ContainsFieldOrProperty(name, memberType, memberTypeMatching, Static))
{
new PrivateType(type).SetStaticFieldOrProperty(name, value);
return true;
}
}
catch (MissingMethodException)
{
// When getter only property name is given, the property is found but fails to set.
return false;
}
return false;
}
private static string GetBackingFieldName(string propertyName)
=> $"<{propertyName}>k__BackingField"; // generated backing field name
private static bool TryFindFieldOrPropertyOwnerType(Type objType, string name, Type memberType, Func memberTypeMatching, BindingFlags bindingFlag, out Type ownerType)
{
ownerType = FindFieldOrPropertyOwnerType(objType, name, memberType, memberTypeMatching, bindingFlag);
return (ownerType != null);
}
private static Type FindFieldOrPropertyOwnerType(Type objectType, string name, Type memberType, Func memberTypeMatching, BindingFlags bindingFlags)
{
if (objectType == null) { return null; }
if (objectType.ContainsFieldOrProperty(name, memberType, memberTypeMatching, bindingFlags))
{
return objectType;
}
return FindFieldOrPropertyOwnerType(objectType.BaseType, name, memberType, memberTypeMatching, bindingFlags);
}
private static bool ContainsFieldOrProperty(this Type objectType, string name, Type memberType, Func memberTypeMatching, BindingFlags bindingFlags)
{
var fields = objectType
.GetFields(bindingFlags)
.Select((x) => new { Type = x.FieldType, Member = x as MemberInfo });
var properties = objectType
.GetProperties(bindingFlags)
.Select((x) => new { Type = x.PropertyType, Member = x as MemberInfo });
var members = fields.Concat(properties);
return members.Any((actual) =>
(memberType == null || memberTypeMatching.Invoke(actual.Type))
&& actual.Member.Name == name);
}
}
}