14

Example, the EntityFramework Microsoft.EntityFrameworkCore.Relational project has the following text in the resource files:

...
<data name="FromSqlMissingColumn" xml:space="preserve">
  <value>The required column '{column}' was not present in the results of a 'FromSql' operation.</value>
</data>
...

which generates the following C# code:

...
/// <summary>
/// The required column '{column}' was not present in the results of a 'FromSql' operation.
/// </summary>
public static string FromSqlMissingColumn([CanBeNull] object column)
{
    return string.Format(CultureInfo.CurrentCulture, GetString("FromSqlMissingColumn", "column"), column);
}
...
private static string GetString(string name, params string[] formatterNames)
{
    var value = _resourceManager.GetString(name);

    Debug.Assert(value != null);

    if (formatterNames != null)
    {
        for (var i = 0; i < formatterNames.Length; i++)
        {
            value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
        }
    }

    return value;
}
...

But when I edit the file in VS and save it, I get only simple properties generated, like:

...
/// <summary>
/// The required column '{column}' was not present in the results of a 'FromSql' operation.
/// </summary>
public static string FromSqlMissingColumn
{
    get { return ResourceManager.GetString("FromSqlMissingColumn"); }
}
...

The files in question can be found here:

So the question again - How did they do it, and how could I get the same result?

peter
  • 11,751
  • 6
  • 52
  • 89

2 Answers2

10

How did they do it?

First it should be obvious that they don't use the standard ResXFileCodeGenerator, but some custom code generation tool.

Currently there are 2 standard ways of generating code - the old school way using a Custom Tool similar to ResXFileCodeGenerator, or the modern way using a T4 Template. So let see.

The correspondig entry inside the Microsoft.EntityFrameworkCore.Relational.csproj file looks like this:

<ItemGroup> 
    <EmbeddedResource Include="Properties\RelationalStrings.resx">
        <LogicalName>Microsoft.EntityFrameworkCore.Relational.Properties.RelationalStrings.resources</LogicalName> 
    </EmbeddedResource> 
</ItemGroup> 

As we can see, definitely they do not use Custom Tool.

So it should be a T4 template. And indeed right after the above item we can see:

<ItemGroup> 
    <Content Include="..\..\tools\Resources.tt"> 
        <Link>Properties\Resources.tt</Link> 
            <Generator>TextTemplatingFileGenerator</Generator> 
            <LastGenOutput>Resources.cs</LastGenOutput> 
            <CustomToolNamespace>Microsoft.EntityFrameworkCore.Internal</CustomToolNamespace> 
    </Content> 
    <Content Include="Properties\Microsoft.EntityFrameworkCore.Relational.rd.xml" /> 
</ItemGroup> 

So there you go!

Now, I don't know what's the purpose of the included xml file without diving into the implementation (it might be something that is used by the generator, like options or something), but the actual code generation is contained in the following Resources.tt file.

how could I get the same result?

I guess you are asking for your own projects. Well, you can do something similar. Select your resx file, go to Properties and clear the Custom Tool. Then add T4 template to your project and write the code generation (I'm not sure if the license allows you to use their code, so if you want to do so, make sure you first check if it is allowed). But the principle would be the same.

Ivan Stoev
  • 159,890
  • 9
  • 211
  • 258
4

I think, EF team uses own custom Custom Tool for that purposes. But visual studio uses PublicResXFileCodeGenerator as a default custom tool for .resx files and this tool have no such functionality as PublicResXFileCodeGenerator and it's base class ResXFileCodeGenerator (both can be found in Microsoft.VisualStudio.Design assembly) is just a wrappers for visual studio around StronglyTypedResourceBuilder.

They are implement IVsSingleFileGenerator (located in Microsoft.VisualStudio.Shell.Interop assembly). So this is the place you can start implementing your own Custom Tool. Start new Class Library, add Microsoft.VisualStudio.Shell.14.0 and Microsoft.VisualStudio.Shell.Interop references. Create new class and implement this interface. Interface IVsSingleFileGenerator is pretty simple. It contains only two methods:

  • DefaultExtension which returns extension for generated file (with a leading period) as out string pbstrDefaultExtension paratemer and VSConstant.S_OK as return value (of course if everything is OK).

  • Generate which is accept:

    • wszInputFilePath - input file path, may be null, do not use it.
    • bstrInputFileContents - input file content, should be used.
    • wszDefaultNamespace - default namespace (cannot tell right now why ResXFileCodeGenerator interops with Visual Studio to get namespace instead of using this parameter).
    • rgbOutputFileContents - array of bytes of generated file. You must include UNICODE or UTF-8 signature bytes in the returned byte array, as this is a raw stream. The memory for rgbOutputFileContents must be allocated using the .NET Framework call, Marshal.AllocCoTaskMem, or the equivalent Win32 system call, CoTaskMemAlloc. The project system is responsible for freeing this memory.
    • pcbOutput - count of bytes in the rgbOutputFileContent array.
    • pGenerateProgress - A reference to the IVsGeneratorProgress interface through which the generator can report its progress to the project system.

    And returns VSConstant.S_OK if everything is fine, or corresponding error code.

Also there is small guide about implementation. But this guide does not tells too much. Most useful thing is how to register own generator.

You'd better dive into ResXFileCodeGenerator code (or simply decompile) for implementation example or to get some hints such as how to interop with visual studio. But I see no reason to interop with VS as everything you need you already provided. .resx file content could be read by ResXResourceReader.FromFileContents.

Rest of all would be simple as you have resource names and values and only need to return array of bytes of generated file. I think, parsing resource values to have a compilation-time error of invalid format (e.g.: {{param}}}) would be the biggest difficulty.

