Integrating a .NET Core project in a TDD/BDD environment
We have been exploring the use of Amazon AWS Lambdas on our project. Since we have a large investment in our C# codebase, we were quite interested when Amazon announced C# support for developing Lambdas, albeit only with .NET Core support.
As a pilot, we have chosen a very simple feature: moving messages from one queue to another. It's not just a throwaway effort - we want an means for moving items from a Dead Letter Queue back into the main queue for reprocessing. But, this effort will mainly let us explore what is involved in developing a .NET Core Lambda as part of a production project. We want it integrated into a project with TDD and BDD tests, with automated deployments.
My first day focused on the .NET Core aspects of the project, not touching on Lambdas yet.
Incorporating the .NET Core project requires a good understanding of target frameworks. And, that requires grasping the role of the .NET Standard framework.
.NET Standard framework is not an actual library of code, whereas the classic .NET frameworks and .NET Core are. You could say that .NET Standard is to .NET Framework and .NET Core as an interface or abstract class is to a concrete implementation. The .NET standard versions define a set of supported libraries that other frameworks implement. When your project targets a .NET Standard version, it corresponds to specific versions of the related frameworks.
Here is a helpful metaphor from David Fowler, likening the relationships between target frameworks to interfaces with inheritance relationships.
Here is the authoritative table showing the relationship between .Net Standard versions and related frameworks.
A couple of effective ways of thinking about .NET Standard framework versions:
- The higher the version, the more APIs are available to you.
- The lower the version, the more platforms implement it
Another way of expressing it:
- App Developers: You target the platform TFM you’re writing for ( netcoreapp1.0, uap10.0 , net452 , xamarin ios , etc.).
- Package/Library Authors: Target the lowest netstandard version you can. You will run on all platforms that support that netstandard version or higher
So, given that guidance, I settled on this strategy:
- Target .NET Standard 1.6 for the project with the core functionality (QueueMover).
- Target .NET Core 1.0 for the a project to provide a thin wrapper for implementing a Lambda (QueueMover.Lambda)
- Target whatever framework is appropriate for the test projects (QueueMover.Tests.Unit and QueueMover.Tests.Functional) as dictated by the tools (and, there are constaints, as we will see).
So, let's look at the project properties for the arrangement I had working at the end of the day:
QueueMover (Core functionality)
{ "version": "1.0.0-*", "dependencies": { "AWSSDK.SQS": "3.3.1.6", "NETStandard.Library": "1.6.0" }, "frameworks": { "netstandard1.6": { "imports": "dnxcore50" }, "netcoreapp1.0": {}, "net46": {} } }
A few interesting things to note here:
"dependencies": { "AWSSDK.SQS": "3.3.1.6", "NETStandard.Library": "1.6.0" },
Targeting .NET Standard 1.6 results in a library compatible with .NET Core 1.0 and .NET Framework 4.6. The only other dependency for this simple project is the AWS SQS SDK.
"frameworks": { "netstandard1.6": { "imports": "dnxcore50" },
Again, targeting .NET Standard 1.6. The "imports": "dnxcore50" enables integration with some NuGet packages that have not been upgraded to use current Target Framework Monikers (TFMs). See https://github.com/aspnet/Home/issues/1540.
"netcoreapp1.0": {}, "net46": {}
These are crucial to having this .NET Standard 1.6 project cooperate with other projects in the solution. Each one of the "frameworks" entries causes the project to generate DLLs for that target framework. (Look in folders under \bin\Debug). The "netcoreapp1.0" was essential for integrations with the QueueMover.Lambda and QueueMover.Tests.Units projects. "net46" was necessary for integration with the QueueMover.Tests.Functional project.
QueueMover.Tests.Unit (Unit Tests)
{ "version": "1.0.0-*", "dependencies": { "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0" }, "AWSSDK.SQS": "3.3.1.6", "Moq": "4.6.38-alpha", "NUnit": "3.6.0", "dotnet-test-nunit": "3.4.0-beta-3", "QueueMover": "1.0.0-*" }, "testRunner": "nunit", "frameworks": { "netcoreapp1.0": { "imports": "dnxcore50" } } }
Several things to comment on here:
"dependencies": { "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0" },
Due to some TDD-related tools (discussed below), this project needs to target the .NET Core 1.0. Using "type" : "platform" indicates that we are counting on that dependency existing in the target environment. See this which includes this helpful quote:
The type "platform" property on that dependency means that at publish time, the tooling will skip publishing the assemblies for that dependency to the published output.
So, this means that .NET Core needs to be installed on the developers machine and in the Continuous Integration environment. Seems like a reasonable expectation and will dramatically reduce the deployed package.
"AWSSDK.SQS": "3.3.1.6",
Needs the AWS SQS SDK to set up mocks for AWS services.
"Moq": "4.6.38-alpha",
.NET Core support in our standard mocking framework, Moq, is a work in progress. Version 4.6.38-alpha was sufficiently stable for my needs.
"NUnit": "3.6.0", "dotnet-test-nunit": "3.4.0-beta-3", . . . "testRunner": "nunit",
NUnit support for .NET Core seems pretty good right now. In addition to the standard NUnit NuGet package, you need the "dotnet-test-nunit" NuGet package to install a compatible test runner.
"QueueMover": "1.0.0-*"
A reference to the project we are testing.
"frameworks": { "netcoreapp1.0": { "imports": "dnxcore50" }
Again, targeting .NET Core 1.0 only, for Moq and NUnit compatibility. Nothing will depend on the unit test project, so no need to target any other frameworks.
This results in a workable unit test project. 'Workable' but not what we have become accustomed to on our project. Two key tools have been left out:
- NCrunch. Support is not there yet. The developer says 'Maybe next month'
- Resharper. Resharper 2016.3 has support for .NET Core. I didn't take the time to upgrade yet. For now, I have been content with the Visual Studio test runner.
(Note: I'm not sure if there is a crucial advantage to having the unit test project based on .NET Core. Alternatively, it could be set up as a traditional .NET Framework class library (see below re: QueueMover.Tests.Functional). To be explored later...)
QueueMover.Tests.Functional (BDD Tests)
Created as a standard Class Library. SpecFlow support for .NET Core will be available RSN (Real Soon Now). Until then, I chose to set up a traditional SpecFlow project. For this, I set the project properties to target .NET Framework 4.6, since that is compatible with .NET Standard 1.6.
Everything proceeded as usual for a SpecFlow project, with one key exception: Even though I have the QueueMover project producing .NET Framework 4.6 DLLs, I could not set up a project reference to it. As a work-around, I succeeded in setting up a direct reference to the QueueMover.dll in the net46 folder in the QueueMover project. With this in place, I can reference interfaces and classes in QueueMover as I implement tests, but debugging in the QueueMover is not possible in the context of a Specflow test. Not acceptable.
While the various alpha/beta versions and minor incompatibilities are yellow flags, this issue is the key red flag in my mind. I am hopeful it can be resolved.
But, for now, on to Lambdas!