How to make settings provider inside a dynamically loaded assembly available for reflection?
Asked Answered
A

1

7

Following this question, I successfully created my custom settings provider in the legacy C# app we are developing. It is referenced via the SettingsProvider attribute:

public sealed class MySettings : SettingsProvider
{
    ...
}

[SettingsProvider(typeof(MySettings))]
internal sealed partial class Settings {}

However, now I ran into another problem.

Our client app includes an autoupdate facility, and it is implemented so that the bulk of the client - including the classes above - is built into a DLL (let's call it client.dll here), which is then used by an EXE. The EXE first checks for updates and downloads the latest one from the update server if needed, replacing all DLLs etc. with their newer version (including client.dll). In order to be able to replace DLLs at runtime, it can't link to them statically. So after the update, it loads client.dll and runs it like this:

Assembly assy = Assembly.LoadFile(
        AppDomain.CurrentDomain.BaseDirectory + "client.dll");
object frm = assy.CreateInstance("Client.Forms.MainForm");
Application.Run((Form)frm);

The unfortunate consequence of this is that the framework can't find my custom settings provider class inside the dynamically loaded assembly. I tried to use LoadFrom instead of LoadFile above but it didn't help. The only working solution I found so far is to implement a proxy class in the loader exe with the same name as the real settings provider, which is found by the framework all right. The proxy then instantiates the real settings provider from the client assembly and delegates all calls to it.

This seems to work but I am not happy with it. Is there a way to help the framework find my class inside a dynamically loaded assembly directly?

Update

The error message I get:

System.Configuration.ConfigurationErrorsException: Failed to load provider type: Client.Properties.MySettings, Client, Version=4.0.1341.0, Culture=neutral, PublicKeyToken=null.
   at System.Configuration.ApplicationSettingsBase.get_Initializer()
   at System.Configuration.ApplicationSettingsBase.CreateSetting(PropertyInfo propInfo)
   at System.Configuration.ApplicationSettingsBase.EnsureInitialized()
   at System.Configuration.ApplicationSettingsBase.get_Properties()
   at System.Configuration.SettingsBase.GetPropertyValueByName(String propertyName)
   at System.Configuration.SettingsBase.get_Item(String propertyName)
   at System.Configuration.ApplicationSettingsBase.GetPropertyValue(String propertyName)
   at System.Configuration.ApplicationSettingsBase.get_Item(String propertyName)
   at Client.Properties.Settings.get_SomeConfigSetting()
   at ...

Via debugging and log messages I determined that the initializer method of the class is never called, so the class is never instantiated. In other words, there seems to be no hidden initialization error behind the above exception.

One more potentially important bit: the client is currently running on .NET 2.0 and there are no plans to upgrade in the foreseeable future.

Update 2

I started to investigate the AppDomain as suggested by @jwddixon's answer. First I wanted to check whether Client.dll really ends up in a different app domain than that of the caller EXE. So I listed the assemblies in the current app domain, and saw that Client is in fact there. But to my surprise, I noticed that there are actually two assemblies named Client in the list - both the EXE and the DLL have the same assembly name, but different version (4.0.0.0 for the EXE, 4.0.1352.0 for the DLL at present). I was not fully aware of this so far, and this may be important. I will try changing the EXE assembly name next...

Update 3

...and that actually fixed the problem! Aaargh... Towards my unknown predecessors who invented this contorted scheme for who knows why, I have very unfond thoughts at this moment... but kudos to all of you guys for contributing questions and ideas which eventually lead to the solution!

Area answered 1/3, 2013 at 8:24 Comment(14)
Try to create "Client.Forms.MainForm" using Activator class. msdn.microsoft.com/en-us/library/system.activator.aspxModigliani
When you say "the framework can't find my custom settings provider class", is there an error? Could you expand a bit more?Piliform
So it's more an assembly load problem, right? Have you tried to specify the provider type name to something different, like "Client.Properties.MySettings, Client" w/o version, etc.Piliform
@SimonMourier, I am not sure I understand you - the assembly was already loaded successfully by Assembly.LoadFile() as shown above. To my best knowledge I don't specify assembly name or version with the provider type name.Seumas
I was under the impression that, once loaded, an assembly could not be unloaded without unloading the AppDomain. This documentation seems to support that. Are you sure the updated client.dll is actually being loaded?Feeley
Oh, ok, I think got it now :-) How about hooking the AppDomain.CurrentDomain.AssemblyResolve event prior to any action, and point to the good assembly once requested?Piliform
@JimEvans, yes, the assembly is loaded successfully and is never unloaded. The Client.Properties.Settings class, mentioned in the exception stack trace, actually sits in the assembly (along with a host of other classes in the call chain, not shown here).Seumas
Assembly.LoadFile() is an extremely evil method, never use it. Update your question with the trace you get out of Fuslogvw.exe if Load or LoadFrom still fails.Acrogen
@HansPassant, it is not LoadFile or LoadFrom which is failing here. The assembly itself is found and loaded without any error message. I did as you suggested though, but Fuslogvw did not show any relevant binding failures.Seumas
@HansPassant, would you give more details on why LoadFile is evil? I found contradicting information about LoadFrom vs LoadFile so far, and no clear explanation on their exact difference(s).Seumas
FYI finally I had time to return to this problem - I added an update with my recent findings.Seumas
@SimonMourier, I tried to hook up AssemblyResolve but it actually never gets called (probably due to having two assemblies with the same name, the framework always happily finds the wrong one without asking).Seumas
Yes, seems logical. Have you tried the TypeResolve event then?Piliform
@SimonMourier, I did, and it isn't called either. Documentation says it may not always get called, e.g. if the framework can see that the type is not present in a known static assembly - this seems to be the case here.Seumas
C
1

I haven't tried this but how about a something like:

AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
AppDomain domain = AppDomain.CreateDomain("myAppDomain", null, setup)
setup.ApplicationBase = file;

Assembly assy = domain.Load(AssemblyName.GetAssemblyName(
AppDomain.CurrentDomain.BaseDirectory +
 "client.dll"));

object frm = assy.CreateInstance("Client.Forms.MainForm");
Application.Run((Form)frm);
Cannonball answered 6/3, 2013 at 10:56 Comment(4)
Sounds promising (I was also thinking along this line, adding some information on where to get assembly info etc.), but could elaborate? What does it do?Esthete
It seems plausible that you essentially want to access classes outside your current application domain. Microsoft uses application domains to ensure you aren't straying outside your allocated boundaries. In order to get full access outside the domain, you first need to make your own, then load the DLL into it. At this point (as long as you have set it up correctly) you can create local instances of external objects. You may need to manually load into local memory the classes you don't have access to as well.Cannonball
The file variable is referred in your example but never defined - could you add it?Seumas
Although you didn't give the exact solution, your answer gave important hints towards finding the root cause, so I award you the bounty (otherwise it would be lost anyway).Seumas

© 2022 - 2024 — McMap. All rights reserved.