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
$ ./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
and users
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
I’ll pop into VS Code and create a new “HelloWorldController.cs”
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...";
}
}
I made sure to add the .NET debugger, though I don’t think I need it just yet
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
And I could see the resulting endpoint live
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
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.
I made a bit of a mistake and launched
And didn’t see my View
But then I realized I neglected to update the Controller class
public IActionResult Index()
{
return View();
}
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.
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”
And to update my title to say “Patient Index”
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; }
}
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
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
The one part we want to change it to use CloudSQL here
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
If I do try to go to “Patients” it gives a dump
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
I tried creating a new entry
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.
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);
Now Create works
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:
I can now see that reflected
Clicking a Query shows it inserted
The “End to End” really shows it is use ‘mypatientsdb’ instance and the ‘patientsdb’ database and ‘Patient’ table
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
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
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.
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
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
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:
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
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
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
Whenever I am ready to be done, I can delete the instance and stop incurring charges
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.