A project option is an option provided to the dotnet new
command that affects the generated project in some way.
In this example, we add a new project option named hello-world
that logs a hello message when our application starts up.
Sample output of a generated project with the new hello-world
project option:
$ dotnet new steeltoe-webapi --output MyHelloWorldApp --hello-world
$ dotnet run --project MyHelloWorldApp
info: MyHelloWorldApp.Startup[0]
Hello World from MyHelloWorldApp
...
We'll use a TDD style to implement our new project option. A major tenet of TDD is that you should first see your tests fail prior to making or implementing changes. The nature of template development makes honoring this tenet a bit cumbersome, however we'll attempt to placate the TDD tenet somewhat by using 2 branches:
- one to write our tests and ultimately implement our project option
- one to develop our option to satisfy the tests
The general flow of the implementation is:
- create a basic test and define the option so that the test passes
- implement the template changes for the option and add tests to verify those changes
Clone this repository and create 2 branches:
$ git clone [email protected]:SteeltoeOSS/NetCoreToolTemplates.git
$ cd NetCoreToolTemplates
$ git branch hello-world # ultimate branch for our new option
$ git branch hello-world-dev # temporary development branch
Switch to the ultimate branch:
$ git switch hello-world
The hello-world
project option is a boolean option, which in use will look something like:
$ dotnet new steeltoe-webapi # hello-world is false
$ dotnet new steeltoe-webapi --hello-world # hello-world is true
$ dotnet new steeltoe-webapi --hello-world false # hello-world is false
$ dotnet new steeltoe-webapi --hello-world true # hello-world is true
There are several abstract test classes in the project to assist in option development.
The abstract class ProjectOptionTest should be used for project options such as hello-world
.
The ProjectOptionTest
constructor takes 3 arguments:
- option name
- option description
- logger (injected)
The description is what will be output by the dotnet new
help engine.
Create the file test/NetCoreTool.Template.WebApi.Test/HelloWorldProjectOptionTest.cs
:
using Xunit.Abstractions;
namespace Steeltoe.NetCoreTool.Template.WebApi.Test
{
public class HelloWorldProjectOptionTest : ProjectOptionTest
{
public HelloWorldProjectOptionTest(ITestOutputHelper logger) : base("hello-world", "Say 'Hi' to the world", logger)
{
}
}
}
Run the new test class, expecting several failures:
$ dotnet test --filter 'FullyQualifiedName~Steeltoe.NetCoreTool.Template.WebApi.Test.HelloWorldProjectOptionTest'
...
Failed! - Failed: 9, Passed: 0, Skipped: 0, Total: 9, Duration: 5 s ...
We'll implement our project option and fix the broken tests in the hello-world-dev
temporary branch.
Before proceeding to those steps, add the new test case to the ultimate branch:
$ git add test/NetCoreTool.Template.WebApi.Test/HelloWorldProjectOptionTest.cs
$ git commit -m'Create hello-world test stub'
Switch to the temporary branch.
$ git switch hello-world-dev
dotnew new
options are defined as symbols in the file .template.config/template.json
.
By this project's convention, project option symbol names are defined with the suffix Option
so the new symbol will be named HelloWorldOption
.
Add the new symbol to the symbols
object in src/Content/NetCoreTool.Template.WebApi/CSharp/.template.config/template.json
:
"symbols": {
...
"HelloWorldOption": {
"type": "parameter",
"datatype": "bool",
"description": "Say 'Hi' to the world",
"defaultValue": "false"
},
...
The symbol will be made available to our template C# code as a preprocessor directive which we'll leverage in later steps.
Now that symbol has been defined, we need to configure the dotnew new
command so that the option --hello-world
maps to the new symbol.
The option-to-symbol mapping is configured in the file .template.config/dotnetcli.host.json
.
Add the new option to the symbolInfo
object in src/Content/NetCoreTool.Template.WebApi/CSharp/.template.config/dotnetcli.host.json
:
"symbolInfo": {
...
"HelloWorldOption": {
"longName": "hello-world",
"shortName": ""
},
...
If you haven't already, install the project template so that it can be invoked using dotnet new
:
$ dotnet new --install src/Content
Run the steeltoe-webapi
template with the --help
option to see the newly added option:
$ dotnet new steeltoe-webapi --help
...
Steeltoe Web API (C#)
...
--hello-world Say 'Hi' to the world
bool - Optional
Default: false
...
Add the changes to the temporary branch:
$ git add src/Content/NetCoreTool.Template.WebApi/CSharp/.template.config/template.json
$ git add src/Content/NetCoreTool.Template.WebApi/CSharp/.template.config/dotnetcli.host.json
$ git commit -m'Define the hello-world option'
Switch to the ultimate branch.
$ git switch hello-world
The ProjectOptionTest
defines 3 categories of tests:
- Smoke
- ProjectGeneration
- ProjectBuild
We'll use the Smoke test category to test the new project option. The Smoke test category includes a single test case that:
- runs
dotnet new steeltoe-webapi --help
and verifies the option description - runs
dotnet new steeltoe-webapi --<option>
and verifies the command return code is 0
The ProjectGeneration and ProjectBuild test categories are covered later in this document.
Run the smoke test and see the test fail:
$ dotnet test --filter 'FullyQualifiedName~Steeltoe.NetCoreTool.Template.WebApi.Test.HelloWorldProjectOptionTest&Category=Smoke'
...
Failed! - Failed: 1, Passed: 0, Skipped: 0, Total: 1, Duration: 1 s ...
Merge in the temporary branch and see the smoke test pass:
$ git merge hello-world-dev
$ dotnet test --filter 'FullyQualifiedName~Steeltoe.NetCoreTool.Template.WebApi.Test.HelloWorldProjectOptionTest&Category=Smoke'
Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: 4 s ...
Switch to the temporary branch.
$ git switch hello-world-dev
We'll edit Startup.cs
so that a hello log message is output when the app starts.
The message will be logged from the Configure
method so its signature will need to be changed to inject a logger.
There's also a corresponding using
statement for the ILogger
type.
However, we only want projects that are generated with the hello-world
option to have these changes so we'll use the C# HelloWorldOption
preprocessor directive around our new code.
Edit src/Content/NetCoreTool.Template.WebApi/CSharp/Startup.cs
:
...
#if (HelloWorldOption)
using Microsoft.Extensions.Logging;
#endif
...
#if (HelloWorldOption)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
#else
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
#endif
{
#if (HelloWorldOption)
logger.LogInformation("Hello, World, from {Name}", "Company.WebApplication.CS");
#endif
if (env.IsDevelopment())
...
Create a couple projects to try out the hello-world
option.
$ dotnet new steeltoe-webapi --output MyHelloWorldApp --hello-world
▶ dotnet run --project MyHelloWorldApp
info: MyHelloWorldApp.Startup[0]
Hello, World, from MyHelloWorldApp
...
Add the changes to the temporary branch:
$ git src/Content/NetCoreTool.Template.WebApi/CSharp/Startup.cs
$ git commit -m'Implement the hello-world option'
Switch to the ultimate branch.
$ git switch hello-world
The ProjectOptionTest
abstract class provides a ProjectGeneration test category that tests an option with each valid combination of Steeltoe version and .NET framework.
The tests focus on generated project code, e.g. packages in the .csproj
file and snippets in Startup.cs
.
For each combination, a test ensures the following command return code is 0:
dotnet new steeltoe-webapi --steeltoe <version> --framework <framework> --<option> --no-restore
Subclasses of ProjectOptionTest
can override various hooks to control what is validated during the tests.
The available hooks are:
AssertCsprojPackagesHook(SteeltoeVersion steeltoeVersion, Framework framework, List<(string, string)> packages)
AssertCsprojPropertiesHook(SteeltoeVersion steeltoeVersion, Framework framework, Dictionary<string, string> properties)
AssertProgramCsSnippetsHook(SteeltoeVersion steeltoeVersion, Framework framework, List<string> snippets)
AssertStartupCsSnippetsHook(SteeltoeVersion steeltoeVersion, Framework framework, List<string> snippets)
AssertValuesControllerCsSnippetsHook(SteeltoeVersion steeltoeVersion, Framework framework, List<string> snippets)
AssertAppSettingsJsonHook(List<Action<SteeltoeVersion, Framework, AppSettings>> assertions)
AssertLaunchSettingsHook(List<Action<SteeltoeVersion, Framework, LaunchSettings>> assertions)
The hello-world
ProjectGeneration tests need to ensure that Startup.cs
contains our new code when the option is specified.
Add the following override to the test/NetCoreTool.Template.WebApi.Test/HelloWorldProjectOptionTest.cs
:
protected override void AssertStartupCsSnippetsHook(SteeltoeVersion steeltoeVersion, Framework framework, List<string> snippets)
{
snippets.Add("using Microsoft.Extensions.Logging;");
switch (framework)
{
default:
snippets.Add("public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)");
break;
}
snippets.Add($"logger.LogInformation(\"Hello, World, from {{Name}}\", \"{Sandbox.Name}\");");
}
The order of the snippets in the list doesn't matter, but adding them in the order as they appear in the generated code makes it easier to follow.
Run the project generation tests and see the tests fail:
$ dotnet test --filter 'FullyQualifiedName~Steeltoe.NetCoreTool.Template.WebApi.Test.HelloWorldProjectOptionTest&Category=ProjectGeneration'
...
Failed! - Failed: 4, Passed: 0, Skipped: 0, Total: 4, Duration: 3 s ...
Merge in the temporary branch and see the tests pass:
$ git merge hello-world-dev
$ dotnet test --filter 'FullyQualifiedName~Steeltoe.NetCoreTool.Template.WebApi.Test.HelloWorldProjectOptionTest&Category=ProjectGeneration'
Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4, Duration: 3 s ...
Add the project generation tests to the ultimate branch:
$ git add test/NetCoreTool.Template.WebApi.Test/HelloWorldProjectOptionTest.cs
$ git commit -m'Add hello-world project generation tests'
The ProjectBuild test category tests ensure generated projects can be compiled. Similarly to the ProjectGeneration tests, an option is tested with valid combinations for Steeltoe versions and .NET frameworks. For each combination, a test ensures the following command return codes are 0:
dotnet new steeltoe-webapi --steeltoe <version> --framework <framework> --<option>
dotnet build /p:TreatWarningsAsErrors=True
Run the project build tests and see if any test fail:
$ dotnet test --filter 'FullyQualifiedName~Steeltoe.NetCoreTool.Template.WebApi.Test.HelloWorldProjectOptionTest&Category=ProjectBuild'
...
Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4, Duration: 20 s ...
The project build tests pass so the implementation of the new project option is complete.
If a project build test should fail:
- fix the error
- update the project generation tests if needed
- rerun the project build tests