.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.
Discover Microsoft .NET’s cross-platform development capabilities by building a Dockerized ASP.NET and Entity Framework application on Linux.
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.
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:
- IDE: Visual Studio Code
- Database management and query tool: DBeaver
- Database: Microsoft SQL Server (Linux Installation)
- Build tools: .NET SDK Command Line Interface (CLI)
- Virtual machine and containers: Docker
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. As a bonus exercise on top of this tutorial, you can 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. See the full repository for an overview of our solution.
.NET on Linux Just Works
.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.
Further Reading on the Toptal Blog:
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.
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.