.Net Core and SQL Server In Docker - Part 1 : Building the Service

Traditionally, Asp.Net web applications deployed on Windows Server and IIS are not known for being cloud friendly. That is now a thing of the past, thanks to the open source cross platform .Net Core and SQL Server For Linux. When combined with Docker and a container management platform (such as Kontena), it is now possible to run high performance .Net services in the cloud with the same ease that Ruby, Python and Go developers are used to.

Getting Started

First things first, we need to install our development tools. For this tutorial we will be using a mix of text editor and the command line. For editing cross platform C# projects I highly recommend Microsoft's lightweight Visual Studio Code. For the command line I will be assuming a Bash shell. Bash is the default shell for the macOS terminal, and is now also available on Windows starting with Windows 10.

For .Net Core, this tutorial assumes you are using a minimum version of 1.0.4.

Finally, you need to install Docker. Docker runs natively on Linux, but there are integrated VM solutions available for macOS and Windows (Windows 10 or later only, older versions of Windows should use a VM).

Creating the .Net Project

From the terminal, we are going to create a new project directory and initialize a new C# webapi project:

$ mkdir dotnet-example
$ cd dotnet-example
$ dotnet new webapi

Next, let's restore our NuGet dependencies and run our API:

$ dotnet restore
$ dotnet run

And finally, in a second terminal window, let's test out the API with curl:

$ curl http://localhost:5000/api/values

One change you will also want to make is to register the service to run on hostnames other than localhost. This is important later when we run our service inside of Docker. Open up Program.cs and modify the startup code:

var host = new WebHostBuilder()  
    .UseUrls("https://*:5000")
    .UseKestrel()
    // etc

Adding SQL Server

Now it's time to add a database. Thanks to Docker and SQL Server for Linux, it's super fast and easy to get this started. From the terminal, let's download and run a new instance of SQL Server as a Docker container.

$ docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Testing123' -p 1433:1433 --name sqlserver -d microsoft/mssql-server-linux

That's all that's needed to have a SQL Server development database server up and running. Note that if you are running Docker for Windows or Docker for Mac you need to allocate at least 4GB of RAM to the VM or SQL Server will fail to run.

Next, let's add a new API controller to our application that interacts with the database. First we need to add Entity Framework to our csproj file, which should look like this:

<Project Sdk="Microsoft.NET.Sdk.Web">  
  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="1.1.1" />
  </ItemGroup

Next, we create a new DbContext. In our Models folder, create a file ApiContext.cs and edit as follows:

using Microsoft.EntityFrameworkCore;

namespace Kontena.Examples.Models  
{
    public class ApiContext : DbContext
    {
        public ApiContext(DbContextOptions<ApiContext> options)
            : base(options)
        {
            this.Database.EnsureCreated();
        }

        public DbSet<Product> Products { get; set; }
    }
}

Next is the model class. In the Models folder, create a file Product.cs, and create the Product model:

using System.ComponentModel.DataAnnotations;

namespace Kontena.Examples.Models  
{
    public class Product
    {
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

And finally, let's create a new API Controller. In the Controllers folder, create a file ProductsController.cs and add the following code:

using System.Linq;  
using Microsoft.AspNetCore.Mvc;  
using Kontena.Examples.Models;

namespace Kontena.Examples.Controllers  
{
    [Route("api/[controller]")]
    public class ProductsController : Controller
    {
        private readonly ApiContext _context;

        public ProductsController(ApiContext context)
        {
            _context = context;
        }

        // GET api/values
        [HttpGet]
        public IActionResult Get()
        {
            var model = _context.Products.ToList();

            return Ok(new { Products = model });
        }

        [HttpPost]
        public IActionResult Create([FromBody]Product model)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            _context.Products.Add(model);
            _context.SaveChanges();

            return Ok(model);
        }

        [HttpPut("{id}")]
        public IActionResult Update(int id, [FromBody]Product model)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            var product = _context.Products.Find(id);

            if (product == null)
            {
                return NotFound();
            }

            product.Name = model.Name;
            product.Price = model.Price;

            _context.SaveChanges();

            return Ok(product);
        }

        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            var product = _context.Products.Find(id);

            if (product == null)
            {
                return NotFound();
            }

            _context.Remove(product);
            _context.SaveChanges();

            return Ok(product);
        }
    }
}

This should be enough for us to provide a simple CRUD style REST interface over our new Product model. The final step needed is to register our new database context with the Asp.Net dependency injection framework, and fetch the SQL Server credentials. In the file Startup.cs, modify the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)  
{
    // Add framework services.
    services.AddMvc();

    var hostname = Environment.GetEnvironmentVariable("SQLSERVER_HOST") ?? "localhost";
    var password = Environment.GetEnvironmentVariable("SQLSERVER_SA_PASSWORD") ?? "Testing123";
    var connString = $"Data Source={hostname};Initial Catalog=KontenaAspnetCore;User ID=sa;Password={password};";

    services.AddDbContext<ApiContext>(options => options.UseSqlServer(connString));
}            

Note that we are pulling our SQL Server credentials from environment variables, defaulting to the values we used above for setting up our SQL Server container. In a production application you would probably use the more sophisticated Asp.Net core configuration framework and a SQL Server user other than "sa".

Testing out our API

Time to test out our new API. In your terminal window, restore and start up the API again:

$ dotnet restore && dotnet run

In another window, let's use curl to POST some data to our API:

$ curl -i -H "Content-Type: application/json" -X POST -d '{"name": "6-Pack Beer", "price": "5.99"}' http://localhost:5000/api/products

If all goes well, you should see a 200 status response, and our new Product returned as JSON (with a proper database generated id).

Next, let's modify our data with a PUT and change the price:

$ curl -i -H "Content-Type: application/json" -X PUT -d '{"name": "6-Pack Beer", "price": "7.99"}' http://localhost:5000/api/products/1

Of course, we can also GET our data:

$ curl -i http://localhost:5000/api/products

And finally we can DELETE it:

$ curl -i -X DELETE http://localhost:5000/api/products/1

Putting it in Docker

Now that we have our service, we need to get it in Docker. The first step is to create a new Dockerfile that tells Docker how to build our service. Create a file in the root folder called Dockerfile and add the following content:

FROM microsoft/dotnet:runtime

WORKDIR /dotnetapp  
COPY out .  
ENTRYPOINT ["dotnet", "dotnet-example.dll"]  

Next, we need to compile and "publish" our application, and use the output to build a Docker image with the tag dotnet-example:

$ dotnet publish -c Release -o out
$ docker build -t dotnet-example .

And finally, we can run our new container, linking it to our SQL Server container:

$ docker run -it --rm -p 5000:5000 --link sqlserver -e SQLSERVER_HOST=sqlserver dotnet-example

You should be able to access the API via curl the same as we did earlier.

Next time

In our next installment, we will show you how to take your new API and run the whole thing inside Docker. And then we will move those containers into the cloud with Kontena.

Accompanying source code for this tutorial can be found at https://github.com/kontena/dotnet-example.

About Kontena

Want to learn about real life use cases of Kontena, case studies, best practices, tips & tricks? Need some help with your project? Want to contribute to a project or help other people? Join Kontena Forum to discuss more about Kontena Platform, chat with other happy developers on our Slack discussion channel or meet people in person at one of our Meetup groups located all around the world. Check Kontena Community for more details.

Image Credits: Clouds Dawn Dusk Electricity by Ho JJ.