With the help of ILSpy (and later confirmed by this answer and this one) I've discovered that async methods (as well as iterator classes) are generated across nested helper classes that represent the state machine. These helper classes can be found in the NestedTypes property of the main type.
I could find multiple calls to the method which I was looking for (getter of my IViewLocalizer) when I added to my search all methods in inner types. This worked:
var allMethods = view.GetMethods(findAll).ToList();
allMethods.AddRange( // also search in methods of nested types
view.GetNestedTypes(findAll).SelectMany(x => x.GetMethods(findAll)));
foreach(var method in allMethods)
{
var usages = ((MethodBase)method).GetMethodUsageOffsets(get);
if (usages.Count() > 0)
Debug.WriteLine($"{method.ReflectedType.FullName}.{method.Name}");
}
All calls which I was expecting to find in AspNetCore.Views_Home_Index3.ExecuteAsync() were actually spread over these methods in inner types:
AspNetCore.Views_Home_Index3+<ExecuteAsync>d__22.MoveNext
AspNetCore.Views_Home_Index3+<<ExecuteAsync>b__22_0>d.MoveNext
AspNetCore.Views_Home_Index3+<<ExecuteAsync>b__22_1>d.MoveNext
AspNetCore.Views_Home_Index3+<<ExecuteAsync>b__22_2>d.MoveNext
AspNetCore.Views_Home_Index3+<<ExecuteAsync>b__22_3>d.MoveNext
AspNetCore.Views_Home_Index3+<<ExecuteAsync>b__22_4>d.MoveNext
AspNetCore.Views_Home_Index3+<<ExecuteAsync>b__22_5>d.MoveNext
AspNetCore.Views_Home_Index3+<<ExecuteAsync>b__22_6>d.MoveNext
... etc... all states from the state machine
Using Mono.Cecil it became even easier since I don't even need this GetMethodUsageOffsets
extension, since Cecil can interpret bytecodes easily:
var module = ModuleDefinition.ReadModule(System.IO.Path.Combine(appFolder, "MyApp.Web.Views.dll"));
var type = module.Types.First(x => x.Name.EndsWith("Index3"));
var allInstructions = typeDefinition.Methods
.Union(typeDefinition.NestedTypes.SelectMany(x => x.Methods))
.Where(m => m.HasBody)
.SelectMany(x => x.Body.Instructions).ToList();
var calls = allInstructions.Where(i => i.ToString().Contains(
"callvirt Microsoft.Extensions.Localization.LocalizedString Microsoft.AspNetCore.Mvc.Localization.IHtmlLocalizer::GetString(System.String)")).ToList();
foreach (var call in calls) // previous is where string parameter is loaded
System.Diagnostics.Debug.WriteLine(i.Previous.ToString());
Results:
IL_020a: ldstr "More Options"
IL_0241: ldstr "Add Text"
IL_15c8: ldstr "Reset"
IL_15ff: ldstr "Save Text Box Settings"
IL_0019: ldstr "None"
...etc
Edit of 2020-09-23:
I've just discovered that some methods can be split into multiple levels, so searching only for first-level NestedTypes doesn't work. Since I'm currently using Cecil (instead of pure reflection), this is my current code:
// All assemblies that I want to scan
var modules = new List<ModuleDefinition>();
modules.Add(ModuleDefinition.ReadModule(System.IO.Path.Combine(appFolder, "MyProject.Web.Views.dll")));
modules.Add(ModuleDefinition.ReadModule(System.IO.Path.Combine(appFolder, "MyProject.Web.dll")));
// All types in my Assemblies
var allTypes = modules.SelectMany(m => m.Types).ToList();
// Then for each of those types I get all Nested Types:
// Get all nested types
for(int i = 0; I < allTypes.Count(); i++)
allTypes.AddRange(allTypes[i].NestedTypes);
// Get all instructions using Cecil
var allInstructions = allTypes.SelectMany(x => x.Methods)
.Where(m => m.HasBody)
.SelectMany(x => x.Body.Instructions).ToList();
var i18nInstructions = allInstructions.Where(i =>
i.ToString().Contains("GetString(System.String)")
|| i.ToString().Contains("Microsoft.AspNetCore.Mvc.Localization.HtmlLocalizerExtensions::GetHtml(Microsoft.AspNetCore.Mvc.Localization.IHtmlLocalizer,System.String)")
).ToList();
// This prints all values of "ldstr 'value'"
// which happen before GetString() or GetHtml()
foreach (var i in i18nInstructions)
System.Diagnostics.Debug.WriteLine(i.Previous.Operand.ToString());