Reflection
editThe helpers of this page use reflection: the trimmer and Native AOT cannot see the members they reach. Several of them are marked with RequiresUnreferencedCode, so the trim analyzer warns when you call them from code that is trimmed.
Call private members
ReflectionDynamicObject wraps an object, or a type, in a dynamic object that reaches its private members. Use it in tests, or to call a member of a library that is not public:
dynamic counter = new ReflectionDynamicObject(new Counter());
// Read and write private fields and properties
counter._count = 41;
Assert.Equal(41, counter.Count);
// Call private methods
counter.Increment();
Assert.Equal(42, counter._count);
// Wrap the type to reach static members and to call a public constructor
dynamic counterType = new ReflectionDynamicObject(typeof(Counter));
Assert.Equal("counter", (string)counterType.Name);
dynamic createdCounter = counterType.CreateInstance(10);
Assert.Equal(10, createdCounter.Count);The example uses this class:
private sealed class Counter
{
private int _count;
public Counter()
{
}
public Counter(int count) => _count = count;
private static string Name => "counter";
private int Count => _count;
private void Increment() => _count++;
}- Wrap an instance to reach its fields, properties, indexers and methods, public or not, including the ones declared by its base types.
- Wrap a
Typeto reach its static members, and to create an instance with CreateInstance. CreateInstance tries the public constructors that have as many parameters as the arguments, and throws anArgumentExceptionwhen none of them accepts them. - The values are
dynamic: cast them when an overload must be chosen, as in(string)counterType.Name. - Casting the wrapper returns the wrapped object when it is of the target type, such as a base type or an interface, and uses
Convert.ChangeTypefor the other conversions of anIConvertiblevalue. - An indexer is chosen by its parameters: the ones whose types accept the indexes as they are come first, then the public ones before the private ones and the explicit interface implementations. An exception thrown by the indexer, such as a
KeyNotFoundException, is rethrown as is. - A member that does not exist throws a
RuntimeBinderExceptionwhen it is used. - The debugger and the tools that list the members of a
dynamicobject see the instance members for an object and the static members for a type. Indexers and compiler-generated fields, such as the backing fields of auto-properties, are not listed.
Check attributes
Assert.True(typeof(FileAttributes).HasCustomAttribute<FlagsAttribute>());
Assert.False(typeof(DayOfWeek).HasCustomAttribute<FlagsAttribute>());
// The same with a Type, for instance one read from a configuration file
Assert.True(typeof(FileAttributes).HasCustomAttribute(typeof(FlagsAttribute)));
// Members, parameters, modules and assemblies have the same methods
var method = typeof(string).GetMethod(nameof(string.Format), [typeof(string), typeof(object[])]);
Assert.NotNull(method);
Assert.True(method.GetParameters()[1].HasCustomAttribute<ParamArrayAttribute>());HasCustomAttribute exists for Type, MemberInfo, ParameterInfo, Module and Assembly, with the attribute type as a generic argument or as a Type. It does not search the base types unless you pass inherit: true. With inherit: true, the attributes of an overridden property, event or method, and of the parameters of an overridden method, are found too, unlike MemberInfo.IsDefined that ignores inherit for properties, events and parameters.
Read assembly information
var assembly = typeof(AssemblyUtilities).Assembly;
// The value of AssemblyInformationalVersionAttribute, or null when the assembly has none
var version = assembly.GetInformationalVersion();
Assert.NotNull(version);
// Throws an ArgumentException that lists the available resources instead of returning null
Assert.Throws<ArgumentException>(() => assembly.GetRequiredManifestResourceStream("missing.txt"));- GetInformationalVersion returns the value of
AssemblyInformationalVersionAttribute, the version you usually show in an "About" screen, ornullwhen the assembly has none. GetRequiredManifestResourceStream(name)andGetRequiredManifestResourceStream(type, name)return an embedded resource, likeAssembly.GetManifestResourceStream, but throw anArgumentExceptionthat lists the available resource names instead of returningnull. The overload with a type looks for the resource in the namespace of the type.- GetLinkerTimestampUtc reads the build date stored in the header of the file. Deterministic builds, the default of the .NET SDK, store a hash there instead of a date: the method then returns
null.
Inspect types and methods
Assert.True(typeof(int?).IsNullableOfT());
Assert.False(typeof(int).IsNullableOfT());
Assert.True(typeof(FileAttributes).IsFlagsEnum());
Assert.False(ReflectionUtilities.IsFlagsEnum<DayOfWeek>());
// string declares an implicit conversion to ReadOnlySpan<char>
var conversion = ReflectionUtilities.GetImplicitConversion("text", typeof(ReadOnlySpan<char>));
Assert.NotNull(conversion);
Assert.Equal("op_Implicit", conversion.Name);
// The conversions built into the language, such as int to long, are not operators
Assert.Null(ReflectionUtilities.GetImplicitConversion(42, typeof(long)));- IsNullableOfT tells whether a type is a
Nullable<T>, such asint?. - IsFlagsEnum tells whether a type is an enum marked with
[Flags]. It returnsfalsefor any other type. GetImplicitConversion(value, targetType)returns theop_Implicitoperator that converts the value to the target type, declared by the type of the value or by the target type, ornull. It does not see the conversions built into the language.
GetMethodLocation finds the source file and the first line of a method, from the portable PDB of its assembly, embedded or next to the file:
Func<int> getAnswer = () => 42;
// Reads the portable PDB, embedded or next to the assembly
var location = getAnswer.Method.GetMethodLocation();
Assert.NotNull(location);
Assert.EndsWith("ReflectionUtilitiesTests.cs", location.Value.FilePath);
Assert.True(location.Value.SequencePoint.StartLine > 0);It returns null when the assembly has no file, as in a single-file or Native AOT application, when the PDB cannot be found, or when the method has no source information. GetMethodLocationAsync reads the assembly file asynchronously.
Parse enums
EnumExtensions adds Parse and TryParse to every enum type, with the case sensitivity as a parameter. They accept a string or a ReadOnlySpan<char>:
// Parse and TryParse are available on every enum type
var day = DayOfWeek.Parse("friday", ignoreCase: true);
Assert.Equal(DayOfWeek.Friday, day);
Assert.True(DayOfWeek.TryParse("Sunday", ignoreCase: false, out var sunday));
Assert.Equal(DayOfWeek.Sunday, sunday);They call Enum.Parse<TEnum> and Enum.TryParse<TEnum>, so they accept the names and the numeric values, and the combinations of names of a [Flags] enum.
Combine expressions
AndAlso, OrElse and Negate combine Expression<Func<T, bool>> predicates into a new predicate, for instance to build a query filter from optional criteria:
Expression<Func<int, bool>> isPositive = n => n > 0;
Expression<Func<int, bool>> isSmall = n => n < 10;
// The parameters are rebound: the result is one lambda expression with one parameter
var isPositiveAndSmall = isPositive.AndAlso(isSmall);
var isNotPositiveOrNotSmall = isPositiveAndSmall.Negate();
var isNegativeOrSmall = isPositive.Negate().OrElse(isSmall);
int[] numbers = [-5, 3, 42];
Assert.Equal([3], numbers.AsQueryable().Where(isPositiveAndSmall));
Assert.Equal([-5, 42], numbers.AsQueryable().Where(isNotPositiveOrNotSmall));
Assert.Equal([-5, 3], numbers.AsQueryable().Where(isNegativeOrSmall));The parameters of both expressions are replaced by a single one, so the result is a lambda expression that a LINQ provider can translate, not an invocation of the original expressions.
Evaluate a property path
DataBinder.Eval reads a value from a path of properties given as a string, such as a path read from a template:
var order = new
{
Number = 42,
Customer = new { Name = "Ada" },
Lines = new[] { "Keyboard", "Mouse" },
Settings = new Dictionary<string, string>(StringComparer.Ordinal) { ["theme"] = "dark" },
};
// Properties are separated by dots, and names are case-insensitive
Assert.Equal("Ada", DataBinder.Eval(order, "Customer.Name"));
Assert.Equal("Ada", DataBinder.Eval(order, "customer.name"));
// Indexes use [] or (): a number for arrays and lists, a quoted string for other indexers
Assert.Equal("Mouse", DataBinder.Eval(order, "Lines[1]"));
Assert.Equal("dark", DataBinder.Eval(order, "Settings['theme']"));
// The overload with a format applies a composite format string to the value
Assert.Equal("Order #42", DataBinder.Eval(order, "Number", "Order #{0}"));- The parts of the path are separated by dots. Property names are case-insensitive.
- An index is written between
[]or(): a number for arrays and lists, a number or a quoted string for any type with an indexer, such as a dictionary. A quoted string can contain dots and brackets, as inSettings["a.b"]. - Eval returns
nullwhen the container or an intermediate value isnull. A property that does not exist throws anArgumentException. - The overload with a format returns a string: an empty string for
null, otherwise the value formatted withstring.Formatand the current culture. - GetPropertyValue and GetIndexedPropertyValue evaluate one part of a path.
Walk an object graph
ObjectGraphVisitor walks an object and everything it references: the values of its public properties and the items of its collections, recursively. Derive from it and override VisitValue, called once for every object, or VisitProperty, called for every property with its value:
private sealed class StringCollector : ObjectGraphVisitor
{
public List<string> Strings { get; } = [];
// Called once for every object of the graph: the root, property values and collection items
protected override void VisitValue(object value)
{
if (value is string text)
{
Strings.Add(text);
}
}
}var order = new
{
Customer = new { Name = "Ada" },
Lines = new[] { "Keyboard", "Mouse" },
};
var collector = new StringCollector();
collector.Visit(order);
Assert.Equal(["Ada", "Keyboard", "Mouse"], collector.Strings.Order(StringComparer.Ordinal));- Each object is visited once, compared by reference, so cycles do not loop forever. A value type is not visited again inside itself, such as
DateTime.Datethat returns anotherDateTime. - The properties of primitive types, enums,
decimal,DateTime,DateTimeOffset,TimeSpan,DateOnly,TimeOnlyandGuidare not visited: these values are visited, but not walked into. - Indexers and write-only properties are skipped. Strings are visited as values, not as collections of characters.
- The properties of every object are visited, including the ones of arrays and collections, such as
LengthorCount.