Skip to content

MSBuild Resource Naming Convention Breaking Change #16964

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
benvillalobos opened this issue Feb 3, 2020 · 11 comments · Fixed by #17044
Closed

MSBuild Resource Naming Convention Breaking Change #16964

benvillalobos opened this issue Feb 3, 2020 · 11 comments · Fixed by #17044
Assignees
Labels
breaking-change Indicates a .NET Core breaking change

Comments

@benvillalobos
Copy link
Member

benvillalobos commented Feb 3, 2020

Resource Naming Conventions Have Changed

For those that would like a slightly higher level explanation and maybe even a mediocre diagram of what's going on here, see this gist I created.

Version introduced

3.0, via applying DependentUpon convention, but more specifically applying that convention to localized resources. Here is the related MSBuild issue that tracked this.

Old behavior

When the DependentUpon metadata was set, the manifest name would be FullTypeName.resources where FullTypeName is Namespace.Classname. When the DependentUpon metadata was NOT set, the manifest names default to: FullTypeName.FolderPathRelativeToRoot.Culture.resources.

New behavior

When a resx is associated with a type like in the winforms case, the generally presumed convention is that the resource is called FullTypeName.resources (the culture may or may not be present, FullTypeName.Culture.resources for example).

From this presumed convention, the rules around how RootNamespace and folders impact the name chosen mirror how VS picks a namespace and name for a new class: RootNamespace.FoldersSeparatedByDots.FileName.(resx|resources|cs)

So if you leave all the defaults when you add files and resources in VS, then when you have Type.cs next to Type.resx, you will get FullNameOfType.resources. However, when you stray from the defaults, there’s more to it.

The points below are the rules with which manifest resource names are generated:

1. If the metadata LogicalName %(EmbeddedResource.LogicalName) is set, then use it as is.

Examples:

      <EmbeddedResource Include="X.resx" LogicalName="SomeName.resources" />
      <EmbeddedResource Include="X.fr-FR.resx" LogicalName="SomeName.resources" />

Then the resource is SomeName.resources, irrespective of the .resx file name, culture, or any other metadata.

2. else if the metadata ManifestResourceName %(EmbeddedResource.ManifestResourceName) is set then add the .resources extension and use it.

Examples:

<EmbeddedResource Include="X.resx" ManifestResourceName="SomeName" />

Then the resource name is SomeName.resources

<EmbeddedResource Include="X.fr-FR.resx" ManifestResourceName="SomeName.fr-FR" />

Then the resource name is SomeName.fr-FR.resources

3. else if %(EmbeddedResource.DependentUpon) is set to a source file, then parse the source file and add culture and .resources extension to the full type name of the first class in the file. This allows types to be renamed and have the corresponding resource change names accordingly.

Given Q.cs:

  namespace SomeNamespace { class SomeType {} }}

Examples:

<EmbeddedResource Include="X.resx" DependentUpon="Q.cs">

Then the resource name is SomeNamespace.SomeType.resources

<EmbeddedResource Include="X.fr-FR.resx" DependentUpon="Q.cs">

Then the resource name is SomeNamespace.SomeType.fr-FR.resources

4. else if EmbeddedResourceUseDependentUponConvention is set to true (which is true by default for projects targeting .net core 3+), then choose DependentUpon as follows and perform rule 3 above.

If there is a source file in the same folder that has the same name as the resx when not including culture or file extensions, choose DependentUpon from the resx to the source file.

Example:

  Src/
    Q.cs: namespace SomeNamespace { class SomeType {} }}
    Q.resx --> resource name is SomeNamespace.SomeType.resources
    Q.fr-Fr.resx --> resource name is SomeNamespace.SomeType.fr-FR.resources

5. else let RelativePath = %(EmbeddedResource.Link) if set, else %(EmbeddedResource.Identity). (This is the path in solution explorer from project root to file.) Then use $(RootNamespace).(RelativePathWithDotsForSlashes).Culture?.resources

Reason for change

We had various issues(1, 2, 3) revolving around the desire to get users to migrate to .NET Core and how MSBuild handles embedded resources being a blocker for that.

Recommended action

If you're unsatisfied with the manifest names that are currently being generated, simply add this line to your project file:

<EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>

Setting this tag to false is how you opt out of this convention entirely. Do note that rules 1 and 2 above still apply.

