Welcome Guest! To enable all features please Login or Register.

Notification

Icon
Error

Best method for referencing third-party assemblies
MarcChu
#1 Posted : Wednesday, March 2, 2016 5:34:45 PM(UTC)
Rank: Member

Groups: Registered
Joined: 9/26/2014(UTC)
Posts: 22

Thanks: 3 times
Was thanked: 1 time(s) in 1 post(s)
I noticed some strange behavior in my tests that was causing differences between how my application works when run in a normal context and when run within the test runner.

References:
Project A (test project) -> Project B (application project) -> Project C (helper assembly project) -> 3rd party dependencies (located in SDK installation folder)

When some code in Project C gets exercised, something unexpectedly fails upon a call to the 3rd party dlls. Apparently, these 3rd party libraries use some reflection in order to find other related dependencies, using System.Reflection.Assembly.GetAssembly(typeof(<TYPE>)). The problem is, when I run my tests in NCrunch, the type that is referenced is found in the SDK installation directory. This is problematic because it actually finds a dependency that it isn't supposed to find (it searches for it by default, but since we don't have a license for this particular component, it throws an exception). In normal running, the GetAssembly() call above will resolve to the local bin directory, which only has the particular dependencies that we specifically reference in our project.

So I assume that whatever dependency chain NCrunch uses in order to load these dependencies actually directly references the 3rd party assemblies (in the SDK directory), rather than loading them from some local workspace directory (I know that nothing I've tried will copy the 3rd party deps into the Project A bin directory).

So the question is: how can I configure Project A so that the 3rd party dependencies are loaded from their expected sandboxed location, so that these other assemblies aren't incorrectly found with them?

I've tried different combinations of "Copy referenced assemblies to workspace", "Include static references in workspace", "Additional files to include", "Implicit project dependencies", and "Pre-load all assembly references into test environment".
Remco
#2 Posted : Wednesday, March 2, 2016 9:52:48 PM(UTC)
Rank: NCrunch Developer

Groups: Administrators
Joined: 4/16/2011(UTC)
Posts: 7,144

Thanks: 959 times
Was thanked: 1290 time(s) in 1196 post(s)
Hi, thanks for sharing this issue.

It looks to me as though something is not behaving right in NCrunch's resolution in your solution. When NCrunch constructs a workspace for your project, by default, it will copy all assemblies directly referenced by the project into an _ncrunchreferences directory inside the workspace. NCrunch then hooks AppDomain.AssemblyResolve with code designed to direct any assembly resolution calls towards _ncrunchreferences at test run time.

Unfortunately, hooking AssemblyResolve isn't fully reliable, as the CLR has its own internal resolution logic that sometimes has precedence over this point. If an assembly is co-located with the referencing assembly (i.e. you have a referenced assembly next to your .dll copied in because of a custom build step or the 'Copy referenced assemblies to workspace' setting), NCrunch won't have a chance to redirect the CLR and the assembly will be loaded from the co-located file instead. The GAC also will override this logic. If a referenced assembly has a strong name and it is installed in the GAC, the GAC always wins.

The 'Pre-load all assembly references into test environment' is intended to mitigate this by pre-loading all referenced assemblies into the test environment before any tests start executing. This prevents the need for the AssemblyResolve hook as the assemblies are already loaded when the CLR requests them. My experience suggests that the GAC can still override this though.

When 'Include static references in workspace' is disabled, NCrunch will avoid copying referenced assemblies into _ncrunchreferences, and will instead reference them from their source location in your foreground solution. Thus you should be sure you have this setting enabled for ALL the projects in your solution if you want to avoid any loose references to the foreground solution.

Enabling 'Copy referenced assemblies to workspace' setting usually will result in a strong preference for the CLR to load the correct assemblies from the workspace, as NCrunch will ensure the files copied to the workspace's bin directory will be the right ones. Unfortunately for this to be fully effective, you'll need to enable it for ALL the projects in your solution, and the performance impact of doing this is considerable.

With consideration to all of the above, if you have correctly configured your projects/solution and NCrunch is working properly, you shouldn't see your runtime environment loading referenced assemblies from outside the workspace unless they are stored in the GAC. If you're confident you have the settings right and you're seeing the wrong assemblies being loaded up in the Debug/Modules view under VS, then there is something we're missing here. Check that there isn't anything freaky in these 3rd party libraries that is somehow painted directly at your SDK directory (an absolute path or environment variable perhaps?). Code inside NCrunch's workspace should be completely isolated from your foreground solution.
MarcChu
#3 Posted : Thursday, March 3, 2016 3:16:52 PM(UTC)
Rank: Member

Groups: Registered
Joined: 9/26/2014(UTC)
Posts: 22

Thanks: 3 times
Was thanked: 1 time(s) in 1 post(s)
Well, I got the code to function correctly by setting "Copy referenced assemblies to workspace" for every project in this chain (A, B, and C, but not for every project in the solution).

I'm fairly certain nothing is loaded from the GAC.

Also, I was in contact with tech support with the SDK vendor, and they didn't mention anything about a hard-coded reference to the installation directory or environment variable. They were the ones, however, that informed me about the GetAssembly reflection that was mistakenly loading the assembly I didn't want to load. Their code was obfuscated, so I couldn't reflect on it. So I placed that reflection code in front of our call into their code. When it failed, the location was the SDK directory. Now, it points to an NCrunch workspace that looks like it was created for Project C. The 3rd-party dependencies directly referenced by C are indeed in that bin directory.

(Experimenting while I'm typing this, sorry)

I started flipping my "Copy referenced assemblies to workspace" settings back to false. When A went to false, everything still worked correctly. Went B went to false, still working. It only fails when C goes back to false. I found what looked to be the workspace for Project C. The bin directory contains only the output of that project, and none of the referenced dependencies. The _ncrunchreferences folder contains one .dll that the project references, but none of the other 3rd-party dependencies. Glancing at the modules window in the debugger, I can see that most of the 3rd-party dependencies (not necessarily direct references of Project C) are loaded from _ncrunchreferences folders (from different workspaces). However, almost all of the dependencies for Project C are loaded from wherever their locations are on disk, as referenced in the .csproj.

So it looks like the problem is that these 3rd-party dependencies for Project C aren't being copied to the _ncrunchreferences folder. But it looks like every project in my solution has "Include static reference in workspace" set to True.



BTW, on a completely unrelated note, it seems that whenever I make a new post here, and it gets replied to, I get notifications for other posts I've created here in the past, dated to that time (I just received two notifications from 2014).
Remco
#4 Posted : Thursday, March 3, 2016 9:50:55 PM(UTC)
Rank: NCrunch Developer

Groups: Administrators
Joined: 4/16/2011(UTC)
Posts: 7,144

Thanks: 959 times
Was thanked: 1290 time(s) in 1196 post(s)
MarcChu;8370 wrote:
Well, I got the code to function correctly by setting "Copy referenced assemblies to workspace" for every project in this chain (A, B, and C, but not for every project in the solution).

I'm fairly certain nothing is loaded from the GAC.

Also, I was in contact with tech support with the SDK vendor, and they didn't mention anything about a hard-coded reference to the installation directory or environment variable. They were the ones, however, that informed me about the GetAssembly reflection that was mistakenly loading the assembly I didn't want to load. Their code was obfuscated, so I couldn't reflect on it. So I placed that reflection code in front of our call into their code. When it failed, the location was the SDK directory. Now, it points to an NCrunch workspace that looks like it was created for Project C. The 3rd-party dependencies directly referenced by C are indeed in that bin directory.

(Experimenting while I'm typing this, sorry)

I started flipping my "Copy referenced assemblies to workspace" settings back to false. When A went to false, everything still worked correctly. Went B went to false, still working. It only fails when C goes back to false. I found what looked to be the workspace for Project C. The bin directory contains only the output of that project, and none of the referenced dependencies. The _ncrunchreferences folder contains one .dll that the project references, but none of the other 3rd-party dependencies. Glancing at the modules window in the debugger, I can see that most of the 3rd-party dependencies (not necessarily direct references of Project C) are loaded from _ncrunchreferences folders (from different workspaces). However, almost all of the dependencies for Project C are loaded from wherever their locations are on disk, as referenced in the .csproj.

So it looks like the problem is that these 3rd-party dependencies for Project C aren't being copied to the _ncrunchreferences folder. But it looks like every project in my solution has "Include static reference in workspace" set to True.


Can you share any details about how Project C is referencing these 3rd party assemblies? Is this done using <Reference> tags inside the project XML, or is there another mechanism of some kind? Also, what is the full directory path to the SDK? Is this under Program Files?


MarcChu;8370 wrote:
BTW, on a completely unrelated note, it seems that whenever I make a new post here, and it gets replied to, I get notifications for other posts I've created here in the past, dated to that time (I just received two notifications from 2014).


Thanks for letting me know about this issue. I'll look into it!
MarcChu
#5 Posted : Friday, March 4, 2016 3:17:55 PM(UTC)
Rank: Member

Groups: Registered
Joined: 9/26/2014(UTC)
Posts: 22

Thanks: 3 times
Was thanked: 1 time(s) in 1 post(s)
These are all the 3rd party references from the .csproj.

Code:
    
<Reference Include="Aspose.BarCodeRecognition">
      <HintPath>C:\Program Files (x86)\Aspose\Aspose.BarCode for .NET\bin\net4.0\Aspose.BarCodeRecognition.dll</HintPath>
    </Reference>
    <Reference Include="Aspose.Cells">
      <HintPath>C:\Program Files (x86)\Aspose\Aspose.Cells for .NET\Bin\net2.0\Aspose.Cells.dll</HintPath>
    </Reference>
    <Reference Include="Aspose.Pdf">
      <HintPath>C:\Program Files (x86)\Aspose\Aspose.Pdf for .NET\Bin\net4.0\Aspose.Pdf.dll</HintPath>
    </Reference>
    <Reference Include="Aspose.Slides">
      <HintPath>C:\Program Files (x86)\Aspose\Aspose.Slides for .NET\Bin\net4.0\Aspose.Slides.dll</HintPath>
    </Reference>
    <Reference Include="Aspose.Words">
      <HintPath>C:\Program Files (x86)\Aspose\Aspose.Words for .NET\bin\net2.0\Aspose.Words.dll</HintPath>
    </Reference>
    <Reference Include="Atalasoft.dotImage, Version=10.4.1.61293, Culture=neutral, PublicKeyToken=2b02b46f7326f73b, processorArchitecture=AMD64" />
    <Reference Include="Atalasoft.dotImage.Ocr, Version=10.4.1.61293, Culture=neutral, PublicKeyToken=2b02b46f7326f73b, processorArchitecture=AMD64" />
    <Reference Include="Atalasoft.dotImage.Ocr.Tesseract3, Version=10.4.1.61293, Culture=neutral, PublicKeyToken=2b02b46f7326f73b, processorArchitecture=AMD64" />
    <Reference Include="Atalasoft.dotImage.Pdf, Version=10.4.1.61293, Culture=neutral, PublicKeyToken=2b02b46f7326f73b, processorArchitecture=AMD64" />
    <Reference Include="Atalasoft.dotImage.PdfReader, Version=10.4.1.61293, Culture=neutral, PublicKeyToken=2b02b46f7326f73b, processorArchitecture=AMD64" />
    <Reference Include="Atalasoft.Shared, Version=10.4.1.61293, Culture=neutral, PublicKeyToken=2b02b46f7326f73b, processorArchitecture=MSIL" />
    <Reference Include="dtSearchNetApi4">
      <HintPath>C:\Program Files (x86)\dtSearch Developer\bin64\dtSearchNetApi4.dll</HintPath>
    </Reference>
    <Reference Include="Ionic.Zip">
      <HintPath>..\..\packages\DotNetZip.1.9.1.8\lib\net20\Ionic.Zip.dll</HintPath>
    </Reference>


The Ionic.Zip is the only one that showed up in _ncrunchreferences. It also happens to be the only one that's in the same directory structure as the code I'm running.

The Atalasoft references are the ones that I'm having a problem with, specifically, which are located in a Program Files (x86) subdirectory. However, I do recall the Aspose .dll's also being loaded from C:\Program Files (x86)\Aspose, rather than an/the NCrunch workspace.
MarcChu
#6 Posted : Friday, March 4, 2016 3:33:10 PM(UTC)
Rank: Member

Groups: Registered
Joined: 9/26/2014(UTC)
Posts: 22

Thanks: 3 times
Was thanked: 1 time(s) in 1 post(s)
Hm....so looking at those references gives me the sinking feeling that those actually are located in the GAC. I've been trying to browse and search in order to find them in there, though, and still can't find anything.
Remco
#7 Posted : Friday, March 4, 2016 9:16:43 PM(UTC)
Rank: NCrunch Developer

Groups: Administrators
Joined: 4/16/2011(UTC)
Posts: 7,144

Thanks: 959 times
Was thanked: 1290 time(s) in 1196 post(s)
Although these references aren't being referenced using a path, they are being resolved to your Program Files directory, possibly via an extended search path (MSBuild search logic is very complex). NCrunch has a piece of logic inside it that stops it from copying assemblies inside 'Program Files' into the workspace, because these files are considered to be global/static in nature and they don't usually need to be copied. This is an important consideration for some frameworks that expect their references to be loaded from a global path and spew out errors when they aren't.

It looks to me like you've found the inverse of this situation - a framework that expects to be loaded from a local path, and throws errors when loaded from its global source.

This library behaves normally when MSBuild copies the references into the build output directory, which is why it works when 'Copy referenced assemblies to workspace' is turned on.

There isn't a change that I can safely make to NCrunch that will allow it to work 'out of the box' under these conditions without seriously breaking it for other toolsets/libraries, so it looks like there are two options for resolving this problem:

1. Turn on 'Copy referenced assemblies to workspace' for the projects involved. This will allow the libraries to behave as normal, but your build times will be elevated.
2. Move the references out of Program Files and put them somewhere else. Reference them using a 'HintPath' in the project XML. Sometimes it can be useful to place them under a relative path in your solution directory (i.e. 'References') if the library vendor's license agreement permits this.
1 user thanked Remco for this useful post.
MarcChu on 3/7/2016(UTC)
MarcChu
#8 Posted : Monday, March 7, 2016 1:09:28 PM(UTC)
Rank: Member

Groups: Registered
Joined: 9/26/2014(UTC)
Posts: 22

Thanks: 3 times
Was thanked: 1 time(s) in 1 post(s)
Okay, well 'Copy referenced assemblies to workspace' isn't really a killer, since it's only necessary for the one project. The over-arching point of this post was to figure out the right way to do things, because the amount of different options available can tend to make things confusing, though they thankfully seem to only be necessary when stuff doesn't work right. Normally, everything works out of the box, but it's the crazy exceptions like this that cause us to take the dreaded look under the hood. But it's good to know how all this stuff is supposed to work.

Anyways, I blame this whole thing on Atalasoft and their silly bit of reflection.
Remco
#9 Posted : Monday, March 7, 2016 10:15:54 PM(UTC)
Rank: NCrunch Developer

Groups: Administrators
Joined: 4/16/2011(UTC)
Posts: 7,144

Thanks: 959 times
Was thanked: 1290 time(s) in 1196 post(s)
MarcChu;8384 wrote:
Okay, well 'Copy referenced assemblies to workspace' isn't really a killer, since it's only necessary for the one project. The over-arching point of this post was to figure out the right way to do things, because the amount of different options available can tend to make things confusing, though they thankfully seem to only be necessary when stuff doesn't work right. Normally, everything works out of the box, but it's the crazy exceptions like this that cause us to take the dreaded look under the hood. But it's good to know how all this stuff is supposed to work.


Agreed. This is another example of a nasty tooling clash creating a confusing situation. The library vendor has designed their tooling around standard build behaviour, and NCrunch has changed this behaviour ever so slightly. You were unlucky here - these sorts of problems are getting rarer and rarer as more people use NCrunch and overall compatibility improves. I'm sorry for the amount of time this issue has cost you.
Users browsing this topic
Guest (2)
Forum Jump  
You cannot post new topics in this forum.
You cannot reply to topics in this forum.
You cannot delete your posts in this forum.
You cannot edit your posts in this forum.
You cannot create polls in this forum.
You cannot vote in polls in this forum.

YAF | YAF © 2003-2011, Yet Another Forum.NET
This page was generated in 0.076 seconds.
Trial NCrunch
Take NCrunch for a spin
Do your fingers a favour and supercharge your testing workflow
Free Download