.NET 8 MVC with GCP CloudSQL PostgreSQL

Published: Aug 13, 2024 by Isaac Johnson

Sometimes I write a blog post to look into a new offering or tool and sometimes it’s to show an idea or process. But today is one of those, “This should not be this hard” type of learning posts.

I’ve been working an example for a .NET webapp to engage with a PostgreSQL CloudSQL database for a bit and I keep going in loops using AIs like Copilot and Gemini.

Let’s do this together - we want to build out a sample 2-tier app with .NET 8.0 and PostgreSQL

PostgreSQL in CloudSQL

Note: many of the files and final output can be found at this Github URL if you want to follow along.

This part is pretty easy

I’ll first login and set my project in GCP

$ gcloud auth login
$ gcloud config set project myanthosproject2

I did these as scripts so I could do it again:

$ cat 00_start.sh
#!/bin/bash

set -x

gcloud auth login

gcloud config set project myanthosproject2

I then created the database with a random password

$ cat 01_gcloud_create.sh
#!/bin/bash
set -x

export PASSWD=c7f21efbb1307854dc22a5a680636bece4d6a475
export PGINST=mypatientsdb

# Create Database
gcloud sql instances create $PGINST \
    --database-version=POSTGRES_14 \
    --region=us-central1 \
    --tier=db-f1-micro \
    --activation-policy=ALWAYS

# set "postgres" user password
gcloud sql users set-password postgres \
    --instance=$PGINST \
    --password=$PASSWD

Which I ran

