Time Tortoise: Resolving Dependencies Part 2

Dependencies

This is one in a series of articles about Time Tortoise, a Universal Windows Platform app for planning and tracking your work schedule. For more on the development of this app and the ideas behind it, see my Time Tortoise category page.

Last week, I wrote about techniques to find a list of the .NET assemblies that xUnit.net needs to run unit tests. To review:

  • When Visual Studio compiles a unit test project, it doesn’t copy all of the required dependencies into the output directory.
  • Therefore, we need a way to manually find these dependencies.
  • To find the dependencies, we can attempt to run unit tests and look for detailed error messages. One messages is shown when a FileLoadException occurs. Another one is found in Assembly Binding Log Viewer output: “All probing URLs attempted and failed.” Using these messages, we can manually copy dependencies to the unit test project output directory.
  • If these debugging techniques uncover the need for two or more assemblies with different versions, binding redirection can be used to define a single version to use.

Missing dependencies prevent Visual Studio from discovering unit tests (i.e., finding them so they can be listed in Test Explorer). Using these debugging techniques, we should be able to successfully discover all of the unit tests in a project. The next step is to try to run the discovered tests.

System.TypeLoadException

Although it can be tedious to track down dependencies for the purpose of resolving test discovery problems, there’s a fairly deterministic process (summarized above) to do so. But that’s not the complete solution to getting unit tests working. Successful unit test discovery seems to depend only on the discovery engine finding the correct assemblies in the output directory. Correct seems to be based on the assembly full name (name, version, culture, and public key).

But actually running unit tests leads to a much stricter process, as the machine uses the manually-copied assemblies to resolve method calls and other dependencies. For example, several Time Tortoise integration tests cause this method call to be executed as they start up:

Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServiceCollectionExtensions.AddQuery(IServiceCollection serviceCollection)

Although I had a set of dependencies that caused all of the integration tests to be successfully discovered, I observed this error when running some of them:

System.TypeLoadException : Inheritance security rules violated by type: 'Remotion.Linq.Parsing.RelinqExpressionVisitor'. Derived types must either match the security accessibility of the base type or be less accessible.

Since the code in question works fine for running the Time Tortoise app itself, it’s unlikely that this error message is due to a bug in Microsoft.EntityFrameworkCore or Remotion.Linq. Instead a combination of two or more assembly versions must be incompatible with each other.

BadImageFormatException

In a .NET program, it’s sometimes necessary to call unmanaged modules. For Time Tortoise, the module in question is sqlite.dll. In running unit tests, I encountered this exception:

System.BadImageFormatException : An attempt was made to load a program with an incorrect format.

One of the causes of this exception is having a mix of 32-bit and 64-bit modules. Since SQLite is available in both formats, it’s possible to resolve this error by ensuring that the copy in the unit test output directory is the right one.

Finding the Correct Assembly Version

For non-unit test projects, NuGet is supposed to take care of downloading the appropriate assemblies, their dependent assemblies, the dependent assemblies of those assemblies, and so on. Although this process doesn’t work for unit test projects, it’s possible to use it indirectly by manually copying DLLs from the output directory of a non-unit test project to the output directory of a unit test project. If your app runs correctly when you launch it normally, that’s an indication that the combination of assemblies is valid.

Nevertheless, it’s not usually sufficient just to copy an entire output directory, since unit test projects tend to have a wider range of dependencies than any single non-unit test project. So it’s necessary to use a combination of debugging techniques like those explained this week and last week.