ILens

Version 12 — user guide

MCP server for inspecting .NET assemblies

Why ILens

An AI agent that needs to look inside a compiled .NET assembly usually shells out to ildasm (an IL dump) or ilspycmd (a full C# decompile) through a generic Bash tool. Both dump an entire file as text. That output stays in the agent's context as input tokens and is sent again on every later turn, so inspecting a single class can cost thousands of tokens, and the cost recurs for the rest of the session.

The target measured below is ICSharpCode.Decompiler.dll 11.2.0.10050, a 3.0 MB assembly with 749 public types. A full decompile of it costs the same however narrow the question is, which is why the baseline column repeats.

Taskilspycmd costILens toolILens cost
List the public types in a namespace1,240,053 tokensfind_types987 tokens
See the API surface of one mid-sized class1,240,053 tokenslist_members692 tokens
Find which types expose a given method1,240,053 tokensfind_methods668 tokens

Measured against ICSharpCode.Decompiler.dll 11.2.0.10050 on 2026-11-08; token figures are character counts divided by 4, not a real tokenizer.

ILens turns assembly inspection into bounded, targeted lookups instead of full-file dumps — which is what makes it cheap enough to leave registered as an always-available tool.

Overview and scope

ILens is an MCP server that speaks JSON-RPC over stdio and exposes tools in four categories: discovery (finding types and methods by name or signature), inspection (a type's members, cross-reference analysis, and what a Harmony mod depends on), decompilation (C# source for a type or member, and a pattern search through it), and comparison (what differs between two builds). It is read-only, inspects only assemblies under the directories named by --allow-root, makes no network calls, and ships as a single self-contained Windows binary.

Scope. ILens is built for inspecting C# assemblies, and its output is C#. The metadata-driven tools — find_types, find_methods, list_members, analyze — read language-agnostic metadata and work on an assembly produced by any .NET language. decompile and analyze's call_sites kind always emit C#, and search_code searches that C#. An assembly compiled from F#, VB.NET, or C++/CLI still decompiles, but constructs the source language has and C# does not may render unfaithfully.

Built on ILSpy

ILens would be nothing without ILSpy. The decompiler, the analyzer infrastructure, and the type system that lets ILens talk about a .NET assembly in C# terms at all are not ILens's work — they belong to the ILSpy team. ILens links ICSharpCode.Decompiler for the decompiler proper and ICSharpCode.ILSpyX for the analyzer infrastructure, and is, candidly, a thin MCP shim on top of both.

That upstream work is MIT-licensed, which is the only reason ILens can exist at all. If ILens is useful to you, please consider giving ILSpy a star. They earned it.

To be clear: bug reports and feature requests about ILens belong on the ILens issue tracker, not ILSpy's. The upstream maintainers should not have to inherit downstream problems on top of their own.

Installation

System requirements: Windows 10/11, x64. The binary is self-contained — no separate .NET runtime install is required.

Recommended: winget

winget install Tadis.ILens

A standard winget portable install. It downloads the release ZIP, extracts it under %LOCALAPPDATA%\Microsoft\WinGet\Packages\Tadis.ILens_*\, and registers ilens as a command alias in %LOCALAPPDATA%\Microsoft\WinGet\Links\ — a directory already on your user PATH if you have ever installed another winget portable package. No admin elevation is needed.

For alternative installers see Appendix A; for updating and uninstalling see Appendix B; for installation troubleshooting see Appendix C.

Integrating with Claude Code

Add an entry to .mcp.json in your project root. The command field is the literal string "ilens" — the installer puts the binary on your PATH, so no full path is needed.

{
  "mcpServers": {
    "ilens": {
      "command": "ilens",
      "args": [
        "--allow-root", "C:\\path\\to\\dlls"
      ]
    }
  }
}

A first install needs a full restart, not just a new session. Two independent gates have to clear before a freshly installed ilens actually launches as an MCP server in a Claude app session.

The first is PATH propagation. Windows captures a process's PATH from the registry when that process starts, and later registry changes do not retroactively reach already-running processes. If ilens is a brand-new entry on your user PATH — true for a first-ever winget portable install, which adds %LOCALAPPDATA%\Microsoft\WinGet\Links\, and for any install.ps1 run, which adds %LOCALAPPDATA%\Programs\ILens\ — an already-running Claude app holds a stale environment, and every Claude Code session inside it inherits that stale environment from the parent app. Subsequent winget updates of an already-installed package do not touch PATH and do not trigger this gate.

The second is MCP-server registration. Claude Code reads .mcp.json only at session start, so editing it inside a running session does not register the new server in that session.

Recovering takes both: fully exit and relaunch Claude app, so the new app process gets the current registry PATH, and then start a new Claude Code session in the relaunched app. Existing Claude Code sessions survive a Claude app restart — the relaunched app reattaches to surviving session processes with their original environments intact — so a resumed session picks up neither change. Only a freshly spawned session inside the freshly launched app clears both gates.

The recipe for a first install via the Claude app: close Claude app entirely → install via winget (or install.ps1) → relaunch Claude app → start a new Claude Code session, not a resumed one.

Each --allow-root flag adds a directory tree from which assemblies may be loaded. Without any --allow-root flags the server cannot load anything at all. Pass the flag more than once to configure several roots. ILens also looks at the top of each root for the assemblies an inspected assembly references, so the assemblies a mod builds against can sit in a root of their own and still resolve when the mod is decompiled.

Three more options exist. --max-total-size <MB> sets the memory budget described under Security model. --max-search-seconds <n> sets how long one search_code call may decompile before it answers with how far it came, 45 seconds unless given. A client built on the MCP SDK's defaults gives up on a request after 60 seconds, and the 45 seconds leave a call the time to finish its answer within that, with room for a slower machine; with 0, each call finishes at least one type. --version prints the installed version and exits. Any other argument stops the server at startup.

For the equivalent claude mcp add CLI command see Appendix D; for Claude Desktop see Appendix E.

Project-level guidance

Registering the server makes the tools available. It does not by itself make Claude prefer them: left to its habits, a model may still run ildasm or ilspycmd in a shell, read the assembly file directly, or search the web for source that does not match your build. ILens closes that gap itself, through the MCP server instructions it sends, so no project needs a block that names ILens's tools. The README divides setup into two steps: Step 1 registers the server, as described under Integrating with Claude Code, and Step 2 reads as follows.

Clients that do not read server instructions

When a client connects, ILens sends it MCP server instructions: what ILens is for, which directories it can read, and which tool answers which question. Claude Code reads them at the start of every session, in the terminal and in the Claude desktop app's Code tab, so with Claude Code, Step 1 is the whole setup.

Claude Desktop's chat does not read server instructions. For it, and for any other client that does not, add this paragraph to the client's instructions and replace <allow-root> with the directory configured above. It names no tool, so it stays correct when a release renames or merges tools:

## Inspecting .NET assemblies

Assemblies under `<allow-root>` are reachable through the `ilens` MCP server.
Prefer its tools over running `ildasm` or `ilspycmd` in a shell, reading
assembly files directly, or searching the web for source.

If your project's CLAUDE.md or a client's instructions hold the block an earlier ILens README asked you to paste, delete it: it names tools that no longer exist. A client that does not read server instructions takes the paragraph above in its place.

Security model

Tool reference

Nine tools, all read-only. Every tool except list_allowed_roots takes an assembly path that must resolve inside a configured allow-root; compare takes two, assembly_a and assembly_b. The path and budget errors every assembly-loading tool shares are collected under Troubleshooting rather than repeated per tool.

A value with exactly one reading is taken as the form the schema has, on every parameter of every tool: a lone value for a parameter typed as an array (kinds: "field" means ["field"], parameter_types: "string" means ["string"]); a listed name in another case, without its underscores or with spaces around it (UsedBy means used_by); a whole number given as text, with a zero fraction or with an exponent; "true" or "false" given as text; and null for an optional parameter, which counts as the parameter left out. parameter_types also takes several types in one text, split by the tools themselves: "string, int" means ["string", "int"], and a comma inside angle or square brackets stays in its pattern, as in "Dictionary<int, string>". Any other value a parameter does not take is an error, see Strict arguments.

Conventions every tool shares

Lists. Every tool that answers with a list takes limit (default 100); only decompile and list_allowed_roots take none. The answer opens with a header that counts every entry, not just the ones shown, and an empty answer is the header's own form, No types or No users of ..., so it cannot be mistaken for a truncated one. When the limit cuts the list, a line after it says by how much. A limit of 0 lists nothing, so the answer is the header, which holds the count, and that line; a negative limit counts as 0. Lists are sorted by the name they print, so a limit cuts predictably and the order stays stable across builds of the inspected assembly. Every answer is plain text. find_types on ICSharpCode.Decompiler.dll with namespace ICSharpCode.Decompiler.Metadata and limit 3:

44 types:
ICSharpCode.Decompiler.Metadata.AssemblyNameReference
ICSharpCode.Decompiler.Metadata.AssemblyReference
ICSharpCode.Decompiler.Metadata.AssemblyReferenceClassifier
... (41 more; raise limit to see all)
Not shown: 12 internal or private

The last line is the one find_types, find_methods and list_members end with when their accessibility filter left matches out, so that a list without a private member does not read as "the type has none"; with accessibility public it counts them as non-public. Under an empty answer the line also counts the compiler-generated matches and names the settings that list everything counted, as for the namespace CallShapes.Concealed of the test assembly CallShapes.dll:

No types
Not shown: 1 internal or private, 1 compiler-generated; include_compiler_generated with accessibility all lists them

analyze, which has no accessibility filter, puts such a line under the header of an answer that is empty only because compiler-generated results were left out, before any line of its own, and adds none under a list: No members exposing CallShapes.Callers, then Not shown: 2 compiler-generated; include_compiler_generated lists them. A list whose entries belong to members, the calls analyze lists for call_sites and the lines search_code matches, names each member once, on a line of its own, with its entries beneath it, indented by two spaces and in the order they stand in the code. The header and limit count the entries, not the lines, so a limit can cut inside a member's entries.

Names. ILens prints a type by its fully qualified name, with . between a nested type and the type enclosing it, a generic type definition in C#'s unbound form, System.Func<,> or System.Collections.Generic.Dictionary<,>.Enumerator, and a constructed type with its type arguments, System.Collections.Generic.List<int>. It prints a member in reference form, Type.Method(int, string), with a constructor as .ctor, a generic method with its type parameters after its name, CallShapes.Namesakes.Store.Get<T>(string), a method made for type arguments with those arguments, CallShapes.Namesakes.Store.Get<int>(string) in a uses answer, and a conversion operator with its return type after it, CallShapes.Namesakes.Amount.op_Explicit(Amount) → long, since two of them can differ in nothing else. Parameter types and type arguments are written as C# writes them, by keyword or short name: a parameter passed by reference with its keyword, ref, out, in or ref readonly; a nullable value type as int? or T?, except before a member's name, System.Nullable<int>.GetValueOrDefault(); a two-dimensional array as int[,]; and an array of arrays with the outermost array's brackets first, int[][,] for an array of two-dimensional arrays. A line that would read the same as another line of its answer in this short form is written in full, every type with its namespace and the classes enclosing it, as find_methods writes the two explicit conversions of CallShapes.Namesakes.Voucher, whose return types share a short name:

static CallShapes.Namesakes.Voucher.op_Explicit(CallShapes.Namesakes.Voucher) → CallShapes.Namesakes.Alpha.Badge
static CallShapes.Namesakes.Voucher.op_Explicit(CallShapes.Namesakes.Voucher) → CallShapes.Namesakes.Beta.Badge

Only list_members and compare's member lists print declarations, which show what the reference form leaves out: this before an extension method's first parameter, params, a parameter's default, and the @ C# needs before a name that is a keyword, object @object.

Every tool takes these forms back, so a name from one answer can be passed to the next tool. type_name also accepts the reflection form (Outer+Inner, System.Func`2) and an argument list (System.Func<int, bool>, whose argument count is the arity). A name is looked up among the types the assembly defines, references and forwards to another assembly together. A name written without arity means the one type of that name without type parameters where there is one, and otherwise the only type of that name, so a name the assembly gives only to generic types of its own means the type without type parameters it references or forwards. A name several generic types share, such as System.Func, is an error that lists them. compare first reads a name as written in both builds, so a class only one build has is answered as that build's, and a name without brackets that finds a different class in each build is an error naming both: Type name 'BaseShift.Arity.Crate' matches BaseShift.Arity.Crate<> in A and BaseShift.Arity.Crate<,> in B. Name one as listed. An array type written as C# writes it, byte[] or Shop.Part[][,], is taken by analyze with kind extension_methods and by decompile, analyze and compare with member_name, to find the extension methods code can call on such an array; every other question refuses it.

Member lookup. decompile, analyze and compare find a member of type_name by member_name. A name without a hint means the member of the most derived class that declares it, whatever kind of member that is, so a field that hides a base class's method of its name is the field; the method stays reachable through a hint or by asking the base class. A constructor is .ctor or the type's own simple name, Vault for CallShapes.Vault, and is looked for on that type alone. A static constructor is .cctor only, and a struct that declares no constructor has none, so where one is created is a question for analyze's instantiated_by. An accessor is named by its IL name, get_Count, set_Count, add_Changed or remove_Changed. A member C# implements explicitly, which it names after the interface, System.IDisposable.Dispose, is reached by its plain name only where no other member has that name. A name C# writes with @ before a keyword, @base, means the keyword.

Brackets after a method's name give its type parameters: Get<T>, Get<int> and Get<> each mean the Get with one type parameter, and Take<U> beside Take<T> selects by the names. A name without brackets means the methods without type parameters wherever the name has both, so Get on CallShapes.Namesakes.Store is Get(string), not Get<T>(string). Overloads are told apart by a hint of up to three parameters, each of which needs member_name: parameter_count; parameter_types, the parameter types in order as type patterns; and return_type, which selects among conversion operators that differ in their return type alone. The hint is a filter, not a tie-breaker: a lone method that does not satisfy it is not returned, and that no overload matches is an answer of its own. It is matched against the type, then each base class, then the extension methods, then the explicit interface implementations, and the most derived level that satisfies it wins, so an overload a base class declares is found. It is held as an exact fit first, through that whole search, and only where nothing fits exactly is it held loosely, as find_methods holds a pattern, so an exact fit on a base class comes before a loose one on the class asked about. So Truncate with parameter_types decimal means Truncate(decimal) beside Truncate(ref decimal). A hint that no method without type parameters satisfies is held against the generic ones as well, so Put with parameter_types T on CallShapes.Namesakes.Store means Put<T>(T) beside Put(string). Where the name or the hint leaves several methods, or none, the error lists them and says what tells them apart, see Troubleshooting.

Where no member of the name answers, the name finds an extension method the inspected assembly declares for the type, one of its base classes or one of its interfaces: any static method marked as an extension method, as a C# static class, a Visual Basic module or an F# class declares one, with its receiver passed by value or by ref, in or ref readonly. One whose receiver is a type parameter is found through every type that meets its constraints, by C#'s rule as the decompiler checks it, which differs from C# in four cases: it takes unmanaged for struct, lets a ref struct meet a method without constraints, fails a constraint that names a second type parameter, and lets a generic type asked without type arguments meet a constraint only where every construction of it does. A property, field or event of the name comes before any number of extension methods, except for call_sites and compare, which look for a method. The answer names the method under the class that declares it and marks it with the type it extends, here asked through another library's class of the test assembly ExternalUser.dll:

// ExternalUser.Extended.KettleExtensions.Boil(Kettle) → void [extension method on ExternalLib.Extended.Kettle]

A method whose receiver is a type parameter is marked with the type parameter and its constraints, [extension method on T where T : ExternalLib.Extended.IVessel], and one for an array type with the array, [extension method on System.Byte[]]. Through an array type, a public instance member of System.Array or System.Object of the name comes first and is answered as through that class, while a static one such as System.Array.Reverse does not; then come the extension methods C# would bind on such an array, except those for Span<T> and ReadOnlySpan<T>, to which C# 14 converts an array, so a method for a span is not found through an array. Through a type another assembly defines, see Types from other assemblies.

Type patterns. parameter_types and return_type take patterns rather than exact names. A pattern is a type's name, its full name, the end of its full name after any ., or a C# keyword (int, string). It matches the declared type only, not a base or derived one. List matches any List<T>, List<> any one-argument List, and List<int> only that one. Brackets at the end are read as C# writes an array, the outermost array's first and a comma for each dimension past the first: int[,] matches a two-dimensional array and int[][,] an array of them. A trailing * matches a pointer, and a tuple matches as the ValueTuple<...> it prints as. A trailing ? is read as C# reads it: int? matches the nullable type alone, string? what string matches, and T? both the nullable form of a struct's type parameter and a type parameter. A ref, out or in prefix matches only a parameter passed by reference, whichever keyword it has. A type copied from a line that names a member as declared, as find_methods and the headers write it, matches what it came from, and so does a nullable type copied from the decompiled code, such as string? or T?. A uses line names a member as the code uses it, so its parameter list passes back only where it reads as the declaration's: Crate<T>.Fill(T, List<T>) does not find Crate<>.Fill(TItem, List<TItem>).

find_methods holds a pattern loosely: int also matches int?, [] an array of any number of dimensions, and a pattern without a prefix a parameter passed by value or by reference. An overload hint in decompile, analyze or compare is held exactly first, see Member lookup: a pattern without a prefix fits only a parameter passed by value, int does not fit int?, a name without type arguments fits only a type without them, [] fits only an array of one dimension, and T? fits a type parameter only where the code marks it T?.

Generated code. The tools that list types or members, find_types, find_methods, list_members, search_code, analyze and compare, take include_compiler_generated. Left false, they hide what the compiler or a source generator created: closures, lambdas, iterator and async state machines, anonymous types, backing fields and the other fields a compiler adds beside the member they belong to, the field an enum keeps its value in, <PrivateImplementationDetails>, and Unity and Burst source-generator output. A generated type named by type_name is listed or searched whatever the switch says, in list_members, find_methods and search_code, since it was asked for by name; search_code searches it where the decompiler writes it as a type of its own. In search_code, a nested type the decompiler writes inside the type around it, such as a lambda's class or an iterator's or async method's state machine, has no lines of its own in the decompiled code, so naming it in type_name is an error that names the nearest type around it with lines of its own, whose search finds its contents. analyze lists generated results when the symbol it is asked about is generated itself: the member where member_name is given, and the type otherwise. Code the compiler moves out of the method it is written in, a lambda, a local function, an iterator or an async method, is reported under that method by analyze and search_code and compared as part of it by compare.

Accessibility. The tools that list API, find_types, find_methods and list_members, take accessibility: public, public_protected (the default, which also covers protected internal and private protected), or all, which adds internal and private. What it leaves out is counted in a Not shown: line, see Lists. The other tools are never filtered by accessibility, because an analysis, a comparison or a search of the code must see everything.

Types from other assemblies. A type the inspected assembly uses but another assembly defines is known to ILens by the assembly's references to it, and a type the assembly forwards to another assembly by that forward. A tool that needs the type's definition says where it lies, so you know which assembly to inspect instead: System.String is not defined in this assembly; it comes from netstandard, Version=2.0.0.0. for the test assembly CallShapes.dll, and System.String is not defined in this assembly, which forwards it to System.Private.CoreLib, Version=10.0.0.0. for the .NET runtime's System.Runtime.dll. analyze answers questions about such a type from the inspected assembly's own references to it, list_members with include_inherited reads the members of base classes another assembly defines, and decompile, analyze and compare find by name an extension method the inspected assembly declares for such a type, as through a type it defines.

Through another library's type, a member of the type with the name comes first where C# would bind it for a call from the inspected assembly, and keeps its answer, which for decompile is the error naming where the type comes from. That is a public instance member, an internal or protected internal one where the library grants the inspected assembly its internals, and for an interface a member of a base interface. A protected, private protected, private or static member, and an internal one the library does not grant, do not keep the extension method out. With an overload hint, every method the inspected assembly's code can name comes first where the hint fits it, static and protected ones included. Where the type's library is not found, the members the inspected assembly's references name on the type come first, an extension method is found only for the type itself or through a type parameter without constraints, and the answer says what was not read:

// Not checked: whether ExternalLib.Extended.Kettle or its base types have a member Boil, which C# would take first; ILens could not load ExternalLib, Version=1.0.0.0

An answer that leaves something out for want of another assembly's class says so in a clause of an error, a Not shown: line under list_members, or a Not checked: line under decompile, analyze and compare, and gives one of three reasons. ILens takes the first file of the library's name it finds, beside the inspected assembly, then at the top of each allow-root in the order given, then among the installed .NET libraries, so the remedy is always to make the right file the first one found:

Reference assemblies. An assembly marked as a reference assembly holds a library's declarations without their code: a .NET reference pack, a .NET Framework reference assembly, a compiler's reference assembly, or a copy made for building against, such as a publicized assembly in a build folder. Its method bodies are placeholders, so a question that reads code is refused there rather than answered from them, where "nothing uses it" would read as a fact about the library and a placeholder as the member's code: analyze with used_by, instantiated_by, read_by, assigned_by, call_sites or uses, decompile, search_code, and find_harmony_dependencies. The error says where to ask instead, here for System.Collections.dll of the .NET 10 reference pack:

Error: ArgumentException: This assembly is a reference assembly, which holds a library's declarations without their code, so 'used_by' cannot be answered from it; ask the build the library runs as. exposed_by, extension_methods, applied_to, overridden_by, implemented_by and implements read declarations alone and answer here.

decompile answers, for a whole type and for any member alike: This assembly is a reference assembly, which holds a library's declarations without their code, so there is no code to decompile; ask the build the library runs as. find_methods and list_members read declarations alone and answer here. search_code answers the same, with no code to search. The build the library runs as is the one the runtime loads, such as the library of the same name in the installed .NET's shared folder for a .NET reference pack. The questions that read declarations alone answer as for any assembly, and compare compares no body of a reference assembly and says so beneath its lists.

Strict arguments. An argument name a tool does not declare is rejected with the list of valid names. A parameter that does not apply to the question asked is rejected too, such as parameter_name with a kind other than call_sites, or an overload hint without member_name in decompile, analyze or compare. A value a parameter does not take is an error that names the parameter and what it takes, with every wrong value of a call in one answer, as here for a find_types call:

Error: ArgumentException: Invalid value 7 for argument 'accessibility'. Valid values: public, public_protected, all. Invalid value 1.5 for argument 'limit'. Expected an integer.

A required parameter left out or null is Missing required argument 'kind'. An exact type_name or namespace that names nothing is an error too, not an empty answer; analyze, which answers about types the assembly only uses, ends its empty answer about such a name with a line saying that the assembly neither defines nor references a type of that name. A silently dropped filter, or a value read as another one, would return a plausible answer to a question nobody asked.

Discovery

find_methods read-only

Find methods across the assembly by name, parameter types or return type, or list one type's methods with type_name. Any combination of filters narrows the search.

Parameters

assembly required string
Path to the .NET assembly to inspect (must be under an allowed root).
name_contains optional string
Case-insensitive substring of the method's name, without namespace or declaring type. A constructor's name here is .ctor.
return_type optional string
Return-type pattern, matched by the rules of parameter_types.
parameter_types optional string[]
Parameter-type patterns, in order; the method takes exactly these. Each is matched loosely: int also matches int?, and a trailing [] an array of any number of dimensions, while int[,] matches a two-dimensional one. See type patterns.
parameter_count optional int
Number of parameters the method takes. If parameter_types is also given, the two must agree.
namespace optional string
Exact namespace of the declaring type, e.g. System.IO; its sub-namespaces are not included.
type_name optional string
Fully qualified declaring type, e.g. System.IO.File. An exact name asserts that the type exists, so a name that finds no type is an error rather than an empty list. A compiler-generated type named here has its methods listed whatever include_compiler_generated says.
type_name_contains optional string
Case-insensitive substring of the declaring type's name, without namespace; cannot be combined with type_name. Matching nothing is an ordinary empty result.
accessibility optional public | public_protected | all
Default public_protected, which also covers protected internal and private protected; all adds internal and private. It filters the methods by their own accessibility. See accessibility.
include_compiler_generated optional bool
Include what the compiler or a source generator created, such as names starting with <. Default false. See generated code.
limit optional int
Maximum number of methods to list. Default 100.

Returns

A count line, then one method per line, sorted: static when it is, then the method in reference form and its return type. Asked of ICSharpCode.Decompiler.dll with name_contains DecompileTypeAsString:

1 method:
ICSharpCode.Decompiler.CSharp.CSharpDecompiler.DecompileTypeAsString(FullTypeName) → string

That is the form decompile and analyze take, and a parameter list copied from it works as parameter_types. Constructors appear as .ctor, the static one as .cctor, and operators as op_*; a struct or an enum that declares no constructor has no .ctor line. Property and event accessors are left out; decompile and analyze reach one by its IL name, such as get_Count. A generic method carries its type parameters after its name, so it prints apart from a namesake without them, as CallShapes.Namesakes.Store.Get(string) → object and CallShapes.Namesakes.Store.Get<T>(string) → T do. A parameter passed by reference shows its keyword, ref, out, in or ref readonly, but no parameter shows this or params, which tell no two methods apart. Lines that would read alike, such as overloads that take classes of one name from two namespaces, are written in full; see names.

When the accessibility filter left methods out, the list ends with a line that counts them. Asked of CallShapes.dll, one of the test assemblies in ILens's repository, for the methods of a class with an internal method, a private one and a static constructor:

5 methods:
CallShapes.Vault..ctor() → void
CallShapes.Vault..ctor(int) → void
CallShapes.Vault.Meter(int) → Func<int>
CallShapes.Vault.Read() → int
CallShapes.Vault.Rekey() → void
Not shown: 3 internal or private

No match: No methods, followed, where the default filters left matches out, by the line that counts them and names the settings that list them, as in Not shown: 2 internal or private; accessibility all lists them. See lists.

Errors

See also

find_types to find the type first; decompile with member_name to read a match; analyze with call_sites for its callers.

find_types read-only

Find the types the assembly defines, by namespace or by a substring of their name; with no filter, every type.

Parameters

assembly required string
Path to the .NET assembly to inspect (must be under an allowed root).
namespace optional string
Exact namespace, e.g. System.IO; its sub-namespaces are not included.
name_contains optional string
Case-insensitive substring of the type's own name, without namespace or enclosing type. Stream finds MemoryStream and FileStream.
accessibility optional public | public_protected | all
Default public_protected, which also covers protected internal and private protected; all adds internal and private. See accessibility.
include_compiler_generated optional bool
Include what the compiler or a source generator created, such as closure classes and state machines. Default false. See generated code.
limit optional int
Maximum number of types to list. Default 100.

Returns

A count line, then one fully qualified type name per line, sorted, in the form type_name takes. Asked of ICSharpCode.Decompiler.dll with namespace ICSharpCode.Decompiler.TypeSystem and limit 2:

79 types:
ICSharpCode.Decompiler.TypeSystem.Accessibility
ICSharpCode.Decompiler.TypeSystem.AccessibilityExtensions
... (77 more; raise limit to see all)
Not shown: 8 internal or private

The last line counts the types the accessibility filter left out; with accessibility public it reads Not shown: 8 non-public. An empty answer counts the compiler-generated types as well and names the settings that list everything it counts. Asked of CallShapes.dll, one of the test assemblies in ILens's repository, for the namespace CallShapes.Concealed, whose one type is internal and holds a class the compiler generated for a lambda:

No types
Not shown: 1 internal or private, 1 compiler-generated; include_compiler_generated with accessibility all lists them

See lists for the header, the truncation line and the Not shown: line every listing tool shares.

Errors

See also

list_members to read one result's members; find_methods to search by signature instead of by type name; search_code to search the code itself.

list_allowed_roots read-only

List the directories from which assemblies can be loaded. Every assembly argument passed to another tool must point to a file inside one of them.

Parameters

None.

Returns

One configured root per line, each as the --allow-root argument gave it, made absolute; or, when the server was started without one, No allowed roots configured. The server cannot load any assemblies. A root given through a symbolic link or junction is listed as given, and an assembly under it may be asked for through the link or by its real path; see the security model. The server instructions name the same directories, the last ones only as a count when the full list would make them too long.

Errors

None of its own; an argument it does not declare is refused as for every tool, see Troubleshooting.

See also

Any tool taking an assembly parameter.

Inspection

analyze read-only

Run cross-reference analysis on a type or one of its members: who uses, reads, writes, instantiates, overrides or implements it, what it uses, and every call to a method with the arguments each call passes.

Parameters

assembly required string
Path to the .NET assembly to inspect (must be under an allowed root).
type_name required string
Fully qualified type name, e.g. System.IO.File. A type the assembly only references or forwards is answered from the assembly's references to it, and an array type such as byte[] is taken by kind extension_methods and with member_name; see Types from other assemblies and Extension methods below.
kind required enum
Which analysis to run. Valid values depend on what the symbol is:
  • Type — used_by, instantiated_by, exposed_by, extension_methods, applied_to, implemented_by
  • Method, an accessor included — used_by, call_sites, overridden_by, implemented_by, uses, implements
  • Property — used_by, read_by, assigned_by, uses, overridden_by, implemented_by, implements
  • Field — read_by, assigned_by
  • Event — used_by, overridden_by, implemented_by, implements
member_name optional string
Member name, e.g. ReadAllText; omit it to analyze the type itself. .ctor or the type's own name, such as Vault, names a constructor and .cctor the static one; get_X, set_X, add_X or remove_X names an accessor. Type parameters in brackets select a generic method, Get<T>, and a name written with the @ C# needs before a keyword, @base, is read as C# reads it. See member lookup.
parameter_count optional int
Number of parameters the method takes, to select one overload. If parameter_types is also given, the two must agree. Needs member_name.
parameter_types optional string[]
Parameter-type patterns that select one overload, held as an exact fit first, so int means int before int?; see member lookup and type patterns. Needs member_name.
return_type optional string
Return-type pattern that selects one overload, as among conversion operators that differ in their return type alone: op_Explicit with return_type int. Needs member_name.
parameter_name optional string
With kind call_sites, show only the argument passed for this parameter; overloads without it are left out. Written with the @ C# needs before a keyword, @switch, it is read as C# reads it.
non_default_only optional bool
With parameter_name, keep only the calls whose argument is not provably the parameter's default. Default false.
include_compiler_generated optional bool
Include results the compiler or a source generator created, such as names starting with <. Default false. When the symbol asked about is itself compiler-generated, the answer lists such results without it. Does not apply to kind call_sites, which reports a call in generated code under the member it was written in. See generated code.
limit optional int
Maximum number of results to list; for call_sites, of calls. Default 100.

Returns

A header that counts the results in the kind's own words, then one result per line, sorted. The examples below are asked of ICSharpCode.Decompiler.dll and of the test assemblies in ILens's repository. Asked of ICSharpCode.Decompiler.dll who uses one overload of DecompileType:

1 user of ICSharpCode.Decompiler.CSharp.CSharpDecompiler.DecompileType(FullTypeName) [declared]:
ICSharpCode.Decompiler.CSharp.CSharpDecompiler.DecompileTypeAsString(FullTypeName)

The other kinds count readers of, writers of, symbols instantiating, members exposing, extension methods for, symbols marked with, overrides of, implementations of, symbols used by, interface members implemented by, and calls to. No result: the header's own form, No users of .... A result is a type, a member in reference form, or an attribute's place on the assembly or the module (below). A member's header says where it was found: [declared], [inherited from Base], or [extension method on X]. An extension method found through the type it extends is named under the class that declares it, and X is the type it extends. Where another member of its name would read alike, the header writes the member in full: 1 user of CallShapes.Namesakes.Basket.Take(CallShapes.Namesakes.Alpha.Badge) [declared]:. A use inside a lambda, local function, iterator or async method is reported under the member it was written in, and a use inside an accessor under its property or event.

An answer that is empty only because compiler-generated results were left out says so under its header. Asked of CallShapes.dll what an async method uses whose code only awaits a task:

No symbols used by CallShapes.AsyncUses.Clerk.AwaitOnly(Task) [declared]
Not shown: 17 compiler-generated; include_compiler_generated lists them

Lines after the results name the route by which code can reach the member without naming it, Override: and Implementation:, or what the answer could not check, Not checked:; each is described below with the question it ends. A Not checked: line that names a class ILens did not read ends with what the reference search made of the class's library, which other assemblies explains with what to do for each case. Where it reads ILens could not load Lib, Version=1.0.0.0 and nothing more, giving the folder that holds that library as another --allow-root lets ILens read it.

Who uses a type. used_by on a type lists the types and members that name it: in their code, their signatures, their base types and interfaces, the constraints on their type parameters or their attributes, using a member the type declares included. An enum is a user of its underlying type. The type's own members and nested types are not listed, nor the classes the compiler generated inside it; a member of a nested class someone wrote is. An attribute names a type when its class is that type or names it as a type argument, and when an argument names it: a typeof on its own, in an array or of a type built from it, as typeof(List<Part>) names Part, or a value of it for an enum. So used_by on an attribute class lists every place the attribute is applied, and on an enum every place that carries an attribute given a value of it. An enum value in an attribute is read at any width, whether or not the enum's assembly can be loaded. Where an attribute's bytes fit two widths and the two readings disagree on whether it names the type, its carrier is not listed, and the answer ends with a line such as Not checked: whether ExternalUser.Widths.GaugeAttribute on ExternalUser.Widths.ReadTwoWays names it; its bytes fit more than one size of ExternalLib.Grain. An attribute on the assembly or on the module is a line of its own, the word C# writes the target with and the name the assembly or the module has. Asked of ExternalUser.dll, which applies ExternalLib.ScopeAttribute to both:

2 users of ExternalLib.ScopeAttribute:
assembly ExternalUser
module ExternalUser.dll

instantiated_by lists the members that create an instance of the type, with new or, for a struct, default, and exposed_by the members, private ones aside, whose type, return type or parameters name the type. applied_to lists what carries an attribute, the assembly and the module included. It requires an attribute class, also one whose base attribute class another assembly defines. Where a base class cannot be loaded, the question is answered, and an answer without any result ends with Not checked: whether ExternalUser.DerivedWidget is an attribute; its base class ExternalLib.Widget was not read, since ILens could not load ExternalLib, Version=1.0.0.0.

Who uses a method. used_by on a method lists the members whose code names the method itself: a call, a new, or a reference to it as a delegate. A call of a base class's member that the method hides names that member and is listed under it. C# compiles a call to a virtual method as a call to the declaration that introduced it, so used_by and call_sites on that declaration list every such call, also a call on an object of a subclass whose compiled form names the subclass's override. A C# compiler names the override for a method with a type parameter of its own and for an override that returns a more specific type, the Visual Basic and F# compilers for any virtual method. So used_by on CallShapes.BaseCalls.Gate.Pass<T>(T) lists CallShapes.BaseCalls.Porter.Hand(Vault), whose call vault.Pass(5) the compiler wrote as a call to the override in Vault. This holds for a method the inspected assembly defines; for a method of another assembly's class, the answer lists what names that method itself, so a call that names an override of it stands under that override. A call through base. inside a lambda, an iterator or an async method counts under the member it is written in, although the compiler moves it into a method of its own. On a constructor, what carries an attribute applied through exactly that constructor counts beside the code that calls it, an attribute on a lambda or a local function under the method it is written in. This tells the constructors of an attribute class apart, where applied_to on the class lists every place the attribute is applied.

Fields. read_by and assigned_by on a field follow every use of it, including the uses that take its address: calling a method on a struct field, passing the field by ref, or writing one of its members. When the code that receives the address is in the inspected assembly, ILens reads it to decide, and a method of a framework type that never changes its value, such as int or DateTime, only reads it. A field passed by in or ref readonly is read, whatever the method. Otherwise ILens cannot tell whether the field is read or written, so the method is listed under both questions, and its line names what received the address. Asked of FieldAccess.dll:

5 writers of FieldAccess.Holder.Number [declared]:
FieldAccess.Holder.BumpNumber()
FieldAccess.Holder.FillNumber()
FieldAccess.Holder.OverwriteNumber()
FieldAccess.Holder.ParseNumber(string) (may write: passes it by ref to System.Int32.TryParse(string, ref int))
FieldAccess.Holder.SetThroughRefLocal(bool)

The read_by answer marks the same line (may read: ...). The mark names the method as the compiled call writes it, with ref for every parameter passed by reference, so TryParse's out parameter reads ref int there. A constant, a decimal constant included, cannot be asked about: the compiler copies its value into each place that uses it, so no instruction names it, and the question is an error that says so. For a value of an enum, the error points to used_by on the enum.

Properties and events. On a property or event, the usage questions are answered through the accessors, because that is what code references: read_by consults the getter, assigned_by the setter, used_by both, and uses both for the outgoing direction. On an event, used_by covers the add, remove and invoke accessors and, for a field-like event of C# or Visual Basic, the field holding its subscriber list, so the answer includes the code that raises the event and not only the code that subscribes. The header names what was consulted: (via get_Level, set_Level), or for an event (via add_Announced, remove_Announced, the subscriber list). uses is not offered on an event; uses on a method names the event whose subscriber list its code reads or writes.

Attributes that set a member. An attribute that sets a field or property of its class by name, as [Label(Text = "x")] sets Text, writes the member without any instruction. assigned_by on such a field or property, and used_by on the property or on its setter, list what carries such an attribute, unmarked, beside the code that sets the member; call_sites on the setter counts those attributes on a line of its own (see Call sites). An attribute of a class that inherits the member counts for the class that declares it, one of a class that declares the name anew does not, and one of a class that overrides the property counts for the declaration it overrides. A member of another assembly's attribute class is known from the attributes alone. Where ILens cannot read the library that says whether an attribute class inherits the name, the answer ends with a line such as Not checked: whether ExternalLib.OwnTraitAttribute, ExternalLib.SubTraitAttribute inherit Kind from ExternalLib.TraitAttribute; ILens could not load ExternalLib, Version=1.0.0.0. An attribute the compiler stores as flags or table rows rather than as an attribute, such as StructLayout, DllImport and MarshalAs, is not read.

What a member uses. uses lists the members a method's code uses and the types it names outright. The code of its lambdas, local functions, iterator and async body counts as its own, a call through base. there included. A type gets a line where the code names it in a cast, a type test (is, as), an array creation, a default or new of a struct, a sizeof or a typeof. A type built from others gives a line for each, so (Rack<Bolt>)value gives CallShapes.TypeUses.Bolt and CallShapes.TypeUses.Rack<>. It leaves out a type that only a local has or that only the signature of a member used names; used_by on the type is the full test. The compiler's own use of the same instructions gives a line too: the element type of the array a params call builds, and System.Nullable<> where a nullable is set to null. A member is written as the code uses it, with its type arguments, and a type parameter of the method or its class by its name, so each instantiation of a generic method has a line of its own. Asked of CallShapes.dll about a method that calls a generic method and its namesakes:

7 symbols used by CallShapes.Namesakes.Customer.Use(Store) [declared]:
CallShapes.Namesakes.Store.Get(string)
CallShapes.Namesakes.Store.Get<int>(string)
CallShapes.Namesakes.Store.Only<int>(int)
CallShapes.Namesakes.Store.Run()
CallShapes.Namesakes.Store.Run<List<int>>()
CallShapes.Namesakes.Store.Take<int>(int)
CallShapes.Namesakes.Store.Take<string, int>(string)

In a generic class, a line names the class's type parameter, CallShapes.GenericUses.Crate<T>.Fill(T, List<T>). On an async method, uses lists what its author wrote: the task builder's calls, the awaiter's calls and type, and the GetAwaiter call an await compiles to are left out, as in the AwaitOnly answer above, and include_compiler_generated lists them. A GetAwaiter().GetResult() the author writes in the same method, on an awaiter type one of its awaits uses, is left out with them; the rethrow an await foreach brings stays. A called method's parameter passed by reference is written with the keyword its declaration has, out, in or ref readonly; ILens looks for the declaration where decompile looks for an assembly's references. Where it reads no declaration, it writes ref and the answer ends with a line that says why. Asked of ExternalUser.dll, whose library ExternalLib.dll lies in a folder that is no allow-root:

3 symbols used by ExternalUser.Keywords.Clerk.Count(Vault) [declared]:
ExternalLib.Vault.Peek(ref int)
ExternalLib.Vault.TryTake(int, ref string)
ExternalLib.Vault.Weigh(ref int, ref int)
Not checked: whether ref on a method of ExternalLib.Vault is out, in or ref readonly; ILens could not load ExternalLib, Version=1.0.0.0

With that folder as another --allow-root, the lines read ExternalLib.Vault.Peek(ref readonly int), ExternalLib.Vault.TryTake(int, out string) and ExternalLib.Vault.Weigh(in int, ref int), and the last line goes. Where the library found does not declare the method as called, the line ends ExternalLib.Vault as loaded declares no such method; for a framework class, that happens where the .NET installed on the machine is another version than the one the assembly runs on.

Call sites. call_sites lists every call to a method, each calling member once on a line of its own with its calls beneath it, indented by two spaces and in the order the code makes them. It covers all overloads of the name unless a hint selects one. limit counts calls, not lines, and can cut inside a member's calls. Asked of CallShapes.dll, where Thrice calls three times and WithLambda once in its own code and once in a lambda:

6 calls to CallShapes.Repeats.Bell.Ring(int):
CallShapes.Repeats.Ringers.Once(Bell)
  bell.Ring()
CallShapes.Repeats.Ringers.Thrice(Bell)
  bell.Ring(3)
  bell.Ring()
  bell.Ring(2)
CallShapes.Repeats.Ringers.WithLambda(Bell)
  bell.Ring(5)
  bell.Ring(4)

A call is written as decompiled C# with its arguments in parameter order, so it need not read as the source spelled it: a named or reordered argument stands at its parameter's position, with the defaults the compiler filled in before it, trailing arguments equal to their default are left out, and a call to an extension method is written as a static call with the receiver as its first argument, KettleExtensions.Boil(kettle) for kettle.Boil(). A call through base. in a lambda, an iterator or an async method stands under the member it is written in: call_sites on CallShapes.BaseCalls.Gate.Open(int) shows base.Open(width) and base.Open(width + 1) beneath CallShapes.BaseCalls.Lodge.OpenTwice(int), which makes both calls in lambdas. With parameter_name, each line shows only the argument that parameter receives, as the compiler bound it, so an omitted argument shows the parameter's default; a value that provably is the default is marked (default). non_default_only keeps the other calls, and its header says how many of all calls they are, naming the default where every overload has the same: 4 of 6 calls to CallShapes.Repeats.Bell.Ring(int) pass times other than its default 1:.

A call that C# writes without call syntax is listed like any other, in the form the code has, and the header's count includes it, so an answer about an operator or a base class's constructor can hold thousands of calls. Asked of CallShapes.dll, these lines stand beneath their members:

Every call is counted twice, once in the IL and once in the decompiled C#. A caller whose counts differ is not dropped but named after the calls, in a block for each kind of difference: a sentence with the sum and the members beneath it. In a Not shown: block a member's count follows its name unless it is its one call, as in : 2 of 3. Each block is cut by limit on its own, and its sentence stays when the limit shows no member, so an empty answer is a proof of absence. Asked about the true operator, which a condition calls:

0 calls to CallShapes.Forms.Coin.op_True(Coin):
Not shown: 1 call in 1 member, which the decompiled C# does not show as a call:
  CallShapes.Forms.Sums.IfSet(Coin)

The decompiled C# does not show as a call the creation of a delegate, the true and false operators a condition calls, a property read through a pointer, an indexer with a params array, or the copy method of a record that a with expression calls. The members that fail to decompile stand in a block whose sentence ends which failed to decompile:, and those whose decompiled C# shows more calls than their IL holds in one that begins Check:, as in Check: 1 member shows more calls in the decompiled C# than its IL holds:. On a constructor or a property's setter, the answer keeps to calls and ends with a line that counts the attributes applied through the constructor or setting the property by name, as for a constructor of an attribute class of ExternalUser.dll: Not shown: 9 uses as an attribute; used_by lists them.

Overrides. overridden_by lists the members that override the one asked about, directly or through further overrides, an override that returns a more specific type included; an override of a member declared anew with new virtual stands under that member, not under the member it hides. On an accessor, it lists the accessors that override it. call_sites on an override lists the calls whose receiver's type is the override's class or derives from it, including calls through base., and used_by on an override lists what names the override itself. Both end with a line naming the declaration code can call instead. Asked of CallShapes.dll:

3 calls to CallShapes.Animal.Die(int):
CallShapes.Dog.Die(int)
  base.Die(cause + 1)
CallShapes.Undertaker.OnAnimal(Animal)
  animal.Die(2)
CallShapes.Undertaker.OnDog(Dog)
  dog.Die(3)
Override: code can call CallShapes.Creature.Die(int) instead; listed are those calls whose receiver's type is CallShapes.Animal or derives from it.

Under used_by, the line ends so only references to this override are listed; 'call_sites' lists those calls whose receiver's type is CallShapes.Animal or derives from it., naming the accessors for a property or an event. The line says "can call", since a call whose compiled form names the override itself stands in the list above it. When the declaration lies in another assembly, the line says so, and call_sites cannot count the calls made through it: Override: code can call the ExternalLib.Widget member it overrides instead, which another assembly defines, so those calls are not counted here.

Interfaces. implements lists the interface members a method, property, event or accessor implements, also of interfaces another assembly or the core library defines; an accessor is answered with the interface's accessor. implemented_by on an interface member lists the members classes bind it to, and an override of such a member that runs in its place; a member that only shares the interface member's name and parameters, such as one that hides the bound member, has another return type or is not public, is not listed. On an accessor it lists the accessors that implement it, and a line under the question about a property can be a method, where a compiler wrote accessor methods without a property. A member that only classes below its own bind, such as a member of a base class without the interface, is marked with those classes, since it implements the interface member for their objects alone. Asked of ExplicitMembers.dll, in which five classes list ITag and inherit Tag from TagBase:

1 implementation of ExplicitMembers.Inherited.ITag.Tag() [declared]:
ExplicitMembers.Inherited.TagBase.Tag() (for ExplicitMembers.Inherited.TagA, ExplicitMembers.Inherited.TagB, ExplicitMembers.Inherited.TagC and 2 more)

implements on TagBase.Tag() marks the line of ExplicitMembers.Inherited.ITag.Tag() the same way, and answers for a member that implements by a metadata row alone, whatever its name, such as the MoveNext of a class the compiler generated or a Visual Basic member that implements under a name of its own. implemented_by on a type requires an interface and lists the types that implement it. Where a base type could not be read, either answer ends with a line such as Not checked: what ExternalLib.IGauge and its base types declare; ILens could not load ExternalLib, Version=1.0.0.0; implements has it only for a member a base type could bind, one that is public and not static, or overrides.

A call through an interface names the interface's member, so used_by, read_by, assigned_by and call_sites on a member that implements one list only what names the member itself, and end with a line naming the interface members code can call instead. Asked of ExplicitMembers.dll about a method that implements the Reset of two interfaces:

No users of ExplicitMembers.Twins.Both.Reset() [declared]
Implementation: code can call ExplicitMembers.Twins.ILeft.Reset(), ExplicitMembers.Twins.IRight.Reset() instead; ask about those members for their uses.

With one interface member, the line ends ask about that member for its uses. It stands for every member that implements, an ordinary public one, an explicit implementation and one bound by a metadata row alike. A member that implements and overrides has both lines, Implementation: first. Where a base type of the member's class was not read, the answer ends with Not checked: whether it implements an interface member; ILens could not load ExternalLib, Version=1.0.0.0, or more interface members where some were found.

Extension methods. extension_methods lists the extension methods whose receiver is the type, passed by value or by ref, in or ref readonly: every static method marked as an extension method, in a C# static class, a Visual Basic module or an F# class, for a type of another assembly as well. It lists the type's own, not those for its base types. A method whose receiver is a type parameter is listed under each type its constraints name, and one whose constraints name no type under System.ValueType for a struct constraint and System.Object otherwise. type_name takes an array type here as C# writes it, such as byte[], int[,] or int[][], and the list holds the methods for exactly that array and for an array of a type parameter the array fits; a method for an interface, System.Array, object or a type parameter itself stays under that type. A receiver written nint[] or dynamic[] counts for System.IntPtr[] or object[]. Asked of CallShapes.dll:

3 extension methods for System.Byte[]:
CallShapes.Arrays.ArrayExtensions.Crest<T>(T[])
CallShapes.Arrays.ArrayExtensions.Hex(byte[])
CallShapes.Arrays.ArrayExtensions.InBounds<T>(T[], int)

With member_name, every kind is answered for an extension method found through the type it extends, by the rules under member lookup. The mark of a method whose receiver is a type parameter shows its constraints: [extension method on T where T : CallShapes.Constraints.IShape], [extension method on T[] where T : struct, System.IComparable<T>], and the mark of one found through an array type names the array: 1 user of CallShapes.Arrays.ArrayExtensions.Hex(byte[]) [extension method on System.Byte[]]:.

An extension method the inspected assembly declares for a type of another assembly is found through that type and answered with every kind, call_sites and uses included. Types from other assemblies says which member of that type comes first, and member lookup that call_sites looks for a method alone. Asked of ExternalUser.dll with the folder of its library ExternalLib.dll as an allow-root, Pour through ExternalLib.Extended.Kettle answers for Kettle.Pour(int), Pour with parameter_types Kettle, string for the extension method KettleExtensions.Pour(Kettle, string), and Reset for the extension method, since Kettle's own Reset is static. Where the library is not read, the members this assembly's references name on the type come first, only the extension methods for the type itself are found, and the answer says what it could not check. Asked with that folder outside the allow-roots:

1 user of ExternalUser.Extended.KettleExtensions.Boil(Kettle) [extension method on ExternalLib.Extended.Kettle]:
ExternalUser.Extended.Cook.Brew(Kettle)
Not checked: whether ExternalLib.Extended.Kettle or its base types have a member Boil, which C# would take first; ILens could not load ExternalLib, Version=1.0.0.0

The same line stands where a base class of a type of this assembly could not be read. Through another assembly's type whose library is not read, a method whose receiver is a type parameter is found only where it has no constraint. Through an array of such a type, a list ends with Not checked: whether more extension methods apply to ExternalLib.Arrays.Beam[] through what ExternalLib.Arrays.Beam derives from; ..., a method found by name with Not checked: whether other extension methods named Glue apply to ExternalLib.Arrays.Beam[] through what ExternalLib.Arrays.Beam derives from; ..., and the error for a name that finds nothing with the clause what ExternalLib.Arrays.Beam derives from was not read, since ....

Types from other assemblies. For a type the assembly does not define, every type kind and used_by, read_by and assigned_by on a member are answered from the inspected assembly's references; see other assemblies. A method's header writes each parameter passed by reference with the keyword its declaration has, 2 users of ExternalLib.Vault.TryTake(int, out string): where ExternalUser.dll is asked with its library's folder as an allow-root; where ILens read no declaration, it writes ref and the answer ends with the Not checked: whether ref on a method of ... line shown under What a member uses. call_sites, uses, implements, overridden_by and implemented_by on such a member are refused with the reason, unless the name finds an extension method this assembly declares, which every kind answers. An empty answer says why it is empty:

A constant leaves no reference, since the compiler copies its value into each use. A question about a member that is a constant in the library ILens finds is the constant error, and where no assembly ILens found defines the type, the empty answer ends with ILens cannot tell whether Fold is a constant, whose value the compiler copies into each use: no assembly it found defines ExternalLib.Widget, which comes from ExternalLib, Version=1.0.0.0. implemented_by on such a type is an error only when the assembly derives a class from it, and its answer ends with Types deriving from a class in another assembly may also implement it through that class; this assembly does not show which.

Errors

See also

decompile to read a result's body; find_methods for name-shaped rather than reference-shaped search; search_code for text in the decompiled code.

find_harmony_dependencies read-only

List what a Harmony-patching assembly depends on: every patch target, every field it reaches by reflection, in a transpiler or as a ___name patch parameter, and every method, property, event or constructor of another assembly it looks up by reflection, names in a transpiler or takes as a delegate patch parameter. Built to answer "do my patches still bind on the new version?"

Harmony, by Andreas Pardeike, is the library mods use to patch .NET methods at run time. ILens reads its attributes and calls in an assembly without including or running Harmony.

Parameters

assembly required string
Path to the Harmony-patching assembly to scan (must be under an allowed root).
limit optional int
Maximum number of lines to list. Default 100; see Lists.

Returns

A count line, then one dependency per line, sorted by target so that everything depending on one member stands together. A line holds its first word, the target, <-, the method that depends on it, or the class for a target a class names itself, and in brackets how the target was found. The answer for HarmonyMod.dll, a test assembly of the ILens repository, has 373 lines; ten of them, in the answer's order:

Postfix ? <- HarmonyMod.LoopPatches.AfterAny [PatchCall in HarmonyMod.LoopPatches.Apply] ×3
Prefix HarmonyMod.TargetA.Compute(int) <- HarmonyMod.FieldAccessPatch.Prefix [TypedAttribute]
Postfix HarmonyMod.TargetA.PrivateMethod <- HarmonyMod.StringTargetedPatch.Postfix [StringTargeted]
Unapplied HarmonyMod.TargetB.SomeMethod <- HarmonyMod.MisnamedPatch.SomeMethodPostfix [TypedAttribute]
Field HarmonyMod.TargetI.count <- HarmonyMod.InjectedFieldsPatch.Prefix [Injected]
Prefix HarmonyMod.TargetK.Run <- HarmonyMod.ManualPatches.BeforeRun [PatchCall in HarmonyMod.ManualPatches.Apply]
Event System.AppDomain.AssemblyLoad <- HarmonyMod.MemberLookups.Use [GetEvent]
Member System.Text.StringBuilder.? <- HarmonyMod.MemberLookups.Use [GetMembers]
Method System.Text.StringBuilder.Append(string) <- HarmonyMod.MemberLookups.Use [AccessToolsMethod]
Property System.Text.StringBuilder.Length <- HarmonyMod.MemberLookups.Use [AccessToolsProperty]

Each line stands once, however many places in the code give it. A ? marks a part the scan could not read, and a line with one that the scan found more than once ends with ×N, how often, since what those places name may differ; the ? of a nullable value type, as in int?, marks nothing unread. Nothing found: No dependencies.

Patches. The patch kinds are Prefix, Postfix, Transpiler, Finalizer and ReversePatch, found as Harmony finds them: by the method's exact name or by its marker attribute, such as [HarmonyPrefix]. A class counts as a patch class when it carries [HarmonyPatch], has a TargetMethod or TargetMethods, or has a method carrying a [HarmonyPatch] of its own. Patches applied through PatchProcessor, Harmony 2.4's inner prefixes and postfixes, and a delegate type's attributes give no patch line. Unapplied stands in place of the patch kind for a target named where Harmony applies no patch: the target of a method whose own [HarmonyPatch] names one but whose name and attributes make it no patch method, and, for a class without a patch method, what its TargetMethod gives or else its own attribute's target:

Unapplied HarmonyMod.TargetB.SomeMethod <- HarmonyMod.MisnamedPatch.SomeMethodPostfix [TypedAttribute]
Unapplied HarmonyMod.TargetA.Compute(string) <- HarmonyMod.TargetOnlyPatch [TargetMethod]

Harmony never applies such a patch. It still runs such a class's TargetMethod, and a target missing there stops the mod's PatchAll. An Unapplied line needs a target with a class, so it never reads ?. A method a Harmony.Patch call passes gets the call's line instead, and a class that patches nothing and names no target, such as a container of nested patch classes, gets no line. A patch target is found by one of these:

LabelWhere the target comes from
TypedAttribute, StringTargeted, Attribute The [HarmonyPatch] attributes on the patch class and the patch method, merged as Harmony merges them. How the three are told apart is explained below.
TargetMethodThe one target a TargetMethod returns.
DynamicTargetMethodA TargetMethod with more than one target, typically chosen by a ternary or a switch, or with a target the scan cannot read, so its binding depends on the branch taken at run time.
TargetMethodsEach target a TargetMethods returns or yields.
TargetMethodSearch, TargetMethodsSearchA target the body finds by searching one class's methods for an exact name. The line names no overload, since the search's own tests pick one.
PatchCall in X, PatchCallSearch in XA Harmony.Patch call: a line for each patch method it passes, of the kind its argument's position gives, and each target. X is the member that writes the values out: the calling member, or the caller or construction its values come through. The Search form marks a target found by a search, as above.

Fields. A Field line names a field the assembly looks up by reflection, names in a transpiler, or takes from Harmony as a patch parameter. Lookups are read in every class of the assembly, such as the static holders mods keep their field references in, not only in patch classes, and a field of the mod's own classes is listed too:

LabelLookup
AccessToolsFieldAccessTools.Field, which also searches the base classes
DeclaredFieldAccessTools.DeclaredField, which looks in the class alone
FieldRefAccess, StaticFieldRefAccess, StructFieldRefAccessHarmony's field-reference accessors, in all their overloads
TraverseFieldTraverse.Field, also as Field<T>
GetField.NET's Type.GetField
GetFields, GetDeclaredFields, GetFieldNames An enumeration of a class's fields, whose field is written ?
LoadField, StoreFieldA transpiler's CodeInstruction.LoadField and StoreField, read as AccessTools.Field is
LoadsField, StoresField, GetFieldInfo CodeMatch.LoadsField and StoresField and SymbolExtensions.GetFieldInfo, given a lambda written at the call that reads the field
InjectedA ___name parameter of a prefix, postfix or finalizer, applied by attribute or by a Harmony.Patch call, through which Harmony hands the patch a field. It is listed on each class the patch targets; Harmony looks the field up in the class that declares the patched method, which differs when that method is inherited. ___ followed by digits names a field by its index and prints ?.

Members looked up. A Method, Property, Event or Member line names a member of another assembly's class that the assembly looks up by reflection, names in a transpiler, or takes as a delegate: Method for a method, an accessor or a constructor, Property for a property or an indexer, Event for an event, and Member for Type.GetMembers, which finds every kind. A lookup gets a line only where it adds something: none for a member of a class the scanned assembly defines, which no other assembly's update can break, and none where a patch line shows its result as the target. A second lookup of the same member keeps its line, and so does one the scan cannot read in full. A property or event lookup names the property or event, unless the code takes an accessor from it right at the call: typeof(Version).GetProperty("Build").GetGetMethod() gives Method System.Version.get_Build <- HarmonyMod.MemberLookups.Use [GetProperty]. An enumeration names the class alone, Event System.AppDomain.? <- HarmonyMod.MemberLookups.Use [GetEvents]. The label is the name of the call, of the property read or of the class constructed, with AccessTools or Traverse before it where the bare name is a kind of member:

LabelLookupLine
AccessToolsMethod, DeclaredMethod, TraverseMethod, GetMethodAccessTools.Method and DeclaredMethod, Traverse.Method, Type.GetMethodMethod T.M, with the parameter types where the call pins an overload
MethodDelegateAccessTools.MethodDelegate given a method as "Namespace.Type:Method" textMethod T.M
PropertyGetter, PropertySetter, DeclaredPropertyGetter, DeclaredPropertySetterAccessTools' accessor lookupsMethod T.get_P or T.set_P
AccessToolsProperty, DeclaredProperty, TraverseProperty, GetPropertyAccessTools.Property and DeclaredProperty, Traverse.Property, Type.GetPropertyProperty T.P
Indexer, DeclaredIndexer, IndexerGetter, IndexerSetter, DeclaredIndexerGetter, DeclaredIndexerSetterAccessTools' indexer lookupsProperty T.Item, or its accessor, Method T.get_Item(int) or T.set_Item(int, ?)
AccessToolsEvent, DeclaredEvent, GetEvent AccessTools.Event and DeclaredEvent, Type.GetEventEvent T.E
EventAdder, EventRemover, DeclaredEventAdder, DeclaredEventRemoverAccessTools' event accessor lookups Method T.add_E or T.remove_E
AccessToolsConstructor, DeclaredConstructor, GetConstructor, TypeInitializerAccessTools.Constructor and DeclaredConstructor, Type.GetConstructor, Type.TypeInitializerMethod T..ctor(int), T..ctor() without parameter types, or T..cctor
AccessToolsFinalizer, DeclaredFinalizer AccessTools.Finalizer and DeclaredFinalizerMethod T.Finalize
GetDeclaredMethods, FirstMethod, GetMethodNames, TraverseMethods, GetMethodsAn enumeration of a class's methodsMethod T.?
GetDeclaredProperties, FirstProperty, GetPropertyNames, TraverseProperties, GetProperties An enumeration of a class's propertiesProperty T.?
GetDeclaredConstructors, FirstConstructor, GetConstructorsAn enumeration of a class's constructorsMethod T..ctor or T..cctor
GetEvents, GetMembersType.GetEvents, Type.GetMembersEvent T.?, Member T.?
GetMethodInfoThe method a SymbolExtensions.GetMethodInfo lambda callsMethod T.M(int), the overload the lambda calls
Call, Calls, CodeMatchA transpiler's CodeInstruction.Call, by class and name, as "Namespace.Type:Method" text, or in a lambda written at the call; CodeMatch.Calls and new CodeMatch(...) in such a lambdaMethod T.M
Injected, HarmonyDelegateA delegate patch parameter; AccessTools.HarmonyDelegate<T>()Method T.M
GetMethodByModuleAndTokenAccessTools.GetMethodByModuleAndToken, which names a method by a module and a metadata tokenMethod ?

A lambda that makes a delegate from a method group, () => Helper, names that method, for the transpiler's helpers and for SymbolExtensions.GetMethodInfo alike; a lambda the call is given through a parameter or a local reads ?. A method of the mod's own class named in a transpiler adds no line, as no other lookup of one does, while a field of it does. A prefix, postfix or finalizer that takes a parameter of a delegate type the mod defines and marks with [HarmonyDelegate] or [HarmonyPatch], naming a method of another assembly's class, gives a Method line labeled Injected, since Harmony hands the patch a delegate to that method and fails the patch where it is missing: Method System.Random.Next <- HarmonyMod.DelegatePatches.Before [Injected]. A parameter whose name starts with __ is one of Harmony's own and gets none, nor does a postfix's first parameter where the postfix returns its type, which takes the result. AccessTools.HarmonyDelegate<T>() gives the same line labeled HarmonyDelegate, and ? where T is a type parameter or a delegate type of another assembly. A marked delegate type that no patch method takes gives no line, since Harmony then looks nothing up.

What the scan reads. Harmony's extension spelling of a lookup, typeof(X).Method("Y"), counts as the lookup it spells. In TargetMethod and TargetMethods bodies, Harmony.Patch calls and lookups alike, the scan reads a method from AccessTools' method, accessor, constructor and indexer lookups and Harmony 2.4's event and finalizer lookups, from AccessTools.EnumeratorMoveNext and AsyncMoveNext and Harmony 3.0's StateMachineMoveNext, Lambdas and LocalFunction, from Type.GetMethod, Type.GetConstructor and Type.TypeInitializer, from a property's accessor taken from its PropertyInfo and an event's add or remove method taken from its EventInfo, from the method a SymbolExtensions.GetMethodInfo lambda calls or names as a method group, and from a search of one class's methods by an exact name, each also when the code makes a generic method of it with MakeGenericMethod. It reads a class from typeof; from text, as in "Namespace.Type:Member", AccessTools.TypeByName, Type.GetType, Assembly.GetType and Traverse.CreateWithType, as the class typeof would give where exactly one assembly ILens loads defines that full name or the text qualifies the name with the assembly that defines it, and otherwise by the name as written; from AccessTools.Inner or GetNestedType for a nested class; and from GetType() on the calling object, read as the class that declares the calling member. It follows a value back to where the assembly writes it: through locals, through a helper's parameters to each call of the helper, through the fields of a patch object to each place that builds it, through the mod's own methods to what they return, at most four members away, and through a list of names the code writes out and loops over. A Traverse takes the class the decompiler gives the object it was created from; that is a static type, so it names a base class when the field is declared in a subclass.

How a target is written. Targets are written as the other tools name them, so a target passes back to decompile, analyze and compare: a generic class without type arguments in C#'s unbound form, System.Collections.Generic.Dictionary<,>.Enumerator.MoveNext, a constructed class with each class's own type arguments, HarmonyMod.TargetO<int>.Slot<string>.Fill, and parameter types by keyword or short name, int?, int[,], List<int>[], Dictionary<int, string>.Enumerator, with a generic class named without type arguments by its name alone and a class nested in another by its own name, HarmonyMod.TargetN.Part.Join(Part). A type the code makes with MakeByRefType is written ref int, since Harmony binds it to a parameter passed by ref, out or in alike: Prefix System.Int32.TryParse(string, ref int). A generic method made for type arguments carries them, Postfix HarmonyMod.TargetL.Parse<int>, with ? for one the scan cannot read, whether MakeGenericMethod, AccessTools.Method's generics argument or a GetMethodInfo lambda makes it; the lambda's target keeps the parameter types the method declares, HarmonyMod.TargetM.Store<int>(T), and a lookup line names the type arguments of a method the code makes generic right at the lookup.

A target carries its parameter types where the patch pins an overload and Harmony reads them; without them the line names no overload. Harmony binds a static constructor, a finalizer and an accessor named by its property or event without reading parameter types, so such a target prints none even where its attribute names some: Prefix HarmonyMod.TargetH..cctor <- HarmonyMod.IgnoredTypesStaticConstructorPatch.Prefix [TypedAttribute]. A getter or setter patch names the accessor, get_X or set_X; an indexer patch the indexer's accessor, named from the class when the scanned assembly defines it and otherwise by C#'s default, get_Item or set_Item, with ? for a setter's value type it cannot read; an event patch add_X or remove_X; a constructor patch .ctor or .cctor, written .ctor() without argument types because Harmony then binds the parameterless one; a finalizer patch Finalize; and an operator patch the operator's method, such as op_Addition. Code the compiler generates for a method is named by that method with the kind after it, (Enumerator), (Async), (StateMachine), (Lambdas) or (LocalFunction Name), as in Postfix System.IO.File.ReadLines(string) (Enumerator), since compare judges a method together with its generated code. A constructor lookup names the static constructor, .cctor, where its searchForStatic is true or its binding flags hold Static and not Instance; given parameter types as well, it names .cctor(int), a constructor no class has. One that asks for both kinds, as AccessTools.all does, names the instance constructors, and a flag the scan cannot read gives both, which makes a TargetMethod a DynamicTargetMethod.

What the scan cannot read prints ? instead of being left out, so a missing line is a proof of absence. A target it cannot determine is ?. A field or member is written ?.name or Class.? when one part is unread, and ? when both are; a field name a helper takes from its callers gets a line of its own, ?.name while the class stays unread. A Harmony.Patch call the IL shows but the decompiled code does not prints ? ? <- ? [PatchCall in X], and such a field lookup a Field ? line. Two arguments of one call that each have several values are not paired, and the call reads as ?. A TargetMethod's targets are the values it returns or yields: one it cannot read adds a ? line beside the targets it reads, so an incomplete list says so, while a lookup that only serves a condition counts for nothing. A lookup given a FieldInfo or MethodInfo prints nothing of its own, since the lookup that found it is listed on its own.

TypedAttribute and StringTargeted tell a target named through typeof and nameof from one named by a string, which breaks silently when the member is renamed. ILens tells them apart by whether the named member is public on the target class, since nameof cannot reach a non-public member, and it can check that only when the target class lives in the scanned assembly. A class named by a string, through HarmonyPatch(string typeName, string methodName, MethodType), is StringTargeted wherever it lives, and a patch Harmony binds by its kind alone, on a constructor, finalizer, operator or indexer, is TypedAttribute wherever it lives, since typeof then names the whole target. Other targets in another assembly, the usual case for a mod, show as Attribute. Harmony 2.x and 3.0, the HarmonyLib namespace, are recognized; the attributes and calls of Harmony 1.x, under the Harmony namespace, are not read.

Errors

See also

compare on two builds of the patched assembly, to see whether each target and lookup still binds and what changed in it.

list_members read-only

List a type's members, grouped by kind, one declaration per line without bodies.

Parameters

assembly required string
Path to the .NET assembly to inspect (must be under an allowed root).
type_name required string
Fully qualified type name, e.g. System.IO.File. A compiler-generated type named here has its members listed whatever include_compiler_generated says.
kinds optional (method | property | field | event | nested_type)[]
Kinds of member to list, all when omitted.
accessibility optional public | public_protected | all
Default public_protected, which also covers protected internal and private protected; all adds internal and private. See accessibility.
name_contains optional string
Case-insensitive substring of the member's name. A constructor is found by its type's name, under which it is listed, and by .ctor or .cctor.
include_inherited optional bool
Include members declared on base types, stopping at System.Object, base classes another assembly defines included. Default false.
include_compiler_generated optional bool
Include what the compiler or a source generator created, such as the field that holds an event's subscribers. Default false. See generated code.
limit optional int
Maximum number of members to list, counted across all groups. Default 100.

Returns

A header with the total and the type's heading as C# declares it, then a count line per non-empty kind and its members, sorted by name with constructors first. The groups stand in the order methods, properties, fields, events, extension blocks, nested types. Asked of BaseShift.dll, one of the test assemblies in ILens's repository:

5 members of public class BaseShift.Gauge:
1 method:
  public Gauge()
3 properties:
  public int Level { get; set; }
  public int Limit { get; set; }
  public int Steady { get; set; }
1 event:
  public event EventHandler Changed
Not shown: 3 internal or private

The last line counts the members the accessibility filter left out. An empty answer counts the compiler-generated members as well and names the settings that list them: an enum without constants answers No members of public enum CallShapes.Defaults.Blank followed by Not shown: 1 compiler-generated; include_compiler_generated lists it, the field every enum keeps its value in. See lists.

The heading. The header writes the type's accessibility, modifiers, kind and name, its base types, the in or out of a variant type parameter and the conditions on the type's own type parameters, as in 1 member of public interface BaseShift.Variance.IMapper<in TIn, TKeep, out TOut> where TKeep : class: and 4 members of public class InterfaceShapes.Conditions.Holder<T> : Base where T : class, ISlot, new():.

Declarations. Each line reads as C# declares the member. A constructor is listed under the type's name among the methods, public Vault(int stored), and the static one as static Vault(); a struct or an enum that declares no constructor has no constructor line. A generic method's line ends with the conditions on its type parameters, public void Pair<TKey, TValue>(TKey key, TValue value) where TKey : class, IComparable<TKey>, new() where TValue : struct, which an override and an explicit implementation do not repeat. An extension method has this before its first parameter, public static void Drain(this ref Charge charge), and a parameter that takes any number of arguments has params, on an array or on a collection, public int Count(params ReadOnlySpan<int> values); the reference form that find_methods and the other answers print writes neither. A parameter's default and a constant's value read as C# writes them, an enum's value by its member's name: public void Go(Pace pace = Pace.Fast), public void Odd(Pace pace = (Pace)7) where no member has the value or the enum's assembly was not read, public const decimal Price = 1.25m, public const int Most = int.MaxValue. A name that C# reads as a keyword carries the @ C# declares it with, public Lever(bool @switch). An override reads override, also one that returns a more specific type than the member it overrides, public override Letter Copy(), and an abstract one abstract override. An accessor shows its own accessibility where it differs from the property's, and init where it is one. An enum lists its constants, public const Pace Slow = 1. A member inherited from a base type is marked [from Base] after its line, and an obsolete one [Obsolete] before it: [Obsolete] public int Attempts { get; }.

Every member. Two members of one type are never merged: a method beside a generic method of its name and parameters, and conversion operators that differ in their return type alone, each get a line. Lines that would read alike are written in full, every type with its namespace and enclosing classes. Asked of CallShapes.dll, another test assembly:

7 members of public class CallShapes.Namesakes.Basket:
7 methods:
  public Basket()
  public void Hold(System.Collections.Generic.List<CallShapes.Namesakes.Alpha.Badge> badges)
  public void Hold(System.Collections.Generic.List<CallShapes.Namesakes.Beta.Badge> badges)
  public void Take(CallShapes.Namesakes.Alpha.Badge badge)
  public void Take(CallShapes.Namesakes.Bag.Entry entry)
  public void Take(CallShapes.Namesakes.Beta.Badge badge)
  public void Take(CallShapes.Namesakes.Box.Entry entry)

Interfaces. An interface's method, property or event carries the word that stands for the body its line does not show: none for a member the implementing type supplies, virtual for one with a default implementation, sealed for one no implementing type can replace, and static, static abstract or static virtual for a static one; every line carries its accessibility. On a class's member, no modifier means a body that is not virtual. An interface's implementation of a base interface's member reads as a class's explicit implementation does, and one that makes the member abstract again has abstract. Asked of InterfaceShapes.dll, a test assembly:

4 members of public interface InterfaceShapes.IFeed:
2 methods:
  public virtual void Close()
  public void Open()
1 property:
  public int Count { get; }
1 event:
  public event EventHandler Moved

Extension blocks. A class that declares C# 14 extension blocks lists the members of each block under a count line that names the block, 5 members of extension(Gauge gauge):, each member as the decompiled class declares it. A property is followed by the names of the methods the compiler made of its accessors, in brackets, which the other tools take: public int Doubled { get; } [get_Doubled]. The methods the compiler made of the blocks' members are not listed among the methods. Two blocks whose lines read alike, such as two for one receiver that differ only in an attribute on it, share one group. kinds takes a block's member by its own kind, so property lists Doubled, and name_contains finds a block's property by the names of its accessors' methods as well. Asked of InterfaceShapes.dll, where IsZero stands in a second block for Gauge whose receiver carries an attribute:

6 members of public static class InterfaceShapes.Language.GaugeExtensions:
5 members of extension(Gauge gauge):
  public int Doubled { get; }  [get_Doubled]
  public static Gauge Empty()
  public bool IsZero()
  public int Offset { get; set; }  [get_Offset, set_Offset]
  public void Reset()
1 member of extension<T>(List<T> readings) where T : IComparable<T>:
  public T First()
Not shown: 1 internal or private

Fields a compiler adds. The fields a compiler adds beside the member they belong to are left out unless include_compiler_generated is set: the field that holds an event's subscribers, Visual Basic's field for an auto-property or a WithEvents variable, the fields F# adds, the lambdas an older C# compiler kept in fields, and the field every enum keeps its value in.

Inherited members. With include_inherited, a base class's member is left out only where a class below declares one that hides it as C# does: of the same kind and name, and for a method or an indexer with the same parameter types and, for a method, the same count of type parameters. An override hides what it overrides, while Find(string) leaves a base class's Find<T>(string) listed. The members of a base class another assembly defines are read from that assembly where ILens finds it: beside the inspected one, at the top of an allow-root, or in the installed .NET. An enum or a struct thereby also lists what System.Enum or System.ValueType declares. A base class ILens cannot read ends the list with a line that names it and why. Asked of ExternalUser.dll, a test assembly whose base class Widget comes from ExternalLib.dll, kept in a folder that is no allow-root:

3 members of public class ExternalUser.DerivedWidget : Widget:
2 methods:
  public DerivedWidget()
  public override void Draw()
1 property:
  public int Size { get; set; }
Not shown: what ExternalLib.Widget and its base types declare; ILens could not load ExternalLib, Version=1.0.0.0

The reason after the semicolon is one of three, which other assemblies explains with what to do for each. Here, with that folder as another --allow-root, the answer lists the 10 members, those of Widget marked [from ExternalLib.Widget].

Errors

See also

decompile when bodies are needed; find_methods with type_name for a type's methods in the form the other tools take.

Decompilation

decompile read-only

Decompile a type, or one of its members, to C#. Omit member_name for the whole type.

Parameters

assembly required string
Path to the .NET assembly to inspect (must be under an allowed root).
type_name required string
Fully qualified type name, e.g. System.IO.File; see names. With member_name, it may also be an array type as C# writes it, such as byte[] or int[,], to read an extension method code can call on such an array.
member_name optional string
Member name, e.g. ReadAllText. A constructor is .ctor or the type's own name, the static constructor only .cctor, and an accessor get_X, set_X, add_X or remove_X. A method name may end in its type parameters, Get<T>, Get<int> or Get<>, and Get alone means the method without type parameters where the name has both; see member lookup for how a name and the three hints below find a member.
parameter_count optional int
Number of parameters the method takes, to select one overload. If parameter_types is also given, the two must agree.
parameter_types optional string[]
Parameter-type patterns that select one overload, e.g. ["int", "bool"]; see type patterns.
return_type optional string
Return-type pattern that selects one overload. It tells apart conversion operators that differ in their return type alone: on CallShapes.dll, op_Explicit with return_type long picks CallShapes.Namesakes.Amount.op_Explicit(Amount) → long among three.

Returns

Without member_name, the whole type as C#: the most expensive read in the tool set. With it, a comment that names what the lookup picked, then the code. Asked of ICSharpCode.Decompiler.dll for DecompileTypeAsString on ICSharpCode.Decompiler.CSharp.CSharpDecompiler:

// ICSharpCode.Decompiler.CSharp.CSharpDecompiler.DecompileTypeAsString(FullTypeName) → string
using ICSharpCode.Decompiler.TypeSystem;

public string DecompileTypeAsString(FullTypeName fullTypeName)
{
	return SyntaxTreeToString(DecompileType(fullTypeName));
}

The comment names a method in reference form with its return type, so an overload other than the one meant shows at once, and its parameter list works as parameter_types. A generic method carries its type parameters, CallShapes.Namesakes.Store.Get<T>(string) → T, and a constructor reads ..ctor, as in ICSharpCode.Decompiler.TypeSystem.FullTypeName..ctor(string) → void. Where another member of the type would read alike in that short form, the comment writes every type with its namespace: CallShapes.Namesakes.Basket.Take(CallShapes.Namesakes.Beta.Badge) → void, beside a Take that takes the Badge of another namespace.

A method gives its body. A property or event name gives the whole declaration with its accessor bodies, and a field name the field's declaration with the initializer the compiler moved into the constructors: on BaseShift.dll, BaseShift.Fields.Gauge.Size gives public int Size = 5;. A member that implements an interface member explicitly is reached by the interface member's plain name when no ordinary member has that name, and the comment names it in full: // ExplicitMembers.Circle.ExplicitMembers.IShape.Area() → double.

An accessor, get_X, set_X, add_X or remove_X, gives its part of the declaration of the property, indexer or event that owns it, with the using lines the whole declaration needs: an auto-property's getter reads get;, an init setter init;, and an accessor of a property written with C# 14's field keyword writes field for the backing field the compiler declares. An accessor the declaration does not write, such as an accessor of a field-like event or the getter of a property written with =>, comes on its own, with its body. Asked of InterfaceShapes.dll for get_Level on InterfaceShapes.Language.Gauge, whose setter calls Math.Max:

// InterfaceShapes.Language.Gauge.get_Level() → int
using System;

get
{
	return field;
}

Where a member was found. When the type asked about does not declare the member, the comment ends with a mark. [inherited from X] names the base class that declares it. [extension method on X] marks an extension method found from the type it extends: the comment names the method under the class that declares it, and X is the type its first parameter extends. Asked of CallShapes.dll for Dim on CallShapes.Receivers.DeskLamp, which derives from Lamp:

// CallShapes.Receivers.LampExtensions.Dim(Lamp) → void [extension method on CallShapes.Receivers.Lamp]
public static void Dim(this Lamp lamp)
{
}

Any static method marked as an extension method counts, whether a C# static class, a Visual Basic module or an F# class declares it, also with its first parameter passed by ref, in or ref readonly: CallShapes.Receivers.LampExtensions.Drain(ref Charge) → void [extension method on CallShapes.Receivers.Charge]. X can be a base class or an interface of the type asked about, a type of another assembly that the inspected assembly declares the method for, or an array type. For a method whose first parameter is a type parameter, X is that type parameter with its constraints, as C# writes them: [extension method on T where T : CallShapes.Constraints.IShape], or [extension method on T[] where T : struct, System.IComparable<T>] for an array of one. Which member a name finds first, through a type of this assembly, of another assembly or an array type, is under member lookup.

What the lookup could not read. Where the lookup that found an extension method could not read a class it needed, such as one that might declare a member of the name, which C# would bind first, a second comment line starting Not checked: says so. Asked of ExternalUser.dll through a class of a library that lies in no allow-root:

// ExternalUser.Extended.KettleExtensions.Boil(Kettle) → void [extension method on ExternalLib.Extended.Kettle]
// Not checked: whether ExternalLib.Extended.Kettle or its base types have a member Boil, which C# would take first; ILens could not load ExternalLib, Version=1.0.0.0
using ExternalLib.Extended;

public static void Boil(this Kettle kettle)
{
	((Appliance)kettle).Plug();
}

Give the folder that holds the library as an allow-root, and the line goes; the line's other reasons are under types from other assemblies. A member that a base class from another assembly declares is not decompiled here: the error names the file that holds it.

A whole type. A top-level type comes with its using lines, its namespace and its nested types. A nested type comes alone, under a comment that names it, // SearchText.Outer.Inner, with the using lines it needs and without the types around it. A type the compiler generates for a lambda, an iterator or an async method, which the decompiler writes into a method of the class around it, comes alone in the same way, as the compiler generated it: with its fields and the methods that hold the code, such as <InCapturingLambda>b__0() or the iterator's MoveNext(). Asked of SearchText.dll, a test assembly, for the class of a lambda that captures a variable:

// SearchText.Shapes.<>c__DisplayClass1_0
using System.Runtime.CompilerServices;

[CompilerGenerated]
private sealed class <>c__DisplayClass1_0
{
	public string where;

	internal void <InCapturingLambda>b__0()
	{
		Marker.Hit(where + " captured");
	}
}

A top-level type the decompiler leaves out of the code it writes comes the same way, as the compiler generated it, under a comment that names it: the class that holds the compiler's helpers, <PrivateImplementationDetails>, with the data of constant arrays and the structs nested in it; an anonymous type such as <>f__AnonymousType0<,>, with its properties and its Equals; and a <Module> without members, as internal class <Module> with empty braces.

Errors

The messages shared with analyze and compare are explained under Troubleshooting.

See also

list_members first, to see what a type has without paying for bodies; search_code to find where the code does something; analyze for who uses what you are reading; compare for what changed between two builds.

search_code read-only

Search the decompiled C# of one type, one namespace or the whole assembly for lines that match a regular expression, each listed under the member it belongs to.

Parameters

assembly required string
Path to the .NET assembly to inspect (must be under an allowed root).
regex required string
.NET regular expression, applied to one line at a time and case-sensitive; start it with (?i) to ignore case. E.g. File\.Delete\(.
type_name optional string
Fully qualified type name, to search that type and the types nested in it; see names.
namespace optional string
Exact namespace, to search the types in it with the types nested in them; its sub-namespaces are not included. Without type_name and namespace, the whole assembly is searched.
include_compiler_generated optional bool
Search the types and members the compiler generated as well; see generated code. Default false. A generated type that type_name names is searched either way where the decompiler writes it as a type of its own, such as <>z__ReadOnlyArray<>.
limit optional int
Maximum number of matching lines to list. Default 100.

Returns

A header with the number of matching lines, the pattern and the scope, then each member that holds a match, sorted by name, with its matching lines beneath it, indented by two spaces, in the order they stand in the code. Asked of the test assembly SearchText.dll with catch \( in SearchText.Reader:

3 lines matching catch \( in SearchText.Reader:
SearchText.Reader.Load(string)
  catch (IOException)
  catch (Exception ex2)
SearchText.Reader.TryParse(string, out int)
  catch (FormatException)

A line reads as decompile prints the top-level type that holds it, without its indentation: the source names the second variable ex, the decompiled code ex2. A member is named in the reference form the other tools take, see names. The header ends in namespace <name> for a namespace and names no scope for the whole assembly. limit counts matching lines, not members, so it can cut inside a member's lines; see lists. No match: No lines matching catch \( in SearchText.Reader.

What is searched. The lines of the decompiled C# that belong to a type or a member: bodies, signatures, attributes, fields with their initial values, and a type's own header, its attributes, its declaration line and its opening brace, which stand under the type's name. The lines of a lambda, a local function, an iterator or an async method stand under the member they are written in, and an accessor's lines under its property or event. The using and namespace lines are not searched, since no member holds them. Asked of SearchText.dll with Serializable|Retries|IDisposable in SearchText.Settings:

3 lines matching Serializable|Retries|IDisposable in SearchText.Settings:
SearchText.Settings
  [Serializable]
  public class Settings : IDisposable
SearchText.Settings.Retries
  public int Retries = 3;

The comments the decompiler writes above a declaration stand under that declaration: its documentation, where an XML documentation file lies beside the assembly, a member's under the member and a type's under the type, and notes of its own, such as // C# has no syntax for parameterized property 'Cell'. above the method it writes for the getter of a Visual Basic property that takes a parameter and is not its class's default, since C# cannot declare one. A C# 14 extension block reads as decompile writes its class, and a line of a block's member stands under the method the compiler made of it, whose name the other tools take: an accessor's lines under its accessor's method, and a property's other lines under its getter's, or its setter's where it has none. The block's own lines, extension(Gauge gauge) and its braces, stand under the class. On the test assembly InterfaceShapes.dll, the line public int Doubled => gauge.Level * 2; stands under InterfaceShapes.Language.GaugeExtensions.get_Doubled(Gauge).

One line at a time. The pattern sees each line on its own, without its indentation, so ^ stands before the first character of the code and a match never spans two lines. A statement the decompiler prints over several lines, such as an object creation with its initializer, is found only through a part that stands on one line: on SearchText.dll, Name = "plain", Size finds nothing, while Name = "plain" finds the line Name = "plain",. A blank line never matches. A pattern runs on .NET's non-backtracking engine, which cannot run away; one with a lookaround or a backreference, which that engine does not take, runs on the backtracking engine with one second for each line.

Generated code. Without include_compiler_generated, the types and members the compiler generated are neither searched nor decompiled. The code the compiler moves out of a method, such as a lambda or an iterator, is searched either way, under the method it is written in, since the decompiled C# shows it there. The class that holds the compiler's helpers, <PrivateImplementationDetails>, a struct nested in it, an anonymous type and <Module> are searched as the compiler generated them where type_name names them, and in a search of the whole assembly only with include_compiler_generated, as any generated type is. A nested type the decompiler writes inside the type around it, such as a lambda's class, the class that caches the lambdas that capture nothing, an iterator's or async method's state machine, or a C# 14 extension block's grouping class, has no lines of its own in the decompiled code, so naming it in type_name is an error that names the nearest type around it with lines of its own, whose search finds its contents.

The first search. A search decompiles the types of its scope that no earlier search decompiled, and keeps their text in the server's memory, so later searches of the assembly, in any scope, answer from that text. On a large assembly, the first search over everything takes about a minute, later ones a fraction of a second. A call decompiles for at most 45 seconds, a limit the startup option --max-search-seconds sets, see Integrating with Claude Code. An unfinished call answers with how far it came and with no match, and repeating the call goes on from there. On ICSharpCode.Decompiler.dll, with the server started with --max-search-seconds 5, the first four calls of one search over the whole assembly answered:

Not finished: 296 of 888 types decompiled. Repeat the call to continue; what is done is kept.
Not finished: 527 of 888 types decompiled. Repeat the call to continue; what is done is kept.
Not finished: 664 of 888 types decompiled. Repeat the call to continue; what is done is kept.
Not finished: 871 of 888 types decompiled. Repeat the call to continue; what is done is kept.

The fifth call answered with the matches after 1 second, and the same search then took about a tenth of a second. The numbers count the top-level types of the scope. Every call finishes at least one type, so each repeat gets further. A second search of the same assembly while one decompiles waits for it within its own time limit.

The text is kept for as long as the server runs and the file stays as it was; a file that changed is decompiled anew. The kept text has a quarter of the memory budget that --max-total-size sets. When that share is full, the text of the assembly searched longest ago gives way, and a later search of that assembly decompiles it again. A scope whose text alone does not fit ends in an error, see Errors.

A type that failed to decompile is named in a last line, Not searched: N types that failed to decompile: ..., also under an empty answer, so an answer without that line covers the whole scope.

Errors

See also

decompile to read a member a match stands in; analyze with call_sites for what every call to one method passes, which a pattern finds only where a call stands on one line; find_methods for methods by name or signature.

Comparison

compare read-only

Compare two builds of an assembly, from the types that differ down to one member: without type_name the changed types, with type_name what changed in that type, and with member_name a diff of a method's or accessor's two bodies, a verdict when they print alike, or a constant's two values.

Parameters

assembly_a required string
Path to the first assembly (must be under an allowed root). Answers call it A.
assembly_b required string
Path to the second assembly (must be under an allowed root). Answers call it B.
type_name optional string
Fully qualified type name, to list what changed in that type. A name that fits a type of either build exactly as written means that type, as every name the list of changed types prints does; only a name that fits neither is read by each build's own lookup, see names. With member_name, it may also be an array type such as byte[], to compare an extension method code can call on such an array.
member_name optional string
A member of that type, to compare its two bodies or values. Needs type_name. Each build looks the name up as decompile does: .ctor or the type's own name for a constructor, get_X, set_X, add_X or remove_X for an accessor, and a method's type parameters after its name, Get<T>; see member lookup. Where A has the type and no member of the name, B is asked, so a member only B has is found.
parameter_count optional int
Number of parameters, to select one overload. Needs member_name; if parameter_types is also given, the two must agree.
parameter_types optional string[]
Parameter-type patterns that select one overload; see type patterns. Needs member_name.
return_type optional string
Return-type pattern that selects one overload, such as one of two conversion operators that differ in their return type alone. Needs member_name.
namespace optional string
Exact namespace to restrict the list of changed types to; its sub-namespaces are not included. Applies only without type_name.
include_compiler_generated optional bool
List the generated types and members under their own names as well, compared as they stand; applies only without member_name. Default false. Either way, a method's generated code is compared as part of the method; see generated code.
format optional csharp | il
How to print the two bodies: csharp (the default) decompiles them, il disassembles them as the comparison reads them. Applies only with member_name.
context optional int
Unchanged lines shown around each change of a diff. Default 3; a number larger than the method shows it whole with each change marked, and a negative number counts as 0. Applies only with member_name.
limit optional int
Maximum number of changes to list. Default 100. Applies only without member_name.

Returns

A line of a list starts with + for what only B has, - for what only A has, and ~ for what both have but changed. The examples compare the test assemblies BaseShift.dll as A and BaseShift2.dll as B, which differ on purpose.

Changed types. Without type_name, here with namespace BaseShift.Declarations:

11 changed types:
~ BaseShift.Declarations.Accessors [declarations,bodies]
~ BaseShift.Declarations.Cabinet [declarations]
~ BaseShift.Declarations.Conditions [declarations,bodies]
~ BaseShift.Declarations.Fields [declarations,values]
~ BaseShift.Declarations.Holder<> [metadata]
~ BaseShift.Declarations.Kinds [members,declarations]
~ BaseShift.Declarations.Methods [declarations]
~ BaseShift.Declarations.Motor [declarations]
~ BaseShift.Declarations.Receivers [declarations]
~ BaseShift.Declarations.Reworked [declarations,bodies]
~ BaseShift.Declarations.Shelf [declarations,bodies]

A changed type names what changed, in this order:

A type only one build defines is a + or - line. Where the other build's metadata says where it takes the type from, as after the type moved to a library, the line names that assembly: - <type>, which in B comes from <assembly> for a type B references, or - <type>, which B forwards to <assembly>. A nested type that build does not name takes the clause of the type enclosing it. No change: No changed types.

One type. With type_name, one line per changed member, written as its declaration. A member whose body alone changed is its declaration; a property or event names the accessors whose bodies changed, which are what member_name takes next:

3 member changes in BaseShift.Gauge:
~ public event EventHandler Changed [add_Changed]
~ public int Level { get; set; } [get_Level]
~ public int Limit { get; set; } [set_Limit]

A member whose declaration changed carries both declarations, A's and B's. It ends with [body] where its body changed as well, as in ~ public int Total(int a) → internal int Total(int a) [body], and a property or event with its changed accessors in brackets, as in ~ public int Depth { get; } → public int Depth { get; set; } [set_Depth]:

9 member changes in BaseShift.Declarations.Methods:
~ public Methods() → protected Methods()
~ public void Close() → public virtual void Close()
~ public int Count() → public static int Count()
~ public void Fill(int count) → public void Fill(int amount)
~ public int Gather(IEnumerable<int> values) → public int Gather(params IEnumerable<int> values)
~ public void Open() → private void Open()
~ public T Pick<T>(T item) → public TItem Pick<TItem>(TItem item)
~ public int Spread(params IEnumerable<int> values) → public int Spread(IEnumerable<int> values)
~ public int Sum(int[] values) → public int Sum(params int[] values)

The declaration line is the one list_members prints, so everything it shows counts: accessibility, static, virtual, abstract, sealed, override, readonly, volatile, const, params, this before an extension method's first parameter, the names of parameters and type parameters, the conditions on a method's type parameters, a parameter's default value, as in ~ public void Open(int width = 5) → public void Open(int width = 6), an accessor's own accessibility, set or init, and an accessor gained or lost. A renamed parameter matters to a Harmony patch, whose parameters bind to the patched method's by name. An attribute the line does not show is not compared.

A constant whose value changed shows both values, and one whose type changed is a removal and an addition:

7 member changes in BaseShift.Values.Limits:
+ public const int Added = 7
~ public const int Changed = 5 → 6
~ public const string Empty = null → "set"
~ public const bool Open = false → true
~ public const string Text = "before" → "after"
- public const int Wide = 5
+ public const long Wide = 5L

The compiler copies a constant's value into every place that uses it, so code built against A keeps A's value until it is built again, and the value line is the only place the change shows. An enum member is such a constant: a member inserted before Second in the enum BaseShift.Values.Stage gives ~ public const Stage Second = 1 → 2. Two integral values are the same when they are the same number, so an enum whose underlying type changed with its values kept shows under metadata alone. Two constants or defaults that name the same member of an enum whose members were numbered anew are the same as well.

A type whose own declaration changed, which the list marks metadata, ends its view with both declarations as list_members writes the heading:

No member changes in BaseShift.InterfaceArgumentMover
Declaration: public class BaseShift.InterfaceArgumentMover : IOwner<int> → public class BaseShift.InterfaceArgumentMover : IOwner<string>

A type only one build has lists all its members, and the header says so: 2 member changes in BaseShift.Arity.Command, which only A has: for a class that B replaced by generic classes of its name. Where the other build's metadata says where it takes the type from, the header goes on as the list of changed types does, ..., which only A has; in B it comes from <assembly>: or ; B forwards it to <assembly>:. A member of an interface is written as list_members writes it, with public and without a modifier where the implementing type supplies the member, so + public void Added() in BaseShift.Members.IGrowing is a method the interface gained, although the header does not say that the type is an interface. The fields a compiler adds beside a member, such as an event's subscriber field or an auto-property's backing field, and the field an enum keeps its value in, are listed only with include_compiler_generated. No change: No member changes in ....

How members are matched. A member of A and a member of B are one member when they have the same name, the same count of type parameters, the same parameters with their passing keywords, and the same return type. Each type in that comparison is the type itself, with its namespace, its enclosing classes and an array's number of dimensions, and a type parameter counts by its position. So a changed signature is a removal and an addition, also where a parameter's class gave way to one of the same name in another namespace, while a renamed type parameter is a changed declaration. Lines that would read alike in the short form, or in which one short name would stand for two different classes, are written with every type's namespace:

3 member changes in BaseShift.Overloads.Replaced:
- public int Hold(BaseShift.Overloads.Left.Token token)
+ public int Hold(BaseShift.Overloads.Right.Token token)
~ public T Pass<T>(T item) → public TItem Pass<TItem>(TItem item)

Bodies are compared as IL with tokens resolved to names and file positions left out, so a method a rebuild leaves unchanged does not show as changed. The code the compiler generates for a method, such as its lambdas, local functions, iterator and async state machines, and the copy of its code Burst compiles, is compared as part of that method, with the numbers the compiler gives generated names by position set aside: a method added before others does not make them show as changed, and a change inside a lambda shows on the method that contains it.

One member. With member_name, two bodies that differ come as a unified diff: the two paths, then a hunk for each run of changes under @@ -start,count +start,count @@, with context unchanged lines around it, each line marked with a space, with - for a line only A's body has, or with + for one only B's has. For Settle on BaseShift.Diffs.Ledger, paths shortened:

--- A: ...\BaseShift.dll
+++ B: ...\BaseShift2.dll
@@ -24,6 +24,10 @@
 	_count += amounts.Length;
 	int num6 = num2 - num3;
 	int num7 = ((amounts.Length != 0) ? (num / amounts.Length) : 0);
+	if (strict && num4 > 0)
+	{
+		return -num4;
+	}
 	int num8 = num6 * 2 + num7;
 	if (num8 > 1000)
 	{
@@ -44,5 +48,5 @@
 		num11 += 10;
 	}
 	_total -= num12;
-	return num11 + _count * 2;
+	return num11 + _count * 3;
 }

C# is compared line by line as printed. In IL, a line is compared without the offsets it carries or names, since an inserted instruction moves every later offset: a removed line prints with A's offsets and every other line with B's, and a jump whose target moved to another instruction shows as a change, as -IL_001f: br.s IL_0026 and +IL_001f: br.s IL_002c do for BaseShift.Diffs.Ledger.Route. The code the compiler generated for the method follows its own body, each part under a line // Generated: <name>, and a hunk that starts inside such a part names it after its @@. There, the numbers the compiler gives such names by position read #1, #2 in order of first appearance. A change inside a lambda of InLambda on BaseShift.Generated.ChangedInside, with format il:

--- A: ...\BaseShift.dll
+++ B: ...\BaseShift2.dll
@@ -41,6 +41,6 @@ // Generated: BaseShift.Generated.ChangedInside.<>c__DisplayClass#1_0.<InLambda>b__0()

 IL_0000: ldarg.0
 IL_0001: ldfld int32 BaseShift.Generated.ChangedInside/'<>c__DisplayClass#1_0'::seed
-IL_0006: ldc.i4.2
+IL_0006: ldc.i4.3
 IL_0007: mul
 IL_0008: ret

A whole body is one call away: decompile on either build for its C#, or a context larger than the method for both bodies in one diff, each change marked.

Two bodies the comparison finds equal get a verdict line alone, in either format, since a copy would show nothing that changed:

// BaseShift.Generated.Renumbered.LocalFunctionCapturing(int) → int — bodies are identical in both assemblies.

When the C# reads alike while the IL differs, the line ends — bodies decompile alike in both assemblies, but their IL differs; format il shows where. A method that has a body in neither build is no pair of equal bodies: the line ends — neither assembly has a body for it: abstract in A, extern in B., with each build's reason, abstract, extern or provided by the runtime, where its metadata gives one. A method only one build has is the whole difference, so its code follows a line that says which build has it: // BaseShift.Generated.Renumbered.Inserted(int) → long — only B has it.

Where the member's declaration changed, its two declarations come first, in a line of the member list's form, before the verdict, the diff or a constant's values, since none of them shows the declaration. For an accessor, the line is its property's or event's, which alone can show init: // public string Name { get; set; } → public string Name { get; init; } stands above the verdict on get_Name. A line before the diff names the method only where it says more than the call did:

// public int Total(int a) → internal int Total(int a)

--- A: ...\BaseShift.dll
+++ B: ...\BaseShift2.dll
@@ -1,4 +1,4 @@
-public int Total(int a)
+internal int Total(int a)
 {
-	return a + 1;
+	return a + 2;
 }

Each build looks the name up on its own, so the two can find what the member list counts as two members, as after a changed return type, a move to a base class, or a method that gave way to an extension method or to an explicit implementation. A line then names both, each as decompile's comment does, with [inherited from X] or [extension method on X] where the type asked about does not declare it, and the verdict or the diff under it shows whether more than the signature changed:

// BaseShift.Signatures.Returns.Open() → object in A and BaseShift.Signatures.Returns.Open() → string in B are different methods — bodies are identical in both assemblies.

Constants. A constant, an enum member included, is answered with its values in one line: // BaseShift.Values.Limits.Changed — 5 in A, 6 in B., // BaseShift.Values.Limits.Kept — 1 in both assemblies., // BaseShift.Values.Limits.Added = 7 — only B has it., and for two constants the member list counts as two members, with their types: // BaseShift.Values.Limits.Wide → int in A and BaseShift.Values.Limits.Wide → long in B are different constants — 5 in A, 5L in B. An inherited constant carries its mark, // BaseShift.Values.Derived.Limit [inherited from BaseShift.Values.Base] — 5 in A, 6 in B. The name means the constant where the nearest class that has the name declares one, as in C#; a hint asks for a method.

Fields, properties and events. A property or an event has no body of its own. One that only one build has comes with its code, as decompile shows it, or with format il with the IL of each accessor under a line that names it, such as // BaseShift.Sides.Panel.get_Title() → string:

// BaseShift.Sides.Panel.Title — only B has it.

public string Title { get; set; }

A property or event both builds have is answered with an error that names its accessors, which member_name takes instead, each that only one build has marked. A field is answered with an error that names the constructors of its class that set it in each build, read from their code as analyze with assigned_by reads a field's writers, since the compiler moves a field's initializer into them. Where none does, the error points to assigned_by; may set it names a constructor that passes the field's address to code ILens cannot follow, such as a method of another assembly. Both errors name the two declarations where they differ. Where the member list counts the two as two members, as after a changed type or a move to a base class, they say A and B have different fields of that name, or properties or events, and a declaration found off the type asked about is followed by [inherited from X]:

Error: ArgumentException: BaseShift.Gauge.Level is a property; compare its accessors, get_Level, set_Level.
Error: ArgumentException: BaseShift.Declarations.Accessors.Depth is a property, and its declaration changed: public int Depth { get; } → public int Depth { get; set; }; compare its accessors, get_Depth, and set_Depth, which only B has.
Error: ArgumentException: BaseShift.Fields.Gauge.Size is a field, which has no body; BaseShift.Fields.Gauge..ctor() and BaseShift.Fields.Gauge..ctor(string) set it.
Error: ArgumentException: BaseShift.Fields.Late.Start is a field, which has no body; in A no constructor of BaseShift.Fields.Late sets it, and in B BaseShift.Fields.Late..cctor() sets it.
Error: ArgumentException: BaseShift.Fields.Store.Stock is a field, which has no body; no constructor of BaseShift.Fields.Store sets it, and assigned_by lists what does.
Error: ArgumentException: BaseShift.Fields.Tally.Created is a field, which has no body; BaseShift.Fields.Tally..ctor() may set it.
Error: ArgumentException: BaseShift.Fields.Growing.Fresh is a field, which has no body, and only B has it; BaseShift.Fields.Growing..ctor() sets it.
Error: ArgumentException: BaseShift.Retyped.Till.Delay is a field, which has no body, and A and B have different fields of that name: public int Delay → public int Delay [inherited from BaseShift.Retyped.Store]; in A BaseShift.Retyped.Till..ctor() sets it, and in B BaseShift.Retyped.Store..ctor() sets it.
Error: ArgumentException: BaseShift.Declarations.Kinds.Stride is a field, which has no body, and its declaration changed: public static readonly int Stride → public const int Stride = 4; in A BaseShift.Declarations.Kinds..cctor() sets it, and in B it is a constant.

A name the two builds use for members of different kinds is answered with one line that names both kinds and both declarations, then the code of each member that has code, in C# or with format il in IL, under a line that names its build:

// BaseShift.Kinds.Till.Open is a method in A and a property in B: public void Open() → public bool Open { get; }

// A: BaseShift.Kinds.Till.Open() → void
public void Open()
{
}

// B: BaseShift.Kinds.Till.Open
public bool Open => true;

The kinds are field, constant, property, event, method and extension method. A field or a constant has no code, so its line holds all there is of it: // BaseShift.Kinds.Till.Limit is a field in A and a property in B: public int Limit → public int Limit { get; set; } is followed by B's property alone. With format il, the block of a property or an event holds the IL of each accessor under a line that names it, as // BaseShift.Kinds.Till.get_Open() → bool under // B: BaseShift.Kinds.Till.Open. A hint asks for a method, so with one the answer is about methods.

Extension methods and moved types. Each build finds an extension method through the type asked about as decompile does. Where one build finds it and the other declares it as well but its lookup does not take it, the line says why in place of only A has it: — only A finds it through <type>; in B the type has a member of that name, which C# takes first., or for a method whose first parameter is a type parameter:

// BaseShift.Constraints.ShapeExtensions.Corners<T>(T) → int [extension method on T where T : BaseShift.Constraints.IShape] — only A finds it through BaseShift.Constraints.Box; in B BaseShift.Constraints.Box does not meet the method's constraints.

A build that does not define the type asked about but references it, as after the type moved to a library, is asked through the type it references, so a method one build declares on the type and an extension method the other declares for the library's type are compared as two different methods. A line that says only one build has a member then ends with where the other build takes the type from: — only A has it; in B <type> comes from <assembly>. Where a build's lookup could not read a class that might declare a member of the name, the answer starts with the Not checked: line that decompile writes under its comment.

Reference assemblies. A build marked as a reference assembly holds a library's declarations without their code (see reference assemblies), so no body of it is compared or shown. The list of changed types and a type's list of changes mark no body as changed and end with Not compared: bodies; A is a reference assembly, which holds no code. With member_name, the line says — A is a reference assembly, which holds no code to compare with B's. and B's code follows, or — A and B are reference assemblies, which hold no code to compare.; a member only such a build has is shown by its declaration and the line // B is a reference assembly, which holds no code.

For a method Burst compiles, the C# shown is Burst's copy of its code, which the line that names the method marks with (via X$BurstManaged).

Errors

See also

find_harmony_dependencies for the members a mod depends on, to check against a new build; decompile to read a changed type or member in full; list_members for a type's declarations in one build.

Troubleshooting

Every tool error reaches the caller as Error: <ExceptionType>: <message>. The messages below are shared by several tools; the ones particular to a tool are listed under that tool.

No allowed roots configured. The server cannot load any assemblies. Launch with one or more --allow-root <directory> flags.

The server was launched without --allow-root. Add one or more --allow-root <directory> arguments to the args array in .mcp.json and start a new session. list_allowed_roots reports the same condition without failing.

Path is not under any allowed root: ... Allowed roots: ...

The requested assembly lies outside every configured root, as the path is written. The message lists the roots that are configured; a root given through a symbolic link or junction is listed with (resolved: <path>), the folder the link pointed to when the server started, where that is another path. Either point the call at a file inside a root, through the root's link or by its real path, or add the containing directory as another --allow-root. A path written through a link that lies outside every root is refused here even where the link leads into a root.

Path resolves (via a symbolic link or junction) to a location outside any allowed root: ...

The path passed the check as written but resolves to a real target outside the roots. This is the link check doing its job: a link planted inside a root cannot be used to reach outside it. The message lists the roots as the one above does. The same message answers a request through a root's link that was moved to another folder while the server runs, since a root stays the folder its link pointed to at startup; restart the server to follow the link, or ask by the old folder's real path meanwhile. A chain of links that does not end, or cannot be followed, is rejected with Path has too many levels of symbolic links or junctions (possible cycle): ... or Path could not be resolved through its symbolic links or junctions: ....

Path contains '..' which is not allowed: ...

Parent-directory segments are rejected before normalization. Pass an absolute path with no .. components.

Assembly file does not exist: ... or Assembly path is empty.

The path is inside a root but names no file, or no path was given. Check the spelling and that the build actually produced the assembly.

MetadataFileNotSupportedException: PE file does not contain any managed metadata., BadImageFormatException: Unknown file format. or InvalidOperationException: Failed to load type system from ...

The file is not a .NET assembly: the first is a native DLL or executable, the second a file that is no Windows executable at all, and the third a file that holds .NET metadata but no assembly, such as a program database (.pdb). ILens reads compiled .NET assemblies only.

Assembly is N MB, which exceeds the M MB total memory budget on its own: ... Raise the budget with --max-total-size <MB>.

ILens holds an assembly's whole image in memory while it is open, so a file larger than the whole memory budget is refused before it is loaded. Raise the budget with --max-total-size <MB>.

Unknown argument '...' for tool '...'. Valid arguments: ...

An argument name the tool does not declare, often a parameter name from an older ILens release. Most filters are optional, so a silently dropped filter would return an unfiltered result that looks like an answer; the call is rejected instead. The message lists the valid names.

Invalid value ... for argument '...'. ... or Missing required argument '...'.

A value the parameter does not take, such as a kind that analyze does not have, a number or several comma-separated names for a parameter with listed values, or text that is no whole number for limit; or a required parameter left out or given as null. The message names each such parameter of the call and what it takes: Valid values: ... with the listed names, or Expected an integer., Expected an integer of at most 2147483647. or Expected an integer of at least -2147483648. for a whole number beyond that range, Expected a string., Expected true or false., Expected a list of strings. or Expected a list of: .... A value with exactly one reading is taken without an error, see Tool reference.

This assembly is a reference assembly, which holds a library's declarations without their code, so ...

A question that reads code was asked of a reference assembly, whose method bodies are placeholders. Ask the build the library runs as, or for find_harmony_dependencies the build the mod runs as; the messages of analyze, decompile and search_code also name the questions that read declarations alone and answer on this assembly. See Reference assemblies.

Type not found: ...

No type the assembly defines, references or forwards has that name. The message ends with the types the name may have meant: Types of that name: ... where types of that full name exist with another count of type parameters, and otherwise the types of the name's last segment in any namespace or enclosing class, those that differ from the name only in the namespace where any does. Asked of the test assembly CallShapes.dll:

Error: ArgumentException: Type not found: Shop.Vault. Types named Vault: CallShapes.BaseCalls.Vault, CallShapes.Vault.

Types the assembly references or forwards are named in groups with their library, Types named Marker: this assembly defines ...; it references ExternalLib.Namesakes.Marker from ExternalLib, Version=1.0.0.0., and at most 40 types are named, the rest counted. analyze gives such a name an empty answer instead, whose last line says that the assembly neither defines nor references a type of that name and names the same types. find_types with name_contains finds the exact name.

... is not defined in this assembly; it comes from ... or ..., which forwards it to ...

The assembly uses the type, or forwards it, but another assembly defines it. Point the call at that assembly to read the type itself. analyze can still answer who in this assembly uses it, and decompile, analyze and compare still find an extension method this assembly declares for the type, see Types from other assemblies. A forwarded type is named with the assembly it is forwarded to, also where the assembly references it too; a reference that names no assembly reads ... is not defined in this assembly, which only references it. Where the assembly gives the name only to generic types of its own, the message names them, since you may have meant one, as in the test assembly ExternalUser.dll:

Error: ArgumentException: ExternalLib.Namesakes.Marker is not defined in this assembly; it comes from ExternalLib, Version=1.0.0.0. This assembly defines ExternalLib.Namesakes.Marker<>, ExternalLib.Namesakes.Marker<,>.

Type name '...' matches N types: ... Name one as listed.

A name written without arity matches several generic types and no type without type parameters, such as System.Func. Where the types are of more than one kind, defined, referenced or forwarded, they are listed in a group for each kind, as in ExternalUser.dll:

Error: ArgumentException: Type name 'ExternalLib.Namesakes.Crate' matches 2 types: this assembly defines ExternalLib.Namesakes.Crate<,>; it references ExternalLib.Namesakes.Crate<> from ExternalLib, Version=1.0.0.0. Name one as listed.

Types of one kind read matches 17 types this assembly forwards: System.Func<>, ... (the .NET runtime's System.Runtime.dll) or matches N types this assembly references but does not define: .... Pass one of the listed names, which carry their arity.

... is an array type, which only analyze with kind 'extension_methods' and decompile, analyze and compare with member_name take, to find the extension methods code can call on it.

An array type has no members of its own in an assembly. Ask about its element type, or ask one of the named questions for the extension methods that apply to such an array.

An array type writes its element with its type arguments, as C# does: ... names ..., which takes N.

The element of an array type was written without its type arguments, as in System.Collections.Generic.List[]. Write it as C# does, such as List<int>[].

Namespace not found: ...

No type of the assembly lies in that namespace or below it. Namespaces are matched exactly and case-sensitively.

Member '...' not found on ..., its base types, or as an extension method.

The name is not on the type, its base classes or among the extension methods that apply. list_members lists what the type has; an accessor is named with its prefix, get_Count, and a constructor .ctor or by the type's name. With an overload hint, and for analyze's call_sites, which looks for a method, the message begins Method '...'. Where a usual name does not apply, the message ends with what does: ; its indexer is named Chars: System.String.get_Chars(int) for get_Item, set_Item or Item on a type whose indexer has another name (asked of the .NET runtime's System.Private.CoreLib.dll); ; a struct that declares no constructor has none in the assembly, and instantiated_by lists where it is created, ; an enum has no constructor or ; its static constructor is named '.cctor' for .ctor or the type's name; and ; the methods named Run are CallShapes.Namesakes.Store.Run(), CallShapes.Namesakes.Store.Run<T>() for brackets whose count of type parameters fits no method of the name. For the name of a property of a C# 14 extension block, which the class holds only as the methods the compiler made of its accessors, the message ends with those methods, the ones to ask instead: ; Doubled is a property of an extension block: ...get_Doubled(Gauge). When the search stopped at a base class ILens could not read, the message says the base types ILens could load and ends with the class and the reason, as in ExternalUser.dll without the folder of ExternalLib.dll among the allow-roots:

Error: ArgumentException: Member 'Level' not found on ExternalUser.Inherited.Probe, the base types ILens could load, or as an extension method; ExternalLib.Inherited.Sensor and its base types were not searched, since ILens could not load ExternalLib, Version=1.0.0.0.

The three reasons and their remedies are under Types from other assemblies; once the class can be read, the search goes on into it. Through an array type the message reads Member '...' not found on System.Byte[]: no public instance member of System.Array or System.Object and no extension method of this assembly that applies to it has that name., with ; what ... derives from was not read, since ... where the element's class could not be read.

... inherits ... from a base class this assembly does not define; ... comes from ...

The member is declared on a base class another assembly defines, so the inspected assembly holds no code for it. The message names the file that does: call decompile, analyze or compare on that file to read the member. A member the type's code cannot reach, a private one or an internal one its library does not grant this assembly, is passed over as C# passes over it, so an extension method of the name answers in its place; for such a member the error stands only where nothing else answers to the name.

'...' on ... has N matching overloads; ...: ...

The name, or the name with the hint given, fits several methods. The message lists them and says what tells them apart: specify parameter_count or parameter_types to disambiguate; they are same-arity overloads — specify parameter_types to disambiguate; write the type parameters of the one meant after its name, as listed; they differ in their return type alone, which return_type selects; these parameter types fit more than one; write those of the one meant as listed, as where a short type name fits classes of that name in two namespaces, and likewise this return type fits more than one; write that of the one meant as listed; or the candidates share their parameter types and cannot be narrowed further. Asked of CallShapes.dll:

Error: ArgumentException: 'op_Explicit' on CallShapes.Namesakes.Amount has 3 matching overloads; they differ in their return type alone, which return_type selects: CallShapes.Namesakes.Amount.op_Explicit(Amount) → int, CallShapes.Namesakes.Amount.op_Explicit(Amount) → long, CallShapes.Namesakes.Amount.op_Explicit(Amount) → decimal.

No overload of '...' on ... matches ...

A parameter_count, parameter_types or return_type hint was given and nothing satisfies it. The message lists every method of that name found across the inheritance chain, so the right hint is usually visible in the error itself, and a confirmed "no such overload" is often the answer that was being sought.

Property '...' on ... has N overloads — ...

The name is shared by several indexers, which C# names alike. Name the accessor of the one meant instead, get_Item or set_Item, with parameter_types; the message lists the candidates.

'...' on ... names N explicit interface implementations: ... Pass the full name of one instead.

The type implements several interface members of that name explicitly, such as two GetEnumerator methods. Pass the full name of the one meant, as listed, which includes its interface.

Property '...' on ... has no getter.

An accessor's name was asked for a property that lacks that accessor; the message reads has no setter likewise, and Event '...' on ... has no add accessor. or has no remove accessor. for an event.

Member '...' on ... is ambiguous — it exists as more than one member kind ...

One type declares the name as more than one kind of member, which only obfuscated or non-C# metadata produces. Names that classes at different levels of the inheritance chain use for different kinds do not trigger it: the member of the most derived class that declares the name answers. Pass an overload hint if the method is meant. An ordinary event does not trigger this either: the field the compiler keeps its subscribers in is recognized as part of the event.

parameter_count (N) contradicts parameter_types.Length (M).

Both overload hints were given and disagree on the number of parameters. Pass one of them, or make them agree.

parameter_count, parameter_types and return_type select a method overload, so they need member_name.

An overload hint was given to decompile, analyze or compare without a member to select. Add member_name, or leave the hint out to ask about the type.

Decompiled code shows numbers where names belong, or Unknown result type comments

A referenced assembly is not in an allowed folder. ILens finds an assembly's references beside it, at the top of each allow-root, and in the installed .NET; a reference it cannot find leaves the decompiler without its types, so enum values come out as numbers, such as (Glaze)1 for a value of another library's enum, and expressions of those types are marked as unknown. Add the folder that holds the missing assemblies, such as a game's own assemblies for a mod, as another --allow-root. The same missing library makes other answers end with a line that names it, see Types from other assemblies.

The tools do not appear at all

Almost always the two gates described under Integrating with Claude Code: an already-running Claude app holding a stale PATH, or a session that started before .mcp.json existed. Close Claude app entirely, relaunch it, and start a new session. The server also refuses to start when its own arguments are wrong. It then writes the reason to stderr as a line starting with [ILens] and exits, which the MCP client typically reports as a server that failed to connect:

Appendix A: Alternative installers

For machines where winget is unavailable or locked down by policy, or when you would rather inspect the binary before granting it execution.

PowerShell one-liner

irm https://raw.githubusercontent.com/tadis174/ILens/main/install.ps1 | iex

Installs to %LOCALAPPDATA%\Programs\ILens\ and adds that directory to your user PATH. No admin elevation is needed.

Manual ZIP

Download ILens-windows-x64.zip from the Releases page and extract it anywhere. Then either add the directory to PATH yourself, or give the binary's full path in .mcp.json's command field.

Appendix B: Updating and uninstalling

winget. winget upgrade Tadis.ILens to update; winget uninstall Tadis.ILens to remove.

PowerShell one-liner. Re-run irm https://raw.githubusercontent.com/tadis174/ILens/main/install.ps1 | iex to update — the script overwrites the install directory in place and stops any running ILens processes first. Uninstall with irm https://raw.githubusercontent.com/tadis174/ILens/main/uninstall.ps1 | iex.

Manual ZIP. No automatic update mechanism — re-download ILens-windows-x64.zip and re-extract on each new release. To uninstall, delete the extracted folder and remove the PATH entry if you added one.

Appendix C: Installation troubleshooting

Antivirus or Windows Defender quarantines ILens.exe

Self-contained .NET binaries are sometimes false-positive-flagged on first sight. Check the quarantine and restore the file, or add an exception for the install directory — %LOCALAPPDATA%\Microsoft\WinGet\Packages\Tadis.ILens_*\ for winget, %LOCALAPPDATA%\Programs\ILens\ for the PowerShell script — before re-running the installer.

SmartScreen shows "Windows protected your PC"

This affects browser-downloaded ZIP installs, which carry Mark-of-the-Web. The winget and PowerShell paths do not. For a manual browser download, click "More info → Run anyway".

winget upgrade Tadis.ILens fails with remove: Access is denied: "...\ILens.exe"

A running ILens MCP server — a Claude Code session, Claude Desktop, or another AI client that registered ILens — holds an exclusive lock on the binary winget is trying to replace. Close every AI client with ILens registered and re-run the upgrade. The PowerShell install.ps1 path handles this automatically by stopping running ILens processes before extraction, so the same upgrade via irm ... | iex needs no manual cleanup. winget's portable installer type has no pre-install hook where an equivalent step could run.

Appendix D: claude mcp add CLI

A programmatic alternative for anyone who would rather not edit .mcp.json by hand:

claude mcp add ilens ilens -- --allow-root "C:\path\to\dlls"

The doubled ilens is intentional: the first is the MCP server alias — the same string that becomes the JSON key in .mcp.json — and the second is the binary on PATH after install. The -- separator before --allow-root is required so claude mcp add does not consume it as one of its own options.

Appendix E: Claude Desktop

Claude Desktop reads %APPDATA%\Claude\claude_desktop_config.json. The entry is equivalent to the Claude Code one:

{
  "mcpServers": {
    "ilens": {
      "command": "ilens",
      "args": [
        "--allow-root", "C:\\path\\to\\dlls"
      ]
    }
  }
}

Claude Desktop's chat does not read server instructions and has no per-project CLAUDE.md, so paste the paragraph from Project-level guidance into your Claude Desktop Project's custom instructions.

Version, source, license

This guide documents ILens 12. Source and issue tracker: github.com/tadis174/ILens.

ILens is released under the MIT License; the full text ships as LICENSE beside this guide in the release ZIP. Every third-party dependency and bundled runtime component is mapped to its upstream license in third-party-licenses/INDEX.md, included in the same ZIP alongside the per-source license texts.