ASP.NET core and Docker. A Step by step guide.

Diegman GMD
13 min readFeb 27, 2019

--

In the following guide you will learn from scratch the basics about ASP.NET core and Docker.

Firsts things first:

What is ASP.NET Core?.

ASP.NET Core is a cross-platform, high-performance, open-source framework for building a modern, cloud based, internet-connected application. With ASP.Net Core, you can build web apps and services, IoT apps, and mobile backends. Use your favorite development tools on windows, macOS and linux. Deploy to the cloud or on-permises and run on .NET core or. NET Framework.

And what is Docker?

Docker is a tool designed to make it easier to creat, deploy and run applications by using containers. Containers allow a developer to package up an application with all the parts it needs, such as libraries and other dependencies, and ship it all out as un package. By doing so, thanks to the container, the developer can rest assured that the application will run on any other Linux machine regardless of any customized settings that machine might have that could differ from the machine used for writing and testing the code.

Getting Started.

This example demonstrates how to build, run and dockerize a simple ASP.NET Core web application.

Pre-Requisites:

For more information on how to install this use the following links:

Visual Code: https://code.visualstudio.com/docs
.NET Core: https://docs.microsoft.com/es-es/dotnet/core/
Docker: https://docs.docker.com/docker-for-windows/
Docker Toolbox: https://docs.docker.com/toolbox/toolbox_install_windows/

Create your app

Open a new command prompt and run the following commands:

>dotnet new webApp -o myWebApp

>cd myWebApp

The dotnet command creates a new application of type webApp for you. The -o parameter creates a directory named myWebApp where your app is stored, and populates it with the required files. The cd myWebApp command puts you into the newly created app directory.

Several files were created in the myWebApp directory, to give you a simple web application that is ready to run. Startup.cs contains all the settings and configurations. The myWebApp/Pagesdirectory contains some web pages for the application.

In your command prompt, run the following command:

dotnet dev-certs https --trust

Your operating system may prompt to check if you agree to trust the development certificate. Follow the prompts if you agree.

This certificate allows your web app to run on HTTPS while you are developing on your machine.

Run your app

In your command prompt, run the following command:

dotnet run

Once the command completes, browse to https://localhost:5001

This image is downloaded from a tutorial, it’s not mine.

Congratulations, you’ve built and run your first .NET web app!

Now we’re moving into Razor Pages:

  • Open the integrated terminal.
  • Change directories (cd) to a folder which will contain the project.
  • Run the following commands:

>dotnet new webapp -o RazorPagesMovie
>code -r RazorPagesMovie

The dotnet new command creates a new Razor Pages project in the RazorPagesMovie folder.

The code command opens the RazorPagesMovie folder in a new instance of Visual Studio Code.

  • A dialog box appears with Required assets to build and debug are missing from ‘RazorPagesMovie’. Add them?
  • Select Yes
  • Press Ctrl-F5 to run without the debugger.
  • Trust the HTTPS development certificate by running the following command:

dotnet dev-certs https --trust

  • The preceding command displays the following dialog:
This image is downloaded from a tutorial, it’s not mine.
  • Select Yes if you agree to trust the development certificate.
  • See Trust the ASP.NET Core HTTPS development certificate for more information.
  • Visual Studio Code starts Kestrel, launches a browser, and navigates to http://localhost:5001. The address bar shows localhost:port# and not something like example.com. That's because localhost is the standard hostname for local computer. Localhost only serves web requests from the local computer.
  • On the app’s home page, select Accept to consent to tracking.
  • This app doesn’t track personal information, but the project template includes the consent feature in case you need it to comply with the European Union’s General Data Protection Regulation (GDPR).
The previous image shows the app after you give consent to tracking

Examine the project files

Here’s an overview of the main project folders and files that you’ll work with in later tutorials.

Pages folder

Contains Razor pages and supporting files. Each Razor page is a pair of files:

A .cshtml file that contains HTML markup with C# code using Razor syntax.

A .cshtml.cs file that contains C# code that handles page events.