$ ./01_gcloud_create.sh
+ export PASSWD=c7f21efbb1307854dc22a5a680636bece4d6a475
+ PASSWD=c7f21efbb1307854dc22a5a680636bece4d6a475
+ export PGINST=mypatientsdb
+ PGINST=mypatientsdb
+ gcloud sql instances create mypatientsdb --database-version=POSTGRES_14 --region=us-central1 --tier=db-f1-micro --activation-policy=ALWAYS
Creating Cloud SQL instance for POSTGRES_14...⠛
Creating Cloud SQL instance for POSTGRES_14...⠶
Creating Cloud SQL instance for POSTGRES_14...done.
Created [https://sqladmin.googleapis.com/sql/v1beta4/projects/myanthosproject2/instances/mypatientsdb].
NAME          DATABASE_VERSION  LOCATION       TIER         PRIMARY_ADDRESS  PRIVATE_ADDRESS  STATUS
mypatientsdb  POSTGRES_14       us-central1-a  db-f1-micro  34.123.245.103   -                RUNNABLE
+ gcloud sql users set-password postgres --instance=mypatientsdb --password=c7f21efbb1307854dc22a5a680636bece4d6a475
Updating Cloud SQL user...done.

I’ll then create a user

$ cat 02_create_db_user.sql
CREATE DATABASE patientsdb;

CREATE USER patientuser WITH PASSWORD 'SuperSecretPassword1234';

GRANT ALL PRIVILEGES ON DATABASE patientsdb TO patientuser;


$ cat 03_create_db_user.sh
#!/bin/bash
set -x

export PASSWD=c7f21efbb1307854dc22a5a680636bece4d6a475
export PGINST=mypatientsdb

gcloud sql instances describe $PGINST \
    --format="value(ipAddresses)" > ip_address.txt

PGPASSWORD=$PASSWD psql -h $(cat ip_address.txt | cut -d';' -f1 | cut -d':' -f2 | cut -d',' -f1 | sed 's/"//g' | sed "s/'//g" | tr -d ' ' ) -U postgres -d postgres -f ./02_create_db_user.sql

which I ran after adding my local IP address to the CloudSQL for connectivity

/content/images/2024/08/dotnetapp-01.png

$ ./03_create_db_user.sh
+ export PASSWD=c7f21efbb1307854dc22a5a680636bece4d6a475
+ PASSWD=c7f21efbb1307854dc22a5a680636bece4d6a475
+ export PGINST=mypatientsdb
+ PGINST=mypatientsdb
+ gcloud sql instances describe mypatientsdb '--format=value(ipAddresses)'
++ cat ip_address.txt
++ cut '-d;' -f1
++ cut -d: -f2
++ cut -d, -f1
++ sed 's/"//g'
++ sed 's/'\''//g'
++ tr -d ' '
+ PGPASSWORD=c7f21efbb1307854dc22a5a680636bece4d6a475
+ psql -h 34.123.245.103 -U postgres -d postgres -f ./02_create_db_user.sql
CREATE DATABASE
CREATE ROLE
GRANT

I’ll then add some random data

$ cat 04_create_sample_data.sql
CREATE TABLE patientdata (
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    date_of_birth DATE,
    social_security_number CHAR(11),
    createdDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO patientdata (first_name, last_name, date_of_birth, social_security_number) VALUES
('John', 'Doe', '1985-05-15', '123-45-6789'),
('Jane', 'Smith', '1990-07-22', '987-65-4321'),
('Alice', 'Johnson', '1978-03-10', '111-22-3333'),
('Bob', 'Brown', '1982-11-30', '444-55-6666'),
('Charlie', 'Davis', '1995-01-25', '777-88-9999'),
('Diana', 'Miller', '1988-09-14', '222-33-4444'),
('Eve', 'Wilson', '1992-06-18', '555-66-7777'),
('Frank', 'Moore', '1980-12-05', '888-99-0000'),
('Grace', 'Taylor', '1983-04-27', '333-44-5555'),
('Hank', 'Anderson', '1975-08-19', '666-77-8888');

$ cat 04_create_sample_data.sh
#!/bin/bash
set -x

export PGINST=mypatientsdb
export PASSWD=SuperSecretPassword1234

gcloud sql instances describe $PGINST \
    --format="value(ipAddresses)" > ip_address.txt


PGPASSWORD=$PASSWD psql -h $(cat ip_address.txt | cut -d';' -f1 | cut -d':' -f2 | cut -d',' -f1 | sed 's/"//g' | sed "s/'//g" | tr -d ' ' ) -U patientuser -d patientsdb -f ./04_create_sample_data.sql

Which ran just fine

$ ./04_create_sample_data.sh
+ export PGINST=mypatientsdb
+ PGINST=mypatientsdb
+ export PASSWD=SuperSecretPassword1234
+ PASSWD=SuperSecretPassword1234
+ gcloud sql instances describe mypatientsdb '--format=value(ipAddresses)'
++ cat ip_address.txt
++ cut '-d;' -f1
++ cut -d: -f2
++ cut -d, -f1
++ sed 's/"//g'
++ sed 's/'\''//g'
++ tr -d ' '
+ PGPASSWORD=SuperSecretPassword1234
+ psql -h 34.123.245.103 -U patientuser -d patientsdb -f ./04_create_sample_data.sql
CREATE TABLE
INSERT 0 10

And I can verify it’s setup by seeing the database

/content/images/2024/08/dotnetapp-02.png

and users

/content/images/2024/08/dotnetapp-03.png

Last step is to just validate with a base select statement locally

$ PGPASSWORD=SuperSecretPassword1234 psql -h 34.123.245.103 -U patientuser -d patientsdb
psql (12.12 (Ubuntu 12.12-0ubuntu0.20.04.1), server 14.12)
WARNING: psql major version 12, server major version 14.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

patientsdb=> select * from patientdata;
 first_name | last_name | date_of_birth | social_security_number |        createddate
------------+-----------+---------------+------------------------+----------------------------
 John       | Doe       | 1985-05-15    | 123-45-6789            | 2024-08-09 12:29:21.960533
 Jane       | Smith     | 1990-07-22    | 987-65-4321            | 2024-08-09 12:29:21.960533
 Alice      | Johnson   | 1978-03-10    | 111-22-3333            | 2024-08-09 12:29:21.960533
 Bob        | Brown     | 1982-11-30    | 444-55-6666            | 2024-08-09 12:29:21.960533
 Charlie    | Davis     | 1995-01-25    | 777-88-9999            | 2024-08-09 12:29:21.960533
 Diana      | Miller    | 1988-09-14    | 222-33-4444            | 2024-08-09 12:29:21.960533
 Eve        | Wilson    | 1992-06-18    | 555-66-7777            | 2024-08-09 12:29:21.960533
 Frank      | Moore     | 1980-12-05    | 888-99-0000            | 2024-08-09 12:29:21.960533
 Grace      | Taylor    | 1983-04-27    | 333-44-5555            | 2024-08-09 12:29:21.960533
 Hank       | Anderson  | 1975-08-19    | 666-77-8888            | 2024-08-09 12:29:21.960533
(10 rows)

patientsdb=> \q

Dotnet

I had endless troubles getting .NET 8 to work when my local CLI was still stuck at 6

$ dotnet --version
6.0.101

So I opted to cleanup

$ sudo apt-get remove dotnet-sdk-*
[sudo] password for builder:
Reading package lists... Done
Building dependency tree
Reading state information... Done
Note, selecting 'dotnet-sdk-2.1' for glob 'dotnet-sdk-*'
Note, selecting 'dotnet-sdk-3.1' for glob 'dotnet-sdk-*'
Note, selecting 'dotnet-sdk-5.0' for glob 'dotnet-sdk-*'
Note, selecting 'dotnet-sdk-6.0' for glob 'dotnet-sdk-*'
Note, selecting 'dotnet-sdk-7.0' for glob 'dotnet-sdk-*'
Note, selecting 'dotnet-sdk-8.0' for glob 'dotnet-sdk-*'
Package 'dotnet-sdk-2.1' is not installed, so not removed
Package 'dotnet-sdk-5.0' is not installed, so not removed
Package 'dotnet-sdk-8.0' is not installed, so not removed
Package 'dotnet-sdk-7.0' is not installed, so not removed
The following packages were automatically installed and are no longer required:
  aspnetcore-runtime-3.1 aspnetcore-targeting-pack-3.1 aspnetcore-targeting-pack-6.0 dotnet-apphost-pack-3.1
  dotnet-apphost-pack-6.0 dotnet-targeting-pack-3.1 dotnet-targeting-pack-6.0 libappstream-glib8 liblttng-ust-ctl4
  liblttng-ust0 netstandard-targeting-pack-2.1 python3-crcmod
Use 'sudo apt autoremove' to remove them.
The following packages will be REMOVED:
  dotnet-sdk-3.1 dotnet-sdk-6.0
0 upgraded, 0 newly installed, 2 to remove and 387 not upgraded.
After this operation, 514 MB disk space will be freed.
Do you want to continue? [Y/n] y
(Reading database ... 235942 files and directories currently installed.)
Removing dotnet-sdk-3.1 (3.1.416-1) ...
Removing dotnet-sdk-6.0 (6.0.101-1) ...

Then install the latest with

$ dotnet --version
$ sudo apt-get remove dotnet-sdk-*
$ wget https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
$ udo dpkg -i packages-microsoft-prod.deb
$ sudo apt-get update
$ sudo apt-get install dotnet-sdk-8.0

with output:

$ wget https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
--2024-08-09 07:46:47--  https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb
Resolving packages.microsoft.com (packages.microsoft.com)... 13.107.246.38, 2620:1ec:bdf::38
Connecting to packages.microsoft.com (packages.microsoft.com)|13.107.246.38|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3690 (3.6K) [application/octet-stream]
Saving to: ‘packages-microsoft-prod.deb’

packages-microsoft-prod.deb  100%[==============================================>]   3.60K  --.-KB/s    in 0s

2024-08-09 07:46:47 (1.14 GB/s) - ‘packages-microsoft-prod.deb’ saved [3690/3690]

$ sudo dpkg -i packages-microsoft-prod.deb
(Reading database ... 231282 files and directories currently installed.)
Preparing to unpack packages-microsoft-prod.deb ...
Unpacking packages-microsoft-prod (1.0-ubuntu20.04.1) over (1.0-debian11.1) ...
Setting up packages-microsoft-prod (1.0-ubuntu20.04.1) ...
Installing new version of config file /etc/apt/sources.list.d/microsoft-prod.list ...

$ sudo apt-get update
Get:1 https://apt.releases.hashicorp.com focal InRelease [12.9 kB]
Get:2 https://packages.microsoft.com/repos/azure-cli focal InRelease [3596 B]
Get:3 https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod focal InRelease [3632 B]
Hit:4 http://archive.ubuntu.com/ubuntu focal InRelease
Get:5 https://packages.microsoft.com/ubuntu/20.04/prod focal InRelease [3632 B]
Err:1 https://apt.releases.hashicorp.com focal InRelease
  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY AA16FCBCA621E701
Get:6 http://archive.ubuntu.com/ubuntu focal-updates InRelease [128 kB]
Get:7 http://security.ubuntu.com/ubuntu focal-security InRelease [128 kB]
Get:8 https://packages.microsoft.com/repos/azure-cli focal/main amd64 Packages [1713 B]
Get:9 https://packages.cloud.google.com/apt cloud-sdk InRelease [1616 B]
Get:10 https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod focal/main amd64 Packages [305 kB]
Hit:11 http://archive.ubuntu.com/ubuntu focal-backports InRelease
Get:12 http://ppa.launchpad.net/flatpak/stable/ubuntu focal InRelease [18.1 kB]
Get:13 https://packages.microsoft.com/ubuntu/20.04/prod focal/main all Packages [2714 B]
Err:9 https://packages.cloud.google.com/apt cloud-sdk InRelease
  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY C0BA5CE6DC6315A3
Get:14 https://packages.microsoft.com/ubuntu/20.04/prod focal/main amd64 Packages [305 kB]
Get:15 https://packages.microsoft.com/ubuntu/20.04/prod focal/main armhf Packages [22.4 kB]
Get:16 https://packages.microsoft.com/ubuntu/20.04/prod focal/main arm64 Packages [63.3 kB]
Get:17 http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages [3490 kB]
Get:18 http://ppa.launchpad.net/ubuntu-toolchain-r/test/ubuntu focal InRelease [24.6 kB]
Get:19 http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_20.04  InRelease [1642 B]
Get:20 http://ppa.launchpad.net/flatpak/stable/ubuntu focal/main amd64 Packages [4484 B]
Get:21 http://archive.ubuntu.com/ubuntu focal-updates/main Translation-en [542 kB]
Get:22 http://archive.ubuntu.com/ubuntu focal-updates/main amd64 c-n-f Metadata [17.7 kB]
Get:23 http://archive.ubuntu.com/ubuntu focal-updates/restricted amd64 Packages [3148 kB]
Get:24 http://security.ubuntu.com/ubuntu focal-security/main amd64 Packages [3113 kB]
Get:25 http://archive.ubuntu.com/ubuntu focal-updates/restricted Translation-en [441 kB]
Get:26 http://archive.ubuntu.com/ubuntu focal-updates/restricted amd64 c-n-f Metadata [540 B]
Get:27 http://archive.ubuntu.com/ubuntu focal-updates/universe amd64 Packages [1218 kB]
Get:28 http://archive.ubuntu.com/ubuntu focal-updates/universe Translation-en [293 kB]
Get:29 http://archive.ubuntu.com/ubuntu focal-updates/universe amd64 c-n-f Metadata [27.8 kB]
Get:30 http://archive.ubuntu.com/ubuntu focal-updates/multiverse amd64 c-n-f Metadata [616 B]
Err:19 http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_20.04  InRelease
  The following signatures were invalid: EXPKEYSIG 4D64390375060AA4 devel:kubic OBS Project <devel:kubic@build.opensuse.org>
Get:31 http://security.ubuntu.com/ubuntu focal-security/main Translation-en [462 kB]
Get:32 http://security.ubuntu.com/ubuntu focal-security/main amd64 c-n-f Metadata [14.1 kB]
Get:33 http://security.ubuntu.com/ubuntu focal-security/restricted amd64 Packages [3020 kB]
Get:34 http://security.ubuntu.com/ubuntu focal-security/restricted Translation-en [424 kB]
Get:35 http://security.ubuntu.com/ubuntu focal-security/restricted amd64 c-n-f Metadata [544 B]
Get:36 http://security.ubuntu.com/ubuntu focal-security/universe amd64 Packages [999 kB]
Get:37 http://security.ubuntu.com/ubuntu focal-security/universe Translation-en [212 kB]
Get:38 http://security.ubuntu.com/ubuntu focal-security/universe amd64 c-n-f Metadata [21.0 kB]
Get:39 http://security.ubuntu.com/ubuntu focal-security/multiverse amd64 Packages [24.8 kB]
Get:40 http://security.ubuntu.com/ubuntu focal-security/multiverse Translation-en [5968 B]
Get:41 http://security.ubuntu.com/ubuntu focal-security/multiverse amd64 c-n-f Metadata [540 B]
Fetched 18.5 MB in 3s (6756 kB/s)
Reading package lists... Done
W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: https://apt.releases.hashicorp.com focal InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY AA16FCBCA621E701
W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: https://packages.cloud.google.com/apt cloud-sdk InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY C0BA5CE6DC6315A3
W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_20.04  InRelease: The following signatures were invalid: EXPKEYSIG 4D64390375060AA4 devel:kubic OBS Project <devel:kubic@build.opensuse.org>
W: Failed to fetch https://apt.releases.hashicorp.com/dists/focal/InRelease  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY AA16FCBCA621E701
W: Failed to fetch http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_20.04/InRelease  The following signatures were invalid: EXPKEYSIG 4D64390375060AA4 devel:kubic OBS Project <devel:kubic@build.opensuse.org>
W: Failed to fetch https://packages.cloud.google.com/apt/dists/cloud-sdk/InRelease  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY C0BA5CE6DC6315A3
W: Some index files failed to download. They have been ignored, or old ones used instead.

$ sudo apt-get install dotnet-sdk-8.0
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages were automatically installed and are no longer required:
  aspnetcore-runtime-3.1 aspnetcore-targeting-pack-3.1 aspnetcore-targeting-pack-6.0 dotnet-apphost-pack-3.1
  dotnet-apphost-pack-6.0 dotnet-targeting-pack-3.1 dotnet-targeting-pack-6.0 libappstream-glib8 liblttng-ust-ctl4
  liblttng-ust0 python3-crcmod
Use 'sudo apt autoremove' to remove them.
The following additional packages will be installed:
  aspnetcore-runtime-8.0 aspnetcore-targeting-pack-8.0 dotnet-apphost-pack-8.0 dotnet-host dotnet-hostfxr-8.0
  dotnet-runtime-8.0 dotnet-runtime-deps-8.0 dotnet-targeting-pack-8.0
The following NEW packages will be installed:
  aspnetcore-runtime-8.0 aspnetcore-targeting-pack-8.0 dotnet-apphost-pack-8.0 dotnet-hostfxr-8.0
  dotnet-runtime-8.0 dotnet-runtime-deps-8.0 dotnet-sdk-8.0 dotnet-targeting-pack-8.0
The following packages will be upgraded:
  dotnet-host
1 upgraded, 8 newly installed, 0 to remove and 402 not upgraded.
Need to get 145 MB of archives.
After this operation, 583 MB of additional disk space will be used.
Do you want to continue? [Y/n] Y
Get:1 https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod focal/main amd64 dotnet-host amd64 8.0.7-1 [37.7 kB]
Get:2 https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod focal/main amd64 dotnet-hostfxr-8.0 amd64 8.0.7-1 [108 kB]
Get:3 https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod focal/main amd64 dotnet-runtime-deps-8.0 amd64 8.0.7-1 [2890 B]
Get:4 https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod focal/main amd64 dotnet-runtime-8.0 amd64 8.0.7-1 [23.1 MB]
Get:5 https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod focal/main amd64 aspnetcore-runtime-8.0 amd64 8.0.7-1 [7675 kB]
Get:6 https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod focal/main amd64 dotnet-targeting-pack-8.0 amd64 8.0.7-1 [2803 kB]
Get:7 https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod focal/main amd64 aspnetcore-targeting-pack-8.0 amd64 8.0.7-1 [1917 kB]
Get:8 https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod focal/main amd64 dotnet-apphost-pack-8.0 amd64 8.0.7-1 [3516 kB]
Get:9 https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod focal/main amd64 dotnet-sdk-8.0 amd64 8.0.303-1 [106 MB]
Fetched 145 MB in 6s (23.1 MB/s)
(Reading database ... 231282 files and directories currently installed.)
Preparing to unpack .../0-dotnet-host_8.0.7-1_amd64.deb ...
Unpacking dotnet-host (8.0.7-1) over (6.0.1-1) ...
Selecting previously unselected package dotnet-hostfxr-8.0.
Preparing to unpack .../1-dotnet-hostfxr-8.0_8.0.7-1_amd64.deb ...
Unpacking dotnet-hostfxr-8.0 (8.0.7-1) ...
Selecting previously unselected package dotnet-runtime-deps-8.0.
Preparing to unpack .../2-dotnet-runtime-deps-8.0_8.0.7-1_amd64.deb ...
Unpacking dotnet-runtime-deps-8.0 (8.0.7-1) ...
Selecting previously unselected package dotnet-runtime-8.0.
Preparing to unpack .../3-dotnet-runtime-8.0_8.0.7-1_amd64.deb ...
Unpacking dotnet-runtime-8.0 (8.0.7-1) ...
Selecting previously unselected package aspnetcore-runtime-8.0.
Preparing to unpack .../4-aspnetcore-runtime-8.0_8.0.7-1_amd64.deb ...
Unpacking aspnetcore-runtime-8.0 (8.0.7-1) ...
Selecting previously unselected package dotnet-targeting-pack-8.0.
Preparing to unpack .../5-dotnet-targeting-pack-8.0_8.0.7-1_amd64.deb ...
Unpacking dotnet-targeting-pack-8.0 (8.0.7-1) ...
Selecting previously unselected package aspnetcore-targeting-pack-8.0.
Preparing to unpack .../6-aspnetcore-targeting-pack-8.0_8.0.7-1_amd64.deb ...
Unpacking aspnetcore-targeting-pack-8.0 (8.0.7-1) ...
Selecting previously unselected package dotnet-apphost-pack-8.0.
Preparing to unpack .../7-dotnet-apphost-pack-8.0_8.0.7-1_amd64.deb ...
Unpacking dotnet-apphost-pack-8.0 (8.0.7-1) ...
Selecting previously unselected package dotnet-sdk-8.0.
Preparing to unpack .../8-dotnet-sdk-8.0_8.0.303-1_amd64.deb ...
Unpacking dotnet-sdk-8.0 (8.0.303-1) ...
Setting up dotnet-host (8.0.7-1) ...
Setting up dotnet-targeting-pack-8.0 (8.0.7-1) ...
Setting up aspnetcore-targeting-pack-8.0 (8.0.7-1) ...
Setting up dotnet-apphost-pack-8.0 (8.0.7-1) ...
Setting up dotnet-runtime-deps-8.0 (8.0.7-1) ...
Setting up dotnet-hostfxr-8.0 (8.0.7-1) ...
Setting up dotnet-runtime-8.0 (8.0.7-1) ...
Setting up aspnetcore-runtime-8.0 (8.0.7-1) ...
Setting up dotnet-sdk-8.0 (8.0.303-1) ...
Processing triggers for man-db (2.9.1-1) ...

Which I can now validate

$ dotnet --version
8.0.303

Creating a new MVC .NET App

Let’s create a new one in my workspaces dir

builder@DESKTOP-QADGF36:~/Workspaces$ dotnet new mvc -o MvcPatients
The template "ASP.NET Core Web App (Model-View-Controller)" was created successfully.
This template contains technologies from parties other than Microsoft, see https://aka.ms/aspnetcore/8.0-third-party-notices for details.

Processing post-creation actions...
Restoring /home/builder/Workspaces/MvcPatients/MvcPatients.csproj:
  Determining projects to restore...
  Restored /home/builder/Workspaces/MvcPatients/MvcPatients.csproj (in 64 ms).
Restore succeeded.

We are just going to test that it worked before we do any modifications with dotnet run. I didn’t need to trust certs with Linux but did so anyhow

builder@DESKTOP-QADGF36:~/Workspaces$ cd MvcPatients/
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ dotnet dev-certs https --trust
Trusting the HTTPS development certificate was requested. Trusting the certificate on Linux distributions automatically is not supported. For instructions on how to manually trust the certificate on your Linux distribution, go to https://aka.ms/dev-certs-trust
A valid HTTPS certificate is already present.
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ dotnet run
Building...
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {787d1b97-c0d1-460c-95e6-0452b88ee032} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5227
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /home/builder/Workspaces/MvcPatients
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
      Failed to determine the https port for redirect.

I used the port I saw above to see the boilerplate site

/content/images/2024/08/dotnetapp-04.png

I’ll pop into VS Code and create a new “HelloWorldController.cs”

/content/images/2024/08/dotnetapp-05.png

with the contents:

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

namespace MvcMovie.Controllers;

public class HelloWorldController : Controller
{
    // 
    // GET: /HelloWorld/
    public string Index()
    {
        return "This is my default action...";
    }
    // 
    // GET: /HelloWorld/Welcome/ 
    public string Welcome()
    {
        return "This is the Welcome action method...";
    }
}

/content/images/2024/08/dotnetapp-06.png

I made sure to add the .NET debugger, though I don’t think I need it just yet

/content/images/2024/08/dotnetapp-07.png

This is a bit why I needed to fork from any tutorial which just said ctrl+F5 would do it. That kept prompting for debug. But there is a right-hand option to run without debugging and that finally worked

/content/images/2024/08/dotnetapp-08.png

And I could see the resulting endpoint live

/content/images/2024/08/dotnetapp-09.png

The point here, and the Microsoft Tutorial I based this on is keen to point out, is that the route to see that came from the Program.cs

/content/images/2024/08/dotnetapp-10.png

I have some plans to expand this to use the Patients model, but for the moment, let’s use the basic contents:

@{
    ViewData["Title"] = "Index";
}

<h2>Patient Index</h2>

<p>Hello from our View Template!</p>

In a new cshtml file in Views. I did create the “HelloWorld” folder when creating the file.

/content/images/2024/08/dotnetapp-11.png

I made a bit of a mistake and launched

/content/images/2024/08/dotnetapp-12.png

And didn’t see my View

/content/images/2024/08/dotnetapp-13.png

But then I realized I neglected to update the Controller class

    public IActionResult Index()
    {
        return View();
    }

/content/images/2024/08/dotnetapp-14.png

I have no idea if it was my VSCode or what. But i kept hitting “run” but didn’t see the updated page. I closed VSCode then opened it again and it Play and then saw my view.

/content/images/2024/08/dotnetapp-15.png

I think I can do the same thing by use “Run > Stop Debugging” in the Run menu as well.

I’m not going to change much with the shared layout except to change the default controller from “Home” to “HelloWorld”

/content/images/2024/08/dotnetapp-16.png

And to update my title to say “Patient Index”

/content/images/2024/08/dotnetapp-17.png

Adding a Model

Our next step it to create a Patient model in Models/Patient.cs This matches the setup of our Database

using System.ComponentModel.DataAnnotations;

namespace MvcPatients.Models;

public class Patient
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string SocialSecurityNumber { get; set; }
    public DateTime CreatedDate { get; set; }
}

/content/images/2024/08/dotnetapp-18.png

I’m going to start with SQLLite then pivot back.

So I’ll add the following .NET Nuget packages for SQLite

dotnet tool uninstall --global dotnet-aspnet-codegenerator
dotnet tool install --global dotnet-aspnet-codegenerator
dotnet tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

I won’t waste time without output, but that created quite a lot of output

/content/images/2024/08/dotnetapp-19.png

I have two more that cover PostgreSQL we will need in a bit

dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

Honestly, I’m going to try some new things now and I really want to “undo” it all. So this is a great time to init my workspace as a new repo

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git init
Initialized empty Git repository in /home/builder/Workspaces/MvcPatients/.git/
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git checkout -b main
Switched to a new branch 'main'
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        Controllers/
        Models/
        MvcPatients.csproj
        MvcPatients.generated.sln
        Program.cs
        Properties/
        Views/
        appsettings.Development.json
        appsettings.json
        bin/
        obj/
        wwwroot/

nothing added to commit but untracked files present (use "git add" to track)

I’ll use gitignore.io to create a new gitignore file https://www.toptal.com/developers/gitignore/api/dotnetcore,visualstudiocode,linux

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ vi .gitignore
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .gitignore
        Controllers/
        Models/
        MvcPatients.csproj
        MvcPatients.generated.sln
        Program.cs
        Properties/
        Views/
        appsettings.Development.json
        appsettings.json
        wwwroot/

nothing added to commit but untracked files present (use "git add" to track)
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git add .gitignore
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   .gitignore

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        Controllers/
        Models/
        MvcPatients.csproj
        MvcPatients.generated.sln
        Program.cs
        Properties/
        Views/
        appsettings.Development.json
        appsettings.json
        wwwroot/

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$

I’ll then add and commit my “init” first commit.

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git add -A
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git commit -m "init"
[main (root-commit) 9afd0eb] init
 80 files changed, 74898 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 Controllers/HelloWorldController.cs
 create mode 100644 Controllers/HomeController.cs
 create mode 100644 Models/ErrorViewModel.cs
 create mode 100644 Models/Patient.cs
 create mode 100644 MvcPatients.csproj
 create mode 100644 MvcPatients.generated.sln
 create mode 100644 Program.cs
 create mode 100644 Properties/launchSettings.json
 create mode 100644 Views/HelloWorld/Index.cshtml
 create mode 100644 Views/Home/Index.cshtml
 create mode 100644 Views/Home/Privacy.cshtml
 create mode 100644 Views/Shared/Error.cshtml
 create mode 100644 Views/Shared/_Layout.cshtml
 create mode 100644 Views/Shared/_Layout.cshtml.css
 create mode 100644 Views/Shared/_ValidationScriptsPartial.cshtml
 create mode 100644 Views/_ViewImports.cshtml
 create mode 100644 Views/_ViewStart.cshtml
 create mode 100644 appsettings.Development.json
 create mode 100644 appsettings.json
 create mode 100644 wwwroot/css/site.css
 create mode 100644 wwwroot/favicon.ico
 create mode 100644 wwwroot/js/site.js
 create mode 100644 wwwroot/lib/bootstrap/LICENSE
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
 create mode 100644 wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
 create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
 create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
 create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
 create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
 create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
 create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
 create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
 create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
 create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.js
 create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
 create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
 create mode 100644 wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
 create mode 100644 wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt
 create mode 100644 wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js
 create mode 100644 wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js
 create mode 100644 wwwroot/lib/jquery-validation/LICENSE.md
 create mode 100644 wwwroot/lib/jquery-validation/dist/additional-methods.js
 create mode 100644 wwwroot/lib/jquery-validation/dist/additional-methods.min.js
 create mode 100644 wwwroot/lib/jquery-validation/dist/jquery.validate.js
 create mode 100644 wwwroot/lib/jquery-validation/dist/jquery.validate.min.js
 create mode 100644 wwwroot/lib/jquery/LICENSE.txt
 create mode 100644 wwwroot/lib/jquery/dist/jquery.js
 create mode 100644 wwwroot/lib/jquery/dist/jquery.min.js
 create mode 100644 wwwroot/lib/jquery/dist/jquery.min.map

PostgreSQL context

I’ll now add a new scaffold for PostgreSQL

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ dotnet aspnet-codegenerator controller -name PatientsController -m Patient -dc MvcPatients.Data.MvcPatientsContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries --databaseProvider postgres
Building project ...
Finding the generator 'controller'...
Running the generator 'controller'...

Minimal hosting scenario!
Generating a new DbContext class 'MvcPatients.Data.MvcPatientsContext'
Attempting to compile the application in memory with the added DbContext.
Attempting to figure out the EntityFramework metadata for the model and DbContext: 'Patient'

Using database provider 'Npgsql.EntityFrameworkCore.PostgreSQL'!

Added DbContext : '/Data/MvcPatientsContext.cs'
Added Controller : '/Controllers/PatientsController.cs'.
Added View : /Views/Patients/Create.cshtml
Added View : /Views/Patients/Edit.cshtml
Added View : /Views/Patients/Details.cshtml
Added View : /Views/Patients/Delete.cshtml
Added View : /Views/Patients/Index.cshtml
RunTime 00:00:11.86

The fact we did a git commit beforehand makes it easy to see what was added and/or modified

/content/images/2024/08/dotnetapp-20.png

The one part we want to change it to use CloudSQL here

/content/images/2024/08/dotnetapp-21.png

I’ll use my PublicIP and the patientuser and password in my appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcPatientsContext": "Host=34.123.245.103;Database=patientsdb;Username=patientuser;Password=SuperSecretPassword1234"
  }
}

