Cross-platform development with .NET Core and F#
This is my script for a workshop I will be leading at the University of Arizona IT Summit InterActive day
IT Summit interActive Workshop
Workshop Leader
Marnee Dearman
Chief Applications Architect
College of Medicine
Summary
Cross-platform software development with .NET Core and F#.
This is the script I will follow but it can be used to learn or practice on your own.
Topics
- .NET Core
- Start a new .NET Core project from the standard templates
- Start a new .NET Core project from imported templates
- Create a solution file and associate projects
- Adding dependencies to a project
- Restoring and building a project
- Running your application
- Running tests
- Publishing your application to different target systems (Linux, Linux ARM, MacOS, Windows)
- F#
- Union types and record types for elegant domain modeling and enforcing constraints
- Using Argu to quickly build a command-line tool
- Using Expecto to write tests
- Using SAFE stack to create web applications
- Using FAKE to build, test, run, and deploy applications
Workshop requirements
- .NET Core 2.2 SDK
- Get the latest version here. Select your operating system and follow the installation instructions.
Workshop setup
We need a folder to work in. Let's create one.
Find a file system location that works for you. I am going to do this in my Windows home directory on my Windows Subsystem for Linux environment. This is equivalent to my Windows home directory. If you are on Windows, you can use that too. Use a location that works for you.
Ubuntu
marnee@DESKTOP-BBKBQMF:/mnt/c/Users/Marnee$ pwd
/mnt/c/Users/Marnee
Windows command-line:
Marnee@DESKTOP-BBKBQMF C:\Users\Marnee
> cd
C:\Users\Marnee
Use the command line commands that work for your environment.
BaSH/MacOS Terminal
mkdir interactive-workshop
cd interactive-workshop
DOS/Windows Command Line
mkdir interactive-workshop
cd interactive-workshop
This is the folder where we will do all of our work for the rest of the workshop.
(wait for green stickies)
.NET Core CLI
The .NET Core CLI has a number of commands to help you:
- Scaffold a new project from a built in template or custom project template
- Add and manage packages and dependencies
- Restore dependencies
- Build & compile projects
- Publish applications
- Build and run tests
- Run applications
- Auto-rebuild and re-load while running (easier to make changes)
dotnet new
dotnet new
is what we use to scaffold a new project.
On your command line enter:
dotnet new
You should see a list of options and templates. Let's look at the options:
Options:
-h, --help Displays help for this command.
-l, --list Lists templates containing the specified name. If no name is specified, lists all templates.
-n, --name The name for the output being created. If no name is specified, the name of the current directory is used.
-o, --output Location to place the generated output.
-i, --install Installs a source or a template pack.
-u, --uninstall Uninstalls a source or a template pack.
--nuget-source Specifies a NuGet source to use during install.
--type Filters templates based on available types. Predefined values are "project", "item" or "other".
--dry-run Displays a summary of what would happen if the given command line were run if it would result in a template creation.
--force Forces content to be generated even if it would change existing files.
-lang, --language Filters templates based on language and specifies the language of the template to create.
--l, --list Lists templates containing the specified name. If no name is specified, lists all templates.
Let's use this option to see a list of templates. We will use templates to create our projects. Let's see what kinds of templates we have:
Your templates may look different than mine. That is ok.
dotnet new --list
Result
Templates Short Name Language Tags
------------------------------------------------------------------------------------------------------------------------------------------------
Console Application console [C#], F#, VB Common/Console
Class library classlib [C#], F#, VB Common/Library
Unit Test Project mstest [C#], F#, VB Test/MSTest
NUnit 3 Test Project nunit [C#], F#, VB Test/NUnit
NUnit 3 Test Item nunit-test [C#], F#, VB Test/NUnit
xUnit Test Project xunit [C#], F#, VB Test/xUnit
Razor Page page [C#] Web/ASP.NET
MVC ViewImports viewimports [C#] Web/ASP.NET
MVC ViewStart viewstart [C#] Web/ASP.NET
ASP.NET Core Empty web [C#], F# Web/Empty
ASP.NET Core Web App (Model-View-Controller) mvc [C#], F# Web/MVC
ASP.NET Core Web App webapp [C#] Web/MVC/Razor Pages
ASP.NET Core with Angular angular [C#] Web/MVC/SPA
ASP.NET Core with React.js react [C#] Web/MVC/SPA
ASP.NET Core with React.js and Redux reactredux [C#] Web/MVC/SPA
Razor Class Library razorclasslib [C#] Web/Razor/Library/Razor Class Library
ASP.NET Core Web API webapi [C#], F# Web/WebAPI
global.json file globaljson Config
NuGet Config nugetconfig Config
Web Config webconfig Config
Solution File sln Solution
We have a lot of built-in templates.
The first column is the template, the second is the short name, which is used in the dotnet new
command, and the third is the language supported by that template. Notice we have lots of F# templates available.
These are the templates we are going to start with:
Templates Short Name Language Tags
------------------------------------------------------------------------------------------------------------------------------------------------
Console Application console [C#], F#, VB Common/Console
Class library classlib [C#], F#, VB Common/Library
Solution File sln Solution
Console Application
Scaffolds a project you can use to build a CLI or an executable. We will see more later.
Class Library
This is a class library that can be referenced by other projects. It cannot be executed, or run.
Solution File
A solution file defines a set of projects that are related to each other. This is helpful in IDEs like Visual Studio Code and Visual Studio. The IDEs can use the solution file to organize your projects. The solution file can also help with the compiler.
Use dotnet new
to scaffold projects
First we need a solution structure. This is what I will use and is similar to what I usually do. This mostly follows the principles of clean architecture
or onion architecture
.
interactive-workshop
|
workshop.sln (file)
|
src (folder)
|
workshop.cli (folder)
workshop.domain (folder)
workshop.test (folder)
workshop.web (folder)
We will talk about all of these parts later, but first let's setup the folder structure.
On the bash command-line this looks like this:
mkdir src
mkdir src/workshop.cli
mkdir src/workshop.domain
mkdir src/workshop.test
mkdir src/workshop.web
(Create your folder structure now)
(Wait for green stickies)
Scaffold a new console application
Let's create a Console Application
using the F# language. It is going to live in the workshop.cli
folder.
By default, dotnet new
names your project according to the folder in which you are creating the project. So to get a new workshop.cli
, use cd
to get into src/workshop.cli
, first.
cd src/workshop.cli
dotnet new console -lang F#
A lot of stuff happened but you should see a confirmation message in the output.
The template "Console Application" was created successfully.
Great! You just created a console app. Let's see what dotnet new
created:
BaSH/Terminal
ls -la
DoS
dir
drwxrwxrwx 1 marnee marnee 512 Mar 24 11:27 .
drwxrwxrwx 1 marnee marnee 512 Mar 24 11:25 ..
drwxrwxrwx 1 marnee marnee 512 Mar 24 11:27 obj
-rwxrwxrwx 1 marnee marnee 172 Mar 24 11:26 Program.fs
-rwxrwxrwx 1 marnee marnee 252 Mar 24 11:26 workshop.cli.fsproj
Program.fs
This where all your code will go. This is the entry point for execution.
workshop.cli.fsproj
proj
files are common to all .NET projects. This defines what files are associated with the project, and the compiler will use the proj
file to know what code to compile.
Run a console project
Let's see what this workshop.cli will do.
dotnet run
dotnet run
will do this by default:
restore dependencies
build (compile) the project
a. This means it will turn the code into a binary file.
run the application
Let's see what this command can do, first.
dotnet run -h
You should see the usage and options.
Usage: dotnet run [options] [[--] <additional arguments>...]]
Options:
-h, --help Show command line help.
-c, --configuration <CONFIGURATION> The configuration to run for. The default for most projects is 'Debug'.
-f, --framework <FRAMEWORK> The target framework to run for. The target framework must also be specified in the project file.
-p, --project The path to the project file to run (defaults to the current directory if there is only one project).
--launch-profile The name of the launch profile (if any) to use when launching the application.
--no-launch-profile Do not attempt to use launchSettings.json to configure the application.
--no-build Do not build the project before running. Implies --no-restore.
--no-restore Do not restore the project before building.
-v, --verbosity <LEVEL> Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic].
--runtime <RUNTIME_IDENTIFIER> The target runtime to restore packages for.
--no-dependencies Do not restore project-to-project references and only restore the specified project.
--force Force all dependencies to be resolved even if the last restore was successful.
This is equivalent to deleting project.assets.json.
Additional Arguments:
Arguments passed to the application that is being run.
Cool! We have a lot of options. Let's try running the default options first.
Remember the usage? dotnet run -h
can help.
dotnet run
By default, if no -p, --project
option passed, dotnet
will try to find a project file in the current folder, and if it finds an executeable project, it will do the run on that application.
If it seems slow, this is ok. That is because dotnet run
is restoring and building first.
If the run
worked, you should see some friendly output:
Hello World from F#!
Great success! You just:
- Scaffolded a console application with
dotnet new
- Ran the application with
dotnet run
- And it worked!
(wait for stickies)
Class Library
Let's go through the steps to start a class library.
Remember that
dotnet new
will scaffold a project with the same name as the containing folder, so remember to cd into the desired folder before running the command.
Let's first go to the workshop.domain
folder we created earlier. This is where we will scaffold our class library.
cd ../workshop.domain
(wait for green stickies)
Do you remember how to see the templates?
dotnet new --list
The Class Library templates short name is classlib
.
Remember the usage?
Examples:
dotnet new mvc --auth Individual
dotnet new nunit-test
dotnet new --help
What command do we use to create a class library using F#?
The default language is C# so don't forget to specify the language if you want to use something else.
dotnet new classlib -lang F#
Let's see what it created.
BaSH/Terminal
ls -la
DoS
dir
You should see the new project files.
-rwxrwxrwx 1 marnee marnee 101 Mar 24 13:46 Library.fs
drwxrwxrwx 1 marnee marnee 512 Mar 24 13:46 obj
-rwxrwxrwx 1 marnee marnee 219 Mar 24 13:46 workshop.domain.fsproj
(wait for stickies)
Here we see the proj
file again. And a file called Libary.fs
with a bit of code in it.
Since Class Libraries are not executable, we can't run dotnet run
on it. But we can reference it in an executable project, like a console application, and reference and use the class library in that project.
You can't access code in a separate project without referencing in.
References go in the
proj
file using certain XML elements and attributes.
Let's try that.
dotnet add
First, go up one level to the src
file. This will make it easier to write the commands.
cd ..
Let's reference workshop.domain
in workshop.cli
.
To do that we use the dotnet add
command. Let's see the usage and options.
dotnet add -h
Usage: dotnet add [options] <PROJECT> [command]
Arguments:
<PROJECT> The project file to operate on. If a file is not specified, the command will search the current directory for one.
<PROJECT>
is the path/project file to the project to which you want to add the reference. In this case it is the path to the workshop.cli
project file. We will see this later.
Commands:
package <PACKAGE_NAME> Add a NuGet package reference to the project.
reference <PROJECT_PATH> Add a project-to-project reference to the project.
<PROJECT_PATH> is the path to the class library you want to use in your project.
How would we reference workshop.domain
from workshop.cli
?
dotnet add workshop.cli reference workshop.domain
Pro tip: use tab complete. Type out a few characters and then hit tab. The command line will try to complete the path for you. This is available in both BaSH and DoS.
You should see this output.
Reference `..\workshop.domain\workshop.domain.fsproj` added to the project.
Let's see what happened to the proj file. Let's print the content to the screen.
BaSH/Terminal
cat workshop.cli/workshop.cli.fsproj
DoS
type workshop.cli\workshop.cli.fsproj
Notice this XML element:
<ItemGroup>
<ProjectReference Include="..\workshop.domain\workshop.domain.fsproj" />
</ItemGroup>
(wait for stickies)
(resolve problems if any)
Review
We learned how to:
- scaffold a console and class library using
dotnet new
- run a console app using
dotnet run
- add a reference to the class library using
dotnet add
(take a break and answer questions)
Scaffold a test project
First, get yourself into the workshop.test
folder.
cd workshop.test
Pro tip: tab complete is your best friend.
dotnet new
comes with templates for creating xUnit and nUnit test projects. Those are great, but let's use something functional programming oriented.
Let's use Expecto
.
Expecto publishes a dotnet
template that we can install and then use.
Go to the Expecto template on Github.
There are lots of different templates available.
You'll see instructions on how to install the template from Nuget (Nuget is a .NET package manager and repository).
dotnet new -i Expecto.Template
-i
is the option
for installing new templates.
You should see output that looks like the dotnet new -h
command. Notice in the templates list that there is a new template:
Expecto .net core Template expecto F# Test
Now we can use it to scaffold a new Expecto project.
dotnet new expecto -lang F#
Let's see what it created.
BaSH/Terminal
ls -la
DoS
dir
-rwxrwxrwx 1 marnee marnee 123 Mar 24 20:41 Main.fs*
-rwxrwxrwx 1 marnee marnee 1206 Mar 24 20:41 Sample.fs*
-rwxrwxrwx 1 marnee marnee 639 Mar 24 20:41 workshop.test.fsproj*
Notice that we have a proj
file and two sample test files.
(wait for stickies)
Run the tests
We can use dotnet run
or dotnet test
to run tests because technically they are console apps with a bit of extra code scaffolded to create sample tests.
Let's try it.
dotnet run
You should see a bunch of output and stuff that looks like errors. That's ok. Some of the tests are meant to fail as demonstrations in the sample code. The most interesting bit in the last line.
20:56:42 INF] EXPECTO! 8 tests run in 00:00:00.8850460 for samples – 2 passed, 1 ignored, 5 failed, 0 errored. <Expecto
Here we see a report of the number of tests that failed, were ignored, and passed.
(wait for stickies)
Let's see it with dotnet test
dotnet test
We will write more tests later.
Solution file
Let's create a solution file so we can tie all of our projects together. The solution provides these benefits:
dotnet build
all of our projects at oncedotnet test
all of you test projects at once- Visual Studio and Visual Studio Code use the solution file to organize projects
Go up two levels so you are now in the interactive-workshop
folder.
cd ../..
Check to make sure.
pwd
cd
dotnet build without a solution file
Let's try building (compiling) code without a solution file.
dotnet build
What happened? A whole lotta nothing. But that's ok because we will scaffold a solution file to help us.
MSBUILD : error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file.
Now use dotnet new
to create the solution file.
dotnet new sln
Let's look inside it to see what happened.
ls -la
dir
Notice that dotnet new
created a solution file with the same name as the folder.
Did you see this file?
interactive-workshop.sln
Let's see what is inside.
cat interactive-workshop.sln
type interactive-workshop.sln
Pro tip: tab complete will save you time and money!
That's a lot of weird stuff. It doesn't matter much but it's mostly a bunch of stuff msbuild
and Visual Studio understand.
Notice that there are no references to any of our workshop projects. That's ok. We are going to add them.
But first try a dotnet build
to see what happens.
dotnet
doesn't know what to build. That's ok. We are going to help it.
(wait for stickies)
dotnet sln add
We have a new command to use that helps us with solution files. Let's see what it does.
dotnet sln -h
Commands:
add <PROJECT_PATH> Add one or more projects to a solution file.
list List all projects in a solution file.
remove <PROJECT_PATH> Remove one or more projects from a solution file.
Cool! Looks like we can add projects, list projects, and remove projects.
Let's add the workshop.domain
project.
dotnet sln add src/workshop.domain
If that worked you should be able to build now.
dotnet build
What happened?
Did it look a little like this?
Microsoft (R) Build Engine version 15.9.20+g88f5fadfbe for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
Restoring packages for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/workshop.domain.fsproj...
Generating MSBuild file /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/obj/workshop.domain.fsproj.nuget.g.props.
Generating MSBuild file /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/obj/workshop.domain.fsproj.nuget.g.targets.
Restore completed in 541.07 ms for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/workshop.domain.fsproj.
workshop.domain -> /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/bin/Debug/netstandard2.0/workshop.domain.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:11.74
Great! That worked. Now try to add the .cli
and .test
projects.
dotnet sln add src/workshop.cli
dotnet sln add src/workshop.test
(wait for stickies)
What happens when you dotnet build
now?
(wait for stickies)
Awesome! This will save us time and typing effort and lessen our cognitive burden.
Domain model and domain logic
Get yourself to the workshop.domain
folder. Let's code our domain with a little F#.
cd src/workshop.domain
The domain
We work at a University so let's model a course.
Field Type Constraints
Number int 5 digits
Name string 100 chars
Description string 500 chars
Credits int less than 4
Department int must be a valid department code
Ok that's good enough to get started.
With your favorite editor open the Library.fs file.
Let's model the Department
first. For this we will use a discriminated union
.
It looks like this and you can think of it like an enum.
module Workshop =
type DepartmentCode =
| Engineering
| Geosciences
| FineArts
The department can be for one of three departments:
- Engineering
- Geosciences
- FineArts
Each department has a department code. Let's add a way to get the department code from the DepartmentCode type.
module Workshop =
type Department =
| Engineering
| Geosciences
| FineArts
| NotFound
member this.ToCode() =
match this with
| Engineering -> 100
| Geosciences -> 200
| FineArts -> 300
| NotFound -> 0
override this.ToString() =
match this with
| Engineering -> "Engineering"
| Geosciences -> "Geosciences"
| FineArts -> "Fine Arts"
| _ -> String.Empty
Let's add some more. Remember the domain?
Field Type Constraints
Number int 5 digits
Name string 100 chars
Description string 500 chars
Credits int less than 4
Department int must be a valid department code
Let's create a type that models everything that makes up a course. We will use a Record Type
to represent a course.
Pro tip: copy and paste! Don't type this all.
type Course =
{
Number : int
Name : CourseName
Description : string
Credits : int
Department : Department
}
Record types define the shape of your data. You can think of them like properties on a class.
Let's make sure you don't have any syntax errors. Let's run a build. Remember how to do that?
dotnet build
Did you get any errors? Try to fix them. I'll help.
(wait for stickies)
The code should currently look ike this:
namespace workshop.domain
open System
module Say =
let hello name =
printfn "Hello %s" name
module Workshop =
type Department =
| Engineering
| Geosciences
| FineArts
| NotFound
member this.ToCode() =
match this with
| Engineering -> 100
| Geosciences -> 200
| FineArts -> 300
| NotFound -> 0
override this.ToString() =
match this with
| Engineering -> "Engineering"
| Geosciences -> "Geosciences"
| FineArts -> "Fine Arts"
| _ -> String.Empty
let getDepartment code =
match code with
| 100 -> Engineering
| 200 -> Geosciences
| 300 -> FineArts
| _ -> NotFound
type Course =
{
Number : int
Name : string
Description : string
Credits : int
Department : Department
}
That's nice. Let's code some constraints. Let's look at the domain again.
Field Type Constraints
Number int 5 digits, less than 100000
Name string 100 chars
Let's do Name
first.
Copy and paste this above type Course
, and I will explain it.
type CourseName = private CourseName of string
module CourseName =
let create (s:string) =
match s.Trim() with
| nm when nm.Length <= 100 -> CourseName nm
| nm -> CourseName (nm.Substring(0, 100))
let value (CourseName s) = s
This makes it so that you can only create a CourseName type things through the create function.
Try to build to check for errors:
dotnet build
(wait for stickies)
Now that we have a CourseName
type we can make the Name field in course that type. Like this.
type Course =
{
Number : int
Name : CourseName
Description : string
Credits : int
Department : Department
}
(wait for stickies)
This means that for every instance of a Course type, you will only be able to set the Name to a value that passes the CourseName constraints. Like this.
Name = CourseName.create "Underwater Basket Weaving"
Create a testCourse like this.
let testCourse =
{
Number = 9999
Name = CourseName.create "Underwater Basket Weaving"
Description = "Traditional basket weaving done under water for best effect."
Credits = 3
Department = FineArts
}
The whole file.
namespace workshop.domain
open System
module Say =
let hello name =
printfn "Hello %s" name
module Workshop =
type Department =
| Engineering
| Geosciences
| FineArts
| NotFound
member this.ToCode() =
match this with
| Engineering -> 100
| Geosciences -> 200
| FineArts -> 300
| NotFound -> 0
override this.ToString() =
match this with
| Engineering -> "Engineering"
| Geosciences -> "Geosciences"
| FineArts -> "Fine Arts"
| _ -> String.Empty
let getDepartment code =
match code with
| 100 -> Engineering
| 200 -> Geosciences
| 300 -> FineArts
| _ -> NotFound
type CourseName = private CourseName of string
module CourseName =
let create (s:string) =
match s.Trim() with
| nm when nm.Length <= 100 -> CourseName nm
| nm -> CourseName (nm.Substring(0, 100))
let value (CourseName s) = s
type Course =
{
Number : int
Name : CourseName
Description : string
Credits : int
Department : Department
}
let testCourse =
{
Number = 9999
Name = CourseName.create "Underwater Basket Weaving"
Description = "Traditional basket weaving done under water for best effect."
Credits = 3
Department = FineArts
}
Let's see if your code builds. Do you remember how to do that?
dotnet build
(wait for stickies)
Write tests against your domain code
Now that we have some code we can write tests against it.
First, we will need to reference the domain project in the test project so we can use that code.
Pro tip: We can do everything by path, so we don't have to change directories.
Remember the usage?
Usage: dotnet add [options] <PROJECT> [command]
Commands:
package <PACKAGE_NAME> Add a NuGet package reference to the project.
reference <PROJECT_PATH> Add a project-to-project reference to the project.
First go to the top level folder:
Bash/Terminal
cd ../..
DoS
cd ..\..
Do it like this.
dotnet add src/workshop.test reference src/workshop.domain
Now let's check the proj
file.
BaSH/Terminal
cat src/workshop.test/workshop.test.fsproj
DoS
type src\workshop.test\workshop.test.fsproj
Pro tip: If you aren't using tab complete then you are doing it wrong.
Did you see this?
<ItemGroup>
<ProjectReference Include="..\workshop.domain\workshop.domain.fsproj" />
</ItemGroup>
(wait for stickies)
Great. Now let's write a test.
Create a new file and open it in your editor. I will use vim and Visual Studio Code.
vim src/workshop.test/DomainTests.fs
code src/workshop.test/DomainTests.fs
Add the code:
module DomainTests
open Expecto
open workshop.domain
[<Tests>]
let tests =
testList "Course Tests" [
testCase "Engineering convert to code 100" <| fun _ ->
Expect.equal (Workshop.Engineering.ToCode()) 100 "Engineering course code should be 100"
]
Pro tip: You can copy and paste. Don't type it all in.
Save the file.
Now we need to add the file to the project file. This is so the compiler knows what to compile.
In F# the order of the files matters. The proj file specifies the order of the files.
Open workshop.test.fsproj
. Add the file in the right order. Also, let's remove the Samples.fs
file to keep it simple.
<ItemGroup>
<Compile Include="DomainTests.fs" />
<Compile Include="Main.fs" />
</ItemGroup>
Let's check the code by building the test project. Let's take the easy way and just build the solution.
dotnet build
(wait for stickies)
If it is building, let's run the tests.
dotnet test
Did you notice that we don't need to specify a test project? dotnet
will iterate through each project in the solution file looking for a test project. When it finds one it will try to run the tests.
Build started, please wait...
Skipping running test for project /mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj. To run tests with dotnet test add "<IsTestProject>true<IsTestProject>" property to project file.
Skipping running test for project /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/workshop.domain.fsproj. To run tests with dotnet test add "<IsTestProject>true<IsTestProject>" property to project file.
Build completed.
Test run for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.test/bin/Debug/netcoreapp2.2/workshop.test.dll(.NETCoreApp,Version=v2.2)
Microsoft (R) Test Execution Command Line Tool Version 15.9.0
Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait...
And then the results of the test.
Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
- Total tests : 1
- Passed: 1
- Failed: 0
- Skipped: 0
This looks good. We had one test and it passed. Yay!
(wait for stickies)
If we have time I'll take us through writing another test and talk about Expecto.
dotnet watch
Wouldn't it be cool if we could automatically make the tests run whenever any code changes? You can!
We can use dotnet watch
to do this.
dotnet watch -h
Examples:
dotnet watch run
dotnet watch test
The watch command.
dotnet watch -p src/workshop.test test
The output.
watch : Started
Build started, please wait...
Build completed.
Test run for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.test/bin/Debug/netcoreapp2.2/workshop.test.dll(.NETCoreApp,Version=v2.2)
Microsoft (R) Test Execution Command Line Tool Version 15.9.0
Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait...
Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
Test Run Successful.
Test execution time: 25.3014 Seconds
watch : Exited
watch : Waiting for a file to change before restarting dotnet...
Neat!
Notice dotnet
is patiently waiting for files to change.
watch : Waiting for a file to change before restarting dotnet...
(wait for stickies)
Ok, now what would happen with the watched tests if I changed the domain model? Let's try it.
In workshop.domain
change the
Engineering course code to 500
| Engineering -> 500
and then check back in your command line.
Those of you not using a desktop, maybe you can use screen? Or just play along. I will demo.
Your test should have failed.
Failed Course Tests/Engineering convert to code 100
Error Message:
Engineering course code should be 100.
expected: 100
actual: 500
Now change the code back and see your test pass.
Stop the
dotnet watch
withCtl + C
orCtl + D
(wait for stickies)
Build a command line tool
Ok we are cooking with gas! Let's build a CLI. We are going to use a package called Argu
that will help us quickly write a command line parser.
Add a package reference to Argu
In order to use Argu in workshop.cli
we will need to pull in the package.
First, remember how to add a dependency?
dotnet add -h
Usage: dotnet add [options] <PROJECT> [command]
Commands:
package <PACKAGE_NAME> Add a NuGet package reference to the project.
dotnet add src/workshop.cli package Argu
This will download the package from nuget
and add a reference in the proj
file.
Did you see this?
:
:
:
log : Installing Argu 5.2.0.
info : Package 'Argu' is compatible with all the specified frameworks in project '/mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj'.
info : PackageReference for package 'Argu' version '5.2.0' added to file '/mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj'.
info : Committing restore...
info : Writing lock file to disk. Path: /mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/obj/project.assets.json
log : Restore completed in 7.41 sec for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj.
You're the best!
(wait for stickies)
We also need to reference workshop.domain
so we can use it in our CLI.
Can you figure it out yourself?
dotnet add src/workshop.test reference src/workshop.domain
Let's try to use it in the CLI.
Open src/workshop.cli/Program.fs
.
Here is the code.
// Learn more about F# at http://fsharp.org
open System
open Argu
open workshop.domain
//This is where we difine what options we accept on the command line
type CLIArguments =
| DepartmentCode of dept:int
with
interface IArgParserTemplate with
member s.Usage =
match s with
| DepartmentCode _ -> "specify a course code."
[<EntryPoint>]
let main argv =
let errorHandler = ProcessExiter(colorizer = function ErrorCode.HelpText -> None | _ -> Some ConsoleColor.Red)
let parser = ArgumentParser.Create<CLIArguments>(programName = "workshop", errorHandler = errorHandler)
let cmd = parser.ParseCommandLine(inputs = argv, raiseOnUsage = true)
printfn "I'm doing all the things!"
match cmd.TryGetResult(CLIArguments.DepartmentCode) with
| Some code -> printfn "The department name is [%s]" ((Workshop.getDepartment code).ToString())
| None -> printfn "I could not understand the department code. Please see the usage."
0 // return an integer exit code
Let's build it to check for errors:
dotnet build
(wait for stickies)
Let's run it without building to save tme.
dotnet run --no-build -p src/workshop.cli/
Ooops!
The CLI doesn't know what we want. Argu helped us write that handling in just a few lines.
Let's try that a different way. dotnet
has a way to pass custom parameters to dotnet run
.
First you have your dotnet command followed by --
followed by the parameters.
What is our command usage?
dotnet run --no-build -p src/workshop.cli/ -- --help
USAGE: workshop [--help] [--departmentcode <dept>]
OPTIONS:
--departmentcode <dept>
specify a course code.
--help display this list of options.
Let's try passing the department code like this.
dotnet run --no-build -p src/workshop.cli/ -- --departmentcode 100
If we have time I will show more how to use Argu.
(wait for stickies)
Publish your code to ... somewhere
Ok let's say you are ready to publish your code. You want to share the working version with the world, but you don't want users to have to run the dotnet
command. You want them to just use your cli. You can publish your command and all of its dependencies. You can then execute the command like you would any other program. You can even put a reference in your environment or /usr/bin
. Whatever works for you.
You can find out more in the Microsoft documentation Deploy with CLI.
Let's see the usage.
dotnet publish -h
Usage: dotnet publish [options] <PROJECT>
Lots of options. Let's focus on this one for right now.
-o, --output <OUTPUT_DIR> The output directory to place the published artifacts in.
This will tell dotnet where to put your published files.
Let's try that. First create a publish directory.
mkdir publish
Let's publish. Notice the path
I put there. dotnet
will try to create the publish file in the same directory as the project you are publishing. If we give it the relative path it will publish there, instead.
dotnet publish -o ../../publish src/workshop.cli
(wait for stickies)
Check the publish folder contents.
BaSH/Terminal
ls -la publish
DoS
dir publish
We have a lot of stuff in there.
Let's try to run the published version.
BaSH/Terminal
./publish/workshop.cli.dll
DoS
publish\workshop.cli.dll
What happened? Did you get an error?
Unhandled Exception: System.IO.FileNotFoundException: Could not load file or assembly 'System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.
Yep. This is because the way we published it means we need to use the dotnet command to run it. This means that if you give this to someone else to run, they will need to have dotnet installed. Let's try running it that way and then we will publish a standalone executable.
dotnet publish/workshop.cli.dll
Did you see the output from before? Yes, because you are awesome.
(wait for stickies)
Having a dependency on dotnet
isn't much fun, though. But that is ok because we can publish our cli as a stand alone such that all dependencies are self-contained
. Can you guess what the option will be?
dotnet publish -h
--self-contained Publish the .NET Core runtime with your application so the runtime doesn't need to be installed on the target machine.
The default is 'true' if a runtime identifier is specified.
Let's try that.
dotnet publish --self-contained -o ../../publish src/workshop.cli
(wait for stickies)
Did you get an error?
error NETSDK1031: It is not supported to build or publish a self-contained application without specifying a RuntimeIdentifier. Please either specify a RuntimeIdentifier or set SelfContained to false. [/mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj]
That's ok. We need to specify a runtime identifier. This is basically the environment you want to run it on.
This is the usage.
-r, --runtime <RUNTIME_IDENTIFIER> The target runtime to publish for. This is used when creating a self-contained deployment.
The default is to publish a framework-dependent application.
Let's do that. Here are some common identifiers.
- win10-x64 (Windows 10)
- linux-x64 (Most desktop distributions like CentOS, Debian, Fedora, Ubuntu and derivatives)
- osx.10.14-x64 (MacOS Mojave)
Find the entire catalog here if I didn't list yours above.
dotnet publish -r linux-x64 --self-contained -o ../../publish src/workshop.cli
No errors. Let's see what is inside the publish folder.
ls -la publish
dir publish
That's a lot more stuff than we had before. Do you see
workshop.cli.*
That is your "executeable" program.
(wait for stickies)
What happens if we try to run it?
./publish/workshop.cli
That looks familiar. Let's give it a department code.
./publish/workshop.cli --departmentcode 100
It works!
(wait for stickies)
If we have time I will show how to create a web application using Saturn.
Web Application -- SAFE STACK
You'll need to install the following pre-requisites in order to build SAFE applications
dotnet tool install -g fake-cli
dotnet tool install -g paket
- node.js (>= 8.0)
- yarn (>= 1.10.1) or npm
Install tools
FAKE
dotnet tool install -g fake-cli
paket (package manager)
dotnet tool install -g paket
Node
Find your install method here
Ubuntu
curl -sL https://deb.nodesource.com/setup_11.x | sudo -E bash -
sudo apt-get install -y nodejs
Yarn
Find your install instructions here.
Ubuntu example
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
Scaffold SAFE stack app
Install the template
dotnet new -i SAFE.Template
Create the project
cd workshop.web
dotnet new SAFE -lang F#
Build and run using FAKE
If you don't have a browser this might not work so good. If it works you should see a browser window or tab appear.
fake build --target run
If you are using WSL, if you run the app you can access it from the Windows side with this URL:
http://localhost:8080/
And there ya go. A fully functional web application in only a handful of steps.
Open the build script and walk through it
The dotnet goat path
Review the templates.
dotnet new --list
Create your folder structure. Remember to be inside the folder where you want to create the project before creating the project.
By default, dotnet new will create a project with the same name as the folder you are in. There is an option to specify the project name (
-n, --name
), which also creates the folder. I like to create the folder ahead of time so I can work out the structure first.
dotnet new console -lang F#
This created a console app.
Build the console app
dotnet build <PATH TO CONSOLE PROJECT FOLDER>
Create a class library inside the class library folder you created.
dotnet new classlib -lang F#
Build the class library.
dotnet build <PATH TO LIBRARY PROJECT FOLDER>
Create a test project inside the test project folder you created.
If you want to use Expecto, you need to install the templates first.
dotnet new -i Expecto.Template::*
There are lots of templates out there for all kinds of projects.
dotnet new expecto -lang F#
Run tests
dotnet test <PATCH TO TEST PROJECT>
Create a solution file to help build and test without specifying a <PATH TO PROJECT FOLDER>
.
Put the solution file in a folder above the source code folder like this.
sln
|
src
workshop.cli
workshop.domain
workshop.test
dotnet new sln
By default, this will create a solution file with the same name as the containing folder. You can use the option
-n
or--name
.
Add projects to the soution file
dotnet sln add <PATH TO PROJECT FOLDER>
Build using the sln file
Make sure you are in the same folder as the soltion file. dotnet
will look for the sln file and build everything in the file.
dotnet build
Run test projects using the sln file
dotnet test
dotnet will run any test project it finds in the solution file.
Run a cli using dotnet with arguments
dotnet run -p <PATH TO CONSOLE APP> -- --arg value
Publish self-contained app to target operating system
dotnet publish -r <Runtime IDentifier> --self-contained -o <PATH TO PUBLISH FOLDER> <PATH TO CONSOLE PROJECT>
Run the published app
.<PATH TO PUBLISHED EXECUTEABLE> --argu value
Hello @marnee! This is a friendly reminder that you can download Partiko today and start earning Steem easier than ever before!
Partiko is a fast and beautiful mobile app for Steem. You can login using your Steem account, browse, post, comment and upvote easily on your phone!
You can even earn up to 3,000 Partiko Points per day, and easily convert them into Steem token!
Download Partiko now using the link below to receive 1000 Points as bonus right away!
https://partiko.app/referral/partiko