0

I'm trying to build a simple client for testing an internal authentication service. The main idea is to just have a simple tool for testing to ensure that this service is callable (and successfully authenticates) in each environment.
In any client application that calls this service, a few settings need to be defined in App.config. The primary one that is used is AuthenticationService which contains a value that is a URL to this remote auth service, which is read by a DLL that is included in the client. This URL is different for each environment (Dev, QA, Prod).

For Example:

  <configSections>
    <sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=12345" >
      <section name="Authenticator.Settings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=12345" requirePermission="false"/>
      <section name="RemoteAuthenticator.TokenCache.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=12345" requirePermission="false" />
    </sectionGroup>
  </configSections>

  <applicationSettings>
    <RemoteAuthenticator.Properties.Settings>
      <setting name="AuthenticationService" serializeAs="String">
        <value>https://environment-url</value>
      </setting>
    </RemoteAuthenticator.TokenCache.Properties.Settings>
  </applicationSettings>

The problem I'm running into is building a client that is able to call the service in different environments for testing. The value for the URL in the setting AuthenticationService needs to be changed at runtime when the user makes requests to different environemnts, but I have been unable to find a way to do this.

The client DLL used to call this service provides the following way to add an auth token to an HttpClient:

new AuthorizationHelper().AddAuthorization(httpClient);

These methods from the DLL call the service (at the URL specified within the client App.config, and add the returned auth token to a header within the provided HttpClient.

That is the extent of how this service is called, as the target environment URL from App.config is read by the DLL when this is called. These methods in the DLL are basically a blackbox for me, as I have no way to modify how the URL is retrieved.

So far I have tried accessing this setting directly and via a ConfigurationManager, but both ways show zero settings. After messing around with that for awhile, I tried creating multiple App.config files, one for each environment, and attempted to re-load them at runtime as needed (as outlined in this post: https://stackoverflow.com/a/6151688/1428743). This last solution somewhat works, but only for the first call. Any successive calls to a different environement do not use the URL from the reloaded config, just the URL from the config loaded before the first call to the service.

Is there any way I can modify this setting at runtime in order to call different environments with this tool as needed?

Community
  • 1
  • 1
PseudoPsyche
  • 4,332
  • 5
  • 37
  • 58
  • Can you clarify what you mean by *"the user makes requests to different environments"*? How does the user call different environments? – Ron Beyer Aug 03 '15 at 19:43
  • I'm just making a simple UI to make request to different environments, for example a different button for each. This is just a super simple tool to use for testing to ensure this service is reachable (and successfully authenticates) in each environment. – PseudoPsyche Aug 03 '15 at 19:47
  • Do you mean the user's environment, or the server? I'm just confused by what you mean by "environment" because it looks like you are using it in the context of the environment that the service is running on, not the test is running in... – Ron Beyer Aug 03 '15 at 19:49
  • Yes, the one the service is running in. This client will call the service as it exists in Dev, QA, and Prod to ensure it is working in each of those environments. The client will just be running locally on a developers machine, making calls to each environment that the service exists in. – PseudoPsyche Aug 03 '15 at 19:56
  • I updated the OP a little bit to specify a little more on how the service is called from a client. – PseudoPsyche Aug 03 '15 at 20:20

1 Answers1

0

After quite a bit of digging, I've found that this actually is not possible in my case. I'll outline what I've discovered, as it may help someone later.

Firstly, I couldn't find a way to modify the app settings values in app.config that are used by the loaded assembly. The way I got around this was to create a different app.config for each environment I was testing against and loaded them on the fly. This can be achieved using the approach outlined here: Change default app.config at runtime

Secondly, after an assembly is loaded, it is not possible to change the app.config that it has read without unloading that assembly. It is actually impossible to unload assemblies individually, so you must unload the entire AppDomain. The best way to get this desired behavior (from what I can see) is to create your own AppDomain and create an instance of the object you need from the assembly from within that domain.

For example:

AppDomain newDomain = AppDomain.CreateDomain("newDomain", null, AppDomain.CurrentDomain.SetupInformation);
YourObject obj = newDomain.CreateInstanceAndUnwrap("Your.Assembly.Name", typeof(YourObject).FullName) as YourObject;
// Do stuff
AppDomain.Unload(newDomain);

This will allow you to call any methods you need on an instance of YourObject from within newDomain. What this gains you is the ability to unload this other domain, modify/reload the app.config, then reload the assembly in this other domain using your new app.config. Please note in the above code that you must pass the full type name as the second parameter of CreateInstanceAndUnwrap, as noted here: https://stackoverflow.com/a/20918117/1428743. Also, you must unwrap the value returned by a call to CreateInstance. CreateInstanceAndUnwrap simply does this for you all in one shot. For more details on unwrapping, and why it is necessary: https://stackoverflow.com/a/13366528/1428743. Additionally, you may be wondering why we don't just create a new domain and unload the existing, default domain. The default domain cannot be unloaded, nor can any domain that is running in a thread that can't be stopped immediately. In these cases you will end up with a CannotUnloadAppDomainException: https://msdn.microsoft.com/en-us/library/system.cannotunloadappdomainexception(v=vs.110).aspx

Finally, this may work for you, but only in a specific case. In order for CreateInstance or CreateInstanceAndUnwrap to work, the class you are trying to load from the assembly must inherit from MarshalByRefObject or be marked as Serializable. If neither of these are the case (as it was for me), you will be unable to create an instance to call from the other AppDomain. More details on the difference between these two cases here: https://stackoverflow.com/a/7047153/1428743

So in the end my solution was to simply disallow the user from modifying their environment setting after the assembly has been loaded. While this isn't the best user experience (as it requires a restart of the app), it is the only way to achieve in my case as I do not have access to the code for the class in the assembly.

Community
  • 1
  • 1
PseudoPsyche
  • 4,332
  • 5
  • 37
  • 58