I did a quick run which shows that it could connect (or I would see an error). Though we have yet to define the table so there is nothing to show just yet

/content/images/2024/08/dotnetapp-22.png

If I do try to go to “Patients” it gives a dump

/content/images/2024/08/dotnetapp-23.png

I’m pretty sure this is because I called the patient table “patientdata” instead of Patient which matches the Model

public class Patient

I think I’ll update CloudSQL to match my Dotnet rather than the reverse;

builder@DESKTOP-QADGF36:~/Workspaces/exampleDotNetPatientApp$ cat 05_create_new_sample_data.sql

CREATE TABLE Patient (
    Id SERIAL PRIMARY KEY,
    FirstName VARCHAR(50),
    LastName VARCHAR(50),
    DateOfBirth DATE,
    SocialSecurityNumber CHAR(11),
    CreatedDate TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO Patient (FirstName, LastName, DateOfBirth, SocialSecurityNumber) VALUES
('John', 'Doe', '1985-05-15', '123-45-6789'),
('Jane', 'Smith', '1990-07-22', '987-65-4321'),
('Alice', 'Johnson', '1978-03-10', '111-22-3333'),
('Bob', 'Brown', '1982-11-30', '444-55-6666'),
('Charlie', 'Davis', '1995-01-25', '777-88-9999'),
('Diana', 'Miller', '1988-09-14', '222-33-4444'),
('Eve', 'Wilson', '1992-06-18', '555-66-7777'),
('Frank', 'Moore', '1980-12-05', '888-99-0000'),
('Grace', 'Taylor', '1983-04-27', '333-44-5555'),
('Hank', 'Anderson', '1975-08-19', '666-77-8888');

builder@DESKTOP-QADGF36:~/Workspaces/exampleDotNetPatientApp$ cat 05_create_new_sample_data.sh
#!/bin/bash
set -x

export PGINST=mypatientsdb
export PASSWD=SuperSecretPassword1234

gcloud sql instances describe $PGINST \
    --format="value(ipAddresses)" > ip_address.txt


PGPASSWORD=$PASSWD psql -h $(cat ip_address.txt | cut -d';' -f1 | cut -d':' -f2 | cut -d',' -f1 | sed 's/"//g' | sed "s/'//g" | tr -d ' ' ) -U patientuser -d patientsdb -f ./05_create_new_sample_data.sql

Which I ran

builder@DESKTOP-QADGF36:~/Workspaces/exampleDotNetPatientApp$ ./05_create_new_sample_data.sh
+ export PGINST=mypatientsdb
+ PGINST=mypatientsdb
+ export PASSWD=SuperSecretPassword1234
+ PASSWD=SuperSecretPassword1234
+ gcloud sql instances describe mypatientsdb '--format=value(ipAddresses)'
++ cat ip_address.txt
++ cut '-d;' -f1
++ cut -d: -f2
++ cut -d, -f1
++ sed 's/"//g'
++ sed 's/'\''//g'
++ tr -d ' '
+ PGPASSWORD=SuperSecretPassword1234
+ psql -h 34.123.245.103 -U patientuser -d patientsdb -f ./05_create_new_sample_data.sql
CREATE TABLE
INSERT 0 10

I still see the same error

POSITION: 109
Npgsql.Internal.NpgsqlConnector.ReadMessageLong(bool async, DataRowLoadingMode dataRowLoadingMode, bool readingNotifications, bool isReadingPrependedMessage)

But my test shows the table exists with the same field names as the model

builder@DESKTOP-QADGF36:~/Workspaces/exampleDotNetPatientApp$ PGPASSWORD=SuperSecretPassword1234 psql -h 34.123.245.103 -U patientuser -d patientsdb
psql (12.12 (Ubuntu 12.12-0ubuntu0.20.04.1), server 14.12)
WARNING: psql major version 12, server major version 14.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

patientsdb=> select (Id, FirstName, LastName, DateOfBirth, SocialSecurityNumber, CreatedDate) from Patient
patientsdb-> ;
                                  row
------------------------------------------------------------------------
 (1,John,Doe,1985-05-15,123-45-6789,"2024-08-10 14:09:12.283113")
 (2,Jane,Smith,1990-07-22,987-65-4321,"2024-08-10 14:09:12.283113")
 (3,Alice,Johnson,1978-03-10,111-22-3333,"2024-08-10 14:09:12.283113")
 (4,Bob,Brown,1982-11-30,444-55-6666,"2024-08-10 14:09:12.283113")
 (5,Charlie,Davis,1995-01-25,777-88-9999,"2024-08-10 14:09:12.283113")
 (6,Diana,Miller,1988-09-14,222-33-4444,"2024-08-10 14:09:12.283113")
 (7,Eve,Wilson,1992-06-18,555-66-7777,"2024-08-10 14:09:12.283113")
 (8,Frank,Moore,1980-12-05,888-99-0000,"2024-08-10 14:09:12.283113")
 (9,Grace,Taylor,1983-04-27,333-44-5555,"2024-08-10 14:09:12.283113")
 (10,Hank,Anderson,1975-08-19,666-77-8888,"2024-08-10 14:09:12.283113")
(10 rows)

patientsdb=>

Let me add some “EF Core” migrations, though I did not think I needed these

dotnet ef migrations add InitialCreate
dotnet ef database update

with output:

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ dotnet ef migrations add InitialCreate
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ dotnet ef database update
Build started...
Build succeeded.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (73ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT EXISTS (
          SELECT 1 FROM pg_catalog.pg_class c
          JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace
          WHERE n.nspname='public' AND
                c.relname='__EFMigrationsHistory'
      )
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (44ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT EXISTS (
          SELECT 1 FROM pg_catalog.pg_class c
          JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace
          WHERE n.nspname='public' AND
                c.relname='__EFMigrationsHistory'
      )
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (53ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "__EFMigrationsHistory" (
          "MigrationId" character varying(150) NOT NULL,
          "ProductVersion" character varying(32) NOT NULL,
          CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
      );
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (39ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT EXISTS (
          SELECT 1 FROM pg_catalog.pg_class c
          JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace
          WHERE n.nspname='public' AND
                c.relname='__EFMigrationsHistory'
      )
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (41ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "MigrationId", "ProductVersion"
      FROM "__EFMigrationsHistory"
      ORDER BY "MigrationId";
info: Microsoft.EntityFrameworkCore.Migrations[20402]
      Applying migration '20240810141908_InitialCreate'.
Applying migration '20240810141908_InitialCreate'.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (48ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE "Patient" (
          "Id" integer GENERATED BY DEFAULT AS IDENTITY,
          "FirstName" text NOT NULL,
          "LastName" text NOT NULL,
          "DateOfBirth" timestamp with time zone NOT NULL,
          "SocialSecurityNumber" text NOT NULL,
          "CreatedDate" timestamp with time zone NOT NULL,
          CONSTRAINT "PK_Patient" PRIMARY KEY ("Id")
      );
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (37ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
      VALUES ('20240810141908_InitialCreate', '8.0.7');
Done.

That seemed to work

/content/images/2024/08/dotnetapp-24.png

I tried creating a new entry

/content/images/2024/08/dotnetapp-25.png

An unhandled exception occurred while processing the request.
ArgumentException: Cannot write DateTime with Kind=Unspecified to PostgreSQL type 'timestamp with time zone', only UTC is supported. Note that it's not possible to mix DateTimes with different Kinds in an array, range, or multirange. (Parameter 'value')
Npgsql.Internal.Converters.DateTimeConverterResolver<T>.Get(DateTime value, Nullable<PgTypeId> expectedPgTypeId, bool validateOnly)

DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.

/content/images/2024/08/dotnetapp-26.png

I’ll first try switching from “DateTime” to “DateTimeOffset” which includes UTC offsets. I updated `/Models/Patient.cs’

using System.ComponentModel.DataAnnotations;

namespace MvcPatients.Models;

public class Patient
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTimeOffset DateOfBirth { get; set; }
    public string SocialSecurityNumber { get; set; }
    public DateTimeOffset CreatedDate { get; set; }
}

Then ran some new migrations

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ dotnet ef migrations add FixDateTime
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ dotnet ef database update
Build started...
Build succeeded.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (70ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT EXISTS (
          SELECT 1 FROM pg_catalog.pg_class c
          JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace
          WHERE n.nspname='public' AND
                c.relname='__EFMigrationsHistory'
      )
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (41ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "MigrationId", "ProductVersion"
      FROM "__EFMigrationsHistory"
      ORDER BY "MigrationId";
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (39ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT EXISTS (
          SELECT 1 FROM pg_catalog.pg_class c
          JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace
          WHERE n.nspname='public' AND
                c.relname='__EFMigrationsHistory'
      )
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (40ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT EXISTS (
          SELECT 1 FROM pg_catalog.pg_class c
          JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace
          WHERE n.nspname='public' AND
                c.relname='__EFMigrationsHistory'
      )
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (39ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT "MigrationId", "ProductVersion"
      FROM "__EFMigrationsHistory"
      ORDER BY "MigrationId";
info: Microsoft.EntityFrameworkCore.Migrations[20402]
      Applying migration '20240810143233_FixDateTime'.
Applying migration '20240810143233_FixDateTime'.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (38ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
      VALUES ('20240810143233_FixDateTime', '8.0.7');
Done.

So much closer.. This time I get the error

An unhandled exception occurred while processing the request.
ArgumentException: Cannot write DateTimeOffset with Offset=-05:00:00 to PostgreSQL type 'timestamp with time zone', only offset 0 (UTC) is supported. (Parameter 'value')
Npgsql.Internal.Converters.DateTimeOffsetConverter.WriteCore(PgWriter writer, DateTimeOffset value)

DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)

I finally got clued in by this stackoverflow that I would need to set a legacy behaviour npgsql flag:

AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);

/content/images/2024/08/dotnetapp-27.png

Now Create works

/content/images/2024/08/dotnetapp-28.png

I’m stumped on where it is saving data.

I see no evidence of it in Patients tables

builder@DESKTOP-QADGF36:~/Workspaces/exampleDotNetPatientApp$ PGPASSWORD=SuperSecretPassword1234 psql -h 34.123.245.103 -U patientuser -d patientsdb
psql (12.12 (Ubuntu 12.12-0ubuntu0.20.04.1), server 14.12)
WARNING: psql major version 12, server major version 14.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

patientsdb=> \dt;
                  List of relations
 Schema |         Name          | Type  |    Owner
--------+-----------------------+-------+-------------
 public | Patient               | table | patientuser
 public | __EFMigrationsHistory | table | patientuser
 public | patient               | table | patientuser
 public | patientdata           | table | patientuser
(4 rows)

patientsdb=> select * from Patient;
 id | firstname | lastname | dateofbirth | socialsecuritynumber |        createddate
----+-----------+----------+-------------+----------------------+----------------------------
  1 | John      | Doe      | 1985-05-15  | 123-45-6789          | 2024-08-10 14:09:12.283113
  2 | Jane      | Smith    | 1990-07-22  | 987-65-4321          | 2024-08-10 14:09:12.283113
  3 | Alice     | Johnson  | 1978-03-10  | 111-22-3333          | 2024-08-10 14:09:12.283113
  4 | Bob       | Brown    | 1982-11-30  | 444-55-6666          | 2024-08-10 14:09:12.283113
  5 | Charlie   | Davis    | 1995-01-25  | 777-88-9999          | 2024-08-10 14:09:12.283113
  6 | Diana     | Miller   | 1988-09-14  | 222-33-4444          | 2024-08-10 14:09:12.283113
  7 | Eve       | Wilson   | 1992-06-18  | 555-66-7777          | 2024-08-10 14:09:12.283113
  8 | Frank     | Moore    | 1980-12-05  | 888-99-0000          | 2024-08-10 14:09:12.283113
  9 | Grace     | Taylor   | 1983-04-27  | 333-44-5555          | 2024-08-10 14:09:12.283113
 10 | Hank      | Anderson | 1975-08-19  | 666-77-8888          | 2024-08-10 14:09:12.283113
(10 rows)

patientsdb=> select * from patient;'
 id | firstname | lastname | dateofbirth | socialsecuritynumber |        createddate
----+-----------+----------+-------------+----------------------+----------------------------
  1 | John      | Doe      | 1985-05-15  | 123-45-6789          | 2024-08-10 14:09:12.283113
  2 | Jane      | Smith    | 1990-07-22  | 987-65-4321          | 2024-08-10 14:09:12.283113
  3 | Alice     | Johnson  | 1978-03-10  | 111-22-3333          | 2024-08-10 14:09:12.283113
  4 | Bob       | Brown    | 1982-11-30  | 444-55-6666          | 2024-08-10 14:09:12.283113
  5 | Charlie   | Davis    | 1995-01-25  | 777-88-9999          | 2024-08-10 14:09:12.283113
  6 | Diana     | Miller   | 1988-09-14  | 222-33-4444          | 2024-08-10 14:09:12.283113
  7 | Eve       | Wilson   | 1992-06-18  | 555-66-7777          | 2024-08-10 14:09:12.283113
  8 | Frank     | Moore    | 1980-12-05  | 888-99-0000          | 2024-08-10 14:09:12.283113
  9 | Grace     | Taylor   | 1983-04-27  | 333-44-5555          | 2024-08-10 14:09:12.283113
 10 | Hank      | Anderson | 1975-08-19  | 666-77-8888          | 2024-08-10 14:09:12.283113
(10 rows)

Debugging with CloudSQL

This is where Query Insights can help us. I enabled it on the database and added a new entry:

/content/images/2024/08/dotnetapp-30.png

I can now see that reflected

/content/images/2024/08/dotnetapp-29.png

Clicking a Query shows it inserted

/content/images/2024/08/dotnetapp-31.png

The “End to End” really shows it is use ‘mypatientsdb’ instance and the ‘patientsdb’ database and ‘Patient’ table

/content/images/2024/08/dotnetapp-32.png

Darf! I see the issue. When using the psql CLI, if you do not use double-quotes, it does not respect the casing!

So now I see it if use "Patient" instead of Patient:

patientsdb=> select * from Patient;
 id | firstname | lastname | dateofbirth | socialsecuritynumber |        createddate
----+-----------+----------+-------------+----------------------+----------------------------
  1 | John      | Doe      | 1985-05-15  | 123-45-6789          | 2024-08-10 14:09:12.283113
  2 | Jane      | Smith    | 1990-07-22  | 987-65-4321          | 2024-08-10 14:09:12.283113
  3 | Alice     | Johnson  | 1978-03-10  | 111-22-3333          | 2024-08-10 14:09:12.283113
  4 | Bob       | Brown    | 1982-11-30  | 444-55-6666          | 2024-08-10 14:09:12.283113
  5 | Charlie   | Davis    | 1995-01-25  | 777-88-9999          | 2024-08-10 14:09:12.283113
  6 | Diana     | Miller   | 1988-09-14  | 222-33-4444          | 2024-08-10 14:09:12.283113
  7 | Eve       | Wilson   | 1992-06-18  | 555-66-7777          | 2024-08-10 14:09:12.283113
  8 | Frank     | Moore    | 1980-12-05  | 888-99-0000          | 2024-08-10 14:09:12.283113
  9 | Grace     | Taylor   | 1983-04-27  | 333-44-5555          | 2024-08-10 14:09:12.283113
 10 | Hank      | Anderson | 1975-08-19  | 666-77-8888          | 2024-08-10 14:09:12.283113
(10 rows)

patientsdb=> SELECT
patientsdb->   p."Id",
e",
  p."DateOfBpatientsdb->   p."CreatedDate",
patientsdb->   p."DateOfBirth",
patientsdb->   p."FirstName",
patientsdb->   p."LastName",
patientsdb->   p."SocialSecurityNumber"
patientsdb-> FROM
patientsdb->   "Patient" AS p;
 Id |      CreatedDate       |      DateOfBirth       | FirstName | LastName | SocialSecurityNumber
----+------------------------+------------------------+-----------+----------+----------------------
  1 | 2024-08-10 09:50:00+00 | 2024-08-09 09:50:00+00 | Todd      | Davis    | 123-45-678
  2 | 2024-08-10 09:58:00+00 | 2024-08-07 09:58:00+00 | Jim       | Smith    | 123-44-5555
(2 rows)

patientsdb=> select * from "Patient";
 Id | FirstName | LastName |      DateOfBirth       | SocialSecurityNumber |      CreatedDate
----+-----------+----------+------------------------+----------------------+------------------------
  1 | Todd      | Davis    | 2024-08-09 09:50:00+00 | 123-45-678           | 2024-08-10 09:50:00+00
  2 | Jim       | Smith    | 2024-08-07 09:58:00+00 | 123-44-5555          | 2024-08-10 09:58:00+00
(2 rows)

Let’s do some inserts now with a script. Note the double quotes on the table and column names

builder@DESKTOP-QADGF36:~/Workspaces/exampleDotNetPatientApp$ cat 06_load_sample.sql
INSERT INTO "Patient" ("FirstName", "LastName", "DateOfBirth", "SocialSecurityNumber", "CreatedDate") VALUES
('John', 'Doe', '1985-05-15', '123-45-6789',now()),
('Jane', 'Smith', '1990-07-22', '987-65-4321',now()),
('Alice', 'Johnson', '1978-03-10', '111-22-3333',now()),
('Bob', 'Brown', '1982-11-30', '444-55-6666',now()),
('Charlie', 'Davis', '1995-01-25', '777-88-9999',now()),
('Diana', 'Miller', '1988-09-14', '222-33-4444',now()),
('Eve', 'Wilson', '1992-06-18', '555-66-7777',now()),
('Frank', 'Moore', '1980-12-05', '888-99-0000',now()),
('Grace', 'Taylor', '1983-04-27', '333-44-5555',now()),
('Hank', 'Anderson', '1975-08-19', '666-77-8888',now());

builder@DESKTOP-QADGF36:~/Workspaces/exampleDotNetPatientApp$ cat 06_load_sample.sh
#!/bin/bash
set -x

export PGINST=mypatientsdb
export PASSWD=SuperSecretPassword1234

gcloud sql instances describe $PGINST \
    --format="value(ipAddresses)" > ip_address.txt


PGPASSWORD=$PASSWD psql -h $(cat ip_address.txt | cut -d';' -f1 | cut -d':' -f2 | cut -d',' -f1 | sed 's/"//g' | sed "s/'//g" | tr -d ' ' ) -U patientuser -d patientsdb -f ./06_load_sample.sql

which I can run

builder@DESKTOP-QADGF36:~/Workspaces/exampleDotNetPatientApp$ ./06_load_sample.sh
+ export PGINST=mypatientsdb
+ PGINST=mypatientsdb
+ export PASSWD=SuperSecretPassword1234
+ PASSWD=SuperSecretPassword1234
+ gcloud sql instances describe mypatientsdb '--format=value(ipAddresses)'
++ cat ip_address.txt
++ cut '-d;' -f1
++ cut -d: -f2
++ cut -d, -f1
++ sed 's/"//g'
++ sed 's/'\''//g'
++ tr -d ' '
+ PGPASSWORD=SuperSecretPassword1234
+ psql -h 34.123.245.103 -U patientuser -d patientsdb -f ./06_load_sample.sql
INSERT 0 10

And see reflected when I reload the page

/content/images/2024/08/dotnetapp-33.png

At this point we have a working app so let’s save this revision aside

I’ll also copy over all the database scripts I’ve used thus far

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients/setup_scripts$ ls
00_start.sh          02_create_db_user.sql  04_create_sample_data.sh   05_create_new_sample_data.sh   06_load_sample.sh   dotnet-install.sh
01_gcloud_create.sh  03_create_db_user.sh   04_create_sample_data.sql  05_create_new_sample_data.sql  06_load_sample.sql

And add as a new revision

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git add -A
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git commit -m "working with scripts"
[main c860ddd] working with scripts
 11 files changed, 1996 insertions(+)
 create mode 100755 setup_scripts/00_start.sh
 create mode 100755 setup_scripts/01_gcloud_create.sh
 create mode 100644 setup_scripts/02_create_db_user.sql
 create mode 100755 setup_scripts/03_create_db_user.sh
 create mode 100755 setup_scripts/04_create_sample_data.sh
 create mode 100644 setup_scripts/04_create_sample_data.sql
 create mode 100755 setup_scripts/05_create_new_sample_data.sh
 create mode 100644 setup_scripts/05_create_new_sample_data.sql
 create mode 100755 setup_scripts/06_load_sample.sh
 create mode 100644 setup_scripts/06_load_sample.sql
 create mode 100755 setup_scripts/dotnet-install.sh

Containerize

Let’s create a usable Dockerfile next. We’ll test it locally before pushing it out.

One thing we’ll have to contend with is the appsettings.json which up to this point has baked in a database connection string.

$ cat appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcPatientsContext": "Host=34.123.245.103;Database=patientsdb;Username=patientuser;Password=SuperSecretPassword1234"
  }
}

I’ll substitute it out in the Dockerfile

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ cat Dockerfile
# Use the official .NET 8 SDK image as a build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app

# Copy the project files and restore dependencies
COPY *.csproj ./
RUN dotnet restore

# Copy the remaining files and build the application
COPY . ./
RUN dotnet publish MvcPatients.generated.sln -c Release -o out
RUN dir ./out

# Use the official .NET 8 runtime image as a runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/out .

# Set the environment variables for the connection string
ENV DB_HOST=localhost
ENV DB_PORT=5432
ENV DB_NAME=patientsdb
ENV DB_USER=patientuser
ENV DB_PASSWORD=SuperSecretPassword1234

# Replace the connection string in appsettings.json
RUN sed -i 's|Host=.*;Database=.*;Username=.*;Password=.*|Host=${DB_HOST};Port=${DB_PORT};Database=${DB_NAME};Username=${DB_USER};Password=${DB_PASSWORD}|' appsettings.json

# Expose the port
EXPOSE 80

# Run the application
ENTRYPOINT ["dotnet", "MvcPatients.dll"]

And I can build

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ docker build -t mvcpatients:0.1 .
[+] Building 10.1s (17/17) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                        0.0s
 => => transferring dockerfile: 1.04kB                                                                                                                                      0.0s
 => [internal] load .dockerignore                                                                                                                                           0.0s
 => => transferring context: 2B                                                                                                                                             0.0s
 => [internal] load metadata for mcr.microsoft.com/dotnet/aspnet:8.0                                                                                                        0.2s
 => [internal] load metadata for mcr.microsoft.com/dotnet/sdk:8.0                                                                                                           0.2s
 => [internal] load build context                                                                                                                                           0.0s
 => => transferring context: 51.53kB                                                                                                                                        0.0s
 => [build 1/7] FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:7d0ba26469267b563120456557e38eccef9972cb6b9cfbbd47a50d1218fa7b30                                               0.0s
 => [runtime 1/4] FROM mcr.microsoft.com/dotnet/aspnet:8.0@sha256:3deda593cf10581cbacfa16a1fbb090353d14beaa65adca4611c7c7a458d66b0                                          0.0s
 => CACHED [runtime 2/4] WORKDIR /app                                                                                                                                       0.0s
 => CACHED [build 2/7] WORKDIR /app                                                                                                                                         0.0s
 => CACHED [build 3/7] COPY *.csproj ./                                                                                                                                     0.0s
 => CACHED [build 4/7] RUN dotnet restore                                                                                                                                   0.0s
 => [build 5/7] COPY . ./                                                                                                                                                   0.4s
 => [build 6/7] RUN dotnet publish MvcPatients.generated.sln -c Release -o out                                                                                              7.4s
 => [build 7/7] RUN dir ./out                                                                                                                                               0.5s
 => [runtime 3/4] COPY --from=build /app/out .                                                                                                                              0.3s
 => [runtime 4/4] RUN sed -i 's|Host=.*;Database=.*;Username=.*;Password=.*|Host=${DB_HOST};Port=${DB_PORT};Database=${DB_NAME};Username=${DB_USER};Password=${DB_PASSWORD  0.3s
 => exporting to image                                                                                                                                                      0.4s
 => => exporting layers                                                                                                                                                     0.4s
 => => writing image sha256:b9d26dccf35927b0fec14d8d3f21d7f9101dbeffaa3ec3ee5ab0cdb915c02299                                                                                0.0s
 => => naming to docker.io/library/mvcpatients:0.1    

Note: you can build with more verbose output using --progress

 builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ docker build --progress=plain --no-cache -t mvcpatients:0.1 .
#1 [internal] load build definition from Dockerfile
#1 sha256:da1c6b1803c5a375086dd24bdd9b215bf8439b88d84fb2c4bfe57a5e9871bf46
#1 transferring dockerfile: 38B done
#1 DONE 0.0s

However I get a dump when I try and run

builder@DESKTOP-QADGF36:~/Workspaces/exampleDotNetPatientApp$ docker run -e DB_HOST=34.123.245.103 -p 8888:80 mvcpatients:0.1
Unhandled exception. System.IO.InvalidDataException: Failed to load configuration from file '/app/appsettings.json'.
 ---> System.FormatException: Could not parse the JSON file.
 ---> System.Text.Json.JsonReaderException: '0x0A' is invalid within a JSON string. The string should be correctly escaped. LineNumber: 9 | BytePositionInLine: 122.
   at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
   at System.Text.Json.Utf8JsonReader.ConsumeStringAndValidate(ReadOnlySpan`1 data, Int32 idx)
   at System.Text.Json.Utf8JsonReader.ConsumeString()
   at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker)
   at System.Text.Json.Utf8JsonReader.ReadSingleSegment()
   at System.Text.Json.Utf8JsonReader.Read()
   at System.Text.Json.JsonDocument.Parse(ReadOnlySpan`1 utf8JsonSpan, JsonReaderOptions readerOptions, MetadataDb& database, StackRowStack& stack)
   at System.Text.Json.JsonDocument.Parse(ReadOnlyMemory`1 utf8Json, JsonReaderOptions readerOptions, Byte[] extraRentedArrayPoolBytes, PooledByteBufferWriter extraPooledByteBufferWriter)
   at System.Text.Json.JsonDocument.Parse(ReadOnlyMemory`1 json, JsonDocumentOptions options)
   at Microsoft.Extensions.Configuration.Json.JsonConfigurationFileParser.ParseStream(Stream input)
   at Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider.Load(Stream stream)
   --- End of inner exception stack trace ---
   at Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider.Load(Stream stream)
   at Microsoft.Extensions.Configuration.FileConfigurationProvider.Load(Boolean reload)
   --- End of inner exception stack trace ---
   at Microsoft.Extensions.Configuration.FileConfigurationProvider.Load(Boolean reload)
   at Microsoft.Extensions.Configuration.ConfigurationManager.AddSource(IConfigurationSource source)
   at Microsoft.Extensions.Configuration.ConfigurationManager.Microsoft.Extensions.Configuration.IConfigurationBuilder.Add(IConfigurationSource source)
   at Microsoft.Extensions.Hosting.HostingHostBuilderExtensions.ApplyDefaultAppConfiguration(HostBuilderContext hostingContext, IConfigurationBuilder appConfigBuilder, String[] args)
   at Microsoft.Extensions.Hosting.HostApplicationBuilder..ctor(HostApplicationBuilderSettings settings)
   at Microsoft.AspNetCore.Builder.WebApplicationBuilder..ctor(WebApplicationOptions options, Action`1 configureDefaults)
   at Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder(String[] args)
   at Program.<Main>$(String[] args) in /app/Program.cs:line 4

My normal approach for debug is to get a sleep command at the end so I can interactive debug

# Run the application
#ENTRYPOINT ["dotnet", "MvcPatients.dll"]
ENTRYPOINT ["tail", "-f", "/dev/null"]

I tried a build and run

[+] Building 9.0s (17/17) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                        0.0s
 => => transferring dockerfile: 1.08kB                                                                                                                                      0.0s
 => [internal] load .dockerignore                                                                                                                                           0.0s
 => => transferring context: 2B                                                                                                                                             0.0s
 => [internal] load metadata for mcr.microsoft.com/dotnet/aspnet:8.0                                                                                                        0.2s
 => [internal] load metadata for mcr.microsoft.com/dotnet/sdk:8.0                                                                                                           0.2s
 => [build 1/7] FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:7d0ba26469267b563120456557e38eccef9972cb6b9cfbbd47a50d1218fa7b30                                               0.0s
 => [internal] load build context                                                                                                                                           0.0s
 => => transferring context: 51.57kB                                                                                                                                        0.0s
 => [runtime 1/4] FROM mcr.microsoft.com/dotnet/aspnet:8.0@sha256:3deda593cf10581cbacfa16a1fbb090353d14beaa65adca4611c7c7a458d66b0                                          0.0s
 => CACHED [build 2/7] WORKDIR /app                                                                                                                                         0.0s
 => CACHED [build 3/7] COPY *.csproj ./                                                                                                                                     0.0s
 => CACHED [build 4/7] RUN dotnet restore                                                                                                                                   0.0s
 => [build 5/7] COPY . ./                                                                                                                                                   0.4s
 => [build 6/7] RUN dotnet publish MvcPatients.generated.sln -c Release -o out                                                                                              7.4s
 => [build 7/7] RUN dir ./out                                                                                                                                               0.5s
 => CACHED [runtime 2/4] WORKDIR /app                                                                                                                                       0.0s
 => CACHED [runtime 3/4] COPY --from=build /app/out .                                                                                                                       0.0s
 => CACHED [runtime 4/4] RUN sed -i 's|Host=.*;Database=.*;Username=.*;Password=.*|Host=${DB_HOST};Port=${DB_PORT};Database=${DB_NAME};Username=${DB_USER};Password=${DB_P  0.0s
 => exporting to image                                                                                                                                                      0.0s
 => => exporting layers                                                                                                                                                     0.0s
 => => writing image sha256:763fdd0b991d1a8b43f5bc4f01ad8fa197019627ab74d5f000bec98f4179415d                                                                                0.0s
 => => naming to docker.io/library/mvcpatients:0.2                                                                                                                          0.0s
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ docker run -e DB_HOST=34.123.245.103 -p 8888:80 mvcpatients:0.2

The appsettings.json is not substituting.

# pwd
/app
# cat appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcPatientsContext": "Host=${DB_HOST};Port=${DB_PORT};Database=${DB_NAME};Username=${DB_USER};Password=${DB_PASSWORD}
  }
}#

Let’s just fix it the right way.

I’ll update the Dockerfile to just pass in the main environment variables


# Use the official .NET 8 SDK image as a build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app

# Copy the project files and restore dependencies
COPY *.csproj ./
RUN dotnet restore

# Copy the remaining files and build the application
COPY . ./
RUN dotnet publish MvcPatients.generated.sln -c Release -o out

# Use the official .NET 8 runtime image as a runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/out .

# Set the environment variables for the connection string
ENV DB_HOST=your_host
ENV DB_NAME=patientsdb
ENV DB_USER=your_username
ENV DB_PASSWORD=your_password
ENV DB_PORT=5432

# Expose the port
EXPOSE 80

# Run the application
#ENTRYPOINT ["tail", "-f", "/dev/null"]
ENTRYPOINT ["dotnet", "MvcPatients.dll"]

Then my appsettings.json looks like

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcPatientsContext": "Host=${DB_HOST};Port=${DB_PORT};Database=${DB_NAME};Username=${DB_USER};Password=${DB_PASSWORD}"
  }
}

I can build and run

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ docker build -t mvcpatients:0.3 .
[+] Building 9.5s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                        0.0s
 => => transferring dockerfile: 836B                                                                                                                                        0.0s
 => [internal] load .dockerignore                                                                                                                                           0.0s
 => => transferring context: 2B                                                                                                                                             0.0s
 => [internal] load metadata for mcr.microsoft.com/dotnet/aspnet:8.0                                                                                                        0.2s
 => [internal] load metadata for mcr.microsoft.com/dotnet/sdk:8.0                                                                                                           0.2s
 => [build 1/6] FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:7d0ba26469267b563120456557e38eccef9972cb6b9cfbbd47a50d1218fa7b30                                               0.0s
 => [internal] load build context                                                                                                                                           0.0s
 => => transferring context: 51.63kB                                                                                                                                        0.0s
 => [runtime 1/3] FROM mcr.microsoft.com/dotnet/aspnet:8.0@sha256:3deda593cf10581cbacfa16a1fbb090353d14beaa65adca4611c7c7a458d66b0                                          0.0s
 => CACHED [build 2/6] WORKDIR /app                                                                                                                                         0.0s
 => CACHED [build 3/6] COPY *.csproj ./                                                                                                                                     0.0s
 => CACHED [build 4/6] RUN dotnet restore                                                                                                                                   0.0s
 => [build 5/6] COPY . ./                                                                                                                                                   0.5s
 => [build 6/6] RUN dotnet publish MvcPatients.generated.sln -c Release -o out                                                                                              7.4s
 => CACHED [runtime 2/3] WORKDIR /app                                                                                                                                       0.0s
 => [runtime 3/3] COPY --from=build /app/out .                                                                                                                              0.4s
 => exporting to image                                                                                                                                                      0.4s
 => => exporting layers                                                                                                                                                     0.4s
 => => writing image sha256:c3852bc94aff801a4e7f9bd02fa21e35042af8ec48bd6b4560e1d683cef200b1                                                                                0.0s
 => => naming to docker.io/library/mvcpatients:0.3                                                                                                                          0.0s
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ docker run -e DB_HOST=34.123.245.103 -e DB_NAME=patientsdb -e DB_USER=patientuser -e DB_PASSWORD=SuperSecretPassword1234 -p 8888:8080 mvcpatients:0.3
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
      Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed. For more information go to https://aka.ms/aspnet/dataprotectionwarning
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {ac0daf07-6a07-4e94-a825-e14fe5e24aaf} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:8080
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app

The short version is I ended up solving this by using the ConfigurationPlaceholders nuget package.

This just meant adding a using and an additional AddConfigurationPlaceholders line to my Program.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcPatients.Data;
using ConfigurationPlaceholders;

var builder = WebApplication.CreateBuilder(args);

// Using ConfigurationPlaceholders
builder
     .AddConfigurationPlaceholders( new EnvironmentVariableResolver() ); 

builder.Services.AddDbContext<MvcPatientsContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("MvcPatientsContext") ?? throw new InvalidOperationException("Connection string 'MvcPatientsContext' not found.")));

