When .Net Core came out there was a lot of excitement about the idea of developing .Net code outside of Windows.
The IDE of choice for .Net, Visual Studio, is and probably will always be, a Windows-only program.
There are alternatives though, and today it is possible to fully develop an ASP.NET Core application outside of Windows.
In this blog post we will be looking into that, specifically in Linux.
Please note that all of the tooling described here is cross-platform all of this will work on any platform that supports .Net Core.
The IDE
The full version of Visual Studio is only available in Windows. However, there's an alternative that has a somewhat confusing name: Visual Studio Code.
You might think that VS Code is an inferior version of the full Visual Studio product, but that is not the case.
VS Code is a different project altogether. It's built using a different set of technologies and this brings some features that full VS doesn't have, for example being easily extensible and customizable, and also less demanding in terms of resources.
VS Code offers the ability to extend its functionality by the installation of extensions. For C# you should install the C# extension. You don't need to go through the trouble of looking for it though. When you open a project that has C#, Visual Studio Code will prompt you to install the C# extension if you don't have it already.
In the rest of the blog post we will create a hypothetical ASP.NET Core web application in order to illustrate most of the required tasks when developing in .Net. Namely how to create projects and solutions, reference projects, install nuget packages, use dotnet tools (like entity framework) and run tests.
A Typical Project
The imaginary project we'll be creating is a reminder application. The idea here is to describe a project with sufficient complexity that would require multiple projects that reference each other, a solution file and tests.
The solution will contain an ASP.NET Core project that could, for example, be used to see past and future reminders. A Console application so that we can create reminders from the command line (and to illustrate how we can have a solution with more than one running project) and a class library that will contain all the common logic. We'll also add a test project to illustrate how unit tests can be run from the command line and inside VS Code.
Creating the projects and solution file
By this time you should have installed .Net Core and Visual Studio Code. You should be able to open a terminal window and type
$ dotnet
and get an output something similar to this:
Usage: dotnet [options]
Usage: dotnet [path-to-application]
Options:
-h|--help Display help.
--version Display version.
path-to-application:
The path to an application .dll file to execute.
One of the options of the dotnet command is "new". To check that and other options' help page just do dotnet [option] --help
, in this case dotnet new --help
.
That will display a list of possible project templates to choose from. For example to create an ASP.NET Core MVC project:
$ dotnet new mvc
If you run that as it is, a new MVC project will be created in the current folder, using the name of the current folder as the default namespace and project name.
You might not want that. In case you don't, you can use the --name
and --output
options. Using --name
(or -n
) you can specify the default namespace and with --output
(or -o
) you can specify the folder name to create and where to place the project files. If you use --name
and don't specify --output
, the output folder will implicitly take the same value as --name
.
It is probably a good idea to define the project folder structure before we continue. Here it is:
Reminders
+---+ Reminders.Web
+---+ Reminders.Cli
+---+ Reminders.Common
+---+ Reminders.Tests
First thing we need to do is to create a project folder named Reminders and then inside it do:
$ dotnet new mvc --name Reminders.Web
$ dotnet new console --name Reminders.Cli
$ dotnet new classlib --name Reminders.Common
$ dotnet new xunit --name Reminders.Tests
This will create 4 projects, one MVC, one console, a class library and a xUnit test project.
It's a good idea to create a solution file as well. Especially if eventually you want to open these projects in full Visual Studio, which will prompt you to create one until you eventually do it.
There are advantages in having a solution file other than avoiding full Visual Studio nagging you about creating the .sln file. If you have one you can just go to the Reminders folder and do a dotnet build
or a dotnet restore
and that build/restore all projects referenced in the .sln file.
Also, if you have a .sln
file you can open all projects in VS Code by initiating VS Code in the folder where the .sln
file is located.
Let's create our .sln file, name it Reminders.sln and add the four projects to it (inside the Reminders folder):
$ dotnet new sln --name Reminders
$ dotnet sln add Reminders.Web/Reminders.Web.csproj
$ dotnet sln add Reminders.Cli/Reminders.Cli.csproj
$ dotnet sln add Reminders.Common/Reminders.Common.csproj
$ dotnet sln add Reminders.Tests/Reminders.Tests.csproj
Adding project references
In full Visual Studio when you want to add a reference to a project you can just right click on references and select Add Reference, pick the project you want to add and you're done.
VS Code does not have that functionality. To do this we need to resort to the command line, but it's just as easy. For example, let's reference Reminders.Common in Reminders.Web, Reminders.Cli and Reminders.Tests.
Navigate to the folder where the .sln file is (Reminders) and type:
$ dotnet add Reminders.Web/Reminders.Web.csproj reference Reminders.Common/Reminders.Common.csproj
$ dotnet add Reminders.Cli/Reminders.Cli.csproj reference Reminders.Common/Reminders.Common.csproj
$ dotnet add Reminders.Tests/Reminders.Tests.csproj reference Reminders.Common/Reminders.Common.csproj
If you want to have a look at which other projects are referenced by a particular project you can either open the .csproj file and have a look at the ProjectReference
entries or if you want to do it from the command line: dotnet list PathToCsProj reference
.
You can also navigate to the folder where a particular project is and simply do dotnet add reference pathToOtherProject.csprj
.
Picking the startup project
Open the solution in VS Code. The easiest way to do that is to navigate to the Reminders folder (where the .sln file is) and type code .
.
When you do that you should get a message in VS Code: "Required assets to build and debug are missing from 'Reminders'. Add them?"
Click Yes.
That will create a .vscode
folder.
In that folder there are two files: launch.json
and tasks.json
. launch.json
configures what happens when you press F5 inside VS Code.
For me the project that will run is the Reminders.Cli console project:
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/Reminders.Cli/bin/Debug/netcoreapp2.0/Reminders.Cli.dll",
"args": [],
"cwd": "${workspaceFolder}/Reminders.Cli",
// For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
"console": "internalConsole",
"stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}
Also, the tasks.json
will only compile the Reminders.Cli project:
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Reminders.Cli/Reminders.Cli.csproj"
],
"problemMatcher": "$msCompile"
}
]
}
It is possible to customize these two files so that you can pick which project to run. In this case we want to be able to run either Reminders.Web or Reminders.Cli.
The first thing you should do is to remove the second item in tasks.json
's args array: "${workspaceFolder}/Reminders.Cli/Reminders.Cli.csproj". We don't need it because we have the solution file (Reminders.sln). When dotnet build
is executed in the folder that contains the sln file all the projects referenced in the solution get built.
After that we can now go to launch.json
and click the "Add Configuration..." button in the bottom left corner.
Select ".Net: Launch a local .NET Core Web Application" and in the generated json change "program" and "cwd" (current working directory) to:
"program": "${workspaceRoot}/Reminders.Web/bin/Debug/netcoreapp2.0/Reminders.Web.dll",
"cwd": "${workspaceRoot}/Reminders.Web"
You can now go to the Debug "section" of Visual Studio Code and see the Web and Console app configuration there:
Note that you can change the configuration names if you like, just change the "name" property in launch.json
.
Now when you press F5 inside VS Code the project that will run is the one that is selected in the Debug section of VS Code.
This only affects VS Code though, the dotnet run
command is unaffected by these changes. In the terminal, if you navigate to the solution's folder and type dontet run
you'll get an error (Couldn't find a project to run...). You need to use the --project
argument and specify the path to the csproj file you want to run, e.g.: dotnet run --project Reminders.Web/Reminders.Web.csproj
. Alternatively you can navigate to the project's folder and execute dontet run
there.
Adding Classes and Interfaces
In full Visual Studio you have several templates available when adding new files to a project.
In VS Code the only out of the box option is to create a new file and use the VS Code snippets (e.g. typing class and then TAB).
Fortunately there's a VS Code extension named "C# Extensions" which adds two options to the context menu on VS Code's explorer view: "New C# Class" and "New C# Interface".
This extension also adds the ability to generate a class' constructor from properties, generate properties from the constructor and add read-only properties and initialize them (there are demos of this in the extension's page).
This section wouldn't be complete without mentioning a currently abandoned alternative named yeoman, specifically the generator-aspnet.
Yeoman is a nodejs application that allows you to install yeoman generators which generate code. For ASP.NET Core there was a particularly interesting generator named generator-aspnet.
That generator provided a set of project templates similar to what dotnet new
now offers (mvc application, console app, etc). But that was not the most interesting thing about generator-aspnet. It was its set of subgenerators.
A subgenerator in yeoman is a small generator for creating just one file, for example a class, an interface or an empty html page. Here's an example of using generator-asp to create a new class: yo aspnet:class ClassName
Generator-asp had loads of these:
yo aspnet:angularcontroller [options] <name>
yo aspnet:angularcontrolleras [options] <name>
yo aspnet:angulardirective [options] <name>
yo aspnet:angularfactory [options] <name>
yo aspnet:angularmodule [options] <name>
yo aspnet:appsettings [options]
yo aspnet:bowerjson [options]
yo aspnet:class [options] <name>
yo aspnet:coffeescript [options] <name>
yo aspnet:dockerfile [options]
yo aspnet:gitignore [options]
yo aspnet:gruntfile [options]
yo aspnet:gulpfile [options] <name>
yo aspnet:htmlpage [options] <name>
yo aspnet:interface [options] <name>
yo aspnet:javascript [options] <name>
yo aspnet:json [options] <name>
yo aspnet:jsonschema [options] <name>
yo aspnet:middleware [options] <name>
yo aspnet:mvccontroller [options] <name>
yo aspnet:mvcview [options] <name>
yo aspnet:nuget [options]
yo aspnet:packagejson [options]
yo aspnet:program [options]
yo aspnet:startup [options]
yo aspnet:stylesheet [options] <name>
yo aspnet:stylesheetless [options] <name>
yo aspnet:stylesheetscss [options] <name>
yo aspnet:taghelper [options] <name>
yo aspnet:textfile [options] <name>
yo aspnet:tfignore [options]
yo aspnet:typescript [options] <name>
yo aspnet:typescriptconfig [options] <name>
yo aspnet:typescriptjsx [options] <name>
yo aspnet:usersecrets [options] <name>
yo aspnet:webapicontroller [options] <name>
Unfortunately they were removed in version 0.3.0. This list is from generator-asp@0.2.6
(latest version as of today, 25/2/2018 is 0.3.3).
From the talk on the generator-aspnet github issues, although never mentioned explicitly, it seems that dotnet new is the only way to go from now on. Unfortunately the only generation commands that are similar to these subgenerators in dotnet new are Nuget Config, Web Config, Razor Page, MVC ViewImports and MVC Viewstart (you get a list of them when you type dotnet new --help).
You can still install this specific version of generator-aspnet though (npm install generator-aspnet@0.2.6) and use it (some of the generators are still useful).
However, for creating classes the extension I mentioned previously is the easiest and most convenient to use. Let's use it to create a Reminder class inside the Reminder.Common project.
The Reminder class has an Id property, a Description, a Date and a boolean flag IsFinished to indicate the reminder was acknowledged.
First thing you need to do is install the "C# Extensions extension". Then right click on Reminders.Common in the explorer view and select New Class:
Name it Reminder.cs and create the three properties. In the end it should look like this:
using System;
namespace Reminders.Common
{
public class Reminder
{
public int Id { get; set; }
public string Description { get; set; }
public DateTime Date { get; set; }
public bool IsFinished { get; set; }
}
}
Now put the cursor on the opening "{" after class Reminders and click the light bulb and select "Initialize ctor from properties":
You should end up with this:
using System;
namespace Reminders.Common
{
public class Reminder
{
public Reminder(int id, string description, DateTime date, bool isFinished)
{
this.Id = id;
this.Description = description;
this.Date = date;
this.IsFinished = isFinished;
}
public int Id { get; set; }
public string Description { get; set; }
public DateTime Date { get; set; }
public bool IsFinished { get; set; }
}
}
Although the constructor isn't really necessary (and it won't play well with Entity Framework if you are planning to use it) , it's just an example of some nice features you get from the extension. Also, when defining properties you can use the same snippets that are available in full Visual Studio (i.e. type prop and press TAB).
Razor views
Razor pages (.cshtml) is definitely an area where VS Code is lacking. There's no intellisense or even auto-indent.
There's an extension in the marketplace named ASP.NET Helper which will get you intellisense, however you have to import the namespaces for your models in _ViewImports.chtml
(the extension's requirements are at the bottom of the extensions page). Also in its current version it only seems to work if the model you use is in the same project as the view.
This extension can be useful in some situations but it's very limited.
If this is a deal breaker for you here's the github issue for adding Razor support to the language service that VS Code relies on and which would provide intellisense similar to what is there for the full version of Visual Studio. If you upvote or leave a comment there it will bring visibility to this issue.
If you want a good experience while creating razor pages today and you don't mind paying a little bit of money you can try JetBrains Rider. Rider runs on Windows, Mac and Linux and is from the same people that created ReSharper. I tried it a good few months ago when it was in beta. At that time it required a fairly decent machine to run smoothly and it was lacking some features. I tried it again today while I'm writing this and it seems much much better. Razor support seems perfect.
Even though the experience with Razor in VS Code right now is not ideal, if you go with a front-end JavaScript framework (Angular, React, Vue, etc) instead of Razor, you'll find that VS Code is excellent. I've used mostly Angular and TypeScript and this is an area where VS Code is arguably better then the full version of Visual Studio.
If you are somewhat familiar with Angular and are planning to use ASP.NET Core as a Web Api for your front end check out my other article Angular and ASP.NET Core.
Nuget packages
In the full version of Visual Studio there's an option to Manage NuGet packages. You get an UI where you can search, update and install NuGet packages. In VS Code there's no support for NuGet out of the box.
Thankfully there's an extension you can install that will enable searching, adding and removing NuGet packages from VS Code. The extension is named NuGet Package Manager.
Alternatively you can use the command line. To add a package to a project the syntax is: dotnet add PathToCsproj package PackageName
. Or, if you navigate to the project you want to add the NuGet package to, you can omit the path to the csproj file: dotnet add package PackageName
.
Imagine you wanted to add the Microsoft.EntityFrameworkCore
package to Reminders.Common class library. After navigating to it:
$ dotnet add package Microsoft.EntityFrameworkCore
Adding tools
An example of a CLI tool is for example Entity Framework's tool. That's what runs when you type in a project that has the EF tooling installed:
$ dotnet ef
And you should get this output:
_/\__
---==/ \\
___ ___ |. \|\
| __|| __| | ) \\\
| _| | _| \_/ | //|\\
|___||_| / \\\/\\
Entity Framework Core .NET Command Line Tools 2.0.1-rtm-125
Usage: dotnet ef [options] [command]
Unfortunately there's no automated way of configuring dotnet tooling. If you start with a project template that doesn't have the right setup you'll have to manually edit the .csproj file.
Not only that but you'll have to know the right package name and version.
This is not a problem specific to VS Code or Linux. This is a tooling problem. Here are the relevant issues in github: dotnet add package doesn't honor package type and VS 2017: PackageType=DotnetCliTool fails to install
There's a way to manually do it though. If you open a .csproj file you'll notice that it has ItemGroup
sections. For example, for a new mvc project there's this:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>
For the tooling you should create another ItemGroup
and inside it instead of PackageReference
use DotNetCliToolReference
(there may already be an ItemGroup with DotNetCliToolReference in your project, if that's the case just add to it).
Let's imagine we want to use the Entity Framework tooling in our web project. Navigate to Reminders.Web
and open the csproj file.
Make note of the version of Microsoft.AspNetCore.All
package. For example, let's imagine it's "2.0.0".
Create a new ItemGroup
(or find the ItemGroup that already has DotNetCliToolReferences
) and inside add the DotNetCliToolReference
for the Microsoft.EntityFrameworkCore.Tools.DotNet
with Version="2.0.0"
.
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" /> <!-- ADD THIS -->
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>
You should now be able to run dotnet ef
in the Reminders.Web project.
The reason the version must match "Microsoft.AsNetCore.All" is because that package is a metapackage, i.e. a package that just references other packages. Some of those packages are Entity Framework packages which would cause compatibility issues with the tooling if the version does not match.
If we were installing Entity Framework Core tooling in a console project you wouldn't need to worry about the version. You would however have to add the package Microsoft.EntityFrameworkCore.Design
additionally to manually adding the DotNetCliToolReference
.
Running tests
One thing your development workflow probably involves is writing unit tests.
VS Code comes with out of the box support for running unit tests. For example if you open UnitTest1.cs you'll notice that over the test method there are two links, run test and debug test:
This will allow you to run a unit test at a time, there's no way inside VS Code of running all the tests in a project. You can however navigate to a test project in a terminal and type:
$ dotnet test
This is the output you should get if you do it for Reminders.Tests:
Starting test execution, please wait...
[xUnit.net 00:00:00.4275346] Discovering: Reminders.Tests
[xUnit.net 00:00:00.5044862] Discovered: Reminders.Tests
[xUnit.net 00:00:00.5549595] Starting: Reminders.Tests
[xUnit.net 00:00:00.6980509] Finished: Reminders.Tests
Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
Test Run Successful.
Alternatively you can specify the path to the test project you want to run, for example dotnet test Reminders.Tests/Reminders.Tests.csproj
.
It is also possible to specify which tests to run in the command line, for example say you only want to run the tests in class MyClassTests. Here's how you can do that:
$ dotnet test --filter MyClassTests
The filter functionality has several more options. The example above is short for dotnet test --filter FullyQualifiedName~MyClassTests
(~ means contains).
There are other handy options, for example in xUnit it's possible to specify "traits" for tests, for example:
[Trait("TraitName", "TraitValue")]
public class UnitTest1
{
[Fact]
public void Test1()
{
//...
}
}
You can then run:
$ dotnet test --filter TraitName=TraitValue
And only tests that have that trait will be run. You can specify Trait
on a method or class level.
Although trait is specific to xUnit, there are equivalent constructs on other testing frameworks. For example, mstest has TestCategory
.
If you prefer to do everything inside VS Code you can create tasks for running tests. First go to tasks.json
and make a copy of the build task, change its label to test (or whatever you want to call it) and update the args so that instead of build, test is executed with the path to the test project, for example for Reminders.Tests:
{
"label": "test",
"command": "dotnet",
"type": "process",
"args": [
"test",
"${workspaceFolder}/Reminders.Tests/Reminders.Tests.csproj"
],
"problemMatcher": "$msCompile"
}
You can run tasks in VS Code by using the command palette (Ctrl + Shift + P) and typing "Run Task" and then selecting the task you want to run. You can even set a task as the "test task" and assign a keyboard shortcut for it (the same way you can do Ctrl + Shift + B to trigger a task configured as the "build task").
To set a task as the "test task" open the command palette and select "Tasks: Configure Default Test Task", choose the test task and you're done.
One last tip
Sometimes VS Code (actually Omnisharp) seems to go haywire, for example intellisense stops working. When that happens you can either reload VS Code by using the command palette and selecting "Reload Window" or the less aggressive option is to restart Omnisharp (the command name is: Omnisharp: Restart Omnisharp).
Hope this guide was helpful, let me know in the comments.
Top comments (3)
Great article! I've just started my journey with Linux and dotnet core.I was planning to write the whole series of articles about that. You have recently abandoned my with your post about MVC 😁 I need to think up something new.
Great job!
Thanks!
Do one for getting Blazor up and running on Linux. ;)