Supporting files have names that begin with an underscore. For example, the _Layout.cshtml file configures UI elements common to all pages. This file sets up the navigation menu at the top of the page and the copyright notice at the bottom of the page. For more information, see Layout in ASP.NET Core.

wwwroot folder

Contains static files, such as HTML files, JavaScript files, and CSS files. For more information, see Static files in ASP.NET Core.

appSettings.json

Contains configuration data, such as connection strings. For more information, see Configuration in ASP.NET Core.

Program.cs

Contains the entry point for the program. For more information, see ASP.NET Core Web Host.

Startup.cs

Contains code that configures app behavior, such as whether it requires consent for cookies. For more information, see App startup in ASP.NET Core.

Razor Pages

Razor Pages is enabled in Startup.cs:

public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Includes support for Razor Pages and controllers.
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}

Consider a basic page:

@page<h1>Hello, world!</h1>
<h2>The time on the server is @DateTime.Now</h2>

The preceding code looks a lot like a Razor view file. What makes it different is the @page directive. @page makes the file into an MVC action - which means that it handles requests directly, without going through a controller. @page must be the first Razor directive on a page. @page affects the behavior of other Razor constructs.

Notes:

  • The runtime looks for Razor Pages files in the Pages folder by default.
  • Index is the default page when a URL doesn't include a page.

Razor Pages is designed to make common patterns used with web browsers easy to implement when building an app. Model binding, Tag Helpers, and HTML helpers all just work with the properties defined in a Razor Page class.

For the samples in this document, the DbContext is initialized in the Startup.cs file.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RazorPagesContacts.Data;
namespace myWebApp
{
public class Startup
{
public IHostingEnvironment HostingEnvironment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("name"));

services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}
}

The data model:

using System.ComponentModel.DataAnnotations;namespace myWebApp
{
public class Customer
{
public int Id { get; set; }
[Required, StringLength(100)]
public string Name { get; set; }
}
}

The db context:

using Microsoft.EntityFrameworkCore;namespace myWebApp
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options)
: base(options)
{
}
public DbSet<Customer> Customers { get; set; }
}
}

Here is the Pages/Create.cshtml view file:

@page
@model myWebApp.Pages.CreateModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<html>
<body>
<p>
Enter your name.
</p>
<div asp-validation-summary="All"></div>
<form method="POST">
<div>Name: <input asp-for="Customer.Name" /></div>
<input type="submit" />
</form>
</body>
</html>

The Pages/Create.cshtml.cs page model:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
namespace myWebApp.Pages
{
public class CreateModel : PageModel
{
private readonly AppDbContext _db;
public CreateModel(AppDbContext db)
{
_db = db;
}
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}
}

By convention, the PageModel class is called <PageName>Model and is in the same namespace as the page.

The PageModel class allows separation of the logic of a page from its presentation. It defines page handlers for requests sent to the page and the data used to render the page. This separation allows you to manage page dependencies through dependency injection and to unit test the pages.

The page has an OnPostAsync handler method, which runs on POST requests (when a user posts the form). You can add handler methods for any HTTP verb. The most common handlers are:

  • OnGet to initialize state needed for the page.
  • OnPost to handle form submissions.

The Async naming suffix is optional but is often used by convention for asynchronous functions. The OnPostAsync code in the preceding example looks similar to what you would normally write in a controller. The preceding code is typical for Razor Pages. Most of the MVC primitives like model binding, validation, and action results are shared.

The previous OnPostAsync method:

public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}

The basic flow of OnPostAsync:

Check for validation errors.

  • If there are no errors, save the data and redirect.
  • If there are errors, show the page again with validation messages. Client-side validation is identical to traditional ASP.NET Core MVC applications. In many cases, validation errors would be detected on the client, and never submitted to the server.

When the data is entered successfully, the OnPostAsync handler method calls the RedirectToPage helper method to return an instance of RedirectToPageResult. RedirectToPage is a new action result, similar to RedirectToAction or RedirectToRoute, but customized for pages. In the preceding sample, it redirects to the root Index page (/Index). RedirectToPage is detailed in the URL generation for Pages section.

