Tuesday 15 July 2014

The moment I was able to serialize objects across an ASP.NET AppDomain and an NUnit AppDomain

As you can see at the end of How to debug an Cassini hosted website and the UnitTest that uses WatiN to automate that hosted website, although I was now able to start cassini in the current NUnit process, I was still not able to have direct/native access to the running objects of that website.

Basically what I wanted was to be able to access programatically the live TeamMentor (TM) objects from an NUnit test (note that both are running on separate AppDomains).

Not only this would make some of the tests I want to write possible, it would allow me to much faster setup specific test environments (for example cases when I need a number of users to already exist in TM).

The key problem is that after starting the 'TM website running inside Cassini, triggered from the NUnit test' I was left with two AppDomains:
  • The NUnit AppDomain running the NUnit Test and the Cassini Server
  • The Cassini AppDomain running the TM website
In practice what I wanted to do is to be able to access and edit one of TM objects (for example TeamMentor.Schemas.TM_Config from the NUnit test).

And that is exactly what I was able to do :)

Here is how it was done:

1) I re-used the NUnit test shown below, because it already uses the NUnitTests_Cassini_TeamMentor class, which provides the ability to start/stop Cassini on the TM webroot (note that as you can see here, this class is designed to be inherited by an TestFixture Nunit class (since it contains the TestFixtureSetUp and TestFixtureTearDown methods))



2) In order to confirm that we do get breakpoints hits (from actions trigered from the NUnit test) let's run this under Debug mode (using Resharper so that we don't get NCrunch's timeouts)


3) note that at this stage there is no website running on the http://127.0.0.1:32768 port (which is the one that will be used by Cassini)


4) here is the extra line I added to this start NUnitTest (which will start a REPL script environment with the NUnitTests_Cassini_TeamMentor object passed in as a parameter)


5) after the line shown above executes, we now have a full TeamMentor (TM) website running (which is a complex/multi-tear/web-services/ASP.NET-MVC driven app) and a REPL script environment (running from inside the NUnit AppDomain)


6) to confirm the change (that will be made from the NUnit AppDomain), below is the value we will modify (the Windows Authentication 'Reader Group' value)


7) Here is the script that will run on the NUnit AppDomain (see it on this gist):
  • line 1) get a reference to the API_Cassini object
  • line 3) ensure that the NUnit AppDomain has the TeamMentor.Schemas.dll loaded (or the deserialization from the Cassini AppDomain into the NUnit AppDomain will fail)
  • line 6) gets a reference to the O2Proxy object (see code here) which is an special type of class (included in FluentSharp.CoreLib) that provides (amongst other things) the ability to invoke instance and static methods from the target (proxied) AppDomain
  • line 8) invokes the static get_Current method from the TeamMentor.CoreLib.TMConfig class located inside the TeamMentor.Schemas assembly (ie. the equivalent of doing var tmConfig = TMConfig.Current)
  • line 12) changes the ReaderGroup property from the WindowsAuthentication property from the tmConfig variable
  • line 13) invokes the static set_Current method from the TeamMentor.CoreLib.TMConfig class located inside the TeamMentor.Schemas assembly, with the modified tmConfig object passed as the method's parameter  (ie. the equivalent of doing: TMConfig.Current = tmConfig )
  • line 18) just returns the xml of the serialization of the local tmConfig object (good to debug and to confirm that that value was changed). Note that on this script we are not casting tmConfig into a strongly typed TeamMentor.CoreLib.TMConfig object, which means that the current script does not know what type the tmConfig variable is)


8) Here is the confirmation that the value was changed :)


9) another cool thing that we can do (now that we have this capability) is have a strongly typed tmConfig object and invoke the TM's ExtensionMethods (using that tmConfig object). The script below shows that in action:
  • line 1 to 5) same as above
  • line 9) tells the REPL compiler to add a reference to the TeamMentor.Schemas.dll
  • line 10) tells the REPL compile to add an extra using to the TeamMentor.CoreLib namespace
  • line 12) casts the tmConfig variable (currently known to the compiler as System.Object) into a strongly typed TeamMentor.CoreLib.TMConfig object, and, now that the compiler knows it type, change the WindowsAuthentication.Enabled value directly (i.e. without using reflection to access/modify these properties) 
  • line 15) invokes the static windowsAuthentication_Enabled Extension Method method from the TeamMentor.CoreLib.TMConfig_Utils class located inside the TeamMentor.Users assembly, with the tmConfig object passed as the method's parameter  (ie. the equivalent of doing: return tmConfig. windowsAuthentication_Enabled() )


10) the image above shows that we can set the WindowsAuthentication.Enabled value to true  and the image below shows it being set to false


11) Since we are running this under the debugger, here is the extension method in action (when WindowsAuthentication.Enabled has been set to false)


12) and here is the extension method in action (when WindowsAuthentication.Enabled has been set to true)


13) looking at the stack trace (when we hit the extension method shown above) will also confirm that there was an AppDomain transition between the REPL script (running under the NUnit AppDomain) and the windowsAuthentication_Enabled Extension Method (running under the Cassini AppDomain)


I'm pretty excited with this new capability!

There are tons of UnitTests that I will finally be able to write for TeamMentor, and the best part, is that they will run very quickly (since these tests will mostly be doing in-memory manipulations with a little sprinkle of Web Automation (using FluentSharp.Watin)).

This also opens up a lot of interesting possibilities for writing security-focused UnitTests that will run directly on the target ASP.NET or ASP.NET MVC websites.