... snip ...

I did a build and run

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ docker build -t mvcpatients:0.5 .
[+] Building 9.6s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                        0.0s
 => => transferring dockerfile: 38B                                                                                                                                         0.0s
 => [internal] load .dockerignore                                                                                                                                           0.0s
 => => transferring context: 2B                                                                                                                                             0.0s
 => [internal] load metadata for mcr.microsoft.com/dotnet/aspnet:8.0                                                                                                        0.4s
 => [internal] load metadata for mcr.microsoft.com/dotnet/sdk:8.0                                                                                                           0.2s
 => [runtime 1/3] FROM mcr.microsoft.com/dotnet/aspnet:8.0@sha256:3deda593cf10581cbacfa16a1fbb090353d14beaa65adca4611c7c7a458d66b0                                          0.0s
 => [internal] load build context                                                                                                                                           0.0s
 => => transferring context: 51.79kB                                                                                                                                        0.0s
 => [build 1/6] FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:7d0ba26469267b563120456557e38eccef9972cb6b9cfbbd47a50d1218fa7b30                                               0.0s
 => CACHED [build 2/6] WORKDIR /app                                                                                                                                         0.0s
 => CACHED [build 3/6] COPY *.csproj ./                                                                                                                                     0.0s
 => CACHED [build 4/6] RUN dotnet restore                                                                                                                                   0.0s
 => [build 5/6] COPY . ./                                                                                                                                                   0.4s
 => [build 6/6] RUN dotnet publish MvcPatients.generated.sln -c Release -o out                                                                                              7.6s
 => CACHED [runtime 2/3] WORKDIR /app                                                                                                                                       0.0s
 => [runtime 3/3] COPY --from=build /app/out .                                                                                                                              0.4s
 => exporting to image                                                                                                                                                      0.4s
 => => exporting layers                                                                                                                                                     0.4s
 => => writing image sha256:625a054d61eff44aba587ac0b895cdccdb0b6527d5188845b7ba62051d90ff00                                                                                0.0s
 => => naming to docker.io/library/mvcpatients:0.5                                                                                                                          0.0s
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ docker run -e DB_HOST=34.123.245.103 -e DB_NAME=patientsdb -e DB_USER=patientuser -e DB_PASSWORD=SuperSecretPassword1234 -p 8888:8080 mvcpatients:0.5
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
      Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed. For more information go to https://aka.ms/aspnet/dataprotectionwarning
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {c5fd2899-b8f0-44e0-9579-840c03268bd7} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:8080
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
      Failed to determine the https port for redirect.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (64ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT p."Id", p."CreatedDate", p."DateOfBirth", p."FirstName", p."LastName", p."SocialSecurityNumber"
      FROM "Patient" AS p

And tested

/content/images/2024/08/dotnetapp-34.png

I fixed the Dockerfile line (just because it might mess things up later)

# Expose the port
EXPOSE 8080

and saved this new version as “working dockerfile”

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   Dockerfile
        modified:   MvcPatients.csproj
        modified:   Program.cs
        new file:   Readme.md
        modified:   appsettings.json

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git commit -m "working dockerfile"
[main 8728365] working dockerfile
 5 files changed, 51 insertions(+), 1 deletion(-)
 create mode 100644 Dockerfile
 create mode 100644 Readme.md

At this point, I would really like to test in Kubernetes, so I think a helm chart is in order

Dockerhub

First, let’s put out an image we can use remotely.

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ docker build -t idjohnson/freshmvcapp:0.1 .
[+] Building 9.1s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                        0.0s
 => => transferring dockerfile: 821B                                                                                                                                        0.0s
 => [internal] load .dockerignore                                                                                                                                           0.0s
 => => transferring context: 2B                                                                                                                                             0.0s
 => [internal] load metadata for mcr.microsoft.com/dotnet/aspnet:8.0                                                                                                        0.2s
 => [internal] load metadata for mcr.microsoft.com/dotnet/sdk:8.0                                                                                                           0.2s
 => [runtime 1/3] FROM mcr.microsoft.com/dotnet/aspnet:8.0@sha256:3deda593cf10581cbacfa16a1fbb090353d14beaa65adca4611c7c7a458d66b0                                          0.0s
 => [build 1/6] FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:7d0ba26469267b563120456557e38eccef9972cb6b9cfbbd47a50d1218fa7b30                                               0.0s
 => [internal] load build context                                                                                                                                           0.0s
 => => transferring context: 68.88kB                                                                                                                                        0.0s
 => CACHED [build 2/6] WORKDIR /app                                                                                                                                         0.0s
 => CACHED [build 3/6] COPY *.csproj ./                                                                                                                                     0.0s
 => CACHED [build 4/6] RUN dotnet restore                                                                                                                                   0.0s
 => [build 5/6] COPY . ./                                                                                                                                                   0.5s
 => [build 6/6] RUN dotnet publish MvcPatients.generated.sln -c Release -o out                                                                                              7.2s
 => CACHED [runtime 2/3] WORKDIR /app                                                                                                                                       0.0s
 => [runtime 3/3] COPY --from=build /app/out .                                                                                                                              0.3s
 => exporting to image                                                                                                                                                      0.4s
 => => exporting layers                                                                                                                                                     0.4s
 => => writing image sha256:eee00b66d4805a6bd386eeabe07dc5de4dccbe2189531db833598f46065e8d5d                                                                                0.0s
 => => naming to docker.io/idjohnson/freshmvcapp:0.1                                                                                                                        0.0s
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ docker push idjohnson/freshmvcapp:0.1
The push refers to repository [docker.io/idjohnson/freshmvcapp]
b30ae2760b80: Pushed
cbd8bd8f6f0e: Pushed
698640980ef8: Pushed
58fa834ef12a: Pushed
855e51907d3e: Pushed
ad8af893343b: Pushed
2ea3b529cc6c: Pushed
e0781bc8667f: Pushed
0.1: digest: sha256:34da275ce37beca9a8290f3ca088b5e92d31830639456d15156f149298489235 size: 1998

I can now see it up on Dockerhub here with this 0.1 version here. You should be able to use docker pull idjohnson/freshmvcapp:0.1 to fetch it.

/content/images/2024/08/dotnetapp-35.png

Kubernetes Manifest

I’ll start simple and create a manifest.

I’ll base64 encode the password

$ echo "SuperSecretPassword1234" | tr -d '\n' | base64
U3VwZXJTZWNyZXRQYXNzd29yZDEyMzQ=

I then created a Deployment/manifest.yaml file

apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  DB_PASSWORD: U3VwZXJTZWNyZXRQYXNzd29yZDEyMzQ=
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mvcpatients-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: mvcpatients
  template:
    metadata:
      labels:
        app: mvcpatients
    spec:
      containers:
      - name: mvcpatients-container
        image: idjohnson/freshmvcapp:0.1
        ports:
        - containerPort: 8080
        env:
        - name: DB_HOST
          value: "34.123.245.103"
        - name: DB_NAME
          value: "patientsdb"
        - name: DB_USER
          value: "patientuser"
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: DB_PASSWORD

---
apiVersion: v1
kind: Service
metadata:
  name: mvcpatients-service
spec:
  selector:
    app: mvcpatients
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: ClusterIP

I then created a namespacea and applied it in my cluster

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ kubectl create ns patientsmvc
namespace/patientsmvc created
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ kubectl apply -f ./Deployment/manifest.yaml -n patientsmvc
secret/db-secret created
deployment.apps/mvcpatients-deployment created
service/mvcpatients-service created

Looks good so far

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ kubectl get pods -n patientsmvc
NAME                                      READY   STATUS    RESTARTS   AGE
mvcpatients-deployment-69b4977bc7-d42p9   1/1     Running   0          25s
mvcpatients-deployment-69b4977bc7-5kpst   1/1     Running   0          25s
mvcpatients-deployment-69b4977bc7-f5qwv   1/1     Running   0          25s

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ kubectl get secrets -n patientsmvc
NAME        TYPE     DATA   AGE
db-secret   Opaque   1      40s

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ kubectl get service -n patientsmvc
NAME                  TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
mvcpatients-service   ClusterIP   10.43.110.52   <none>        80/TCP    46s

I’ll port-forward to a service

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ kubectl port-forward svc/mvcpatients-service -n patientsmvc 8888:80
Forwarding from 127.0.0.1:8888 -> 8080
Forwarding from [::1]:8888 -> 8080

Indeed it works

/content/images/2024/08/dotnetapp-36.png

This is easy enough to turn into a proper helm chart next

I’ll create the values file

replicaCount: 3

image:
  repository: idjohnson/freshmvcapp
  tag: 0.1
  pullPolicy: IfNotPresent
  port: 8080

service:
  type: ClusterIP
  port: 80

env:
  dbHost: your_host
  dbName: patientsdb
  dbUser: your_username
  dbPassword: your_db_password # This will be overridden by the secret

resources: {}

nodeSelector: {}

tolerations: []

affinity: {}

Then templates for each resource

/content/images/2024/08/dotnetapp-37.png

We can test by first removing the manifest version

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ kubectl delete -f ./Deployment/manifest.yaml -n patientsmvc
secret "db-secret" deleted
deployment.apps "mvcpatients-deployment" deleted
service "mvcpatients-service" deleted

Now I can helm install with passed in values

$ helm install mypatientmvc -n patientsmvc --set env.dbHost=34.123.245.103 --set env.dbName=patientsdb --set env.dbUser=patientuser --set env.dbPassword=SuperSecretPassword1234 ./Deployment/chart/
NAME: mypatientmvc
LAST DEPLOYED: Sat Aug 10 12:16:54 2024
NAMESPACE: patientsmvc
STATUS: deployed
REVISION: 1
TEST SUITE: None

And the same test worked

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ kubectl get svc -n patientsmvc
NAME                   TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
mypatientmvc-service   ClusterIP   10.43.171.86   <none>        80/TCP    34s
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ kubectl port-forward svc/mypatientmvc-service -n patientsmvc 8888:80
Forwarding from 127.0.0.1:8888 -> 8080
Forwarding from [::1]:8888 -> 8080
Handling connection for 8888
Handling connection for 8888
Handling connection for 8888
Handling connection for 8888
Handling connection for 8888
Handling connection for 8888

Cleanup

Before I check things in, I’ll change the password on the database users since I shared them all over this post:

/content/images/2024/08/dotnetapp-38.png

I’ll rotate the pods just to ensure no cached data

$ kubectl delete pod -l app=mvcpatients-chart -n patientsmvc
pod "mypatientmvc-deployment-7f54bb85c7-sd7vs" deleted
pod "mypatientmvc-deployment-7f54bb85c7-rm6rh" deleted
pod "mypatientmvc-deployment-7f54bb85c7-g4cpx" deleted

I now get an error which is exactly what I want

/content/images/2024/08/dotnetapp-39.png

I did a quick rotate with the updated password

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ helm delete mypatientmvc -n patientsmvc
release "mypatientmvc" uninstalled
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ helm install mypatientmvc -n patientsmvc --set env.dbHost=34.123.245.103 --set env.dbName=patientsdb --set env.dbUser=patientus
er --set env.dbPassword='**********something complicated here**********' ./Deployment/chart/
NAME: mypatientmvc
LAST DEPLOYED: Sat Aug 10 12:25:25 2024
NAMESPACE: patientsmvc
STATUS: deployed
REVISION: 1
TEST SUITE: None
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ kubectl get pods -n patientsmvc
NAME                                       READY   STATUS    RESTARTS   AGE
mypatientmvc-deployment-7f54bb85c7-vvb4t   1/1     Running   0          2s
mypatientmvc-deployment-7f54bb85c7-9kzhg   1/1     Running   0          2s
mypatientmvc-deployment-7f54bb85c7-65vsn   1/1     Running   0          2s
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ kubectl port-forward svc/mypatientmvc-service -n patientsmvc 8888:80
Forwarding from 127.0.0.1:8888 -> 8080
Forwarding from [::1]:8888 -> 8080
Handling connection for 8888
Handling connection for 8888

If I leave this running, there will be a cost. CloudSQL is not free

/content/images/2024/08/dotnetapp-40.png

Right now I’m using just over the provisioned 10Gb of storage so if I left it going, I might likely be looking at US$9.41 for the month. Not terrible, but something to consider

/content/images/2024/08/dotnetapp-41.png

Whenever I am ready to be done, I can delete the instance and stop incurring charges

/content/images/2024/08/dotnetapp-42.png

I added this commit

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git add Readme.md
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git add Deployment/
builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git commit -m "working helm and manifest"
[main f264df2] working helm and manifest
 7 files changed, 160 insertions(+)
 create mode 100644 Deployment/chart/Chart.yaml
 create mode 100644 Deployment/chart/templates/deployment.yaml
 create mode 100644 Deployment/chart/templates/secret.yaml
 create mode 100644 Deployment/chart/templates/service.yaml
 create mode 100644 Deployment/chart/values.yaml
 create mode 100644 Deployment/manifest.yaml

I realized I had one little minor issue that will block me from more experimentation - not exposing the port.

I updated all the relevant sections

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ git diff
diff --git a/Deployment/chart/Chart.yaml b/Deployment/chart/Chart.yaml
index 0c02357..02d7d67 100644
--- a/Deployment/chart/Chart.yaml
+++ b/Deployment/chart/Chart.yaml
@@ -1,5 +1,5 @@
 apiVersion: v2
 name: mvcpatients-chart
 description: A Helm chart for deploying the MVC Patients application
-version: 0.1.0
+version: 0.1.1
 appVersion: "1.0"
diff --git a/Deployment/chart/templates/deployment.yaml b/Deployment/chart/templates/deployment.yaml
index a48fbb5..205847c 100644
--- a/Deployment/chart/templates/deployment.yaml
+++ b/Deployment/chart/templates/deployment.yaml
@@ -26,6 +26,8 @@ spec:
           value: "{{ .Values.env.dbName }}"
         - name: DB_USER
           value: "{{ .Values.env.dbUser }}"
+        - name: DB_PORT
+          value: "{{ .Values.env.dbPort }}"
         - name: DB_PASSWORD
           valueFrom:
             secretKeyRef:
diff --git a/Deployment/chart/values.yaml b/Deployment/chart/values.yaml
index 96f8853..0069a8f 100644
--- a/Deployment/chart/values.yaml
+++ b/Deployment/chart/values.yaml
@@ -2,7 +2,7 @@ replicaCount: 3

 image:
   repository: idjohnson/freshmvcapp
-  tag: 0.1
+  tag: 0.2
   pullPolicy: IfNotPresent
   port: 8080

@@ -14,6 +14,7 @@ env:
   dbHost: your_host
   dbName: patientsdb
   dbUser: your_username
+  dbPort: 5432
   dbPassword: your_db_password # This will be overridden by the secret

 resources: {}
diff --git a/appsettings.json b/appsettings.json
index a367892..e7f4bf9 100644
--- a/appsettings.json
+++ b/appsettings.json
@@ -7,6 +7,6 @@
   },
   "AllowedHosts": "*",
   "ConnectionStrings": {
-    "MvcPatientsContext": "Host=${DB_HOST};Database=${DB_NAME};Username=${DB_USER};Password=${DB_PASSWORD}"
+    "MvcPatientsContext": "Host=${DB_HOST};Port=${DB_PORT};Database=${DB_NAME};Username=${DB_USER};Password=${DB_PASSWORD}"
   }
 }
