loading...

ASP.NET Core Development in Linux

ruidfigueiredo profile image Rui Figueiredo Originally published at blinkingcaret.com ・16 min read

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.

.NET Core Linux

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.

Add Configuration... button

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:

Select configuration

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:

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":

Initialize constructor form 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:

run test, 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.

Posted on Mar 28 '18 by:

ruidfigueiredo profile

Rui Figueiredo

@ruidfigueiredo

Currently working as a contractor, mostly on Node.js and Typescript, also React. Also have a background in academia, I have a PhD in CS and worked as a researcher in AI.

Discussion

markdown guide
 

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!

 

Do one for getting Blazor up and running on Linux. ;)

 
 

I’ve recently switched to Debian and I do .NET development, can’t wait to try this when I get home!

 

Just got home, and I must say, this article helped me out a lot! I feel a lot less dependent on MonoDevelop now thanks to this article.

The only downside is no interface designer, but hey, most of what I have to write is in pure code anyway with no interface designer to speak of since I mostly do game development.