c# path of localized resources for plugin DLLs
Asked Answered
A

2

6

In my C# application, I've a plugin mechanism that loads plugin DLLs from different pathes as specified in a configuration XML file. My application is localizable. The main assembly (the *.exe) has satellite assemblies for the localized languages next to the exe in the standard .NET way (e.g. .\en\en-US\main.resources.dll; .\de\de_DE\main.resources.dll; etc.).

I started localizing a plugin and had to discover that the satellite assembly has to be put in the folders next to the exe. When putting it next to the plugin DLL, the resource manager doesn't find it.

However, since my plugins are interchangable and potentially in different folders, I would highly prefer to put the localized resource assemblies next to the plugins and not to the exe.

Is this possible?!?!

An alternative I could live with would be to embed the localized resources into the DLLs. Is this possible??

Cheers, Felix

Abreast answered 8/7, 2011 at 6:55 Comment(4)
This approach works fine for us. I'm just wondering that your satellite-assemblies are in subfolders wheres I've only seen something like this .\de-DE\Assembly.resources.dll etc.Webby
Oops, you might be right. I only have a "de" resource in practive, but I always thought it would be subfolders for "de-DE". Anyway, do you use plugins / external DLLs in different folders than the executable??Abreast
Yes plugins are in a subfolder of the executable path. . refers to the directory of the application, then the structure is as follows. .\Plugins\PluginX.dll and their resources are stored like this .\Plugins\de-DE\PluginX.resources.dll, .\Plugins\us-GB\PluginX.resources.dll and so on.Webby
Are you sure?? Cause I've the following structure and the ResourceManager doesn't use the localized resources: . is the folder of the executable and working directory containing .\App.exe. .\de\App.resources.dll are the localized resources of the executable. .\plugins\testplugin\testplugin.dll is the plugin DLL. .\plugins\testplugin\de\testplugin.resources.dll contains the localized resources of the plugin. Anyway, I also copied the plugin plus resources into .\plugins\testplugin.dll as in your post, but it still uses the invariant default resource strings embedded in the plugin.Abreast
N
1

I ran into this issue when working on a product for our company. I didn't find an answer anywhere, so I'm going to post my solution to it here in case someone else finds themselves in the same situation.

As of .NET 4.0 there is a solution to this issue, because satellite assemblies now get passed to the AssemblyResolve handler. If you already have a plugin system where assemblies can be loaded from remote directories, you'll probably already have an assembly resolve handler in place, you just need to extend it to use a different search behaviour for satellite resource assemblies. If you don't have one, the implementation is non-trivial since you basically take responsibility for all assembly search behaviour. I'll post the complete code for a working solution so either way you'd be covered. First of all, you need to hook your AssemblyResolve handler somewhere, like this:

AppDomain.CurrentDomain.AssemblyResolve += ResolveAssemblyReference;

Then assuming you've got a couple of variables to hold path information for your main application and your plugin directories, like this:

string _processAssemblyDirectoryPath;
List<string> _assemblySearchPaths;

Then you need a little helper method that looks a little like this:

static Assembly LoadAssembly(string assemblyPath)
{
    // If the target assembly is already loaded, return the existing assembly instance.
    Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
    Assembly targetAssembly = loadedAssemblies.FirstOrDefault((x) => !x.IsDynamic && String.Equals(x.Location, assemblyPath, StringComparison.OrdinalIgnoreCase));
    if (targetAssembly != null)
    {
        return targetAssembly;
    }

    // Attempt to load the target assembly
    return Assembly.LoadFile(assemblyPath);
}

And finally you need the all important AssemblyResolve event handler, which looks a little something like this:

