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.
In January of this year, when I first started experimenting with the code that would become Time Tortoise, I was using Visual Studio 2015 for my development environment. When Visual Studio 2017 was released, I upgraded. Well, technology moves on, and while VS2017 remains my environment of choice, it’s time for a .NET upgrade.
Class Library (Legacy Portable)
To build a traditional Windows Desktop app using the MVVM pattern, I would create a user interface project (using WPF) and have it consume a set of .NET Class Library projects.
My Universal Windows Platform app has a similar design, except that the class libraries have to use .NET Core, the open-source version of .NET.
When I started Time Tortoise development, I specified the additional reqiurement that the class libraries needed to be testable using xUnit.net and standard tools like the Visual Studio Test Explorer and the xUnit console runner.
Until now, I have gotten the best results when I created class libraries using the Class Library (Portable) project template, and set up the project as described in xUnit.net in VS2017. But that process is a bit convoluted, and it doesn’t take advantage of the latest enhancements to Visual Studio projects.
In recent updates to VS2017, the Class Library (Portable) project type has been renamed Class Library (Legacy Portable). And on the project settings page for that project type, a helpful message says: “.NET Standard is the recommended method for sharing code between applications targeting different platforms.” So I figured now was the time to switch.
.NET Standard
.NET Standard is a specification, not an implementation. It describes an API to be implemented, but it doesn’t implement anything itself. You can’t run a program “on .NET Standard.” You have to run it on a .NET implementation that targets a particular version of .NET Standard.
To help developers decide which .NET Standard version to use, the official .NET implementation support table maps .NET Standard versions (like v1.0 or v2.0) to implementations (like .NET Core or Windows). UWP v10.0 (the version of UWP that I’m using) only targets .NET Standard v1.4 or below. So I’ll be using v1.4 for the Time Tortoise class libraries. Just before I published this post, it was announced that UWP v10.0.16299 supports .NET Standard v2.0. But that requires Windows 10 Fall Creators Update, so I’ll probably wait to upgrade until that version is more widely deployed.
Given the preceding definition of .NET Standard, it might be confusing to see that VS2017, in its Add New Project dialog, offers Class Library (.NET Standard) as a project type. If .NET Standard isn’t an implementation, what does this class library run on?
I didn’t find a direct answer to that question, but there’s a lot of discussion on Stack Overflow about the difference between .NET Standard and .NET Core. Here’s how I would summarize the situation: By selecting Class Library (.NET Standard), you’re asking for some implementation of .NET Standard (you don’t care which). This means your class library will be compatible with any implementation in the implementation support table that maps to the same .NET Standard version. For example, if you create a class library targeting .NET Standard v1.4, you’ll be able to consume that library from a .NET Core project, a UWP project, or any of the other implementations in the 1.4 column. If instead you create Class Library (.NET Core) project, you’ll get a class library that is only compatible with the .NET Core implementation. So you’ll only be able to consume it from implementations that support .NET Core.
For now, the only consumers I care about are UWP, .NET Core Console, and xUnit.NET. UWP is for the app itself, xUnit.NET is for testing, and .NET Core Console is for testing and as an alternative UI. However, I haven’t noticed any features that I need which are in .NET Core but not .NET Standard, so I’m sticking with the .NET Standard class library.
Resolving Dependencies (Again)
Rather than having Visual Studio convert my Portable Class Library projects to .NET Standard Class Library projects, I decided it would be best to create the .NET Standard projects from scratch and add my code files manually. That is probably cleaner than relying on conversion, though it did require re-adding the appropriate NuGet references, since those are stored in the project file.
The remaining step was to get unit tests working. As I wrote in Resolving Dependencies and Resolving Dependencies Part 2, compiling a unit test project doesn’t copy all of the DLLs required to run the tests, so they need to be copied manually.
Fortunately, I created a process earlier this year to find and copy the required dependencies, so it didn’t take too much time to get things up and running again. The previous articles discuss the process, but here’s an updated list of steps based on what I did this week:
- Using NuGet Package Manager, update any NuGet packages that are listed as requiring updates. This will update project references and also copy the new DLLs to the local NuGet cache folder, which is required for subsequent steps.
- Build and run the app and do some manual smoke testing.
- If Test Explorer doesn’t show all of your unit tests (or doesn’t show any), it’s likely that DLLs are missing from the unit test output folder.
To automate the process of finding and copying dependencies, I wrote a console app called CopyDependencies. I haven’t pushed it to GitHub because the code is messy, but I might clean it up and publish it if there’s interest. Here’s how I used it to copy the missing DLLs.
First, I updated the text files that the tool uses to find and keep track of dependencies:
- In
sources.txt
, specify the local NuGet package location (usually%UserProfile%\.nuget\packages
). - In
destinations.txt
, specify the locations where the DLLs should be copied. These are the output directories for each unit test project (e.g.,TimeTortoise.Client.Tests\bin\Debug\netstandard1.4
) - In
packages.txt
, specify the names of each required package. For example,Microsoft.Data.Sqlite
is a package name. Optionally, specify the location of the package to override the automatic behavior that the tool uses to locate it. - In
dlls.txt
, mark the correct version of each DLL. The tool doesn’t know how to pick the correct version, so this must be done manually.
Once these files are set up, run CopyDependencies.exe
and build the solution. If all test are discovered, the process is done. Otherwise, some dependencies are still missing and more steps are required.
Running a unit test runner from the command line often provides better diagnostic error messages than running tests from inside Visual Studio. Here’s the process to do that:
- Open a command line to the directory that contains a test project DLL (e.g.,
TimeTortoise.ViewModel.Tests.dll
). - Copy
xunit.console.*.exe
to this directory. - Run one of the
xunit.console
runners (the 32-bit or 64-bit version) on the test DLL. - If you get a
System.IO.FileNotFoundException
, note the name of the assembly in the error message, and use the troubleshooting steps below to find it.
These troubleshooting steps need to be run repeatedly until all tests are discovered. As with compiler errors, it’s advisable to fix the easiest errors first, since fixing one error often makes other errors go away:
- First, verify that the missing assembly is listed in packages.txt. If it isn’t, add it to packages.txt and run CopyDependencies.exe. This will add the names of all associated DLLs to dlls.txt.
- Second, open dlls.txt and find the line containing the assembly/version that was not found. Mark that line by adding
x
(x
followed by a space) to the beginning of the line. Run CopyDependencies.exe again. This will copy all marked DLLs to the destinations specified in destinations.txt. - Third, if the assembly is still not found, use the Assembly Binding Log Viewer (Fuslogvw.exe) to pinpoint which assembly is missing. Use packages.txt and dlls.txt to copy these assemblies as well.
If things still aren’t working, here are a few more troubleshooting steps:
- If you have conflicting requirements (one error message wants assembly X, version Y, and another error message wants assembly X, version Z), use binding redirect to force a specific assembly version to be used.
- In the Fuslogvw output, pay attention to the name and path of the executable that generated the exception. If you need to add a binding redirect command, that’s the executable whose config file needs to be updated.
- If you get stuck fixing problems with the command-line test runner, try going back to the Visual Studio GUI test runner. For xUnit.NET, make sure to install the
xunit.runner.visualstudio
NuGet package in all test assemblies. Restarting Visual Studio is sometimes required to force tests to be discovered. - If all else fails, read the answers to this Stack Overflow question for more ideas.
So that’s the long process to get unit test dependencies copied.