\ No newline at end of file

built and pushed a new image

builder@DESKTOP-QADGF36:~/Workspaces/MvcPatients$ docker build -t idjohnson/freshmvcapp:0.2 . && docker push idjohnson/freshmvcapp:0.2
[+] Building 11.6s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                        0.0s
 => => transferring dockerfile: 38B                                                                                                                                         0.0s
 => [internal] load .dockerignore                                                                                                                                           0.0s
 => => transferring context: 2B                                                                                                                                             0.0s
 => [internal] load metadata for mcr.microsoft.com/dotnet/aspnet:8.0                                                                                                        0.2s
 => [internal] load metadata for mcr.microsoft.com/dotnet/sdk:8.0                                                                                                           0.9s
 => [build 1/6] FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:7d0ba26469267b563120456557e38eccef9972cb6b9cfbbd47a50d1218fa7b30                                               0.0s
 => [runtime 1/3] FROM mcr.microsoft.com/dotnet/aspnet:8.0@sha256:3deda593cf10581cbacfa16a1fbb090353d14beaa65adca4611c7c7a458d66b0                                          0.0s
 => [internal] load build context                                                                                                                                           0.1s
 => => transferring context: 76.26kB                                                                                                                                        0.1s
 => CACHED [build 2/6] WORKDIR /app                                                                                                                                         0.0s
 => CACHED [build 3/6] COPY *.csproj ./                                                                                                                                     0.0s
 => CACHED [build 4/6] RUN dotnet restore                                                                                                                                   0.0s
 => [build 5/6] COPY . ./                                                                                                                                                   1.0s
 => [build 6/6] RUN dotnet publish MvcPatients.generated.sln -c Release -o out                                                                                              8.4s
 => CACHED [runtime 2/3] WORKDIR /app                                                                                                                                       0.0s
 => [runtime 3/3] COPY --from=build /app/out .                                                                                                                              0.3s
 => exporting to image                                                                                                                                                      0.4s
 => => exporting layers                                                                                                                                                     0.3s
 => => writing image sha256:47ed69b446e58d0757e0b036ed6e9ae8a705621058737b255487e0b9f7ddc1fe                                                                                0.0s
 => => naming to docker.io/idjohnson/freshmvcapp:0.2                                                                                                                        0.0s
