Override a resource from standard assembly in ASP.NET
Asked Answered
M

5

8

I want to override a string from a System.ComponentModel.DataAnnotations for an ASP.NET project. Do I need to make a satellite assembly, messing with custom build tasks, al.exe etc.? Even if yes, I couldn't find how to convert .resx to .resources to feed it to al.exe. And if no, where to put the .resx. and how to name it?

UPD: To make it clear: I wanted to use a custom resource string instead of one from the default resource from the assembly. I didn't want to make changes in the every place that uses that string. After all, the resources exist just for overriding them.

Manx answered 29/3, 2010 at 12:52 Comment(0)
M
2

While this is strange, especially for people familiar with open source localization technologies, one cannot build a satellite assembly for any system assembly or even a 3rd-party signed one:

If your main assembly uses strong naming, satellite assemblies must be signed with the same private key as the main assembly. If the public/private key pair does not match between the main and satellite assemblies, your resources will not be loaded.

Whether the same is possible automatically, but without a satellite assembly, is unknown, though I doubt that.

Manx answered 7/4, 2010 at 13:45 Comment(0)
O
5

Phil Haack has an excellent article Localizing ASP.Net MVC Validation which specifically guides you through overriding your strings. This article applies more to DataAnnotations than it does ASP.net MVC. So, this will help however your are using DataAnnotattions.

Below I have listed the simplest steps to add Localized Resources in Visual Studio.

  1. Open the Project Properties dialog.
  2. Select the Resources tab.
  3. Click to create a new default resources file.
  4. This will create two files in your Properties folder.
    • Resources.resx
    • Resources.Designer.cs
  5. When Resources.resx has opened, change it's Access Modifier to Public.
  6. Add your strings.

To add additional resource files for specific cultures you will need to.

  1. Right click your Project in the Solution Explorer.
  2. Select Add -> New Item -> Resource File.
  3. Name it Resources.en-us.resx. (replace 'en-us' with appropriate code)
  4. Click Add
  5. Drag it into the Properties folder.
  6. Open Resources.en-us.resx and change it's Access Modifier to Public.
  7. Add your strings.
  8. Repeat for each Culture you need to support.

During the build VS will convert the .resx files to .resource files and build wrapper classes for you. You can then access via the namespace YourAssembly.Properties.Resources.

With this using statement.

using YourAssembly.Properties;

You can decorate with attributes like this:

[Required(ErrorMessageResourceType = typeof(Resources), ErrorMessageResourceName = "MyStringName")]

Note: I used the Properties folder for consistency. To use the App_GlobalResources move your .resx files there and change your using statement to match the directory name. Like this:

using YourAssembly.App_GlobalResources;

Edit: The closest that you can get to Strongly Typed resource names would be to do something like this:

public class ResourceNames
{
    public const string EmailRequired = "EmailRequired";
}

You can then decorate with attributes like this.

[Required(ErrorMessageResourceType = typeof(Resources), ErrorMessageResourceName = ResourceNames.EmailRequired)]

To enable automatic client culture detection add the globalizationsection to the web.config file.

<configuration>
    <system.web>
        <globalization enableClientBasedCulture="true" culture="auto:en-us" uiCulture="auto:en-us"/>
    </system.web>
<configuration>

Here I have enabled a client based culture and set the culture and the uiculture to "auto" with a default of "en-us".


Creating Separate Satellite Assemblies:

The MSDN Creating Satellite Assemblies article will help as well. If you are new to satellite assemblies make sure you read Packaging and Deploying Resources.

When creating satellite assemblies in the past, I have found it useful to use VS build events. These are the steps I would take.

  1. Create a separate Class Library project in my solution.
  2. Create or Add my .resx files to this project.
  3. Add a Post-Build Event to the Project Properties dialog. (Like the one below)

Sample VS Post-Build Script:

set RESGEN="C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\resgen.exe"
set LINKER="C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\al.exe"
set ASSEMBLY=$(TargetName)
set SOURCEDIR=$(ProjectDir)
Set OUTDIR=$(TargetDir)

REM Build Default Culture Resources (en)
%RESGEN% %SOURCEDIR%en\%ASSEMBLY%.en.resx  %SOURCEDIR%en\%ASSEMBLY%.resources

REM Embed Default Culture
%LINKER% /t:lib /embed:%SOURCEDIR%en\%ASSEMBLY%.resources /culture:en /out:%OUTDIR%%ASSEMBLY%.resources.dll
REM Embed English Culture
IF NOT EXIST %OUTDIR%en\ MKDIR $%OUTDIR%en\
%LINKER% /t:lib /embed:%SOURCEDIR%en\%ASSEMBLY%.resources /culture:en /out:%OUTDIR%en\%ASSEMBLY%.resources.dll