Otherwise, modify your resource file metadata to satisfy one of the above rules.

Category

  • ASP.NET Core
  • C#
  • MSBuild
  • Windows Forms
  • Windows Presentation Foundation (WPF)

Issue metadata

  • Issue type: breaking-change
@lvmajor
Copy link

lvmajor commented Mar 30, 2020

Shouldn't this be well documented/clearly indicated in official globalization docs? Specifically in the Using one resource string for multiple classes part where if performing this as recommended, it won't work at all (that is simply creating a class named SharedResource.cs and placing it in the Resources folder along with the other resx files)

@gewarren
Copy link
Contributor

@os1r1s110 or @benvillalobos Can you elaborate on why the shared resources won't work given this change? I'm happy to update the docs with this info. Is the fix to enable the shared resources to work to set EmbeddedResourceUseDependentUponConvention to false in the project file?

@vocko
Copy link

vocko commented Aug 19, 2020

@gewarren
Here is my example (did cost me a few hours pf my life to figure out what's going on):

I did set this line services.AddLocalization(options => options.ResourcesPath = "Resources");

And then, this works:

services.AddMvc()
                .AddDataAnnotationsLocalization(options => 
                {
                    options.DataAnnotationLocalizerProvider = (type, factory) =>
                    {
                        var assemblyName = new AssemblyName(typeof(DataAnnotations).GetTypeInfo().Assembly.FullName);
                        return factory.Create("DataAnnotations", assemblyName.Name);
                    };  
                });

This does not work:

services.AddMvc()
                .AddDataAnnotationsLocalization(options => 
                {
                    options.DataAnnotationLocalizerProvider = (type, factory) =>
                        factory.Create(typeof(DataAnnotations));
                });

The latter scenario will work though, if you remove the ResourcesPath. This should be clearly stated on the documentation as it is very confusing at the moment.

@JohnYoungers
Copy link

Here's a fun one I ran into today:

I have a test project that includes seed data in scripts (one script per table):

  <ItemGroup>
    <EmbeddedResource Include="**\*.sql" />
  </ItemGroup>

The build was completely ignoring this file:
dbo.bin.sql

Renaming the file to dbo.bin1.sql works fine; I'm guessing there's some sort of filter somewhere ignoring things that end with bin

@Ponant
Copy link

Ponant commented Aug 18, 2021

@benvillalobos , 3 worked if I remove the .cs extension for DependentUpon. On .Net 6.

<EmbeddedResource Include="MyResources/SharedResources.en.resx" DependentUpon="SharedResources"/>

@benvillalobos
Copy link
Member Author

@Ponant Does this happen prior to net6?

@Ponant
Copy link

Ponant commented Aug 18, 2021

@benvillalobos , I can't tell you, I switched from .Net Core 2.1 to .Net 6 as I worked on other frameworks. This issue is found on a new template from visual studio 2022, Version 17.0.0 Preview 3.1, if that helps.
If I put the .cs extension, I get

Severity	Code	Description	Project	File	Line	Suppression State
Error	MSB3577	Two output file names resolved to the same output path: "obj\Debug\net6.0\RPIdentity.SharedResources.fr.resources"	
RPIdentity
C:\Program Files\Microsoft Visual Studio\2022\Preview\MSBuild\Current\Bin\amd64\Microsoft.Common.CurrentVersion.targets	3262	

@benvillalobos
Copy link
Member Author

@Ponant Thanks for bringing this to my attention! Could you file a feedback ticket on the Developer Community site and include binary logs or a minimal project that reproduces the issue with the ticket? Please link the issue here so I can expedite routing it to our team.

@Ponant
Copy link

Ponant commented Aug 18, 2021

@benvillalobos , I would gladly do so but I cannot at the moment because I have a very poor internet connection which makes it already hard to only reply in this comment! I could try without the binary logs as these might be heavy, otherwise you will have to wait for September 5 as I will be back with a good internet connection. Let me know. I am not sure I could upload a project on my github repo either, but I can try. Anyways, the project is a very brand new template with the localization middleware enabled.

@Ponant
Copy link

Ponant commented Aug 18, 2021

@benvillalobos , it fails also for .Net 5.

@Ponant
Copy link

Ponant commented Aug 18, 2021

@benvillalobos , also fails with .Net Core 3.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking-change Indicates a .NET Core breaking change
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants