Web Front-end8 minute read

.NET on Linux: Simpler Than It Seems

Discover Microsoft .NET’s cross-platform development capabilities by building a Dockerized ASP.NET and Entity Framework application on Linux.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Discover Microsoft .NET’s cross-platform development capabilities by building a Dockerized ASP.NET and Entity Framework application on Linux.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Zablon Dawit
Verified Expert in Engineering
5 Years of Experience

Zablon is a full-stack web developer specializing in ASP.NET and JavaScript. He has created enterprise software for various companies, including Pelmorex Corp and S.R.E Software Development.

Share

Developing .NET solutions on Linux has always been challenging because Microsoft’s Visual Studio requires Windows in order to work. After working on several .NET projects, I decided to test the limits of development of .NET on Linux. This simple tutorial focuses on an ASP.NET MVC application with SQL Server to show how elegant and effective .NET development can be on my preferred OS.

Development Environment

First, we must ensure the .NET tools and SDK associated with our particular flavor of Linux are installed using Microsoft’s standard guide.

My preferred development environment consists of a windowed integrated development environment (IDE), a powerful database management and query tool, the database itself, and tools for building and deployment. I use the following tools to achieve solid functionality and enable a beautiful coding experience:

Make sure these tools are properly installed before you proceed with our sample application.

Project Scaffolding

In this sample application, we’ll highlight ASP.NET development and functionality through a series of use cases for a hypothetical shoe store inventory management system. As with any new .NET application, we’ll need to create a solution and then add a project to it. We can leverage the .NET SDK CLI tools to scaffold our new solution:

mkdir Shoestore && cd Shoestore
dotnet new sln

Next, create an ASP.NET project containing an explicit main class for simplicity’s sake, as this project structure is most familiar to ASP.NET developers. Let’s create our project using the MVC pattern:

mkdir Shoestore.mvc && cd Shoestore.mvc
dotnet new mvc --use-program-main=true

Next, add the project into the solution:

# Go to the root of the solution
cd ..
dotnet sln add Shoestore.mvc/

We now have a default solution and its contained ASP.NET project. Before proceeding, ensure that everything builds:

cd Shoestore.mvc/
dotnet restore
dotnet build

Good development practice encourages putting key services and the application runtime into Docker containers for improved deployment and portability. Therefore, let’s create a simple Docker container to support our application.

Application Portability

Docker images typically reference another parent Docker image as an accepted starting point for essential requirements like OS and basic solutions, including databases. Following this Docker best practice, create both a Dockerfile and a Docker Compose file for proper service configuration while referencing Microsoft-published parent images. We’ll use Docker stages to keep our image small. Stages allow us to use the .NET SDK while building our application so that the ASP.NET runtime is required only while our application runs.

Create the Shoestore.mvc Dockerfile with the following contents:

# Shoestore\Shoestore.mvc\Dockerfile
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /shoestore
COPY Shoestore.mvc/*.csproj ./
# Restore project packages
RUN dotnet restore
COPY Shoestore.mvc/* ./
# Create a release build
RUN dotnet build -c Release -o /app/build

# Run the application and make it available on port 80
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
EXPOSE 80
# Assets and views
COPY Shoestore.mvc/Views ./Views
COPY Shoestore.mvc/wwwroot ./wwwroot
COPY --from=build /app/build ./
ENTRYPOINT [ "dotnet", "Shoestore.mvc.dll" ]

Next, we’ll create the docker-compose.yml file in our solution’s root directory. Initially, it will only contain a reference to our application service’s .Dockerfile:

# Shoestore/docker-compose.yml
version: "3.9"
services:
  web:
    build:
      context: .
      dockerfile: Shoestore.mvc/Dockerfile
    ports:
      - "8080:80"

Let’s also configure our environment with a .dockerignore file to ensure that only the build artifacts are copied to our image.

With our application service now stubbed in and its execution environment ready to run, we need to create our database service and connect it to our Docker configuration.

The Database Service

Adding the Microsoft SQL Server to our Docker configuration is straightforward, especially since we are using a Microsoft-provided Docker image without changing it. Add the following configuration block to the bottom of the docker-compose.yml file to configure the database:

  db:
    image: "mcr.microsoft.com/mssql/server"
    environment:
      SA_PASSWORD: "custom_password_123"
      ACCEPT_EULA: "Y"
    ports:
      - "1433:1433"

Here, ACCEPT_EULA prevents the installation from halting, and our ports setting lets the default SQL Server port pass through without translation. With that, our Compose file includes both our application service and database.

Before customizing the application code, let’s verify that our Docker environment works:

# From the root of the solution
docker compose up --build

Assuming no errors appear during startup, our incomplete sample application should be available through a web browser at the local address http://localhost:8080.

Code Generation Tools

Now we get to focus on the fun part: customizing the application code and ensuring that the application data persists in the Microsoft SQL Server database. We’ll use both the Entity Framework (EF) and .NET SDK tools to connect the application to the database and scaffold the application’s model, view, controller, and EF-required configuration.

Before we can specify the tools we need, we must create a tool-manifest file:

# From the root of the solution

dotnet new tool-manifest

Add the EF and SDK tools to this file with these simple commands:

dotnet tool install dotnet-ef
dotnet tool install dotnet-aspnet-codegenerator

To verify the proper installation of these tools, run dotnet ef. If a unicorn appears, they’re installed correctly. Next, run dotnet aspnet-codegenerator to test the ASP.NET tools; the output should be a general CLI usage block.

Now we can use these tools to create our application.

MVC: Model

The first task in building our application is creating the model. Since this model will be added to the database later, we’ll include the MS SQL Server and EF packages in our project:

cd Shoestore.mvc/
dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet restore

Next, create an EF database context object that determines which models are added to the database, and allows our code to easily access and query that data from the database.

Create a Data directory to house the EF-specific code and create the ApplicationDBContext.cs file with the following contents:

// Shoestore/Shoestore.mvc/Data/ApplicationDBContext.cs
using Microsoft.EntityFrameworkCore;

namespace Shoestore.mvc.Data;

public class ApplicationDBContext : DbContext
{
  public ApplicationDBContext(DbContextOptions<ApplicationDBContext> options):base(options){}
}

Next, configure the database connection string, which must match the credentials we configured in our Dockerfile. Set the contents of Shoestore/Shoestore.mvc/appsettings.json to the following:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "Shoestore": "Server=db;Database=master;User=sa;Password=custom_password_123;"
  }
}

With the database connection string configured and database context coded, we’re ready to code our application’s Main function. We’ll include database exception handling to simplify system debugging. Additionally, because a .NET bug in the generated code causes the Docker container to serve our views incorrectly, we’ll need to add specific code to our view service configuration. This will explicitly set the file paths to our view location in our Docker image:

using Microsoft.EntityFrameworkCore;
using Shoestore.mvc.Data;

namespace Shoestore.mvc;

// ...

    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        // Associate our EF database context and configure it with our connection string
        var connectionString = builder.Configuration.GetConnectionString("Shoestore");
        builder.Services.AddDbContext<ApplicationDBContext>(
            options => options.UseSqlServer(connectionString));
        // Middleware to catch unhandled exceptions and display a stack trace
        builder.Services.AddDatabaseDeveloperPageExceptionFilter();

        // Add services to the container.
        // ASP.NET has a known issue where the final built app doesn't know where the view
        // files are (in the Docker container). 
        // The fix is to specifically add view locations.
        builder.Services
            .AddControllersWithViews()
            .AddRazorOptions(options => {
                options.ViewLocationFormats.Add("/{1}/{0}.cshtml");
                options.ViewLocationFormats.Add("/Shared/{0}.cshtml");
            });

Skip down to the IsDevelopment if statement within the same file to add a database migration endpoint to our system when it is in development mode. Add an else statement with the following code:

        // Configure the HTTP request pipeline.
        if (!app.Environment.IsDevelopment())
        {
            // Leave the contents of the if block alone. These are hidden for clarity.
        } else {
            app.UseMigrationsEndPoint();
        }

Next, run a quick test to ensure the new packages and the source code edits compile correctly:

// Go to mvc directory
cd Shoestore.mvc
dotnet restore
dotnet build

Now, let’s populate the model with our required fields by creating the Shoestore.mvc\Models\Shoe.cs file:

namespace Shoestore.mvc.Models;

public class Shoe {
    public int ID { get; set; }
    public string? Name { get; set; }
    public int? Price { get; set; }
    public DateTime CreatedDate { get; set; }
}

EF generates SQL based on the associated model, its context file, and any EF code in our application. SQL results are then translated and returned to our code, as needed. If we add our Shoe model to our database context, EF will know how to translate between MS SQL Server and our application. Let’s do this in the database context file, Shoestore/Shoestore.mvc/Data/ApplicationDBContext.cs:

using Microsoft.EntityFrameworkCore;
using Shoestore.mvc.Models;

namespace Shoestore.mvc.Data;

public class ApplicationDBContext : DbContext
{
  public ApplicationDBContext(DbContextOptions<ApplicationDBContext> options) : base(options) { }

    private DbSet<Shoe>? _shoe { get; set; }
    public DbSet<Shoe> Shoe {
        set => _shoe = value;
        get => _shoe ?? throw new InvalidOperationException("Uninitialized property" + nameof(Shoe));
    }
}

Finally, we’ll use a database migration file to get our model into the database. The EF tool creates a migration file specific to MS SQL Server based on the database context and its associated model (i.e., Shoe):

cd Shoestore.mvc/
dotnet ef migrations add InitialCreate

Let’s hold off on running our migration until we have a controller and view in place.

MVC: Controller and View

We’ll create our controller using the ASP.NET code generation tool. This tool is very powerful but requires specific helper classes. Use the Design style packages for the basic controller structure and its EF integration. Let’s add these packages:

cd Shoestore.mvc\
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design && \
dotnet add package Microsoft.EntityFrameworkCore.Design && \
dotnet restore

Now, creating our default controller is as simple as invoking the following command:

cd Shoestore.mvc\
dotnet dotnet-aspnet-codegenerator controller \
        -name ShoesController \
        -m Shoe \
        -dc ApplicationDBContext \
        --relativeFolderPath Controllers \
        --useDefaultLayout \
        --referenceScriptLibraries

When the code generator creates our controller, it also creates a simple view for that controller. With our MVC foundations complete, we’re ready to get everything running.

Migration and Application Test

EF migrations are usually a simple affair, but when Docker is involved, the process becomes more complex. In the next article in our series, we’ll explore the wonderfully twisty path to making those migrations work in our Docker solution, but for now, we just want our migration to run.

All configuration and migration files are included in our repository. Let’s clone the full project to our local machine and perform the migration:

git clone https://github.com/theZetrax/dot-net-on-linux.git
cd ./dot-net-on-linux
docker composer up

The docker composer operation builds our application, runs the migration, and launches our ASP.NET application with the .NET runtime. To access the running solution, visit http://localhost:8080/Shoes.

Although our application interface is simple, it demonstrates functionality through all tiers, from the view down to the database.

.NET on Linux Just Works

See the full repository for an overview of our solution. The next article will cover our migration in detail, as well as tips and tricks to make our Docker images lean.

.NET on Linux is more than just a pipe dream: It’s a viable language, runtime, and OS combination. Many developers raised on Visual Studio may not have experience using .NET CLI to its fullest, but these tools are effective and powerful.

The Toptal Engineering Blog extends its gratitude to Henok Tsegaye for reviewing the code samples presented in this article.

Understanding the basics

  • What is .NET Core CLI?

    .NET Core CLI, now called .NET CLI, comprises the command-line interfaces available for use on any .NET-supported platform. We use these tools for project creation, scaffolding, building, and ancillary execution.

  • Can you build a .NET framework on Linux?

    Yes, developing with .NET on Linux is well supported.

  • Is .NET Core on Linux good?

    .NET (Core) on Linux is powerful. Both the development tools and runtime are robust enough for production use.

Hire a Toptal expert on this topic.
Hire Now
Zablon Dawit

Zablon Dawit

Verified Expert in Engineering
5 Years of Experience

Addis Ababa, Ethiopia

Member since December 15, 2021

About the author

Zablon is a full-stack web developer specializing in ASP.NET and JavaScript. He has created enterprise software for various companies, including Pelmorex Corp and S.R.E Software Development.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.