REM These are just a byproduct of using the project build event to run the resource build script
IF EXIST %OUTDIR%%ASSEMBLY%.dll DEL %OUTDIR%%ASSEMBLY%.dll
IF EXIST %OUTDIR%%ASSEMBLY%.pdb DEL %OUTDIR%%ASSEMBLY%.pdb

If you would prefer not to use ResGen.exe to convert your .resx files, you could do something like this.

using System;
using System.Collections;
using System.IO;
using System.Resources;

namespace ResXConverter
{
    public class ResxToResource
    {
        public void Convert(string resxPath, string resourcePath)
        {
            using (ResXResourceReader resxReader = new ResXResourceReader(resxPath))
            using (IResourceWriter resWriter = new ResourceWriter(
                    new FileStream(resourcePath, FileMode.Create, FileAccess.Write)))
            {
                foreach (DictionaryEntry entry in resxReader)
                {
                    resWriter.AddResource(entry.Key.ToString(), entry.Value);
                }
                resWriter.Generate();
                resWriter.Close();
            }
        }
    }
}

One of the potential draw backs to doing the conversion this way is the need to reference the System.Windows.Forms.dll. You will still need to use Assembly Linker.

Edit: As wRAR has reminded us if you are signing your assemblies your keys must match.

Often answered 5/4, 2010 at 1:7 Comment(11)
I've already read all these links. The first link suggests placing the resource names in the attributes explicitly, this is not what I want. As for the satellite assemblies, I still can't understand (and even find the relevant place in MSDN) how should the assembly, the namespace and the resource be named to be loaded by the runtime and whether additional requirements exist. System.ComponentModel.DataAnnotations.resources.dll containing System.ComponentModel.DataAnnotations.resources or System.ComponentModel.DataAnnotations.Resources.DataAnnotationsResources.resources apparently is not loaded.Manx
@wRAR: Before we get to naming conventions, are you wanting to specify at runtime what kind of message is displayed or is it that you don't want strings in your attributes?Often
I just want to localize standard strings.Manx
@wRAR: I just got a chance to add the VS section to the answer. Let me know if this solves your problem.Often
Do you mean the solution which is the copy of the one from the first link? I can say about it just the same as I've already said about the first link.Manx
@wRAR: Both of the solutions I have given will do what you seem to be asking. Would you go into more detail as to what specifically you are wanting to achieve? Thanks.Often
First solution involves placing the resource name into all attribute, which is unacceptable, as I've already said in the first comment. Second solution just doesn't work, which I've said in the first comment too.Manx
@wRAR: The Packaging and Deploying Resources link above will clear up your confusion about resource/satellite assembly naming conventions as well as the appropriate directory deployment structure. Let me know if you are still unable to get this working after incorporating my edits from today.Often
OK, I've found this at last. msdn.microsoft.com/en-us/library/ff8dk041.aspx (so the answer is "No way").Manx
@wRAR: Have you considered a custom validation attribute that selects what string to display in the FormatErrorMessage call?Often
That will be more like "a dozen of subclassed data annotation attributes and validator controls".Manx
P
3

I want to provide an answer with the same idea as by Duncan Smart, but for .NET Core 2.2 instead of .NET Framework 4.x.

Here it is.

using System;
using System.Linq;
using System.Reflection;
using System.Resources;

public static class ResourceManagerHack
{
    /// <summary>
    /// If the server doesn't have .NET language packs installed then no matter what CurrentUICulture is set to, you'll always get English in 
    /// DataAnnotations validation messages. Here we override DataAnnotationsResources to use a ResourceManager that uses language .resources 
    /// files embedded in this assembly.
    /// </summary>
    public static void OverrideComponentModelAnnotationsResourceManager()
    {
        EnsureAssemblyIsLoaded();

        FieldInfo resourceManagerFieldInfo = GetResourceManagerFieldInfo();
        ResourceManager resourceManager = GetNewResourceManager();
        resourceManagerFieldInfo.SetValue(null, resourceManager);
    }

    private static FieldInfo GetResourceManagerFieldInfo()
    {
        var srAssembly = AppDomain.CurrentDomain
                                  .GetAssemblies()
                                  .First(assembly => assembly.FullName.StartsWith("System.ComponentModel.Annotations,"));
        var srType = srAssembly.GetType("System.SR");
        return srType.GetField("s_resourceManager", BindingFlags.Static | BindingFlags.NonPublic);
    }
    internal static ResourceManager GetNewResourceManager()
    {
        return new ResourceManager($"{typeof(<YourResource>).Namespace}.Strings", typeof(<YourResource>).Assembly);
    }
    private static void EnsureAssemblyIsLoaded()
    {
        var _ = typeof(System.ComponentModel.DataAnnotations.RequiredAttribute);
    }
}