Assembly ResolveAssemblyReference(object sender, ResolveEventArgs args)
{
    // Obtain information about the requested assembly
    AssemblyName targetAssemblyName = new AssemblyName(args.Name);
    string targetAssemblyFileName = targetAssemblyName.Name + ".dll";

    // Handle satellite assembly load requests. Note that prior to .NET 4.0, satellite assemblies didn't get
    // passed to AssemblyResolve handlers. When this was changed, there is a specific guarantee that if null is
    // returned, normal load procedures will be followed for the satellite assembly, IE, it will be located and
    // loaded in the same manner as if this event handler wasn't registered. This isn't sufficient for us
    // though, as the normal load behaviour doesn't correctly locate satellite assemblies where the owning
    // assembly has been loaded using Assembly.LoadFile where the assembly is located in a different folder to
    // the process assembly. We handle that here by performing the satellite assembly search process ourselves.
    // Also note that satellite assemblies are formally documented as requiring the file name extension of
    // ".resources.dll", so detecting satellite assembly load requests by comparing with this known string is a
    // valid approach.
    if (targetAssemblyFileName.EndsWith(".resources.dll"))
    {
        // Retrieve the owning assembly which is requesting the satellite assembly
        string owningAssemblyName = targetAssemblyFileName.Replace(".resources.dll", ".dll");
        Assembly owningAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((x) => x.Location.EndsWith(owningAssemblyName));
        if (owningAssembly == null)
        {
            return null;
        }

        // Retrieve the directory containing the owning assembly
        string owningAssemblyDirectory = Path.GetDirectoryName(owningAssembly.Location);

        // Search for the required satellite assembly in resource subdirectories, and load it if found.
        CultureInfo searchCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
        while (searchCulture != CultureInfo.InvariantCulture)
        {
            string resourceAssemblyPath = Path.Combine(owningAssemblyDirectory, searchCulture.Name, targetAssemblyFileName);
            if (File.Exists(resourceAssemblyPath))
            {
                Assembly resourceAssembly = LoadAssembly(resourceAssemblyPath);
                if (resourceAssembly != null)
                {
                    return resourceAssembly;
                }
            }
            searchCulture = searchCulture.Parent;
        }
        return null;
    }

    // If the target assembly exists in the same directory as the requesting assembly, attempt to load it now.
    string requestingAssemblyPath = (args.RequestingAssembly != null) ? args.RequestingAssembly.Location : String.Empty;
    if (!String.IsNullOrEmpty(requestingAssemblyPath))
    {
        string callingAssemblyDirectory = Path.GetDirectoryName(requestingAssemblyPath);
        string targetAssemblyInCallingDirectoryPath = Path.Combine(callingAssemblyDirectory, targetAssemblyFileName);
        if (File.Exists(targetAssemblyInCallingDirectoryPath))
        {
            try
            {
                return LoadAssembly(targetAssemblyInCallingDirectoryPath);
            }
            catch (Exception ex)
            {
                // Log an error
                return null;
            }
        }
    }

    // If the target assembly exists in the same directory as the process executable, attempt to load it now.
    string processDirectory = _processAssemblyDirectoryPath;
    string targetAssemblyInProcessDirectoryPath = Path.Combine(processDirectory, targetAssemblyFileName);
    if (File.Exists(targetAssemblyInProcessDirectoryPath))
    {
        try
        {
            return LoadAssembly(targetAssemblyInProcessDirectoryPath);
        }
        catch (Exception ex)
        {
            // Log an error
            return null;
        }
    }

    // Build a list of all assemblies with the requested name in the defined list of assembly search paths
    Dictionary<string, AssemblyName> assemblyVersionInfo = new Dictionary<string, AssemblyName>();
    foreach (string assemblyDir in _assemblySearchPaths)
    {
        // If the target assembly doesn't exist in this path, skip it.
        string assemblyPath = Path.Combine(assemblyDir, targetAssemblyFileName);
        if (!File.Exists(assemblyPath))
        {
            continue;
        }

        // Attempt to retrieve detailed information on the name and version of the target assembly
        AssemblyName matchAssemblyName;
        try
        {
            matchAssemblyName = AssemblyName.GetAssemblyName(assemblyPath);
        }
        catch (Exception)
        {
            continue;
        }

        // Add this assembly to the list of possible target assemblies
        assemblyVersionInfo.Add(assemblyPath, matchAssemblyName);
    }

    // Look for an exact match of the target version
    string matchAssemblyPath = assemblyVersionInfo.Where((x) => x.Value == targetAssemblyName).Select((x) => x.Key).FirstOrDefault();
    if (matchAssemblyPath == null)
    {
        // If no exact target version match exists, look for the highest available version.
        Dictionary<string, AssemblyName> assemblyVersionInfoOrdered = assemblyVersionInfo.OrderByDescending((x) => x.Value.Version).ToDictionary((x) => x.Key, (x) => x.Value);
        matchAssemblyPath = assemblyVersionInfoOrdered.Select((x) => x.Key).FirstOrDefault();
    }

    // If no matching assembly was found, log an error, and abort any further processing.
    if (matchAssemblyPath == null)
    {
        return null;
    }

    // If the target assembly is already loaded, return the existing assembly instance.
    Assembly loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((x) => String.Equals(x.Location, matchAssemblyPath, StringComparison.OrdinalIgnoreCase));
    if (loadedAssembly != null)
    {
        return loadedAssembly;
    }

    // Attempt to load the target assembly
    try
    {
        return LoadAssembly(matchAssemblyPath);
    }
    catch (Exception ex)
    {
        // Log an error
    }
    return null;
}

The first part of that event handler deals with satellite resource assemblies, then the search behaviour I use for regular assemblies follows that. This should be enough to help anyone get a system like this working from scratch.

Niblick answered 27/3, 2017 at 12:19 Comment(0)
B
0

Ok If you want "detach" yoursefl from standart Localization resource binding, and want to have freedom to load an assembly from any location, one of the options is to

a) implement an interface to interact with translations within that assembly

b) use Assembly.Load function to load .NET assembly you want from location you want

Barathea answered 8/7, 2011 at 7:1 Comment(3)
I don't think this answers the question. If it does, please elaborate.Dynamics
I still doubt this applies to resource DLLs.Dynamics
well, I'd still like to use the ResourceManager and stay with the standard. But there might be an option to specify the location of the resources more flexibly...Abreast

© 2022 - 2024 — McMap. All rights reserved.