ASP.NET core and Docker. A Step by step guide.
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:
- Visual Studio Code
- .NET Core SDK 2.2 or later
- C# for Visual Studio Code version 1.17.1 or later
- Docker (If you’re attempting this on Windows 10 Home Edition then you need to have Docker ToolBox installed)
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/Pages
directory 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
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:
- 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 showslocalhost:port#
and not something likeexample.com
. That's becauselocalhost
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).
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 theasp-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&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 handler
parameter 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.
- Create a new file in your workspace named
Dockerfile
- 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 80FROM 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 /appFROM build AS publish
RUN dotnet publish "myWebApp.csproj" -c Release -o /appFROM 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:
- Ctrl +Shift + P
- 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:
- Run
docker inspect -f "{{ .NetworkSettings.Networks.nat.IPAddress }}" myWebApp
- 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.