Coding Tutorials Blog

Intro to .Net 5 with VSCode - Zero to Deploy

March 11, 2021

.Net is Microsoft’s platform for application development and for a long time it has been a “Windows Only” platform. With .Net 5, Microsoft is going cross-platform enabling dotnet development in Linux and MacOS. This makes sense since Microsoft’s bread and butter is no longer operating systems but the cloud, and more developers using their development platform will increase the added value of their cloud platform.

prerequisites

  • dotnet 5 or above installed (check with dotnet --version)
  • VSCode installed with the .Net Extension Pack
  • Docker (for deployment)
  • Heroku CLI (for deployment)

Using VSCODE’s Remote-Container extension you can just spin-up a folder to run in a dockerized .net environment saving you a lot of the hassles on installing especially on Linux

Starting up a New Project

.Net can create a wide variety of applications, and you can see the full list of templates with the command dotnet new --list. We will be using their web framework, ASP.NET, to build an API.

  • run the command dotnet new webapi -o firstapi
  • cd in the folder and install the following packages:
dotnet add package Microsoft.AspNetCore.Mvc.Core --version 2.2.5
dotnet add package Microsoft.AspNetCore.Http.Features --version 5.0.4
dotnet add package EntityFramework --version 6.4.4
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 5.0.2
dotnet add package Microsoft.EntityFrameworkCore.Design --version 5.0.4

ASP.NET CORE is the Web Framework and Entity Framework is the Database ORM

  • create a .gitignore with the following
appsettings.json
/obj
/bin
Trouble shooting

If you get an error that looks like this on debian/ubuntu

The author primary signature's timestamp found a chain building issue: UntrustedRoot: self signed certificate in certificate chain

  • then edit using the file /etc/ca-certificates.conf
  • uncomment (remove !) this line mozilla/VeriSign_Universal_Root_Certification_Authority.crt
  • the update your certs with command sudo update-ca-certificates
SSL Certificates
  • If you are mac and windows run the command dotnet dev-certs https --trust
  • If you are on Linux you may want to run this script Here

This will allow a local SSL certificate to be signed for development purposes

Back to our regularly scheduled programming

  • cd into the new firstapi folder
  • reopen vscode in this folder, VSCode should pick up on that it’s a .net project and you should be able to run it with the debugger (make sure you choose the code/dotnet runtime if it asks) you could also use the command dotnet run to run it from terminal.
  • The website will be served on localhost:5001 (https) and localhost:5000(http), by default it will redirect to the https url.

The Main file for everything is the Startup.cs in the root of the project:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;

namespace firstapi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "firstapi", Version = "v1" });
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // This is a default dev route that may or may not work
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "firstapi v1"));
            }

            app.UseHttpsRedirection(); //comment this out to stop redirection

            app.UseRouting(); // tells servers to match url with endpoint

            app.UseAuthorization(); // enables auth when desired

            // This Middleware here is where we will configure our routes
            app.UseEndpoints(endpoints =>
            {
                /////////////////////////////////////
                // ADD THIS - Routing
                ////////////////////////////////////

                // This is for attribute routing
                endpoints.MapControllers();
                // This is for Pattern Routing
                endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
                });

        }
    }
}

This route is our default route and it will match the following pattern

pattern: "{controller=Home}/{action=Index}/{id?}");

So it will look for a controller by the name of the first part of the url, in that controller an action by that name, then an id parameter if it exists.

Let’s try it out, in the controller folder create a file called HelloWorldController.cs

using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;
using System.Collections;

namespace firstapi.Controllers
{
    public class HelloWorldController : Controller
    {
        //
        // GET: /HelloWorld/

        public Hashtable Index()
        {
            // Returns a hashtable which will be serialized as json
            return new Hashtable(){{"Hello", "World"}};
        }

    }
}
  • restart or start the server and head to localhost:5000/HelloWorld/
  • let’s add another action in the controller
        // Since our route pattern includes a pattern called ID we can receive it as a parameter to our action
        public Hashtable Another(int id)
        {

            return new Hashtable(){{"Hello", id}};
        }
  • restart your server and visit localhost:5000/HelloWorld/Another/5

Creating a Model

Let’s create a model class before we connect to a database…

  • create a folder called Models
  • In that folder create a Person.cs
namespace firstapi.Models
{
    public class Person
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public int age { get; set; }
    }
}

Adding a Database Context

The database context will help interface our model with the database, in the Models folder create a PersonContext.cs.

using Microsoft.EntityFrameworkCore;

namespace firstapi.Models
{
    public class PersonContext : DbContext
    {
        public PersonContext(DbContextOptions<PersonContext> options) : base(options)
        {
        }

        public DbSet<Person> Persons { get; set; }
    }
}

Now we need to add our URL Connection to our appsettings.json

  "DBContextSettings": {
    "ConnectionString": "User ID=test5;Password=test5;Host=localhost;Port=5432;Database=firstdotnet;Pooling=true;"
  }

