This is a series on the latest 2.* .Net Core bits, Following on from the original .Net Core Series

  • Getting Started
  • What's in the box
  • Using Multiple Projects <=(We are here)
  • Testing
  • NuGet
  • Multi-targeting
  • Publishing Portable Applications
  • Self-contained Applications

(At the time of writing, 2.1.4. I use windows, you don't have to!)

We know how to create new projects, we know how to build and run our application and we know (roughly) what the content in the csproj file means. But most projects tend to require multiple projects.

We are going to create a library project, and an application project. We can reference the library from the application, to see how referencing projects work.

File -> New Application

We should be old pros at this bit so let's go:

mkdir MyNewApp
cd MyNewApp
dotnet new console
dotnet run

File -> New Library

As well as creating empty console applications, dotnet can also create other things. If you try typing dotnet new you will see a full list of all templates available, which you may remember from Getting Started:

Usage: new [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.
  --type              Filters templates based on available types. Predefined values are "project", "item" or "other".
  --force             Forces content to be generated even if it would change existing files.
  -lang, --language   Specifies the language of the template to create.


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
xUnit Test Project                                xunit            [C#], F#, VB      Test/xUnit
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                              razor            [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
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
Razor Page                                        page                               Web/ASP.NET
MVC ViewImports                                   viewimports                        Web/ASP.NET
MVC ViewStart                                     viewstart                          Web/ASP.NET

Examples:
    dotnet new mvc --auth Individual
    dotnet new console
    dotnet new --help

(Again the caveat that mine might be longer than yours because of some extra templates I have installed. This is also much changed since dotnet v1)

We want a Library so will use classlib.

cd ../
mkdir MyNewLib
cd MyNewLib
dotnet new classlib
The template "Class library" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on C:\dev\MyComplexApplicationRepo\MyNewLib\MyNewLib.csproj...
  Restoring packages for C:\dev\MyComplexApplicationRepo\MyNewLib\MyNewLib.csproj...
  Generating MSBuild file C:\dev\MyComplexApplicationRepo\MyNewLib\obj\MyNewLib.csproj.nuget.g.props.
  Generating MSBuild file C:\dev\MyComplexApplicationRepo\MyNewLib\obj\MyNewLib.csproj.nuget.g.targets.
  Restore completed in 115.85 ms for C:\dev\MyComplexApplicationRepo\MyNewLib\MyNewLib.csproj.

Restore succeeded.

We briefly looked at the differences between these projects in the previous section What's in the box. (Specifically, the app will have an extra <OutputType>Exe</OutputType> setting in the csproj file, and targeting netcoreapp2.0 for the app and netstandard2.0 for the library.)

.Net Standard Compatibility is a bit of a topic on its own. You can review the Version Compatibility Grid for dotnet to determine which target you want for your library. If you do leave it as netstandard2.0, then it will be compatible with dotnet core runtime version 2.0 or newer, .Net full framework version 4.6.1 or newer, Mono 5.4, and Windows UWP version 10.0.16299 or newer. Click through to see the full table, including Xamarin and Windows Phone/Silverlight compatibility. The idea is the higher your version, the more base class libraries and methods are available, the lower the version gives you wider compatibility.

So the classlib has access to different library classes and methods depending on which version you target. These are implicitly available to you to use (unlike dotnet v1 where the standard library was an explicit dependency). The dotnet application project target netcoreapp2.0 also includes implicitly available core runtime types, classes and methods as well. Additional libraries are then available from NuGet. (More on that later on in the series)

We can build our library, but of course, it cannot be run - its a library.

dotnet build
Microsoft (R) Build Engine version 15.5.180.51428 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restore completed in 22.02 ms for C:\dev\MyComplexApplicationRepo\MyNewLib\MyNewLib.csproj.
  MyNewLib -> C:\dev\MyComplexApplicationRepo\MyNewLib\bin\Debug\netstandard2.0\MyNewLib.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:01.75

PPPPPPP(*)

Before we can reference between the two projects, we probably need something to depend on. We can go into the file Class1.cs and make a few changes.

namespace MyNewLib
{
    public class Class1
    {
    }
}

Our class can be called Calculator, and we can implement a simple method, Add.

using System;

namespace MyNewLib
{
    public class Calculator
    {
        public int Add(int first, int second)
        {
            return first + second;
        }
    }
}

Now it is a bit more interesting, at least.

Project References

Last time we briefly glossed over Frameworks and Dependencies. Let's go over this again in more detail.

First, we want to reference our library from our application.

cd ../MyNewApp
dotnet add reference ../MyNewLib/MyNewLib.csproj
Reference `..\MyNewLib\MyNewLib.csproj` added to the project.

If we look at our csproj file now, we can see the reference has been added.

<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\MyNewLib\MyNewLib.csproj" />
  </ItemGroup>

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

</Project>

You can basically give any relative path to any csproj and this will work.

One to note: Whenever you now build MyNewApp, it will recursively try and build the project MyNewLib as well.

dotnet build
Microsoft (R) Build Engine version 15.5.180.51428 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restoring packages for C:\dev\MyComplexApplicationRepo\MyNewApp\MyNewApp.csproj...
  Restore completed in 21.63 ms for C:\dev\MyComplexApplicationRepo\MyNewLib\MyNewLib.csproj.
  Restore completed in 141.95 ms for C:\dev\MyComplexApplicationRepo\MyNewApp\MyNewApp.csproj.
  MyNewLib -> C:\dev\MyComplexApplicationRepo\MyNewLib\bin\Debug\netstandard2.0\MyNewLib.dll
  MyNewApp -> C:\dev\MyComplexApplicationRepo\MyNewApp\bin\Debug\netcoreapp2.0\MyNewApp.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:02.68

And we will add some code to make the dependency complete. Open MyNewApp's Program.cs file and add a call to our Calculator.

using System;
using MyNewLib;

namespace MyNewApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Calculator calc = new Calculator();
            var answer = calc.Add(18, 24);
            Console.WriteLine($"The answer is {answer}.");
        }
    }
}

Build and Run

From the MyNewApp folder, we can now build and run our application, including the library code from MyNewLib. All of this can be done by just using dotnet run (which will do a restore, if required, as well as a build, if required).

dotnet run

And we can see from the output that both projects are built and we see the correct output from our application.

The answer is 42.

Because we used run, the restore and build output has been done silently. This is not the case if it fails, you will see the errors on restore, or errors on build. Often when errors are encountered, it pays to run the actual command to see all of the output for diagnosis.

Now we have 2 projects, and a reference between them. For convenience, though, we probably want to group these into some kind of working solution.

File -> New Solution

Unlike dotnet v1, dotnet 2.* SDK is using real MSBuild and has native support built into it to not only run against Visual Studio Solution (.sln) files but manage them as well.

We start by creating a new solution in our parent folder:

cd ../
dotnet new sln
The template "Solution File" was created successfully.

"Solution File" is just another template available, which shows some of the flexibility and power of the dotnet template engine. This will have created a file in the parent folder called MyComplexApplicationRepo.sln, which comes from the parent folder. You can rename this file if desired.

As we have come to expect from the dotnet CLI, a minimal sln file is created:

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Global
        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                Debug|Any CPU = Debug|Any CPU
                Debug|x64 = Debug|x64
                Debug|x86 = Debug|x86
                Release|Any CPU = Release|Any CPU
                Release|x64 = Release|x64
                Release|x86 = Release|x86
        EndGlobalSection
        GlobalSection(SolutionProperties) = preSolution
                HideSolutionNode = FALSE
        EndGlobalSection
EndGlobal

To fill it up, we add our projects to the solution using the dotnet sln ... commands:

dotnet sln add .\MyNewApp\MyNewApp.csproj
dotnet sln add .\MyNewLib\MyNewLib.csproj

This updates the sln file.

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyNewApp", "MyNewApp\MyNewApp.csproj", "{32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyNewLib", "MyNewLib\MyNewLib.csproj", "{76D11E26-4764-4051-B597-9E1D88015259}"
EndProject
Global
        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                Debug|Any CPU = Debug|Any CPU
                Debug|x64 = Debug|x64
                Debug|x86 = Debug|x86
                Release|Any CPU = Release|Any CPU
                Release|x64 = Release|x64
                Release|x86 = Release|x86
        EndGlobalSection
        GlobalSection(SolutionProperties) = preSolution
                HideSolutionNode = FALSE
        EndGlobalSection
        GlobalSection(ProjectConfigurationPlatforms) = postSolution
                {32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
                {32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
                {32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}.Debug|x64.ActiveCfg = Debug|x64
                {32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}.Debug|x64.Build.0 = Debug|x64
                {32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}.Debug|x86.ActiveCfg = Debug|x86
                {32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}.Debug|x86.Build.0 = Debug|x86
                {32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
                {32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}.Release|Any CPU.Build.0 = Release|Any CPU
                {32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}.Release|x64.ActiveCfg = Release|x64
                {32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}.Release|x64.Build.0 = Release|x64
                {32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}.Release|x86.ActiveCfg = Release|x86
                {32BA1F6D-4996-4F9E-A55A-D5ECC5A834A1}.Release|x86.Build.0 = Release|x86
                {76D11E26-4764-4051-B597-9E1D88015259}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
                {76D11E26-4764-4051-B597-9E1D88015259}.Debug|Any CPU.Build.0 = Debug|Any CPU
                {76D11E26-4764-4051-B597-9E1D88015259}.Debug|x64.ActiveCfg = Debug|x64
                {76D11E26-4764-4051-B597-9E1D88015259}.Debug|x64.Build.0 = Debug|x64
                {76D11E26-4764-4051-B597-9E1D88015259}.Debug|x86.ActiveCfg = Debug|x86
                {76D11E26-4764-4051-B597-9E1D88015259}.Debug|x86.Build.0 = Debug|x86
                {76D11E26-4764-4051-B597-9E1D88015259}.Release|Any CPU.ActiveCfg = Release|Any CPU
                {76D11E26-4764-4051-B597-9E1D88015259}.Release|Any CPU.Build.0 = Release|Any CPU
                {76D11E26-4764-4051-B597-9E1D88015259}.Release|x64.ActiveCfg = Release|x64
                {76D11E26-4764-4051-B597-9E1D88015259}.Release|x64.Build.0 = Release|x64
                {76D11E26-4764-4051-B597-9E1D88015259}.Release|x86.ActiveCfg = Release|x86
                {76D11E26-4764-4051-B597-9E1D88015259}.Release|x86.Build.0 = Release|x86
        EndGlobalSection
EndGlobal

While there is a lot of noise in here, the main thing is the first few lines where the 2 projects are set.

Now we have a single entry point to be able to build our application.

dotnet build
Microsoft (R) Build Engine version 15.5.180.51428 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restore completed in 15.72 ms for C:\dev\MyComplexApplicationRepo\MyNewLib\MyNewLib.csproj.
  Restore completed in 16.16 ms for C:\dev\MyComplexApplicationRepo\MyNewApp\MyNewApp.csproj.
  MyNewLib -> C:\dev\MyComplexApplicationRepo\MyNewLib\bin\Debug\netstandard2.0\MyNewLib.dll
  MyNewApp -> C:\dev\MyComplexApplicationRepo\MyNewApp\bin\Debug\netcoreapp2.0\MyNewApp.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:03.77

You can also explicitly name the solution file when using the commands like dotnet build .\MyComplexApplicationRepo.sln.

Up Next

Of course, we don't just want to build and ship code, we want to test it actually works. Next time we will create a test project, and write some tests against our library code above.