mirror of
https://github.com/jcreek/jcreek.github.io.git
synced 2026-07-12 18:43:50 +00:00
feat(#16): Add all content, images and features
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# jcreek.co.uk
|
||||
|
||||
When you're finished editing, you can build a static site from your Markdown files with:
|
||||
|
||||
`mkdocs build`
|
||||
|
||||
To customise code blocks, see the documentation at https://squidfunk.github.io/mkdocs-material/reference/code-blocks/
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
---
|
||||
tags:
|
||||
- deploying
|
||||
- docker
|
||||
---
|
||||
|
||||
# Docker Compose for ASP.Net Core with Postgres + S3 backups
|
||||
|
||||
_2021-07-12_
|
||||
|
||||
In this post, I will cover how I set up the following application structure, run from a single `docker-compose` file:
|
||||
|
||||
- ASP.Net Core MVC & Razor Web Application
|
||||
- ASP.Net Core Entity Framework Migrations
|
||||
- PostgreSQL
|
||||
- SMTP Server
|
||||
- S3 Backups for PostgreSQL
|
||||
- S3 Restores for PostgreSQL
|
||||
|
||||
The directory structure, including pertinent files, looks like this.
|
||||
|
||||
```
|
||||
Repository Root Folder
|
||||
│ .dockerignore
|
||||
│ database.env
|
||||
│ docker-compose.yml
|
||||
│
|
||||
└───ASP.Net Core Project Folder
|
||||
│ │ Dockerfile
|
||||
│ │ Migrations.Dockerfile
|
||||
│ │ Setup.sh
|
||||
│
|
||||
└───postgres-backup-s3
|
||||
│ │ Dockerfile
|
||||
│
|
||||
└───postgres-restore-s3
|
||||
│ │ Dockerfile
|
||||
```
|
||||
|
||||
Starting with an almost blank `docker-compose.yml` file, over the course of this post we'll add each service so that the entire infrastructure can be brought up with one single `docker-compose up` command.
|
||||
|
||||
```docker
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
```
|
||||
|
||||
N.B. One optional thing not covered here is to include `restart: always` for each service in the `docker-compose.yml` file to ensure they come back online after the host machine reboots. I wouldn't recommend using this for anything but the core web app, database and mail server.
|
||||
|
||||
## ASP.Net Core Web Application
|
||||
|
||||
For this we want to specify a few key things:
|
||||
|
||||
- the name to give the container, to make it easier to work with than the docker auto-generated names
|
||||
- the internal port to expose on the host machine's external port
|
||||
- the Dockerfile to use to build the application
|
||||
- the folder to map to make log files accessible from the host machine without having to use the terminal
|
||||
- the environment variable to set the app into production mode instead of the default
|
||||
- the other containers this one will depend on
|
||||
|
||||
```docker
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
aspprojectname:
|
||||
container_name: myaspprojectname
|
||||
ports:
|
||||
- "80:80"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: MyAspProjectName/Dockerfile
|
||||
volumes:
|
||||
- ./MyAspProjectName/logs:/app/logs
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
depends_on:
|
||||
- db
|
||||
# - migrations
|
||||
```
|
||||
|
||||
You will notice here that I am commenting out the migrations dependency. This is because with the S3 backup and restore images it's not really needed, and it requires more powerful hardware to run than any of the other images and is a large docker image, so worth avoiding if possible, or only using to set up the database initially, then deleting.
|
||||
|
||||
The Dockerfile itself is pretty standard for ASP.Net Core apps. In this instance, the app is running on dotnet 5.
|
||||
|
||||
```docker
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["MyAspProjectName/MyAspProjectName.csproj", "MyAspProjectName/"]
|
||||
RUN dotnet restore "MyAspProjectName/MyAspProjectName.csproj" --disable-parallel
|
||||
COPY . .
|
||||
WORKDIR "/src/MyAspProjectName"
|
||||
RUN dotnet build "MyAspProjectName.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "MyAspProjectName.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "MyAspProjectName.dll"]
|
||||
```
|
||||
|
||||
## ASP.Net Core Entity Framework Migrations
|
||||
|
||||
This one is pretty similar to the main web app in terms of what needs adding to add the service to the `docker-compose.yml` file.
|
||||
|
||||
```docker
|
||||
migrations:
|
||||
container_name: dbmigrations
|
||||
build:
|
||||
context: .
|
||||
dockerfile: MyAspProjectName/Migrations.Dockerfile
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
depends_on:
|
||||
- db
|
||||
```
|
||||
|
||||
The Dockerfile itself is where the magic happens, building the web app, installing the global dotnet-ef tools needed and then running the migrations.
|
||||
|
||||
```docker
|
||||
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
|
||||
|
||||
WORKDIR /src
|
||||
COPY ["MyAspProjectName/MyAspProjectName.csproj", "MyAspProjectName/"]
|
||||
COPY ["MyAspProjectName/Setup.sh", "MyAspProjectName/"]
|
||||
|
||||
ENV PATH="${PATH}:/root/.dotnet/tools"
|
||||
RUN dotnet tool install --global dotnet-ef
|
||||
|
||||
RUN dotnet restore "MyAspProjectName/MyAspProjectName.csproj" --disable-parallel
|
||||
COPY . .
|
||||
WORKDIR "/src/MyAspProjectName/."
|
||||
|
||||
RUN /root/.dotnet/tools/dotnet-ef migrations add InitialMigrations
|
||||
|
||||
RUN chmod +x ./Setup.sh
|
||||
CMD /bin/bash ./Setup.**sh**
|
||||
```
|
||||
|
||||
Personally, I don't use this unless I have to, for the reasons previously stated, but it's worth sharing in case it's useful to anyone else.
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
There's two key parts to add to the `docker-compose.yml` file for this one. The service itself, with the port to expose and an `env` file to store sensitive information, and the volume mapped to a directory within the container.
|
||||
|
||||
```docker
|
||||
services:
|
||||
db:
|
||||
container_name: myappdb
|
||||
image: "postgres"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env_file:
|
||||
- database.env # configure postgres
|
||||
volumes:
|
||||
- database-data:/var/lib/postgresql/data/ # persist data even if container shuts down
|
||||
|
||||
volumes:
|
||||
database-data: # named volumes can be managed easier using docker-compose
|
||||
```
|
||||
|
||||
That `env` file is remarkably simple.
|
||||
|
||||
```text
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=passwordgoeshere
|
||||
POSTGRES_DB=yourdbname
|
||||
```
|
||||
|
||||
## SMTP Server
|
||||
|
||||
The service for the SMTP server is super simple.
|
||||
|
||||
```docker
|
||||
mail:
|
||||
image: bytemark/smtp
|
||||
```
|
||||
|
||||
## S3 Backups for PostgreSQL
|
||||
|
||||
The `docker-compose.yml` for this is as below, which sets a daily schedule and connection details for both the Postgres database and S3. Note that the Postgres host uses the internal Docker hostname for the database container.
|
||||
|
||||
```docker
|
||||
pgbackups3:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: postgres-backup-s3/Dockerfile
|
||||
links:
|
||||
- db
|
||||
environment:
|
||||
SCHEDULE: '@daily'
|
||||
S3_REGION: eu-west-2
|
||||
S3_ACCESS_KEY_ID: keygoeshere
|
||||
S3_SECRET_ACCESS_KEY: secretkeygoeshere
|
||||
S3_BUCKET: yourapp-backups
|
||||
S3_PREFIX: backup
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_DATABASE: yourdbname
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: passwordgoeshere
|
||||
POSTGRES_EXTRA_OPTS: '--schema=public --blobs'
|
||||
```
|
||||
|
||||
I won't go into how this application works here, that'll be in another post.
|
||||
|
||||
## S3 Restores for PostgreSQL
|
||||
|
||||
The `docker-compose.yml` for this is as below, which sets connection details for both the Postgres database and S3. Note that the Postgres host uses the internal Docker hostname for the database container.
|
||||
|
||||
This container should be run at setup time then stopped and commented out of the `docker-compose.yml` file or removed entirely, to prevent it from being accidentally run and restoring unintentionally, overwriting the database (notice the drop public option - this will wipe everything in the database before restoring).
|
||||
|
||||
```docker
|
||||
pgrestores3:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: postgres-restore-s3/Dockerfile
|
||||
links:
|
||||
- db
|
||||
environment:
|
||||
S3_ACCESS_KEY_ID: keygoeshere
|
||||
S3_SECRET_ACCESS_KEY: secretkeygoeshere
|
||||
S3_BUCKET: yourapp-backups
|
||||
S3_PREFIX: backup
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_DATABASE: yourdbname
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: passwordgoeshere
|
||||
DROP_PUBLIC: 'yes'
|
||||
```
|
||||
|
||||
I won't go into how this application works here, that'll be in another post.
|
||||
|
||||
## Putting it all together
|
||||
|
||||
The complete `docker-compose.yml` file, after the initial `docker-compose up` command has been run, looks like this. Note that migrations is commented out (we don't need it as we have the S3 backup and restore) and that the S3 restore is commented out to prevent accidental restores.
|
||||
|
||||
```docker
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
aspprojectname:
|
||||
container_name: myaspprojectname
|
||||
ports:
|
||||
- "80:80"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: MyAspProjectName/Dockerfile
|
||||
volumes:
|
||||
- ./MyAspProjectName/logs:/app/logs
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
depends_on:
|
||||
- db
|
||||
# - migrations
|
||||
# migrations:
|
||||
# container_name: dbmigrations
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: MyAspProjectName/Migrations.Dockerfile
|
||||
# environment:
|
||||
# - ASPNETCORE_ENVIRONMENT=Production
|
||||
# depends_on:
|
||||
# - db
|
||||
db:
|
||||
container_name: myappdb
|
||||
image: "postgres"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
env_file:
|
||||
- database.env # configure postgres
|
||||
volumes:
|
||||
- database-data:/var/lib/postgresql/data/ # persist data even if container shuts down
|
||||
mail:
|
||||
image: bytemark/smtp
|
||||
pgbackups3:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: postgres-backup-s3/Dockerfile
|
||||
links:
|
||||
- db
|
||||
environment:
|
||||
SCHEDULE: '@daily'
|
||||
S3_REGION: eu-west-2
|
||||
S3_ACCESS_KEY_ID: keygoeshere
|
||||
S3_SECRET_ACCESS_KEY: secretkeygoeshere
|
||||
S3_BUCKET: yourapp-backups
|
||||
S3_PREFIX: backup
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_DATABASE: yourdbname
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: passwordgoeshere
|
||||
POSTGRES_EXTRA_OPTS: '--schema=public --blobs'
|
||||
# pgrestores3:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: postgres-restore-s3/Dockerfile
|
||||
# links:
|
||||
# - db
|
||||
# environment:
|
||||
# S3_ACCESS_KEY_ID: keygoeshere
|
||||
# S3_SECRET_ACCESS_KEY: secretkeygoeshere
|
||||
# S3_BUCKET: yourapp-backups
|
||||
# S3_PREFIX: backup
|
||||
# POSTGRES_HOST: db
|
||||
# POSTGRES_DATABASE: yourdbname
|
||||
# POSTGRES_USER: postgres
|
||||
# POSTGRES_PASSWORD: passwordgoeshere
|
||||
# DROP_PUBLIC: 'yes'
|
||||
|
||||
volumes:
|
||||
database-data: # named volumes can be managed easier using docker-compose
|
||||
```
|
||||
@@ -0,0 +1,313 @@
|
||||
---
|
||||
tags:
|
||||
- deploying
|
||||
- docker
|
||||
---
|
||||
|
||||
# Docker Compose for S3 Backup and Restore of PostgreSQL
|
||||
|
||||
_2021-07-12_
|
||||
|
||||
In this post, I will cover how you can set up two Docker containers for backing up and restoring Postgres databases using S3 in AWS.
|
||||
|
||||
The complete `docker-compose.yml` file looks like this. Note that it's set to link these containers to a database container also configured within the same compose file - this could be modified to connect to an external database if desired by removing the `links` and changing the `POSTGRES_HOST` environment variables.
|
||||
|
||||
```docker
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
pgbackups3:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: postgres-backup-s3/Dockerfile
|
||||
links:
|
||||
- db
|
||||
environment:
|
||||
SCHEDULE: '@daily'
|
||||
S3_REGION: eu-west-2
|
||||
S3_ACCESS_KEY_ID: keygoeshere
|
||||
S3_SECRET_ACCESS_KEY: secretkeygoeshere
|
||||
S3_BUCKET: yourapp-backups
|
||||
S3_PREFIX: backup
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_DATABASE: yourdbname
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: passwordgoeshere
|
||||
POSTGRES_EXTRA_OPTS: '--schema=public --blobs'
|
||||
pgrestores3:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: postgres-restore-s3/Dockerfile
|
||||
links:
|
||||
- db
|
||||
environment:
|
||||
S3_ACCESS_KEY_ID: keygoeshere
|
||||
S3_SECRET_ACCESS_KEY: secretkeygoeshere
|
||||
S3_BUCKET: yourapp-backups
|
||||
S3_PREFIX: backup
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_DATABASE: yourdbname
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: passwordgoeshere
|
||||
DROP_PUBLIC: 'yes'
|
||||
```
|
||||
|
||||
## Backing up PostgreSQL to S3
|
||||
|
||||
The Dockerfile used looks like this:
|
||||
|
||||
```docker
|
||||
FROM alpine:3.13
|
||||
|
||||
RUN apk update \
|
||||
&& apk add coreutils \
|
||||
&& apk add postgresql-client \
|
||||
&& apk add python3 py3-pip && pip3 install --upgrade pip && pip3 install awscli \
|
||||
&& apk add openssl \
|
||||
&& apk add curl \
|
||||
&& curl -L --insecure https://github.com/odise/go-cron/releases/download/v0.0.6/go-cron-linux.gz | zcat > /usr/local/bin/go-cron && chmod u+x /usr/local/bin/go-cron \
|
||||
&& apk del curl \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
ENV POSTGRES_DATABASE **None**
|
||||
ENV POSTGRES_HOST **None**
|
||||
ENV POSTGRES_PORT 5432
|
||||
ENV POSTGRES_USER **None**
|
||||
ENV POSTGRES_PASSWORD **None**
|
||||
ENV POSTGRES_EXTRA_OPTS ''
|
||||
ENV S3_ACCESS_KEY_ID **None**
|
||||
ENV S3_SECRET_ACCESS_KEY **None**
|
||||
ENV S3_BUCKET **None**
|
||||
ENV S3_REGION us-west-1
|
||||
ENV S3_PATH 'backup'
|
||||
ENV S3_ENDPOINT **None**
|
||||
ENV S3_S3V4 no
|
||||
ENV SCHEDULE **None**
|
||||
|
||||
COPY ["postgres-backup-s3/run.sh", "run.sh"]
|
||||
COPY ["postgres-backup-s3/backup.sh", "backup.sh"]
|
||||
|
||||
CMD ["sh", "run.sh"]
|
||||
```
|
||||
|
||||
This is a combination of parts from [here](https://github.com/itbm/postgresql-backup-s3/blob/master/Dockerfile) and the original [here](https://github.com/schickling/dockerfiles/tree/master/postgres-backup-s3) with some further customisation by me.
|
||||
|
||||
It essentially sets up a Linux environment to be able to connect to both S3 and Postgres, and to be able to run two script files.
|
||||
|
||||
### `run.sh`
|
||||
|
||||
This one handles a bit of aws configuration for S3 connections, as well as setting up the behaviour for scheduling or just immediately running the container. I like to set it to run daily so I can set it and forget it.
|
||||
|
||||
```bash
|
||||
#! /bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
if [ "${S3_S3V4}" = "yes" ]; then
|
||||
aws configure set default.s3.signature_version s3v4
|
||||
fi
|
||||
|
||||
if [ "${SCHEDULE}" = "**None**" ]; then
|
||||
sh backup.sh
|
||||
else
|
||||
exec go-cron "$SCHEDULE" /bin/sh backup.sh
|
||||
fi
|
||||
```
|
||||
|
||||
### `backup.sh`
|
||||
|
||||
This one handles checking all the requisite connection details are present, then makes a compressed dump file of the PostgreSQL database, and uploads it to S3.
|
||||
|
||||
```bash
|
||||
#! /bin/sh
|
||||
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if [ "${S3_ACCESS_KEY_ID}" = "**None**" ]; then
|
||||
echo "You need to set the S3_ACCESS_KEY_ID environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${S3_SECRET_ACCESS_KEY}" = "**None**" ]; then
|
||||
echo "You need to set the S3_SECRET_ACCESS_KEY environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${S3_BUCKET}" = "**None**" ]; then
|
||||
echo "You need to set the S3_BUCKET environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${POSTGRES_DATABASE}" = "**None**" ]; then
|
||||
echo "You need to set the POSTGRES_DATABASE environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${POSTGRES_HOST}" = "**None**" ]; then
|
||||
if [ -n "${POSTGRES_PORT_5432_TCP_ADDR}" ]; then
|
||||
POSTGRES_HOST=$POSTGRES_PORT_5432_TCP_ADDR
|
||||
POSTGRES_PORT=$POSTGRES_PORT_5432_TCP_PORT
|
||||
else
|
||||
echo "You need to set the POSTGRES_HOST environment variable."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${POSTGRES_USER}" = "**None**" ]; then
|
||||
echo "You need to set the POSTGRES_USER environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${POSTGRES_PASSWORD}" = "**None**" ]; then
|
||||
echo "You need to set the POSTGRES_PASSWORD environment variable or link to a container named POSTGRES."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${S3_ENDPOINT}" == "**None**" ]; then
|
||||
AWS_ARGS=""
|
||||
else
|
||||
AWS_ARGS="--endpoint-url ${S3_ENDPOINT}"
|
||||
fi
|
||||
|
||||
# env vars needed for aws tools
|
||||
export AWS_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
|
||||
export AWS_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
|
||||
export AWS_DEFAULT_REGION=$S3_REGION
|
||||
|
||||
export PGPASSWORD=$POSTGRES_PASSWORD
|
||||
POSTGRES_HOST_OPTS="-h $POSTGRES_HOST -p $POSTGRES_PORT -U $POSTGRES_USER $POSTGRES_EXTRA_OPTS"
|
||||
|
||||
echo "Creating dump of ${POSTGRES_DATABASE} database from ${POSTGRES_HOST}..."
|
||||
|
||||
pg_dump $POSTGRES_HOST_OPTS $POSTGRES_DATABASE | gzip > dump.sql.gz
|
||||
|
||||
echo "Uploading dump to $S3_BUCKET"
|
||||
|
||||
cat dump.sql.gz | aws $AWS_ARGS s3 cp - s3://$S3_BUCKET/$S3_PREFIX/${POSTGRES_DATABASE}_$(date +"%Y-%m-%dT%H:%M:%SZ").sql.gz || exit 2
|
||||
|
||||
echo "SQL backup uploaded successfully"
|
||||
|
||||
```
|
||||
|
||||
## Restoring PostgreSQL from S3
|
||||
|
||||
The Dockerfile used looks like this:
|
||||
|
||||
```docker
|
||||
FROM alpine:3.13
|
||||
|
||||
RUN apk update \
|
||||
&& apk add coreutils \
|
||||
&& apk add postgresql-client \
|
||||
&& apk add python3 py3-pip && pip3 install --upgrade pip && pip3 install awscli \
|
||||
&& apk add openssl \
|
||||
&& apk add curl \
|
||||
&& curl -L --insecure https://github.com/odise/go-cron/releases/download/v0.0.6/go-cron-linux.gz | zcat > /usr/local/bin/go-cron && chmod u+x /usr/local/bin/go-cron \
|
||||
&& apk del curl \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
ENV POSTGRES_DATABASE **None**
|
||||
ENV POSTGRES_HOST **None**
|
||||
ENV POSTGRES_PORT 5432
|
||||
ENV POSTGRES_USER **None**
|
||||
ENV POSTGRES_PASSWORD **None**
|
||||
ENV S3_ACCESS_KEY_ID **None**
|
||||
ENV S3_SECRET_ACCESS_KEY **None**
|
||||
ENV S3_BUCKET **None**
|
||||
ENV S3_REGION us-west-1
|
||||
ENV S3_PATH 'backup'
|
||||
ENV DROP_PUBLIC 'no'
|
||||
|
||||
COPY ["postgres-restore-s3/restore.sh", "restore.sh"]
|
||||
|
||||
CMD ["sh", "restore.sh"]
|
||||
|
||||
```
|
||||
|
||||
This is once again a combination of parts from [here](https://github.com/itbm/postgresql-backup-s3/blob/master/Dockerfile) and the original [here](https://github.com/schickling/dockerfiles/tree/master/postgres-backup-s3) with some further customisation by me.
|
||||
|
||||
It essentially sets up a Linux environment to be able to connect to both S3 and Postgres, and to be able to run one script file.
|
||||
|
||||
### `restore.sh`
|
||||
|
||||
This one handles checking all the requisite connection details are present, then retrieves the most recent compressed dump file of the PostgreSQL database from S3, drops everything in the public space in PostgreSQL, and restores the backup. Crucially it then deletes the downloaded backup dump file to enable running the same container again for future restores without issue.
|
||||
|
||||
```bash
|
||||
#! /bin/sh
|
||||
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
if [ "${S3_ACCESS_KEY_ID}" = "**None**" ]; then
|
||||
echo "You need to set the S3_ACCESS_KEY_ID environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${S3_SECRET_ACCESS_KEY}" = "**None**" ]; then
|
||||
echo "You need to set the S3_SECRET_ACCESS_KEY environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${S3_BUCKET}" = "**None**" ]; then
|
||||
echo "You need to set the S3_BUCKET environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${POSTGRES_DATABASE}" = "**None**" ]; then
|
||||
echo "You need to set the POSTGRES_DATABASE environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${POSTGRES_HOST}" = "**None**" ]; then
|
||||
if [ -n "${POSTGRES_PORT_5432_TCP_ADDR}" ]; then
|
||||
POSTGRES_HOST=$POSTGRES_PORT_5432_TCP_ADDR
|
||||
POSTGRES_PORT=$POSTGRES_PORT_5432_TCP_PORT
|
||||
else
|
||||
echo "You need to set the POSTGRES_HOST environment variable."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${POSTGRES_USER}" = "**None**" ]; then
|
||||
echo "You need to set the POSTGRES_USER environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${POSTGRES_PASSWORD}" = "**None**" ]; then
|
||||
echo "You need to set the POSTGRES_PASSWORD environment variable or link to a container named POSTGRES."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# env vars needed for aws tools
|
||||
export AWS_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
|
||||
export AWS_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
|
||||
export AWS_DEFAULT_REGION=$S3_REGION
|
||||
|
||||
export PGPASSWORD=$POSTGRES_PASSWORD
|
||||
POSTGRES_HOST_OPTS="-h $POSTGRES_HOST -p $POSTGRES_PORT -U $POSTGRES_USER"
|
||||
|
||||
echo "Finding latest backup"
|
||||
|
||||
LATEST_BACKUP=$(aws s3 ls s3://$S3_BUCKET/$S3_PREFIX/ | sort | tail -n 1 | awk '{ print $4 }')
|
||||
|
||||
echo "Fetching ${LATEST_BACKUP} from S3"
|
||||
|
||||
aws s3 cp s3://$S3_BUCKET/$S3_PREFIX/${LATEST_BACKUP} dump.sql.gz
|
||||
gzip -d dump.sql.gz
|
||||
|
||||
if [ "${DROP_PUBLIC}" == "yes" ]; then
|
||||
echo "Recreating the public schema"
|
||||
psql $POSTGRES_HOST_OPTS -d $POSTGRES_DATABASE -c "drop schema public cascade; create schema public;"
|
||||
fi
|
||||
|
||||
echo "Restoring ${LATEST_BACKUP}"
|
||||
|
||||
psql $POSTGRES_HOST_OPTS -d $POSTGRES_DATABASE < dump.sql
|
||||
|
||||
echo "Restore complete"
|
||||
|
||||
rm -f ./dump.sql
|
||||
|
||||
echo "Deleted dump files"
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
tags:
|
||||
- deploying
|
||||
- docker
|
||||
---
|
||||
|
||||
# Docker Compose for Elasticsearch and Kibana 7.9
|
||||
|
||||
_2021-07-13_
|
||||
|
||||
In this post, I will cover how you can set up Elasticsearch and Kibana with a single `docker-compose.yml` file.
|
||||
|
||||
## Set up and install ELK stack on Ubuntu server
|
||||
|
||||
You must run `sudo sysctl -w vm.max_map_count=262144` to get Elasticsearch to work. To make this permanent, run `sudo nano /etc/sysctl.conf` and add `vm.max_map_count=262144` to the end of the file on a new line, then save and exit.
|
||||
|
||||
Go to the directory with the files in. Make sure you've used SFTP to put the `docker-compose.yml` file in there first. For example you may need to use this command `cd /home/administrator/docker/elk-stack`
|
||||
|
||||
Install it all with `docker-compose up -d` and you're finished! It really is that simple.
|
||||
|
||||
The compose file is as below.
|
||||
|
||||
```docker
|
||||
version: '2.2'
|
||||
|
||||
services:
|
||||
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.9.3
|
||||
container_name: elasticsearch
|
||||
environment:
|
||||
- node.name=elasticsearch
|
||||
- discovery.seed_hosts=elasticsearch
|
||||
- cluster.initial_master_nodes=elasticsearch
|
||||
- cluster.name=docker-cluster
|
||||
- bootstrap.memory_lock=true
|
||||
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
nofile:
|
||||
soft: 65536
|
||||
hard: 65536
|
||||
volumes:
|
||||
- esdata1:/usr/share/elasticsearch/data
|
||||
ports:
|
||||
- 9200:9200
|
||||
|
||||
kibana:
|
||||
image: docker.elastic.co/kibana/kibana:7.9.3
|
||||
container_name: kibana
|
||||
environment:
|
||||
ELASTICSEARCH_URL: "http://elasticsearch:9200"
|
||||
ELASTICSEARCH_SHARDTIMEOUT: 300000
|
||||
ports:
|
||||
- 5601:5601
|
||||
depends_on:
|
||||
- elasticsearch
|
||||
|
||||
volumes:
|
||||
esdata1:
|
||||
driver: local
|
||||
```
|
||||
@@ -0,0 +1,186 @@
|
||||
---
|
||||
tags:
|
||||
- dev teams
|
||||
- git
|
||||
---
|
||||
|
||||
# The way I recommend using git in a dev team
|
||||
|
||||
_2022-03-11_
|
||||
|
||||
Looking back at my old git commits reveals an entertaining, if slightly embarassing mess.
|
||||
|
||||
```
|
||||
$ git log --oneline -10 --author jcreek --before "Wed Nov 5 2014"
|
||||
8b6d283 Updated instructions
|
||||
6b271ba Undid that last change
|
||||
8c4f48a MySQL -> MySQLi changes begun
|
||||
c07ae83 Fixed that bug
|
||||
e5ab087 Some small cosmetic changes and failed fix of bug
|
||||
4467af9 Updated to-do list
|
||||
3578065 Cleanup and to-do list
|
||||
ddefa3a Added admin tools, provided link to admin page
|
||||
8a2f8b6 Further admin tools
|
||||
63cc797 Unimportant changes
|
||||
```
|
||||
|
||||
The contrast with more recent commits is staggering.
|
||||
|
||||
```
|
||||
$ git log --oneline -13 --author jcreek --before "Wed Sep 1 2021"
|
||||
c05c2c5 build(CAN-186): Add missing package information to csproj
|
||||
06aca19 docs(CAN-190): Add documentation to StringHelper
|
||||
4600ebf (tag: 1.0.1) feat(CAN-182): Add filename validity checks to SftpRepository
|
||||
d80cc82 (tag: 1.0.0) chore(CAN-173): Update readme and licence for NuGet
|
||||
615994a refactor(CAN-215): Rename to Creek.FileRepository
|
||||
0fe9924 feat(CAN-171): Add all repositories to factory
|
||||
b22d2f7 feat(CAN-170): Add base implementations of all repositories
|
||||
952a9a9 refactor(CAN-211): Rename FileName to Filename in interface
|
||||
62665fe test(CAN-169): Add SftpRepositoryShould tests
|
||||
ebccd4a feat(CAN-168): Add GenerateStreamFromString helper method for testing
|
||||
82fd258 fix(CAN-135): Initialise content with a new stream so it can be written to
|
||||
4232dd9 fix(CAN-162): Add missing throw to ensure exceptions get passed up the stack
|
||||
26a953d feat(CAN-167): Add initialiser for SftpRepository to use an IConfiguration
|
||||
```
|
||||
|
||||
My recent commits follow a clear convention, are concise and clear at a glance. Without having to do any code comparison you can quickly assess what was done in any given commit. A commit message shows whether a developer is a good collaborator, with good ones reducing the need to re-establish the context of a commit as much as possible.
|
||||
|
||||
I never used to care about my commit messages because it was only me reading them, and because I rarely, if ever, used tools like `git log`, `git blame`, `revert` or `rebase`. Nowadays I use git blame all day, every day, thanks to VSCode git extensions putting the blame on every line of code, and it's incredibly useful as part of a team. If I'm looking at code I always know who worked on it, when they worked on it, and the context of why they did what they did, which makes the codebases infinitely more maintainable.
|
||||
|
||||
There is a single git commit that I think should be required reading for all developers. It is from a developer by the name of [Dan Carley](https://twitter.com/dancarley) working on GOV.UK, and it has the rather unassuming name of ["Convert template to US-ASCII to fix error"](https://github.com/alphagov/govuk-puppet/commit/63b36f93bf75a848e2125008aa1e880c5861cf46).
|
||||
|
||||

|
||||
|
||||
I found it through [this blog](https://dhwthompson.com/2019/my-favourite-git-commit) and I'd highly recommend reading through it, but essentially the thing I like most about this commit is it explains why the change was made, not just what the change was.
|
||||
|
||||
As with most developers, I have my own strong opinions founded in habit for what dev teams should do in terms of Git usage. In the rest of this doc I will outline that - let me know if you have any suggestions for improvements, or if it helped you.
|
||||
|
||||
## Branching, Pull Requests & Merging
|
||||
|
||||
For every ticket, there should be a separate git branch. This code will not be merged into the main development branch until it has been through a pull request and been reviewed.
|
||||
|
||||
If it's a big feature ticket, a feature branch should be created for that ticket, and it should be split into sub-tasks, each of which have their own branch and get merged into the feature branch after PRs are approved. Once all the sub-tasks have been PRed and their branches have been merged into the feature branch, the feature branch can be PRed and merge into the development branch.
|
||||
|
||||
Unless you're working on an open source project, the merge strategy for PRs should not involve squashing. Merge commits are fine, and they leave all individual commits available for comparison, reverting and cherry-picking.
|
||||
|
||||
Not using squashed commits makes having good quality commit messages even more important. Committing often and messily is a bad habit. Instead break down tasks into suitably small sub-tasks, so that they can be worked on in isolation without taking long enough to warrant committing unfinished code.
|
||||
|
||||
A key part of the PR process is reviewing code, not only for bugs, but for quality. The git commit messages are a critical part of this. No developer is perfect, so helping one another in PRs is key to having an excellent git history. If a PR comes in with bad commit messages, it should be rejected and redone with good commit messages.
|
||||
|
||||
More detail about code reviews and PRs can be found in a separate doc.
|
||||
|
||||
## Git Messages
|
||||
|
||||
Git commit messges subjects should always follow this pattern:
|
||||
|
||||
`<type>(scope): <description>`
|
||||
|
||||
Types are:
|
||||
|
||||
- `build`: build-related changes
|
||||
- `ci`: continuous integration-related changes
|
||||
- `chore`: updating grunt tasks etc; no production code change
|
||||
- `docs`: changes to the documentation
|
||||
- `feat`: new feature for the user, not a new feature for build script
|
||||
- `fix`: bug fix for the user, not a fix to a build script
|
||||
- `perf`: a code change that improves performance
|
||||
- `refactor`: refactoring production code, eg. renaming a variable
|
||||
- `revert`: reverting things
|
||||
- `style`: formatting, missing semi colons, etc; no production code change
|
||||
- `test`: adding missing tests, refactoring tests; no production code change
|
||||
|
||||
The scope, generally considered optional, really should be mandatory. Typically this would be a ticket number, enabling devs both now and in the future to easily find commits associated with tickets and tickets associated with commits. Full details of what was being worked on can often be found in a ticket if they are not in the commit message, and in the event a feature's code needs to be examined or reverted. If you use a ticket management system like Jira having the ticket number in your commit message enables the management system to automatically detect it and it can be embedded directly in a card that you are currently working on, and even associated with Pull Requests and Epics.
|
||||
|
||||
The description is also incredibly important. A properly formed description should always be able to complete the following sentence:
|
||||
|
||||
`If applied, this commit will <description>`
|
||||
|
||||
It contains a succinct description of the change:
|
||||
|
||||
- Use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
- No full stop/period at the end
|
||||
|
||||
The body of a commit message should also use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.
|
||||
|
||||
The 7 rules of a great commit message:
|
||||
|
||||
- Separate subject from the body with a blank line
|
||||
- Limit the subject line to 50 characters
|
||||
- Summary in the present tense. Not capitalized.
|
||||
- Do not end the subject line with a period
|
||||
- Use the imperative mood in the subject line
|
||||
- Wrap the body at 72 characters
|
||||
- Use the body to explain what and why vs. how
|
||||
|
||||
By incorporating the above convention, you can quickly determine what changes have been made and what the commit message for the changes would look like. With that format, you as a developer can immediately know (or at least guess), what’s changed in the specific commit. If there’s an issue with a new merge to the master branch, you can also quickly scan the git history to figure out what changes possibly can cause the problem without the need to look at the differences.
|
||||
|
||||
Some very good reasons to use this convention are:
|
||||
|
||||
- Automatically generating CHANGELOGs.
|
||||
- Automatically determining a semantic version bump (based on the types of commits landed).
|
||||
- Communicating the nature of changes to teammates, the public, and other stakeholders.
|
||||
- Triggering build and publish processes.
|
||||
- Making it easier for people to contribute to your projects by allowing them to explore a more structured commit history.
|
||||
|
||||
## Automating Git Commit Message Formatting
|
||||
|
||||
You can tell git to set up your commit messages with a set structure. This is done by running the command `git config --global commit.template ~/.gitmessage` or by manually setting `commit.template` in `~/.gitconfig` using your text editor of choice:
|
||||
|
||||
```
|
||||
[commit]
|
||||
template = ~/.gitmessage
|
||||
```
|
||||
|
||||
Next, create `~/.gitmessage` with your new default template.
|
||||
|
||||
For your convenience, the following git commit template can be used:
|
||||
|
||||
```
|
||||
# A properly formed Git commit subject line should always be able to complete
|
||||
# the following sentence:
|
||||
# * If applied, this commit will <description>
|
||||
#
|
||||
# ** Example:
|
||||
# <type>(scope): <description>
|
||||
#
|
||||
# [optional body]
|
||||
#
|
||||
# [optional footer]
|
||||
|
||||
# ** Type
|
||||
# Must be one of the following:
|
||||
# * build: build-related changes
|
||||
# * ci: continuous integration-related changes
|
||||
# * chore: updating grunt tasks etc; no production code change
|
||||
# * docs: changes to the documentation
|
||||
# * feat: new feature for the user, not a new feature for build script
|
||||
# * fix: bug fix for the user, not a fix to a build script
|
||||
# * perf: a code change that improves performance
|
||||
# * refactor: refactoring production code, eg. renaming a variable
|
||||
# * revert: reverting things
|
||||
# * style: formatting, missing semi colons, etc; no production code change
|
||||
# * test: adding missing tests, refactoring tests; no production code change
|
||||
|
||||
# ** Subject
|
||||
# The subject contains a succint description of the change:
|
||||
# * Use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
# * No full stop/period at the end
|
||||
|
||||
# ** Scope
|
||||
# A scope should be provided to a commit’s type if possible, to provide additional contextual information
|
||||
# and is contained within parenthesis, e.g. feat(JIRA-1234): Add NewGitDocumentation endpoint
|
||||
|
||||
# ** Body
|
||||
# Just as in the subject, use the imperative, present tense: "change" not "changed" nor "changes".
|
||||
# The body should include the motivation for the change and contrast this with previous behavior.
|
||||
|
||||
# ** Rules
|
||||
# The 7 rules of a great commit message
|
||||
# 1. Separate subject from body with a blank line
|
||||
# 2. Limit the subject line to 50 characters
|
||||
# 3. Summary in present tense. Not capitalized
|
||||
# 4. Do not end the subject line with a period
|
||||
# 5. Use the imperative mood in the subject line
|
||||
# 6. Wrap the body at 72 characters
|
||||
# 7. Use the body to explain what and why vs. how
|
||||
```
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
tags:
|
||||
- dev teams
|
||||
- git
|
||||
- readme
|
||||
---
|
||||
|
||||
# How to write an excellent readme file
|
||||
|
||||
_2022-03-16_
|
||||
|
||||
All git projects need a readme file, to explain what it is, why it exists, how it works and how to use it. A good starting point with guidance can be found below.
|
||||
|
||||
```md
|
||||
# Project title
|
||||
|
||||
A little info about the application and an overview that explains **what** the application is for.
|
||||
|
||||
## Motivation
|
||||
|
||||
A short description of the motivation behind the creation and maintenance of the application. This should explain **why** the application exists.
|
||||
|
||||
## How the dev/staging/prod versions are built
|
||||
|
||||
This should be a brief explanation of how the application gets to where it's meant to be used. Typically this will involve linking to build pipelines in Jenkins/Azure/etc.
|
||||
|
||||
## Code style
|
||||
|
||||
Usually this would just link to the documentation for your established code style. In the event that the application uses additional or different styles or linting tooling this can be detailed here.
|
||||
|
||||
## Screenshots
|
||||
|
||||
Include some logos and demos screenshots to briefly give a rough idea of the application.
|
||||
|
||||
## Tech/framework used
|
||||
|
||||
Hopefully this will be identical across most of your organisation's applications, but the value of this section really becomes clear when an application does differ, and developers can see this at a glance here, without having to dig into the code or run the project locally to work it out.
|
||||
|
||||
## Features
|
||||
|
||||
What are the key features of this application? A few bullet points are ideal here.
|
||||
|
||||
## Getting it running locally for development
|
||||
|
||||
Provide a step by step series of examples and explanations about how to get a development environment running. If it is dependent on other local environmental factors, for example a specific database, this should also be covered here. This should be a single source of truth for getting this application running, without being dependent on any external sources of knowledge, even if that means there is some duplication across projects within this readme file.
|
||||
|
||||
## API Reference
|
||||
|
||||
If it's appropriate, a link should be included here to the API documentation, for example OpenAPI docs or a Swagger file.
|
||||
|
||||
## Tests
|
||||
|
||||
Describe and show how to run the tests with basic examples.
|
||||
|
||||
## How to use as a user
|
||||
|
||||
A brief guide on how to use the application AS A USER should be included here. A developer picking up the application should be able to find their way around it quickly as a user based on the guidance included here, to be able to sufficiently understand how most parts of the application are used.
|
||||
|
||||
## Anything else that seems useful
|
||||
|
||||
If there is anything else that seems useful to include in the readme file, this should be included here. Documentation of specific parts of the code SHOULD NOT live in this readme file, instead they should be in external documentation such as Confluence or in the code itself, whichever is more appropriate. References to such documentation could be included in this readme file for significant examples, but largely is not necessary.
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
tags:
|
||||
- home server
|
||||
- truenas scale
|
||||
- home assistant
|
||||
---
|
||||
|
||||
# Installing Home Assistant OS on Truenas Scale
|
||||
|
||||
_2022-03-08_
|
||||
|
||||
Mostly taken from https://www.truenas.com/community/threads/home-assistant-vm-on-scale.91058/post-666766 and cleaned up.
|
||||
|
||||
Make sure to use a location on your data pool as a working directory, don't use any system directory. I made a `hass` folder on my data pool `plex-media`:
|
||||
|
||||
`cd /mnt/plex-nas/plex-media/hass`
|
||||
|
||||
Use wget to get the ova file:
|
||||
|
||||
`wget https://github.com/home-assistant/operating-system/releases/download/6.6/haos_ova-6.6.ova`
|
||||
|
||||
Extract the ova file using tar:
|
||||
|
||||
`tar -xvf haos_ova-6.6.ova`
|
||||
|
||||
Convert the vmdk to a raw image file, I had to use the full working directory for the source:
|
||||
|
||||
`qemu-img convert -f vmdk -O raw /mnt/plex-nas/plex-media/hass/home-assistant.vmdk hassos.img`
|
||||
|
||||
Create a Zvol using the TrueNas Scale GUI - Be sure to make it large enough for dd to complete, I used 35 Gib.
|
||||
|
||||
Use dd to write the image file to your zvol
|
||||
|
||||
`dd if=hassos.img of=/dev/plex-nas/home-assistant`
|
||||
|
||||
Create a virtual machine using the gui and attach the zvol you just created as the hdd.
|
||||
|
||||
Minimum recommended assignments:
|
||||
|
||||
- 2GB RAM
|
||||
- 32GB Storage
|
||||
- 2vCPU
|
||||
|
||||
All these can be extended if your usage calls for more resources.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 433 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 611 KiB |
@@ -0,0 +1,67 @@
|
||||
<!-- ---
|
||||
hide:
|
||||
- navigation
|
||||
- toc
|
||||
- tags
|
||||
--- -->
|
||||
|
||||
# Welcome to the site
|
||||
|
||||
This is the place where I ramble about technology and coding. Sometimes Computer Science education too.
|
||||
|
||||
## About Me
|
||||
|
||||
### Highlights
|
||||
|
||||
- Full Stack Web Developer, with a bit of devops and old-school (literally) network management
|
||||
- Computer Science Educator
|
||||
- Teach First 2015 Ambassador
|
||||
- Writer
|
||||
- Photographer
|
||||
- Content Creator
|
||||
- Google Trusted Tester, plus once worked at Google for 2 weeks
|
||||
|
||||
### Blurb
|
||||
|
||||
I like to spend my time working on agile projects to solve problems in the education and ecommerce spheres, creating educational content, writing poetry, doing portraiture and event photography, playing competitive video games and playing piano and ukulele or singing in a choir.
|
||||
|
||||
I also enjoy recording audiobooks for LibriVox, making YouTube videos on a variety of themes and streaming on Twitch. You can find me using the links below.
|
||||
|
||||
### Technologies
|
||||
|
||||
I code in a variety of languages, and use a number of different tools. Here they are for your perusal.
|
||||
|
||||
- HTML5
|
||||
- CSS3
|
||||
- JavaScipt ES6
|
||||
- SCSS
|
||||
- Node.js
|
||||
- Vue.js
|
||||
- jQuery
|
||||
- C#
|
||||
- ASP.Net Core
|
||||
- SQL/MSSQL/PostgreSQL/MySQL
|
||||
- Wordpress (if I really have to)
|
||||
- MongoDB (if I really have to)
|
||||
- Python (a bit rusty these days but fine for making simple scripts)
|
||||
- Docker & Docker Compose
|
||||
- AWS & Azure
|
||||
- Proxmox & TrueNAS Scale
|
||||
- Windows/MacOS/Linux
|
||||
- Photoshop/Lightroom
|
||||
- Davinci Resolve/Premiere Pro
|
||||
|
||||
### Education
|
||||
|
||||
#### UCL Institute of Education
|
||||
|
||||
Postgraduate Certificate in Education
|
||||
|
||||
#### University of Exeter
|
||||
|
||||
English with Proficiency in French
|
||||
|
||||
### Awards
|
||||
|
||||
- 2011 Headgate Theatre Young Playwrights Award
|
||||
- 2013 Microsoft Imagine Cup World Citizenship Award (Ticklo)
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
layout: default
|
||||
title: Privacy Policy
|
||||
permalink: /privacy-policy/
|
||||
---
|
||||
|
||||
# {{page.title}}
|
||||
|
||||
## Paladins Bounty Marketplace Chrome Extension
|
||||
|
||||
This extension uses your bounty_id cookie (from the bounty marketplace) and makes a POST request to the official endpoint for retrieving the list of current offers on the market. This data is not stored anywhere, is just used within your current browser session, and is only sent to the official endpoint that the bounty marketplace itself uses.
|
||||
|
||||
The below privacy policy applies other than this cookie.
|
||||
|
||||
## What data do we collect?
|
||||
|
||||
None.
|
||||
|
||||
## How do we collect your data?
|
||||
|
||||
I don't.
|
||||
|
||||
## How will we use your data?
|
||||
|
||||
I won't.
|
||||
|
||||
## How do we store your data?
|
||||
|
||||
I don't.
|
||||
|
||||
## Marketing
|
||||
|
||||
I don't market anything to you.
|
||||
|
||||
## What are your data protection rights?
|
||||
|
||||
I would like to make sure you are fully aware of all of your data protection rights. Every user is entitled to the following:
|
||||
|
||||
* The right to access – You have the right to request Our Company for copies of your personal data. We may charge you a small fee for this service.
|
||||
|
||||
* The right to rectification – You have the right to request that Our Company correct any information you believe is inaccurate. You also have the right to request Our Company to complete the information you believe is incomplete.
|
||||
|
||||
* The right to erasure – You have the right to request that Our Company erase your personal data, under certain conditions.
|
||||
|
||||
* The right to restrict processing – You have the right to request that Our Company restrict the processing of your personal data, under certain conditions.
|
||||
|
||||
* The right to object to processing – You have the right to object to Our Company’s processing of your personal data, under certain conditions.
|
||||
|
||||
* The right to data portability – You have the right to request that Our Company transfer the data that we have collected to another organization, or directly to you, under certain conditions.
|
||||
|
||||
If you make a request, I have one month to respond to you. If you would like to exercise any of these rights, please contact me via Twitter.
|
||||
|
||||
## What are cookies?
|
||||
|
||||
Cookies are text files placed on your computer to collect standard Internet log information and visitor behavior information. When you visit our websites, we may collect information from you automatically through cookies or similar technology
|
||||
|
||||
For further information, visit allaboutcookies.org.
|
||||
|
||||
## How do we use cookies?
|
||||
|
||||
I don't.
|
||||
|
||||
## What types of cookies do we use?
|
||||
|
||||
None.
|
||||
|
||||
## How to manage your cookies
|
||||
|
||||
You can set your browser not to accept cookies, and the above website tells you how to remove cookies from your browser. However, in a few cases, some of our website features may not function as a result.
|
||||
|
||||
## Privacy policies of other websites
|
||||
|
||||
This website contains links to other websites. My privacy policy applies only to this website, so if you click on a link to another website, you should read their privacy policy.
|
||||
|
||||
## Changes to our privacy policy
|
||||
|
||||
I keep my privacy policy under regular review and places any updates on this web page. This privacy policy was last updated on 4th January 2022.
|
||||
|
||||
## How to contact us
|
||||
|
||||
If you have any questions about this privacy policy, the data I hold on you, or you would like to exercise one of your data protection rights, please do not hesitate to contact me via Twitter.
|
||||
|
||||
## How to contact the appropriate authorities
|
||||
|
||||
Should you wish to report a complaint or if you feel that I have not addressed your concern in a satisfactory manner, you may contact the Information Commissioner’s Office.
|
||||
@@ -0,0 +1,196 @@
|
||||
---
|
||||
tags:
|
||||
- web development
|
||||
- dotnet
|
||||
- asp
|
||||
- c#
|
||||
- vscode
|
||||
- devcontainer
|
||||
- remote-container
|
||||
- iac
|
||||
- postgres
|
||||
- smtp
|
||||
- dev environment
|
||||
---
|
||||
|
||||
# Setting up remote containers for IAC dev environments in dotnet 6 with postgres and SMTP
|
||||
|
||||
_2022-04-20_
|
||||
|
||||

|
||||
|
||||
In this post I will go through how to set up remote containers for a dotnet 6 API project using PostgreSQL and an SMTP server, so that any new devs picking up the project only have to clone down the git repo and open it in Visual Studio code to get started debugging it.
|
||||
|
||||
No dependencies need to be installed onto developer machines for each project they work on, other than:
|
||||
|
||||
- Docker
|
||||
- Visual Studio Code
|
||||
|
||||
Yes, that's all they need. They don't even need to install any extensions - the only one they need will be prompted the first time they open the project in vscode. Any extensions they need for the project will be loaded in a vscode server instance within the docker container, and then their local vscode will allow access to the vscode server as if they're using it natively, right down to providing terminal access within the container.
|
||||
|
||||
## How to set up the project
|
||||
|
||||
You will need to ensure that you've created a `.gitattributes` file in the root folder if you don't have one already, and specified `* text=auto eol=lf` to default all the line endings to LF, if you're on Windows and find that swapping between the host and the container changes the line endings in all your files. Alternatively, if you're already using Windows line endings, use `crlf` instead of `lf`.
|
||||
|
||||
In the root of the repo create a folder called `.devcontainer`. Within this you need to create three files:
|
||||
|
||||
1. `devcontainer.json`
|
||||
2. `docker-compose.yml`
|
||||
3. `Dockerfile`
|
||||
|
||||
### `devcontainer.json`
|
||||
|
||||
This is customised from the default Microsoft file. You can change the name, the vscode settings (although if you're working with newer dotnet projects I'd recommend leaving `omnisharp.useModernNet` set to `true`), add any extensions you want to use in vscode for this project (find the ID for an extension by installing it in your local vscode, then right clicking it and clicking `Copy Extension ID` - I've included some of my favourites for aspdotnet projects but the only one you *need* is `ms-dotnettools.csharp`), the post-create commands and the remote user.
|
||||
|
||||
There is an issue where Omnisharp, the underlying technology in the C# vscode plugin doesn't work properly unless `dotnet restore` is run at the point of setting up the dev container, so do not remove that command.
|
||||
|
||||
```json linenums="1"
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.231.6/containers/dotnet-postgres
|
||||
{
|
||||
"name": "Dotnet 6 API, Postgres & SMTP",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspace",
|
||||
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"omnisharp.useModernNet": true //Use the build of OmniSharp that runs on the .NET 6 SDK. This build requires that the .NET 6 SDK be installed and does not use Visual Studio MSBuild tools or Mono. It only supports newer SDK-style projects that are buildable with dotnet build. Unity projects and other Full Framework projects are not supported.
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"formulahendry.auto-rename-tag",
|
||||
"ms-dotnettools.csharp",
|
||||
"EditorConfig.EditorConfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"xabikos.JavaScriptSnippets",
|
||||
"PKief.material-icon-theme",
|
||||
"eg2.vscode-npm-script",
|
||||
"christian-kohler.path-intellisense",
|
||||
"esbenp.prettier-vscode",
|
||||
"gencer.html-slim-scss-css-class-completion"
|
||||
],
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [5000, 5001],
|
||||
|
||||
// [Optional] To reuse of your local HTTPS dev cert:
|
||||
//
|
||||
// 1. Export it locally using this command:
|
||||
// * Windows PowerShell:
|
||||
// dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
|
||||
// * macOS/Linux terminal:
|
||||
// dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
|
||||
//
|
||||
// 2. Uncomment these 'remoteEnv' lines:
|
||||
// "remoteEnv": {
|
||||
// "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere",
|
||||
// "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx",
|
||||
// },
|
||||
//
|
||||
// 3. Next, copy your certificate into the container:
|
||||
// 1. Start the container
|
||||
// 2. Drag ~/.aspnet/https/aspnetapp.pfx into the root of the file explorer
|
||||
// 3. Open a terminal in VS Code and run "mkdir -p /home/vscode/.aspnet/https && mv aspnetapp.pfx /home/vscode/.aspnet/https"
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "dotnet restore", // had to use otherwise omnisharp wouldn't work properly, so no intellisense in vscode
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode"
|
||||
}
|
||||
```
|
||||
|
||||
### `docker-compose.yml`
|
||||
|
||||
The docker compose file sets up the three components of this example project:
|
||||
|
||||
1. The dotnet 6 API
|
||||
2. The PostgreSQL database
|
||||
3. The SMTP server
|
||||
|
||||
Take note of the name of the SMTP server container as this will be needed later, as will the environment variables for the database. These should be changed from `postgres`.
|
||||
|
||||
```yml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
# Update 'VARIANT' to pick a version of .NET: 3.1, 5.0, 6.0
|
||||
VARIANT: "6.0"
|
||||
# Optional version of Node.js
|
||||
NODE_VERSION: "lts/*"
|
||||
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
|
||||
network_mode: service:db
|
||||
|
||||
# Uncomment the next line to use a non-root user for all processes.
|
||||
# user: vscode
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
db:
|
||||
image: postgres:14.1
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: postgres
|
||||
|
||||
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
mail:
|
||||
image: bytemark/smtp
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
|
||||
```
|
||||
|
||||
### `Dockerfile`
|
||||
|
||||
This comes directly from Microsoft, I've made no modifications to it.
|
||||
|
||||
```dockerfile
|
||||
# [Choice] .NET version: 6.0, 5.0, 3.1, 6.0-bullseye, 5.0-bullseye, 3.1-bullseye, 6.0-focal, 5.0-focal, 3.1-focal
|
||||
ARG VARIANT="6.0"
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT}
|
||||
|
||||
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
||||
ARG NODE_VERSION="none"
|
||||
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment this line to install global node packages.
|
||||
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
||||
```
|
||||
|
||||
### `appsettings.json` and `appsettings.Development.json`
|
||||
|
||||
To get the SMTP working, use these keys. If you're doing anything more fancy with `bytemark/smtp` than just the default functionality you may need to change the SMTP port.
|
||||
|
||||
```json linenums="1"
|
||||
"SmtpHost": "mail",
|
||||
"SmtpPort": 25,
|
||||
```
|
||||
|
||||
To connect to the Postgres database set your connection string to `"User ID=postgres;Password=postgres;Host=db;Port=5432;Database=postgres;Pooling=true;"`, substituting in your chosen username, password and database name.
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
tags:
|
||||
- web development
|
||||
- dotnet
|
||||
- c#
|
||||
- pattern matching
|
||||
---
|
||||
|
||||
# C# 8 Pattern Matching
|
||||
|
||||
_2022-02-15_
|
||||
|
||||
This serves as introductory documentation to the pattern matching features introduced in C# 8, which I think are particularly useful.
|
||||
|
||||
## Deconstructors & Positional Patterns
|
||||
|
||||
Adding a deconstructor to your class can be done as below. It must be named `Deconstruct` and be a `public void`. Any values we want deconstructed can be set as `out` parameters, then populated. Here all properties are being deconstructed, but a subset could be deconstructed if desired.
|
||||
|
||||
```csharp linenums="1"
|
||||
class Student
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Year { get; set; }
|
||||
public Teacher FormTutor { get; set; }
|
||||
|
||||
public void Deconstruct (out string name, out int year, out Teacher formTutor)
|
||||
{
|
||||
name = Name;
|
||||
year = Year;
|
||||
formTutor = FormTutor;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To use positional parameters, I'll also set up a second model.
|
||||
|
||||
```csharp linenums="1"
|
||||
class Teacher
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Subject { get; set; }
|
||||
|
||||
public void Deconstruct (out string name, out string subject)
|
||||
{
|
||||
name = Name;
|
||||
subject = Subject;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here is an example positional pattern. The discards (`_`) are used to 'match all'.
|
||||
|
||||
```csharp linenums="1"
|
||||
public static class YourClassHere
|
||||
{
|
||||
public static bool IsInYear9English(Student student)
|
||||
{
|
||||
return student is Student(
|
||||
_,
|
||||
9,
|
||||
Teacher (_, "English")
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
While an if statement would probably be easier to read and maintain in this example, positional patterns enable recursion to be used, which can be very useful.
|
||||
|
||||
## Property Patterns
|
||||
|
||||
I've updated the Student model to add a School property.
|
||||
|
||||
```csharp linenums="1"
|
||||
class Student
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Year { get; set; }
|
||||
public string School { get; set; }
|
||||
public Teacher FormTutor { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
I prefer using property patterns to positional patterns as they are much more readable. Here's an example method to check whether a student attends a particular school and has a form tutor who teaches Maths.
|
||||
|
||||
```csharp linenums="1"
|
||||
public static class YourClassHere
|
||||
{
|
||||
public static bool IsStudentInGreenAcademyWithMathsFormTutor(Student student)
|
||||
{
|
||||
return student is {
|
||||
School: "Green Academy",
|
||||
FormTutor: {
|
||||
Subject: "Maths"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This can be made more generic to accept an `object` rather than a `Student`, and check that object is a Student.
|
||||
|
||||
```csharp linenums="1"
|
||||
public static class YourClassHere
|
||||
{
|
||||
public static bool IsStudentInGreenAcademyWithMathsFormTutor(object obj)
|
||||
{
|
||||
return obj is Student student &&
|
||||
student is {
|
||||
School: "Green Academy",
|
||||
FormTutor: {
|
||||
Subject: "Maths"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Switch Expressions
|
||||
|
||||
These can be used in place of standard switch cases. I'm using a discard (`_`) for catching the default case for unmatched patterns.
|
||||
|
||||
```csharp linenums="1"
|
||||
public static class YourClassHere
|
||||
{
|
||||
public static string DisplayPersonInfo (object person)
|
||||
{
|
||||
string result = person switch
|
||||
{
|
||||
Student student => $"Student at {student.School} in Year {student.Year}",
|
||||
Teacher teacher => $"Teacher of {teacher.Subject}",
|
||||
_ => "Person is not a student or a teacher"
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The syntax can make it easier to read, and it can be a lot more powerful. You can also define recursive switch patterns.
|
||||
|
||||
```csharp linenums="1"
|
||||
public static class YourClassHere
|
||||
{
|
||||
public static string DisplayPersonInfo (object person)
|
||||
{
|
||||
string result = person switch
|
||||
{
|
||||
Student student => student switch
|
||||
{
|
||||
_ when student.Year < 10 and student.Year > 6 => "Student in Key Stage 3",
|
||||
_ when student.Year >= 10 and student.Year <= 13> => "Student in Key Stage 4",
|
||||
_ => $"Student in Year {student.Year}"
|
||||
},
|
||||
Teacher teacher => $"Teacher of {teacher.Subject}",
|
||||
_ => "Person is not a student or a teacher"
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tuple Patterns (with Switch Expressions)
|
||||
|
||||
You can also use Switch Expressions with Tuples to write even more useful code. For example, you could be creating a game with crafting, combining two items to make another.
|
||||
|
||||
```csharp linenums="1"
|
||||
public static class YourClassHere
|
||||
{
|
||||
public static CraftingMaterial GetCraftingMaterial (CraftingMaterial item1, CraftingMaterial item2)
|
||||
{
|
||||
return (item1, item2) switch
|
||||
{
|
||||
// Match the items in both positions
|
||||
(CraftingMaterial.MountainFlowers, CraftingMaterial.SoulGems) => CraftingMaterial.DwemerMetal,
|
||||
(CraftingMaterial.SoulGems, CraftingMaterial.MountainFlowers) => CraftingMaterial.DwemerMetal,
|
||||
|
||||
(CraftingMaterial.Ore, CraftingMaterial.DwemerMetal) => CraftingMaterial.Ingots,
|
||||
(CraftingMaterial.DwemerMetal, CraftingMaterial.Ore) => CraftingMaterial.Ingots,
|
||||
|
||||
// Handle both items being the same (discard for both, to match any)
|
||||
(_, _) when item1 == item2 => item1,
|
||||
|
||||
// Default case (with discard)
|
||||
_ => CraftingMaterial.Unknown
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is lovely and easy to read, as well as very powerful.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
layout: default
|
||||
title: Web Development
|
||||
nav_order: 3
|
||||
has_children: true
|
||||
---
|
||||
@@ -0,0 +1,346 @@
|
||||
---
|
||||
tags:
|
||||
- web development
|
||||
- dotnet
|
||||
- asp
|
||||
- c#
|
||||
- serilog
|
||||
- jsnlog
|
||||
---
|
||||
|
||||
# Adding JSNLog to ASP .Net 6 with Serilog
|
||||
|
||||
_2022-01-18_
|
||||
|
||||
In this post I will go through the steps required to get an optimal install of JSNLog in ASP dotnet 6 projects using Serilog. JSNLog is a fantastic tool for logging uncaught javascript exceptions to the backend's logging system, and can be used for a variety of other things. This config includes batching of messages to reduce calls made to the server, as well as buffering, where helpful debugging logs are only sent if there's a fatal log that's also being sent, to make it easier to diagnose bugs without unnecessary diagnostic logs appearing at all times.
|
||||
|
||||
First you need to install `JSNLog` and `Destructurama.JsonNet` via NuGet. After that, make the following changes to these files.
|
||||
|
||||
## _ViewImports.cshtml
|
||||
|
||||
Add `@addTagHelper "*, jsnlog"` to enable the tag helper.
|
||||
|
||||
## Startup.cs
|
||||
|
||||
```csharp linenums="1"
|
||||
using Destructurama;
|
||||
using JSNLog;
|
||||
|
||||
...
|
||||
public Startup(IConfiguration configuration, IWebHostEnvironment environment)
|
||||
{
|
||||
...
|
||||
Log.Logger = SerilogLogger.CreateSerilogLogger(serilogConfiguration).Destructure.JsonNetTypes().CreateLogger();
|
||||
...
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
public void Configure(
|
||||
...,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
...
|
||||
loggerFactory.AddSerilog();
|
||||
|
||||
JsnlogConfiguration jsnlogConfiguration = new JsnlogConfiguration
|
||||
{
|
||||
ajaxAppenders = new List<AjaxAppender> {
|
||||
new AjaxAppender {
|
||||
name = "appender1",
|
||||
storeInBufferLevel = "TRACE", // Log messages with severity smaller than TRACE are ignored
|
||||
level = "WARN", // Log messages with severity equal or greater than TRACE and lower than WARN are stored in the internal buffer, but not sent to the server
|
||||
// Log messages with severity equal or greater than WARN and lower than FATAL are sent to the server on their own
|
||||
sendWithBufferLevel = "FATAL", // Log messages with severity equal or greater than FATAL are sent to the server, along with all messages stored in the internal buffer
|
||||
bufferSize = 20, // Stores the last up to 20 debug messages in browser memory,
|
||||
batchSize = 20,
|
||||
batchTimeout = 2000, // Logs are guaranteed to be sent within this period (in ms), even if the batch size has not been reached yet
|
||||
maxBatchSize = 50 // When the server is unreachable and log messages are being stored until it is reachable again, this is the maximum number of messages that will be stored. Cannot be smaller than batchSize
|
||||
}
|
||||
},
|
||||
loggers = new List<Logger> { // Get the loggers to use the new appender
|
||||
new Logger {
|
||||
appenders = "appender1"
|
||||
}
|
||||
},
|
||||
insertJsnlogInHtmlResponses = false, // There's an outstanding bug setting this to true so the workaround is using the jl-javascript-logger-definitions tag helper in _Layout.cshtml, via the reference in _ViewImports.cshtml
|
||||
productionLibraryPath = null // We're using a fallback from the CDN with hashing, so do not use this
|
||||
};
|
||||
|
||||
// Must be before UseStaticFiles and UseAuthorization
|
||||
app.UseJSNLog(new CustomLoggingAdapter(loggerFactory), jsnlogConfiguration);
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## _Layout.cshtml
|
||||
|
||||
This one has some interesting configuration.
|
||||
|
||||
Firstly, using local fallbacks if the CDN versions of the scripts are unavailable, or do not match their sha384 hashes (i.e. have been hacked or changed).
|
||||
|
||||
Secondly, it injects some additional information into the logs: client IP address, user ID (your model may differ from my example), and the user agent.
|
||||
|
||||
Thirdly, it sets up logging any uncaught JS exceptions, and any exceptions inside promises where no rejection method is provided.
|
||||
|
||||
Fourthly, using stacktrace.js it handles getting the relevant stacktrace using sourcemaps.
|
||||
|
||||
```html linenums="1"
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsnlog/2.30.0/jsnlog.min.js"
|
||||
asp-fallback-src="~/jsnlog.min.js"
|
||||
asp-fallback-test="window.JL"
|
||||
crossorigin="anonymous"
|
||||
integrity="sha384-ANmgu3V8Mc5/Usd/GeIS0xu0spgLKTIqkSMQAgVvV5C2SRp/rICkLLw5XG/u6BQ9">
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/stacktrace.js/2.0.2/stacktrace.min.js"
|
||||
asp-fallback-src="~/stacktrace.min.js"
|
||||
asp-fallback-test="window.StackTrace"
|
||||
crossorigin="anonymous"
|
||||
integrity="sha384-4PjQM+vlPbdcaPnFOyBIOfqz90Hvhp+QHb3rBMOy78OaxDHw9mnmzjUSNqJkn+W5">
|
||||
</script>
|
||||
|
||||
<jl-javascript-logger-definitions />
|
||||
|
||||
<script>
|
||||
async function fetchClientIp() {
|
||||
try {
|
||||
let response = await fetch('https://api.ipify.org?format=json');
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
JL().fatalException("Failed to get client IP for logging", error);
|
||||
}
|
||||
}
|
||||
fetchClientIp().then(data => {
|
||||
const userDetailsForLogging = {
|
||||
'clientIp': data.ip,
|
||||
'userId': '@Model.UserId',
|
||||
'userAgent': navigator.userAgent
|
||||
}
|
||||
@* Log uncaught JavaScript errors to the serverside log *@
|
||||
if (window) {
|
||||
window.onerror = function (errorMsg, url, lineNumber, column, errorObj) {
|
||||
var callback = function(stackframes) {
|
||||
var stringifiedStack = stackframes.map(function(sf) {
|
||||
return sf.toString();
|
||||
}).join('\n');
|
||||
const msgObj = {
|
||||
'msg': 'Uncaught exception',
|
||||
'sourceMapStack': stringifiedStack,
|
||||
'user': userDetailsForLogging
|
||||
};
|
||||
JL('serverLog').fatalException({
|
||||
msg: JSON.stringify(msgObj),
|
||||
errorMsg: errorMsg,
|
||||
url: url,
|
||||
lineNumber: lineNumber,
|
||||
column: column
|
||||
}, errorObj);
|
||||
};
|
||||
var errback = function(err) { console.log(err.message); };
|
||||
StackTrace.fromError(errorObj).then(callback).catch(errback);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
@* Log uncaught JavaScript exceptions inside promises where no rejection method is provided *@
|
||||
if (typeof window !== 'undefined') {
|
||||
window.onunhandledrejection = function (event) {
|
||||
var callback = function(stackframes) {
|
||||
var stringifiedStack = stackframes.map(function(sf) {
|
||||
return sf.toString();
|
||||
}).join('\n');
|
||||
|
||||
const msgObj = {
|
||||
'msg': 'Unhandled promise rejection',
|
||||
'sourceMapStack': stringifiedStack,
|
||||
'user': userDetailsForLogging
|
||||
};
|
||||
JL("onerrorLogger").fatalException({
|
||||
msg: JSON.stringify(msgObj),
|
||||
errorMsg: event.reason ? event.reason.message : null
|
||||
}, event.reason);
|
||||
};
|
||||
var errback = function(err) { console.log(err.message); };
|
||||
StackTrace.fromError(errorObj).then(callback).catch(errback);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## CustomLoggingAdapter.cs
|
||||
|
||||
```csharp linenums="1"
|
||||
using JSNLog;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
|
||||
namespace YourProjectHere
|
||||
{
|
||||
/// <summary>
|
||||
/// This adapter is required to get JavaScript objects logged by JSNLog to appear correctly in Serilog. Regretably it is required
|
||||
/// because of a defficiency in JSNLog that hasn't been fixed in at least 5 years at the time of writing, and depends on slightly
|
||||
/// customised versions of classes and methods taken from that library, included here in this one file for tidiness.
|
||||
/// </summary>
|
||||
public class CustomLoggingAdapter : ILoggingAdapter
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
public CustomLoggingAdapter(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
}
|
||||
public void Log(FinalLogData finalLogData)
|
||||
{
|
||||
ILogger logger = _loggerFactory.CreateLogger(finalLogData.FinalLogger);
|
||||
Object message = LogMessageHelpers.DeserializeIfPossible(finalLogData.FinalMessage);
|
||||
switch (finalLogData.FinalLevel)
|
||||
{
|
||||
case Level.TRACE: logger.LogTrace("{@logMessage}", message); break;
|
||||
case Level.DEBUG: logger.LogDebug("{@logMessage}", message); break;
|
||||
case Level.INFO: logger.LogInformation("{@logMessage}", message); break;
|
||||
case Level.WARN: logger.LogWarning("{@logMessage}", message); break;
|
||||
case Level.ERROR: logger.LogError("{@logMessage}", message); break;
|
||||
case Level.FATAL: logger.LogCritical("{@logMessage}", message); break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
internal class LogMessageHelpers
|
||||
{
|
||||
public static T DeserializeJson<T>(string json)
|
||||
{
|
||||
T result = JsonConvert.DeserializeObject<T>(json);
|
||||
return result;
|
||||
}
|
||||
public static bool IsPotentialJson(string msg)
|
||||
{
|
||||
string trimmedMsg = msg.Trim();
|
||||
return (trimmedMsg.StartsWith("{") && trimmedMsg.EndsWith("}"));
|
||||
}
|
||||
/// <summary>
|
||||
/// Tries to deserialize msg.
|
||||
/// If that works, returns the resulting object.
|
||||
/// Otherwise returns msg itself (which is a string).
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <returns></returns>
|
||||
public static Object DeserializeIfPossible(string msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPotentialJson(msg))
|
||||
{
|
||||
Object result = DeserializeJson<Object>(msg);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns true if the msg contains a valid JSON string.
|
||||
/// </summary>
|
||||
/// <param name="msg"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsJsonString(string msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsPotentialJson(msg))
|
||||
{
|
||||
// Try to deserialise the msg. If that does not throw an exception,
|
||||
// decide that msg is a good JSON string.
|
||||
DeserializeJson<Dictionary<string, Object>>(msg);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/// <summary>
|
||||
/// Takes a log message and finds out if it contains a valid JSON string.
|
||||
/// If so, returns it unchanged.
|
||||
///
|
||||
/// Otherwise, surrounds the string with quotes (") and escapes the string for JavaScript.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string EnsureValidJson(string msg)
|
||||
{
|
||||
if (IsJsonString(msg))
|
||||
{
|
||||
return msg;
|
||||
}
|
||||
return JavaScriptStringEncode(msg, true);
|
||||
}
|
||||
public static string JavaScriptStringEncode(string value, bool addDoubleQuotes)
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
return System.Web.HttpUtility.JavaScriptStringEncode(value, addDoubleQuotes);
|
||||
#else
|
||||
// copied from https://github.com/mono/mono/blob/master/mcs/class/System.Web/System.Web/HttpUtility.cs
|
||||
if (String.IsNullOrEmpty(value))
|
||||
return addDoubleQuotes ? "\"\"" : String.Empty;
|
||||
int len = value.Length;
|
||||
bool needEncode = false;
|
||||
char c;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
c = value[i];
|
||||
if (c >= 0 && c <= 31 || c == 34 || c == 39 || c == 60 || c == 62 || c == 92)
|
||||
{
|
||||
needEncode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!needEncode)
|
||||
return addDoubleQuotes ? "\"" + value + "\"" : value;
|
||||
var sb = new StringBuilder();
|
||||
if (addDoubleQuotes)
|
||||
sb.Append('"');
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
c = value[i];
|
||||
if (c >= 0 && c <= 7 || c == 11 || c >= 14 && c <= 31 || c == 39 || c == 60 || c == 62)
|
||||
sb.AppendFormat("\\u{0:x4}", (int)c);
|
||||
else switch ((int)c)
|
||||
{
|
||||
case 8:
|
||||
sb.Append("\\b");
|
||||
break;
|
||||
case 9:
|
||||
sb.Append("\\t");
|
||||
break;
|
||||
case 10:
|
||||
sb.Append("\\n");
|
||||
break;
|
||||
case 12:
|
||||
sb.Append("\\f");
|
||||
break;
|
||||
case 13:
|
||||
sb.Append("\\r");
|
||||
break;
|
||||
case 34:
|
||||
sb.Append("\\\"");
|
||||
break;
|
||||
case 92:
|
||||
sb.Append("\\\\");
|
||||
break;
|
||||
default:
|
||||
sb.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (addDoubleQuotes)
|
||||
sb.Append('"');
|
||||
return sb.ToString();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,336 @@
|
||||
---
|
||||
tags:
|
||||
- web development
|
||||
- nodejs
|
||||
- typescript
|
||||
- eslint
|
||||
- webpack
|
||||
- babel
|
||||
- sourcemaps
|
||||
- scss
|
||||
- vscode
|
||||
- devcontainer
|
||||
- docker
|
||||
---
|
||||
|
||||
# How to set up a Node.js Typescript project well
|
||||
|
||||
_2022-06-15_
|
||||
|
||||
This post covers how to set up a project with Node.js and Typescript using eslint, webpack, babel, source maps, SCSS and vscode devcontainers using Docker.
|
||||
|
||||
This project makes use of eslint with airbnb styling and typescript support.
|
||||
|
||||
All linting errors can be automatically fixed (usually) by running `npm run lint -- --fix` before you commit your changes.
|
||||
|
||||
## Getting it running locally for development
|
||||
|
||||
### Devcontainer + vscode
|
||||
|
||||
If you have `Visual Studio Code` and `Docker` installed then you can open this project using the `Remote - Containers` extension (ms-vscode-remote.remote-containers). Click the button at the bottom left of vscode, then 'Reopen in container'. You can now run the application using the vscode debugger.
|
||||
|
||||
### Standard local install
|
||||
|
||||
Running `npm run dev` will build the development version in the `dist` folder and enable hot reloading. It will also open the application in a new browser tab automatically.
|
||||
|
||||
Running `npm run build` will create the production assets in the `dist` folder.
|
||||
|
||||
## Devcontainer
|
||||
|
||||
In the `.devcontainer` folder you'll need three files.
|
||||
|
||||
### `base.Dockerfile`
|
||||
|
||||
```dockerfile
|
||||
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
|
||||
ARG VARIANT=16-bullseye
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
|
||||
|
||||
# Install tslint, typescript. eslint is installed by javascript image
|
||||
ARG NODE_MODULES="tslint-to-eslint-config typescript"
|
||||
COPY library-scripts/meta.env /usr/local/etc/vscode-dev-containers
|
||||
RUN su node -c "umask 0002 && npm install -g ${NODE_MODULES}" \
|
||||
&& npm cache clean --force > /dev/null 2>&1
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
||||
# ARG EXTRA_NODE_VERSION=10
|
||||
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
||||
|
||||
```
|
||||
|
||||
### `devcontainer.json`
|
||||
|
||||
```json linenums="1"
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/typescript-node
|
||||
{
|
||||
"name": "Node.js & TypeScript",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
// Update 'VARIANT' to pick a Node version: 18, 16, 14.
|
||||
// Append -bullseye or -buster to pin to an OS version.
|
||||
// Use -bullseye variants on local on arm64/Apple Silicon.
|
||||
"args": {
|
||||
"VARIANT": "16-bullseye"
|
||||
}
|
||||
},
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
"forwardPorts": [3000],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "npm install",
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### `Dockerfile`
|
||||
|
||||
One important thing to note in this file is the installation of `inotify-tools` to the underlying Debian OS, as this is required for webpack to be able to poll and do hot reloading.
|
||||
|
||||
```dockerfile
|
||||
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
|
||||
ARG VARIANT=16-bullseye
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends inotify-tools
|
||||
|
||||
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
||||
# ARG EXTRA_NODE_VERSION=10
|
||||
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
||||
|
||||
# [Optional] Uncomment if you want to install more global node packages
|
||||
# RUN su node -c "npm install -g <your-package-list -here>"
|
||||
|
||||
```
|
||||
|
||||
## Visual Studio Code Debugging
|
||||
|
||||
Within the `.vscode` folder you'll need to create this `launch.json` file:
|
||||
|
||||
```json linenums="1"
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch via NPM",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev"
|
||||
],
|
||||
"runtimeExecutable": "npm",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "pwa-node",
|
||||
"stopOnEntry": true,
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Eslint
|
||||
|
||||
Create the following files:
|
||||
|
||||
### `.eslintignore`
|
||||
|
||||
Assuming you're using git, this will be the same as your `.gitignore` file.
|
||||
|
||||
```text
|
||||
dist
|
||||
node_modules
|
||||
```
|
||||
|
||||
### `.eslintrc.json`
|
||||
|
||||
```json linenums="1"
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"js": "never",
|
||||
"jsx": "never",
|
||||
"ts": "never",
|
||||
"tsx": "never"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Webpack with SCSS and TS support, plus hot reloading
|
||||
|
||||
The structure for this application is to have a `src` folder that contains: all `.html` and `.ts`; `assets` folder for images; `styles` folder for `.scss` files. The files will be compiled to a `dist` folder for deploying to a web server.
|
||||
|
||||
Create the following files:
|
||||
|
||||
### `custom.d.ts`
|
||||
|
||||
```ts linenums="1"
|
||||
declare module '*.svg' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
```
|
||||
|
||||
### `tsconfig.json`
|
||||
|
||||
```json linenums="1"
|
||||
{
|
||||
"compilerOptions": {
|
||||
"preserveConstEnums": true
|
||||
},
|
||||
"include": ["src/**/*", "src/custom.d.ts"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
### `webpack.config.js`
|
||||
|
||||
```js linenums="1"
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: {
|
||||
bundle: path.resolve(__dirname, 'src/index.ts'),
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: '[name].[contenthash].js',
|
||||
clean: true,
|
||||
assetModuleFilename: '[name].[contenthash][ext]',
|
||||
},
|
||||
devtool: 'source-map',
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
port: 3000,
|
||||
open: true,
|
||||
hot: true,
|
||||
compress: true,
|
||||
historyApiFallback: true,
|
||||
},
|
||||
watchOptions: { poll: true },
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
// Use these loaders for any matching scss file types
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
'sass-loader',
|
||||
],
|
||||
},
|
||||
{
|
||||
// Add backwards compatibility
|
||||
test: /\.(js|ts)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env', '@babel/preset-typescript'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Add support for images
|
||||
test: /\.(png|svg|jpg|jpeg|gif)$/i,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
// Specify the order in which to resolve files by their extension
|
||||
extensions: ['*', '.js', '.ts'],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### `package.json`
|
||||
|
||||
```json linenums="1"
|
||||
{
|
||||
"name": "your app name",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"dev": "webpack serve",
|
||||
"lint": "eslint --ignore-path .eslintignore --ext .js,.ts ."
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": ""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.5",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@babel/preset-typescript": "^7.17.12",
|
||||
"@typescript-eslint/eslint-plugin": "^5.28.0",
|
||||
"@typescript-eslint/parser": "^5.28.0",
|
||||
"babel-loader": "^8.2.5",
|
||||
"css-loader": "^6.7.1",
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"sass": "^1.52.3",
|
||||
"sass-loader": "^13.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"typescript": "^4.7.3",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.9.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
tags:
|
||||
- web development
|
||||
- dotnet
|
||||
- c#
|
||||
- vue
|
||||
- asp
|
||||
---
|
||||
|
||||
# Creating a reactive SPA simply within an ASP.Net Core web app with Vue.js
|
||||
|
||||
_2021-07-12_
|
||||
|
||||

|
||||
|
||||
In this post, I will cover how you can quickly and easily use Vue.js to make a reactive 'SPA' within ASP.Net Core. This is a slightly dirty way to do things, but for rapid prototyping or a simple project it does the job just fine without any unnecessary complications. By 'SPA' I mean a Single Page Application, except without any navigation as that's handled by the underlying ASP.Net Core application, as is the API called by Vue.js methods.
|
||||
|
||||
In the `<head>` tags of your Razor page/view, you'll need to include a reference to Vue.js, for example via a CDN.
|
||||
|
||||
`<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>`
|
||||
|
||||
At the bottom of your Razor page/view, you'll need to include a `<script>` tag to hold your Vue instance definition. In Vue.js this includes:
|
||||
|
||||
- the element to have the Vue instance applied to
|
||||
- the data object including any default properties
|
||||
- computed properties (these act as if part of the data object but reactively update like functions)
|
||||
- methods (functions that can interact with the data object)
|
||||
|
||||
In this example, it's a Vue instance for creating invoices and updating stock levels. The above gif shows some of the functionality achieved with this, including dynamically adding classes, and looping through elements in an array with markup for each.
|
||||
|
||||
```html linenums="1"
|
||||
<script>
|
||||
const createInvoice = new Vue({
|
||||
el: '#create-invoice',
|
||||
data() {
|
||||
return {
|
||||
errorMessage: '',
|
||||
customer: {
|
||||
Name: '',
|
||||
Address1: '',
|
||||
Address2: '',
|
||||
Address3: '',
|
||||
PostCode: ''
|
||||
},
|
||||
invoiceNumber: new Date().valueOf(),
|
||||
products: [],
|
||||
successMessage: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
calculatedVat() {
|
||||
// take total and get 20% of it
|
||||
return this.total * 0.2;
|
||||
},
|
||||
subTotal() {
|
||||
// Take total and get 80% of it
|
||||
return this.total * 0.8;
|
||||
},
|
||||
total() {
|
||||
// For each product, add total price
|
||||
let total = 0;
|
||||
this.products.forEach(product => {
|
||||
total += product.totalPrice;
|
||||
});
|
||||
|
||||
return total - this.promoDiscount;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
calculateItemTotal(product) {
|
||||
const totalPrice = product.unitPrice * product.quantity;
|
||||
Vue.set(product, 'totalPrice', totalPrice)
|
||||
},
|
||||
getDuplicateBarcodeClass(product) {
|
||||
const matchingBarcodes = this.products.filter(obj => {
|
||||
return obj.barcode === product.barcode;
|
||||
})
|
||||
|
||||
if (matchingBarcodes.length > 1) {
|
||||
return 'alert-danger';
|
||||
}
|
||||
},
|
||||
removeProduct(product) {
|
||||
const index = this.products.findIndex(
|
||||
(x) => x.barcode === product.barcode
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
this.products.splice(index, 1);
|
||||
}
|
||||
},
|
||||
updateStock() {
|
||||
// Check the user really wants to update the stock and start a new invoice
|
||||
const userResponse = confirm("Are you sure you wish to finish this invoice and update stock levels? You will not be able to print it again.");
|
||||
if (userResponse) {
|
||||
const data = {
|
||||
invoiceNumber: String(this.invoiceNumber),
|
||||
products: this.products,
|
||||
};
|
||||
|
||||
// Update the stock & log the invoice
|
||||
fetch(`Invoices/UpdateStockAndLogInvoice/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.statusCode === 404) {
|
||||
Vue.set(this, "errorMessage", data.message);
|
||||
} else {
|
||||
Vue.set(this, 'successMessage', data.message);
|
||||
|
||||
// If no errors, clear the products list to be ready for the next invoice
|
||||
Vue.set(this, "products", []);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
Vue.set(this, "errorMessage", err);
|
||||
});
|
||||
}
|
||||
},
|
||||
validateNumber: (event) => {
|
||||
// Only accept integers, so prevent anything but digits 0-9
|
||||
let keyCode = event.keyCode;
|
||||
if (keyCode < 48 || keyCode > 57) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
viewAndPrintInvoice() {
|
||||
if (this.isDuplicateBarcode) {
|
||||
alert("There is a duplicate barcode - look for red barcodes and remove duplicates");
|
||||
}
|
||||
else {
|
||||
window.print();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
To hook this into the DOM, just add the element identifier to an element, for example `<div id="create-invoice">` will bind a div to this Vue instance.
|
||||
|
||||
Within that, you are free to use a variety of tools, like `v-if`, interpolation and `v-on` event handlers. In this example, if there's any text in the `successMessage` property of the Vue data object, this alert div will be displayed, interpolating the message, and clearing it (thus hiding the alert) when the close button is clicked.
|
||||
|
||||
```html linenums="1"
|
||||
<div v-if="successMessage" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
<strong>Success:</strong> {{ successMessage }}
|
||||
<button type="button" class="close" aria-label="Close" v-on:click="successMessage = ''">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
In this example, a `v-for` is used to loop through all the products in the array, producing a `<tr>` for each of them, with an appropriate class bound from a method taking that product as a parameter. `v-if` and `v-else` statements enable conditionally showing one (or none) of three different pooltips. The product's `name` property is interpolated and shown, as is the `unitPrice` property, which is nicely formatted for readability.
|
||||
|
||||
The `barcode` field is mapped to a `v-model`, which means as it changes it directly alters the value in the Vue data object, and as that changes this updates reactively. It also makes use of `v-on:blur` to run a method that makes an API call to retrieve product data, conditionally sets the disabled property once a barcode has been entered, and binds a class based on the output of a method taking the product as a parameter.
|
||||
|
||||
The `quantity` field also uses a `v-model`, but specifically forces it to be a number (no need for Typescript here) runs a method on keypress (the `@@` syntax is purely due to Razor escaping, normally you'd only need one) and on change.
|
||||
|
||||
```html linenums="1"
|
||||
<tr v-for="product in products" v-bind:class="getProductRowClass(product)">
|
||||
<td>
|
||||
<div class="tooltip" v-if="product.barcode.length > 0 && product.stockLevel - product.quantity < 0">
|
||||
<i class="bi bi-x-circle-fill text-danger"></i>
|
||||
<span class="tooltiptext">You will need to order more stock to fulfill this order</span>
|
||||
</div>
|
||||
<div class="tooltip" v-else-if="product.barcode.length > 0 && product.stockLevel - product.quantity == 0">
|
||||
<i class="bi bi-exclamation-triangle-fill text-warning"></i>
|
||||
<span class="tooltiptext">You will run out of stock</span>
|
||||
</div>
|
||||
<div class="tooltip" v-else-if="product.barcode.length > 0 && product.stockLevel - product.quantity <= 10">
|
||||
<i class="bi bi-info-circle-fill text-info"></i>
|
||||
<span class="tooltiptext">Stock will be low after this order</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<label>{{product.name}}</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="barcode" v-model="product.barcode" v-on:blur="getProductInformation(product)" :disabled="product.barcode.length > 0" v-bind:class="getDuplicateBarcodeClass(product)">
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" step="1" name="quantity" v-model.number="product.quantity" @@keypress="validateNumber" v-on:change="calculateItemTotal(product)">
|
||||
</td>
|
||||
<td>
|
||||
<label>£{{parseFloat(product.unitPrice).toFixed(2)}}</label>
|
||||
</td>
|
||||
</tr>
|
||||
```
|
||||
|
||||
This is a very simple, quick and dirty example, but hopefully it gives you an idea of the kind of things you can quickly and easily achieve using Vue.js in this way. While this example uses ASP.Net Core, you could easily plug this into any other website, so long as it's using HTML, CSS and JavaScript, and there is an API to make requests to.
|
||||
@@ -0,0 +1,99 @@
|
||||
site_name: Josh Creek's Blog
|
||||
site_url: https://www.jcreek.co.uk/
|
||||
repo_url: https://github.com/jcreek/jcreek.github.io
|
||||
repo_name: Spotted an issue?
|
||||
edit_uri: edit/master/mkdocs/
|
||||
|
||||
theme:
|
||||
name: material
|
||||
icon:
|
||||
repo: fontawesome/brands/github
|
||||
favicon: img/favicon.ico
|
||||
logo: img/noogler-hat-edited-round.png
|
||||
palette:
|
||||
# Palette toggle for light mode
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Switch to dark mode
|
||||
primary: amber
|
||||
accent: amber
|
||||
|
||||
# Palette toggle for dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Switch to light mode
|
||||
primary: amber
|
||||
accent: amber
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.tracking
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
- navigation.sections
|
||||
- toc.follow
|
||||
- toc.integrate
|
||||
- navigation.top
|
||||
- search.suggest
|
||||
- search.highlight
|
||||
- search.share
|
||||
|
||||
copyright: Copyright © 2011 - 2022 Josh Creek
|
||||
|
||||
extra:
|
||||
consent:
|
||||
title: Cookie consent
|
||||
description: >-
|
||||
I use cookies to recognize your repeated visits and preferences, as well
|
||||
as to measure the effectiveness of my documentation and whether users
|
||||
find what they're searching for. With your consent, you're helping me to
|
||||
make my documentation better.
|
||||
actions:
|
||||
- accept
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-MYNBTM1YQW
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/jcreek
|
||||
- icon: fontawesome/brands/twitter
|
||||
link: https://twitter.com/jcreek23
|
||||
- icon: fontawesome/brands/linkedin
|
||||
link: https://www.linkedin.com/in/jrcreek
|
||||
- icon: fontawesome/brands/instagram
|
||||
link: https://www.instagram.com/scruffy238/
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- tags
|
||||
|
||||
markdown_extensions:
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- pymdownx.superfences
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Containers & Deploying:
|
||||
# - deploying/index.md
|
||||
- Docker Compose for ASP.Net Core with Postgres + S3 backups: deploying/docker-compose-for-asp-net-core-with-postgres-and-s3-backups.md
|
||||
- Docker Compose for S3 Backup and Restore of PostgreSQL: deploying/docker-compose-s3-postgres-backup-and-restore.md
|
||||
- Docker Compose for Elasticsearch and Kibana 7.9: deploying/elasticsearch-and-kibana.md
|
||||
- Development Teams:
|
||||
- The way I recommend using git in a dev team: dev-team/git.md
|
||||
- How to write an excellent readme file: dev-team/readme-files.md
|
||||
- Home Server:
|
||||
- TrueNAS Scale:
|
||||
- Installing Home Assistant OS on Truenas Scale: home-server/truenas-scale/installing-homeassistant-on-truenas-scale.md
|
||||
- Web Development:
|
||||
- Setting up remote containers for IAC dev environments in dotnet 6 with postgres and SMTP: web-dev/dev-environment-container-vscode.md
|
||||
- Adding JSNLog to ASP .Net 6 with Serilog: web-dev/jsnlog-asp-net-6.md
|
||||
- How to set up a Node.js Typescript project well: web-dev/node-js-typescript.md
|
||||
- Creating a reactive SPA simply within an ASP.Net Core web app with Vue.js: web-dev/simple-vue-spa-in-asp-dotnet-core.md
|
||||
- Dotnet & C#:
|
||||
- C# 8 Pattern Matching: web-dev/dotnet-csharp/pattern-matching.md
|
||||
Reference in New Issue
Block a user