CLR & Concurrency · Lesson 3

Explicit member discovery with BindingFlags

Discover only the intended members and avoid relying on reflection enumeration order.

External .NET 10 SDK / desktop runtime. OS-thread experiments run only here; the .NET 9 browser companion has the narrower scope stated in the guide.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

The runtime contract

Reflection member discovery should state the intended visibility, instance or static scope, and inheritance boundary explicitly. The first query combines Public, Instance, and DeclaredOnly to select methods introduced by Probe itself. A separate ordinary lookup demonstrates an inherited public member, and a final NonPublic query locates a private method declared on Probe. Sorting selected names makes the teaching output independent of discovery order. These are actual reflection calls, not a list of invented metadata records. Deployment can still limit available metadata, so a successful desktop lookup is not an unrestricted trimming or AOT guarantee.

Trace the portable program

Probe declares the public instance method Own and the private method Secret, while Parent declares Inherited. Only Own matches the first flags, producing declared:Own. The ordinary public lookup can find Inherited through the base relationship, producing inherited:True. The explicit private lookup finds Secret and reports its IsPrivate metadata as true. The final line is private:True. Finding a member and invoking it are separate operations; this program only discovers methods. A common mistake is to omit an instance-or-static flag and then interpret an empty result as proof the members do not exist.

Execute the actual desktop lab

The external project repeats the real discovery under desktop .NET 10 and makes the selected flags part of the source contract. Reflection APIs expose methods for property accessors and other generated members too, so a more complex type can produce names beyond the handwritten methods a reader first notices. If an application is discovering commands, validate signatures and attributes instead of treating every public method as a command. The metadata-router project shows that bounded approach separately. Sorting output is for a deterministic report; it does not change which members match the original discovery predicate.

Pitfalls and boundary limits

DeclaredOnly excludes inherited members from that particular enumeration, while the ordinary GetMethod call demonstrates a different lookup. BindingFlags.NonPublic is not a substitute for knowing whether a member is private, protected, inherited, static, or declared on the target type. Ambiguous overloads may require explicit parameter information rather than a name-only lookup. Metadata preservation also matters after trimming. The sample does not invoke Secret or establish a security boundary around private reflection. Do not claim that one successful development-time lookup proves a production native artifact retained every member or that private visibility alone prevents all reflection access in every hosting environment.

Practice one: isolate behavior

First add another public instance method named Another to Probe and predict the explicitly sorted first report as Another followed by Own. Hint: choose the expected order from the source's ordinal sort rather than from metadata emission order. Then add a public static method and verify it is excluded by the current instance-only flags. Keep a separate query for static members. These modifications challenge the selection contract independently of the class's inheritance, and they help reveal a discovery implementation that accidentally depends on all public members having the same shape.

Practice two: design and verify

For another exercise, add two overloads of one public method and design a signature-specific lookup that chooses the intended one. Hint: a name alone may no longer identify a unique callable member. Define supported parameter and return types before creating delegates or invoking discovered commands. Then describe which metadata annotations or explicit registrations would be needed if the host is trimmed. Keep those deployment checks separate from the deterministic name report; passing an enumeration test does not establish that an arbitrary invocation or native publication will succeed.

Review the evidence

Review a reflection query by writing its visibility, instance/static scope, inheritance policy, expected signature, and output-order requirement explicitly. Test both an included and an excluded member so an overly broad query cannot pass unnoticed. The portable and external programs demonstrate real discovery for a small known type. They do not replace member validation, trust decisions, or deployment metadata planning. Reflection is most useful when its search space and interpretation are deliberate; otherwise a minor type change can turn a convenient name scan into an ambiguous or unexpectedly executable application boundary. Property accessors also appear as methods in reflection, so a command-discovery contract should deliberately include or exclude them rather than accidentally exposing a generated getter as an application operation.

External .NET 10 SDK / desktop runtime. OS-thread experiments run only here; the .NET 9 browser companion has the narrower scope stated in the guide.

Explicit member discovery with BindingFlags — desktop runtime lab

Set up the SDK project

  1. Save both files at their exact paths in a fresh owned directory.
  2. Use a .NET 10 SDK, then run dotnet run --project RuntimeLab.csproj. This project intentionally uses C# 13 syntax.
  3. Workers and waits are bounded; a timeout or a violated invariant fails the process. No benchmark or NativeAOT publication is implied by an ordinary run.

Download complete SDK project ↗

Project files

Program.csDownload file ↗
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
string[] names = typeof(Probe).GetMethods(flags).Select(method => method.Name).OrderBy(name => name, StringComparer.Ordinal).ToArray();
Console.WriteLine("declared:" + string.Join(",", names));
Console.WriteLine($"inherited:{typeof(Probe).GetMethod(nameof(Parent.Inherited)) is not null}");
Console.WriteLine($"private:{typeof(Probe).GetMethod("Secret", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)!.IsPrivate}");
class Parent { public void Inherited() { } }
sealed class Probe : Parent { public void Own() { } private void Secret() { } }
RuntimeLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>13.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>false</PublishAot>
  </PropertyGroup>
</Project>

Expected result

Exact behavioral stdout on the stated desktop lab: declared:Own inherited:True private:True Variable diagnostics, where emitted, go to stderr and must not be treated as portable timing or allocation guarantees.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
string[] names = typeof(Probe).GetMethods(flags).Select(method => method.Name).OrderBy(name => name, StringComparer.Ordinal).ToArray();
Console.WriteLine("declared:" + string.Join(",", names));
Console.WriteLine($"inherited:{typeof(Probe).GetMethod(nameof(Parent.Inherited)) is not null}");
Console.WriteLine($"private:{typeof(Probe).GetMethod("Secret", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)!.IsPrivate}");
class Parent { public void Inherited() { } }
sealed class Probe : Parent { public void Own() { } private void Secret() { } }
Open C# practice

C# practice output

declared:Own
inherited:True
private:True
Try this
Remove DeclaredOnly in a separate experiment and sort the result. Finding a private member is not a deployment-wide promise that all metadata survives trimming.

Further reading

Interview questions for this lesson