And I call this like so:

public static void Main(string[] args)
{
    ResourceManagerHack.OverrideComponentModelAnnotationsResourceManager();
    CreateWebHostBuilder(args).Build().Run();
}

Furthermore, I created a ~/Resources/<YourResource>.resx file and populated it with the default values and changed them at will. Lastly I created a public empty class <YourResource>.

Parthenia answered 9/8, 2019 at 10:38 Comment(1)
Also great reference for .NET 8 as this still works. Open topic for five years in as MS doesn't allow to override the DataAnnotations default messages by configuration (github.com/dotnet/aspnetcore/issues/4848). This post save me a lot of time. Thanks!Discount
M
2

While this is strange, especially for people familiar with open source localization technologies, one cannot build a satellite assembly for any system assembly or even a 3rd-party signed one:

If your main assembly uses strong naming, satellite assemblies must be signed with the same private key as the main assembly. If the public/private key pair does not match between the main and satellite assemblies, your resources will not be loaded.

Whether the same is possible automatically, but without a satellite assembly, is unknown, though I doubt that.

Manx answered 7/4, 2010 at 13:45 Comment(0)
C
2

If the server doesn't have .NET language packs installed then no matter what CurrentUICulture is set to, you'll always get English in DataAnnotations validation messages. This epic hack works for us.

  • Go to "Microsoft .NET Framework 4.6.1 Language Pack" download page https://www.microsoft.com/en-us/download/details.aspx?id=49977
  • Select language and download
  • Extract NDP461-KB3102436-x86-x64-AllOS-{LANG}.exe with 7-Zip
  • Extract CAB file x64-Windows10.0-KB3102502-x64.cab with 7-Zip
  • Locate "msil_system.componentmod..notations.resources_...."
  • ... in which you'll find "system.componentmodel.dataannotations.resources.dll"
  • Open .resources.dll with ILSpy, locate Resources and click Save button above String Table to save as System.ComponentModel.DataAnnotations.Resources.DataAnnotationsResources.{LANGUAGE}.resources
  • Add to your project under say a "Resources"
  • Ensure the files Build Action property of the resources files is set to "Embedded Resource"

Then in a PreStart method of your project you overwrite the System.ComponentModel.DataAnnotations.Resources.DataAnnotationsResources.resourceMan private static field (told you it was a hack) with the ones you have in your project.

using System;
using System.Linq;
using System.Reflection;
using System.Resources;

[assembly: WebActivator.PreApplicationStartMethod(typeof(ResourceManagerUtil), nameof(ResourceManagerUtil.PreStart))]

class ResourceManagerUtil
{
    public static void PreStart()
    {
        initDataAnnotationsResourceManager();
    }

    /// <summary>
    /// If the server doesn't have .NET language packs installed then no matter what CurrentUICulture is set to, you'll always get English in 
    /// DataAnnotations validation messages. Here we override DataAnnotationsResources to use a ResourceManager that uses language .resources 
    /// files embedded in this assembly.
    /// </summary>
    static void initDataAnnotationsResourceManager()
    {
        var embeddedResourceNamespace = "<YourProjectDefaultNamespace>.<FolderYouSavedResourcesFilesIn>";
        var dataAnnotationsResourcesName = "System.ComponentModel.DataAnnotations.Resources.DataAnnotationsResources";
        var thisAssembly = typeof(ResourceManagerUtil).Assembly;
        var dataAnnotationsAssembly = typeof(System.ComponentModel.DataAnnotations.ValidationAttribute).Assembly;

        var resourceManager = new ResourceManager(embeddedResourceNamespace + "." + dataAnnotationsResourcesName, thisAssembly);

        // Set internal field `DataAnnotationsResources.resourceMan`
        var dataAnnotationsResourcesType = dataAnnotationsAssembly.GetType(dataAnnotationsResourcesName);
        var resmanProp = dataAnnotationsResourcesType.GetField("resourceMan", BindingFlags.NonPublic | BindingFlags.Static);
        resmanProp.SetValue(null, resourceManager);
    }
}
Coston answered 6/10, 2017 at 13:49 Comment(0)
I
1

Assuming that you want to override the default error message strings in the validation attributes, you can do that by setting the ErrorMessageResourceName and the ErrorMessageResourceType properties like this:

[Required(ErrorMessageResourceName = "Required_Username", ErrorMessageResourceType = typeof(MyResourceFile)]
public string Username { get; set; }

You can create a resource file called MyResourceFile.resx that contains Required_Username with the error message you want.

Hope this helps.

Involucre answered 3/4, 2010 at 5:59 Comment(1)
I know this, but I don't want to change all attributes.Manx

© 2022 - 2024 — McMap. All rights reserved.