When the submitted form has validation errors (that are passed to the server), theOnPostAsync handler method calls the Page helper method. Page returns an instance of PageResult. Returning Page is similar to how actions in controllers return View. PageResult is the default return type for a handler method. A handler method that returns void renders the page.

The Customer property uses [BindProperty] attribute to opt in to model binding.

public class CreateModel : PageModel
{
private readonly AppDbContext _db;
public CreateModel(AppDbContext db)
{
_db = db;
}
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Customers.Add(Customer);
await _db.SaveChangesAsync();
return RedirectToPage("/Index");
}
}

Razor Pages, by default, bind properties only with non-GET verbs. Binding to properties can reduce the amount of code you have to write. Binding reduces code by using the same property to render form fields (<input asp-for="Customer.Name" />) and accept the input.

The home page (Index.cshtml):

@page
@model myWebApp.Pages.IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<h1>Contacts</h1>
<form method="post">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
@foreach (var contact in Model.Customers)
{
<tr>
<td>@contact.Id</td>
<td>@contact.Name</td>
<td>
<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>
<button type="submit" asp-page-handler="delete"
asp-route-id="@contact.Id">delete</button>
</td>
</tr>
}
</tbody>
</table>
<a asp-page="./Create">Create</a>
</form>

The associated PageModel class (Index.cshtml.cs):

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesContacts.Data;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
namespace myWebApp.Pages
{
public class IndexModel : PageModel
{
private readonly AppDbContext _db;
public IndexModel(AppDbContext db)
{
_db = db;
}
public IList<Customer> Customers { get; private set; } public async Task OnGetAsync()
{
Customers = await _db.Customers.AsNoTracking().ToListAsync();
}
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
var contact = await _db.Customers.FindAsync(id);
if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}
return RedirectToPage();
}
}
}

The Index.cshtml file contains the following markup to create an edit link for each contact:

<a asp-page="./Edit" asp-route-id="@contact.Id">edit</a>

The Anchor Tag Helper used the asp-route-{value} attribute to generate a link to the Edit page. The link contains route data with the contact ID. For example, http://localhost:5000/Edit/1.

The Pages/Edit.cshtml file:

@page "{id:int}"
@model myWebApp.Pages.EditModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "Edit Customer";
}
<h1>Edit Customer - @Model.Customer.Id</h1>
<form method="post">
<div asp-validation-summary="All"></div>
<input asp-for="Customer.Id" type="hidden" />
<div>
<label asp-for="Customer.Name"></label>
<div>
<input asp-for="Customer.Name" />
<span asp-validation-for="Customer.Name" ></span>
</div>
</div>

<div>
<button type="submit">Save</button>
</div>
</form>

The first line contains the @page "{id:int}" directive. The routing constraint"{id:int}"tells the page to accept requests to the page that contain int route data. If a request to the page doesn't contain route data that can be converted to an int, the runtime returns an HTTP 404 (not found) error. To make the ID optional, append ? to the route constraint:

@page "{id:int?}"

The Pages/Edit.cshtml.cs file:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorPagesContacts.Data;
namespace myWebApp.Pages
{
public class EditModel : PageModel
{
private readonly AppDbContext _db;
public EditModel(AppDbContext db)
{
_db = db;
}
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Customer = await _db.Customers.FindAsync(id);
if (Customer == null)
{
return RedirectToPage("/Index");
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_db.Attach(Customer).State = EntityState.Modified; try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
throw new Exception($"Customer {Customer.Id} not found!");
}
return RedirectToPage("/Index");
}
}
}

The Index.cshtml file also contains markup to create a delete button for each customer :

<button type="submit" asp-page-handler="delete" 
asp-route-id="@contact.Id">delete</button>

When the delete button is rendered in HTML, its formaction includes parameters for:

  • The customer contact ID specified by the asp-route-id attribute.
  • The handler specified by the asp-page-handler attribute.

Here is an example of a rendered delete button with a customer contact ID of 1:

<button type="submit" formaction="/?id=1&amp;handler=delete">delete</button>

