2

I have Razor Views with localizated resources using IViewLocalizer like this:

@inject  Microsoft.AspNetCore.Mvc.Localization.IViewLocalizer Localizer
...
@Localizer.GetString("Click here to Log in")

I'm trying to extract all those strings from my precompiled views (to build a PO file dictionary), so I think I'll have to find all references of IViewLocalizer injected into my views, something like this:


var findAll = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic |
                                         BindingFlags.Instance | BindingFlags.Static;
var asm = Assembly.LoadFile("MySite.Web.Views.dll"));
var view = asm.GetType("AspNetCore.Views_Home_Index");
var props = view.GetProperties(findAll).Where(p=>p.PropertyType.FullName == "Microsoft.AspNetCore.Mvc.Localization.IViewLocalizer").ToList();

// I want to find all references that use this this IViewLocalizer Localizer.get()
var getter = props.First().GetGetMethod().MetadataToken; 

// How can I do it?
var allMethods = view.GetMethods(findAll);
foreach(var method in allMethods)
{
    // How can I check my methods for usages of that property getter, and in case get the strings?
}

I tried using the code from this answer which uses MethodBase.GetMethodBody().GetILAsByteArray() but it didn't work - looks like the method public override Task ExecuteAsync() is very short, so it looks like my view is rendered somewhere else.

And additionally, I'm not sure if I should look for the MetadataToken of the getter (IViewLocalizer injected into my views) or if I should look for usages of any concrete method which implements IViewLocalizer.GetString(string) and IViewLocalizer.GetString(string, params object[]) .

PS: I'm aware that I can use Regex over my cshtml views, as long as the injected localizer follows some naming standard. That's Plan B in case I can't figure out the reflection solution.

drizin
  • 1,737
  • 1
  • 18
  • 44

1 Answers1

2

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());
drizin
  • 1,737
  • 1
  • 18
  • 44