make sure to create your database `createdb firstdotnet

Then we need to add our database connection as a service in the ConfigureServices method in Startup.cs

Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using firstapi.Models;
using Microsoft.EntityFrameworkCore;

namespace firstapi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();

            // Setup our datanase connection string and add our database connection
            var connectionString = Configuration["DbContextSettings:ConnectionString"];
            services.AddDbContext<PersonContext>(opt => opt.UseNpgsql(
                connectionString));

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "firstapi", Version = "v1" });

            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "firstapi v1"));
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                // This is for attribute routing
                endpoints.MapControllers();
                // This is for Pattern Routing
                endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

Build Our Migrations

Make sure you have the dotnet-ef tool installed

dotnet tool install --global dotnet-ef
  • dotnet ef migrations add InitialCreate will create the migration
  • dotnet ef database update

To update just make another migration (it’ll figure everything out), then run the migration with the above command, easy!

Creating a Person Controller

Let’s create a new controller and use the constructor to add our database connection via the PersonContext we created, we will call it People.

using firstapi.Models;
namespace firstapi.Controllers
{
    public class PersonController
    {
        private readonly PersonContext People;

        public PersonController(PersonContext people){
            People = people;
        }
    }
}

Now let’s use Attribute Routing (We used Pattern Routing Earlier) to create a traditional set of CRUD Routes.

using firstapi.Models;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Linq;

namespace firstapi.Controllers
{
    [Route("people")] // designate controller for "/people" endpoints
    public class PersonController
    {

        // declare property to hold Database Context
        private readonly PersonContext People;

        // define constructor to receive databse context via DI
        public PersonController(PersonContext people){
            People = people;
        }

        [HttpGet] // get request to "/people"
        public IEnumerable<Person> index(){
            // return all the people
            return People.Persons.ToList();
        }

        [HttpPost] // post request to "/people"
        public IEnumerable<Person> Post([FromBody]Person person){
            // add a person
            People.Persons.Add(person);
            // save changes
            People.SaveChanges();
            // return all the people
            return People.Persons.ToList();
        }

        [HttpGet("{id}")] // get requestion to "people/{id}"
        public Person show(long id){
            // return the specified person matched based on ID
            return People.Persons.FirstOrDefault(x => x.Id == id);
        }

        [HttpPut("{id}")] // put request to "people/{id}
        public IEnumerable<Person> update([FromBody]Person person, long id){
            // retrieve person to be updated
            Person oldPerson = People.Persons.FirstOrDefault(x => x.Id == id);
            //update their properties, can also be done with People.Persons.Update
            oldPerson.Name = person.Name;
            oldPerson.age = person.age;
            // Save changes
            People.SaveChanges();
            // return updated list of people
            return People.Persons.ToList();
        }

        [HttpDelete("{id}")] // delete request to "people/{id}
        public IEnumerable<Person> destroy(long id){
            //retrieve existing person
            Person oldPerson = People.Persons.FirstOrDefault(x => x.Id == id);
            //remove them
            People.Persons.Remove(oldPerson);
            // saves changes
            People.SaveChanges();
            // return updated list of people
            return People.Persons.ToList();
        }

    }
}
  • run your server, test it all out! Feel good and then let’s get on with deployment!

Deployment

Heroku doesn’t natively support dotnet projects, but anything can be hosted on Heroku via a Docker Container.

Making Our Container

  1. Create a .dockerignore file with the follow:
bin/
obj/
  1. Create a Dockerfile file with the following
# Dockerfile
# This grabs an image to copy over our source code and build
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build-env
WORKDIR /app

# Copy csproj and restore as distinct layers
# copies over our project and install all dependencies
COPY *.csproj ./
RUN dotnet restore

# Copy everything else and build
# Publishes our final project to /out
COPY . .
RUN dotnet publish -c Release -o out

# Build runtime image
# We will copy our compiled code into this container for running it
FROM mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=build-env /app/out .

# Run the app on container startup
# Use your project name for the second parameter
# e.g. MyProject.dll
ENTRYPOINT [ "dotnet", "firstapi.dll" ]
  • to build your docker image run docker build -t firstdotnetapi . make sure not to forget the period at the end of the command and just wait, this will take a while (unless you’ve done this before it has to download all the containers to do all the things)
  • if you want to test it out, run command sudo docker run -d -p 8080:80 --name myapi firstdotnetapi1 and your api will be visible on port 8080
  • turn off the container with the command docker rm --force abc

Getting Our Container on Heroku

  • create a new app at heroku.com, make sure you know its name, mine is firstdotnetapiam
  • with the heroku cli login to the heroku container repository, heroku container:login
  • then push the container to your project heroku container:push -a firstdotnetapiam web notice I used the name of the heroku project followed by web so it knows it’s a web application
  • once that is done, release the site with this command heroku container:release -a firstdotnetapiam web
  • go the website and it should still not be working, mainly cause we need to make sure our is able to receive its port assignment from Heroku but we’ll take care of our database while we’re at it.
Setting Up the Database
  • on your project dashboard on Heroku provision a Postgres database
  • go to the database dashboard and get the database credentials and update your connection string in appsettings.json, should look like this (notice the extra settings).
    "ConnectionString": "User ID=yourherokudata;Password=yourherokudata;Host=yourherokudata;Port=5432;Database=yourherokudata;Pooling=true;SSL Mode=Require;TrustServerCertificate=True;"
  • run dotnet ef database update to migrate your heroku db
  • update your Dockerfile so we pass the Heroku config var in the startup command and migrate our database

    Dockerfile

FROM mcr.microsoft.com/dotnet/sdk:5.0 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 mcr.microsoft.com/dotnet/aspnet:5.0
WORKDIR /app
COPY --from=build-env /app/out .

# Run the app on container startup
# Use your project name for the second parameter
# e.g. MyProject.dll
#ENTRYPOINT [ "dotnet", "firstapi.dll" ]
CMD ASPNETCORE_URLS=http://*:$PORT dotnet firstapi.dll
  • rebuild container
  • push image
  • release image… and you’re good!!!

If you run into CORS issues, Read This


© alexmercedcoder.dev 2020