chore(#16): Remove jekyll files

This commit is contained in:
Josh Creek
2022-10-20 22:15:01 +01:00
parent 38b094f99e
commit a931890dcd
28 changed files with 0 additions and 2931 deletions
@@ -1,190 +0,0 @@
---
layout: post
parent: Web Development
nav_order: 3
title: "Setting up remote containers for IAC dev environments in dotnet 6 with postgres and SMTP"
date: 2022-04-20 15:51:03 +0000
categories: dotnet core asp vscode remote-container remote container iac postgress smtp dev environment
---
# {{page.title}}
_{{page.date}}_
![](/assets/devcontainer.gif)
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
// 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
"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.
@@ -1,192 +0,0 @@
---
layout: post
parent: Web Development
nav_order: 3
title: "C# 8 Pattern Matching"
date: 2022-02-15 19:07:21 +0000
categories: dotnet csharp
---
# {{page.title}}
_{{page.date}}_
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
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
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
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
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
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
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
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
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
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.
-6
View File
@@ -1,6 +0,0 @@
---
layout: default
title: Web Development
nav_order: 3
has_children: true
---
-345
View File
@@ -1,345 +0,0 @@
---
layout: post
parent: Web Development
nav_order: 2
title: "Adding JSNLog to ASP .Net 6 with Serilog"
date: 2022-01-18 21:43:32 +0000
categories: dotnet core asp serilog jsnlog
---
# {{page.title}}
_{{page.date}}_
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
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
<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
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
}
}
}
```
-330
View File
@@ -1,330 +0,0 @@
---
layout: post
parent: Web Development
nav_order: 4
title: "How to set up a Node.js Typescript project well"
date: 2022-06-15 19:26:00 +0000
categories: nodejs typescript eslint webpack babel sourcemaps scss vscode devcontainer docker
---
# {{page.title}}
_{{page.date}}_
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
// 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
{
"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
{
"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
declare module '*.svg' {
const content: any;
export default content;
}
```
### `tsconfig.json`
```json
{
"compilerOptions": {
"preserveConstEnums": true
},
"include": ["src/**/*", "src/custom.d.ts"],
"exclude": ["node_modules", "**/*.spec.ts"]
}
```
### `webpack.config.js`
```js
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
{
"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"
}
}
```
@@ -1,199 +0,0 @@
---
layout: post
parent: Web Development
nav_order: 1
title: "Creating a reactive SPA simply within an ASP.Net Core web app with Vue.js"
date: 2021-07-12 21:42:57 +0100
categories: vue dotnet core asp
---
# {{page.title}}
_{{page.date}}_
![](/assets/vuejsexample1.gif)
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
<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
<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">&times;</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
<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>&#163;{{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.