When the button is selected, a form POST request is sent to the server. By convention, the name of the handler method is selected based on the value of the handlerparameter according to the scheme OnPost[handler]Async.

Because the handler is delete in this example, the OnPostDeleteAsync handler method is used to process the POST request. If the asp-page-handler is set to a different value, such as remove, a page handler method with the name OnPostRemoveAsync is selected.

public async Task<IActionResult> OnPostDeleteAsync(int id)
{
var contact = await _db.Customers.FindAsync(id);
if (contact != null)
{
_db.Customers.Remove(contact);
await _db.SaveChangesAsync();
}
return RedirectToPage();
}

The OnPostDeleteAsync method:

  • Accepts the id from the query string.
  • Queries the database for the customer contact with FindAsync.
  • If the customer contact is found, they’re removed from the list of customer contacts. The database is updated.
  • Calls RedirectToPage to redirect to the root Index page (/Index).

Dockerizing our .Net Core Web App.

Docker support for VS Code is provided by an extension. To install the Docker extension, open the Extensions view by pressing Ctrl+Shift+X and search for docker to filter the results. Select the Microsoft Docker extension.

Dockerfiles

With Docker, you can build images by specifying the step by step commands needed to build the image in a Dockerfile. A Dockerfile is just a text file that contains the build instructions.

VS Code understands the structure of Dockerfiles as well as the available set of instructions, so you get a great experience when authoring these files.

  1. Create a new file in your workspace named Dockerfile
  2. Press Ctrl+Space to bring up a list of snippets corresponding to valid Dockerfile commands. Pressing the 'i' Read More button on the right will show a fly-out with details and a link to the Docker Online documentation.

Since we’re makin a Linux container this is the Dockerfile we’re going to use.

FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80
FROM microsoft/dotnet:2.2-sdk AS build
WORKDIR /src
COPY ["myWebApp.csproj", "./"]
RUN dotnet restore "./myWebApp.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "myWebApp.csproj" -c Release -o /app
FROM build AS publish
RUN dotnet publish "myWebApp.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "myWebApp.dll"]

And this is the Dockerfile you should use for Windows containers.

FROM microsoft/dotnet:sdk AS build-env
WORKDIR /app

# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out

# Build runtime image
FROM microsoft/dotnet:aspnetcore-runtime
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "myWebApp.dll"]

To make your build context as small as possible add a .dockerignore file to your project folder and copy the following into it.

node_modules
npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
.env
*/bin
*/obj
README.md
LICENSE
.vscode

To make things easier, the Docker extension comes with many command lines that will help you with this.

in Visual Studio Code: Ctrl + Shift + P , then type “Docker:” and you’ll see the different comand lines at your disposal. We’re gonna use Docker: Add Docker files to <name of project> myWebApp. A new tab will pop out asking to select the application platform: ASP.Net Core. Then we’re going to select the Operating System: Linux and what port will our App listen to, We’re gonna keep it in 80. That’s the default. We’re set to go. Now we have at our disposal the Dockerfile already created and the .dockerignore file.

Now we need to build our containers and images to be able to run our app. We can do it in the comand prompt but since we’re in Visual Studio Code we’re gonna do it here. In Visual Studio Code: Ctrl +Shift + p, then type “Docker: build”. VS Code will automatically select your latest image created, but you can choose the one you need, in this example is: myWebApp:latest.

Now we’re going to run the app:

  1. Ctrl +Shift + P
  2. Docker: Run.

View the web page running from a container

  • Go to localhost:8080 to access your app in a web browser.
  • If you are using the Nano Windows Container and have not updated to the Windows Creator Update there is a bug affecting how Windows 10 talks to Containers via “NAT” (Network Address Translation). You must hit the IP of the container directly. You can get the IP address of your container with the following steps:
  1. Run docker inspect -f "{{ .NetworkSettings.Networks.nat.IPAddress }}" myWebApp
  2. Copy the container IP address and paste into your browser. (For example, 172.16.240.197)

Or if you have Kitematic installed you can do it in a more interactive way. Go to My Images, select the one you need, in this case is myWebApp , click on create and there you go.

Is that simple!

--

--