The push refers to repository [docker.io/idjohnson/freshmvcapp]
cbcdc5e41533: Pushed
cbd8bd8f6f0e: Layer already exists
698640980ef8: Layer already exists
58fa834ef12a: Layer already exists
855e51907d3e: Layer already exists
ad8af893343b: Layer already exists
2ea3b529cc6c: Layer already exists
e0781bc8667f: Layer already exists
0.2: digest: sha256:49b7a711a741f90fc56cd86d6a7ebb85366f625f913f892bb0f41838684f35e1 size: 1998

Sharing

I went ahead and pushed all the changes and files mentioned above to a new public Github repo at idjohnson/mvcpatients.

You can find all the mentioned commits in this history at https://github.com/idjohnson/mvcpatients/commits/main/

Summary

Let’s review what we did. We created a basic GCP CloudSQL PostgreSQL database and populated it with content using gcloud CLI and the psql command line client.

We then moved on to creating a new .NET 8 MVC app from scratch on Linux (via WSL). We started with a Microsoft Tutorial for the most-part but pivoted as we build out the Model/View/Controllers and contexts for PostgreSQL.

After successfully getting it to work locally, we created a working Container image and pushed to Dockerhub. We then launched into Kubernetes with both a Kubernetes manifest YAML and Helm chart.

Lastly, we wrapped with a Readme and some cost saving cleanups.

GCP Kubernetes Containers CloudRun CloudSQL Aiven Dotnet

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes