Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I prevent foreign language resource generation by overriding MSBuild targets?

I am working on decreasing compile time for a large C# / ASP.NET solution. Our solution is translated into about a dozen foreign languages using the usual resx file method. The parsing and compiling of these resource files greatly slows down our compile time and is a daily frustration.

I am aware that it is possible to create custom resource providers and get away from .resx files. For now, please assume we must stick with .resx files.

By excluding all but the default locale .resx files from our .csproj files, I am able to cut our compile time in half. Our developers don't need to be compiling a dozen other languages during day-to-day development.

I'm looking for ways to prevent the compilation of the foreign language .resx files. I've come up with two methods, and I'm looking for advice on whether one is superior, or whether there are other better methods.

The two I've come up with:

  • Write scripts that can strip out and add back in the non-default .resx files in the various .csproj files. Perhaps we'd keep the minimal .csproj files in version control, and have a separate build process for re-adding .resx files recursively in order to re-integrate new translations, perform testing and do our deployment builds.
  • Figure a way to override the built-in MSBuild targets that perform resx parsing and compilation, effectively disabling them for all but the default language. Developers could possibly enable/disable this behavior with a simple compile flag or build switch. I've not yet dug deep into the provided .target files from Microsoft to see how reasonable or maintainable this solution actually is.

Update

I wrote the following Powershell script to move all my foreign language EmbeddedResources and Compile elements into a new ItemGroup which has a Conditional attribute.

$root = "C:\Code\solution_root\"

# Find all csproj files.
$projects = Get-ChildItem -Path $root -Recurse -ErrorAction SilentlyContinue -Filter *.csproj | Where-Object { $_.Extension -eq '.csproj' }

# Use a hashtable to gather a unique list of suffixes we moved for sanity checking - make sure we didn't 
# relocate anything we didn't intend to. This is how I caught I was moving javascript files I didn't intend to.
$suffixes = @{}

# Find foreign resources ending in .resx and .Designer.cs
# Use a regex capture to so we can count uniques.
$pattern = "\.(?<locale>\w{2}(-\w{2,3})?\.(Designer.cs|resx))$"

foreach ($project in $projects)
{
    "Processing {0}" -f $project.FullName

    # Load the csproj file as XML
    $xmlDoc = new-object XML
    $xmlDoc.Load($project.FullName)

    # Set namespace for XPath queries
    $ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
    $ns.AddNamespace("ns", $xmlDoc.DocumentElement.NamespaceURI)

    $count = 0 

    $embeds = $xmlDoc.SelectNodes("//ns:EmbeddedResource",$ns)
    $compiles = $xmlDoc.SelectNodes("//ns:Compile",$ns)

    # Create new conditional ItemGroup node if it does not exist.
    # Side-effect - every csproj will get this new element regardless of whether it 
    # contains foreign resources. That works for us, might not for you.
    $moveToNode = $xmlDoc.SelectSingleNode("//ns:ItemGroup[@Condition=`" '`$(Configuration)'=='Release' `"]", $ns)
    if ($moveToNode -eq $null) {
        # When creating new elements, pass in the NamespaceURI from the parent node.
        # If we don't do this, elements will get a blank namespace like xmlns="", and this will break compilation. 
        # Hat tip to https://stackoverflow.com/questions/135000/how-to-prevent-blank-xmlns-attributes-in-output-from-nets-xmldocument

        $conditionAtt = $xmlDoc.CreateAttribute("Condition")
        $conditionAtt.Value = " '`$(Configuration)'=='Release' "
        $moveToNode = $xmlDoc.CreateElement("ItemGroup", $xmlDoc.Project.NamespaceURI)
        $ignore = $moveToNode.Attributes.Append($conditionAtt)
        $ignore = $xmlDoc.LastChild.AppendChild($moveToNode)
    }

    # Loop over the EmbeddedResource and Compile elements.
    foreach ($resource in ($embeds += $compiles)) {

        # Exclude javascript files which I found in our Web project.
        # These look like *.js.resx or *.js.Designer.cs and were getting picked up by my regex.
        # Yeah, I could make a better regex, but I'd like to see my kids today.
        if ($resource.Include -notmatch "js\.(Designer.cs|resx)$" -and $resource.Include -match $pattern ) {

            # We have a foreign-language resource.

            # Track unique suffixes for reporting later.
            $suffix = $matches['locale']
            if (!$suffixes.ContainsKey($suffix)) {
                $ignore = $suffixes.Add($suffix,"")
            }

            $ignore = $moveToNode.InsertBefore($resource, $null)

            # Count how many we moved per project.
            $count += 1
        }
    }
    "Moved {0} resources in {1}.`n" -f $count, $project.Name
    $xmlDoc.Save($project.FullName)
}
echo "The following unique suffixes were processed."
$suffixes.Keys | sort
like image 646
Larry Silverman Avatar asked Oct 31 '22 20:10

Larry Silverman


1 Answers

You could make use of existing mechanisms in MSBuild that allow you to handle this scenario, like the <Choose> element documentated here and the option to introduce your own build configurations. If your developers only build Debug configurations during their day-to-day work you may not even need your own build configuration: you can set up your project so that that all the foreign language resources are only included in Release builds.

This project file section would make sure that Polish resources are only built in the Release configuration (the language codes are probably wrong, but you get the idea):

<Choose>
  <When Condition=" '$(Configuration)'=='Release' ">
  <ItemGroup>
    <EmbeddedResource Include="Properties\Resources.pl.resx">
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.pl.Designer.cs</LastGenOutput>
      <SubType>Designer</SubType>
    </EmbeddedResource>
    <Compile Include="Properties\Resources.pl.Designer.cs">
      <DependentUpon>Resources.pl.resx</DependentUpon>
      <AutoGen>True</AutoGen>
    </Compile>

    <!-- TODO: add other language resources as necessary-->

  </ItemGroup>
  </When>
</Choose>

In case your developers need to build Release configurations you can create your own configuration ReleaseResx and include the foreign language .resx files only in that configuration.

Both your suggestions would work, too, I just think this approach is cleaner.

like image 115
Jenszcz Avatar answered Nov 14 '22 03:11

Jenszcz