When values parsed and parameters for your upcoming method found you can generate code (again as an example you can refer to ResXFileCodeGenerator and StronglyTypedResourceBuilder, or do it by your self the way you want, via CodeDom or compose source code text manually). It is also should not be difficult as you already has an example of the methods you need to generate in the question you posted.

Compile your own generator, register it, set it in Custom Tool property of your .resx files and you will get resources classes with methods instead of properties.

Also you could share it on github with others. :)


Here is instruction from custom tool registration (as msdn link could die soon or later):

To make a custom tool available in Visual Studio, you must register it so Visual Studio can instantiate it and associates it with a particular project type.

  1. Register the custom tool DLL either in the Visual Studio local registry or in the system registry, under HKEY_CLASSES_ROOT.

    For example, here's the registration information for the managed MSDataSetGenerator custom tool, which comes with Visual Studio:

    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\CLSID\{E76D53CC-3D4F-40A2-BD4D-4F3419755476}]
    @="COM+ class: Microsoft.VSDesigner.CodeGenerator.TypedDataSourceGenerator.DataSourceGeneratorWrapper"
    "InprocServer32"="C:\\WINDOWS\\system32\\mscoree.dll"
    "ThreadingModel"="Both"
    "Class"="Microsoft.VSDesigner.CodeGenerator.TypedDataSourceGenerator.DataSourceGeneratorWrapper"
    "Assembly"="Microsoft.VSDesigner, Version=14.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a"
    
  2. Create a registry key in the desired Visual Studio hive under Generators\GUID where GUID is the GUID defined by the specific language's project system or service. The name of the key becomes the programmatic name of your custom tool. The custom tool key has the following values:

    • (Default) - Optional. Provides a user-friendly description of the custom tool. This parameter is optional, but recommended.

    • CLSID - Required. Specifies the identifier of the class library of the COM component that implements IVsSingleFileGenerator.

    • GeneratesDesignTimeSource - Required. Indicates whether types from files produced by this custom tool are made available to visual designers. The value of this parameter needs to be (zero) 0 for types not available to visual designers or (one) 1 for types available to visual designers.

    Note, you must register the custom tool separately for each language for which you want the custom tool to be available.

    For example, the MSDataSetGenerator registers itself once for each language:

    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\Generators\{164b10b9-b200-11d0-8c61-00a0c91e29d5}\MSDataSetGenerator]
    @="Microsoft VB Code Generator for XSD"
    "CLSID"="{E76D53CC-3D4F-40a2-BD4D-4F3419755476}"
    "GeneratesDesignTimeSource"=dword:00000001
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\Generators\{fae04ec1-301f-11d3-bf4b-00c04f79efbc}\MSDataSetGenerator]
    @="Microsoft C# Code Generator for XSD"
    "CLSID"="{E76D53CC-3D4F-40a2-BD4D-4F3419755476}"
    "GeneratesDesignTimeSource"=dword:00000001
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\Generators\{e6fdf8b0-f3d1-11d4-8576-0002a516ece8}\MSDataSetGenerator]
    @="Microsoft J# Code Generator for XSD"
    "CLSID"="{E76D53CC-3D4F-40a2-BD4D-4F3419755476}"
    "GeneratesDesignTimeSource"=dword:00000001
    
lorond
  • 3,636
  • 2
  • 35
  • 50