Compare commits
12 Commits
b12a09de0c
...
main
Author | SHA1 | Date | |
---|---|---|---|
55778b8fb8 | |||
a19c3bcff4 | |||
af884061b6 | |||
df8f43ba5a | |||
140e1db769 | |||
d139b94a62 | |||
a2b40910c2 | |||
19381030cb | |||
1974c5328d | |||
589793841e | |||
f944bf6934 | |||
7f394c94ea |
@ -24,6 +24,7 @@ export default defineConfig({
|
|||||||
link: '/dotnet/',
|
link: '/dotnet/',
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
|
{ text: 'Background Services', link: '/dotnet/background-service' },
|
||||||
{ text: 'Blazor with an API', link: '/dotnet/blazor-with-api' },
|
{ text: 'Blazor with an API', link: '/dotnet/blazor-with-api' },
|
||||||
{ text: 'Database Seeding', link: '/dotnet/database-seed' },
|
{ text: 'Database Seeding', link: '/dotnet/database-seed' },
|
||||||
{ text: 'Dockerising Blazor', link: '/dotnet/dockerising-blazor' },
|
{ text: 'Dockerising Blazor', link: '/dotnet/dockerising-blazor' },
|
||||||
@ -35,6 +36,16 @@ export default defineConfig({
|
|||||||
{ text: 'Google Sign in without Identity', link: '/dotnet/google-sign-in-without-identity' },
|
{ text: 'Google Sign in without Identity', link: '/dotnet/google-sign-in-without-identity' },
|
||||||
{ text: 'Service Testing', link: '/dotnet/service-testing' },
|
{ text: 'Service Testing', link: '/dotnet/service-testing' },
|
||||||
{ text: 'Controller Testing', link: '/dotnet/controller-testing' },
|
{ text: 'Controller Testing', link: '/dotnet/controller-testing' },
|
||||||
|
{ text: 'API Key Authentication', link: '/dotnet/api-key-auth'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Angular',
|
||||||
|
link: '/angular/',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Application Settings', link: '/angular/application-settings' },
|
||||||
|
{ text: 'Base Components', link: '/angular/base-components' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -55,6 +66,16 @@ export default defineConfig({
|
|||||||
{ text: 'Text Width HR', link: '/css/text-width-hr' },
|
{ text: 'Text Width HR', link: '/css/text-width-hr' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Docker',
|
||||||
|
link: '/docker/',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Docker Exec', link: '/docker/exec-into-container' },
|
||||||
|
{ text: 'Local DB (MSSQL)', link: '/docker/local-db-mssql' },
|
||||||
|
{ text: 'Local DB (PostgreSQL)', link: '/docker/local-db-pg' },
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'EF Core',
|
text: 'EF Core',
|
||||||
link: '/ef-core/',
|
link: '/ef-core/',
|
||||||
|
87
docs/angular/application-settings.md
Normal file
87
docs/angular/application-settings.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# Application Configuration
|
||||||
|
|
||||||
|
Often you will need to use some kind of configuration in your application. For most backends this can be done using envionment variables or settings files, however in Angular applications this is not as straight forward.
|
||||||
|
A common approach I like to take is to setup a `appsettings.json` file, that is served alonside the application and can be used to store configuration values. The application will read this file, pass it around as a provider and use it as needed.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Start by creating a new file in the `public` directory called `appsettings.json`. This file will contain all the configuration values for the application.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"someSetting": "SomeValue"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next we will update the `main.ts` to read the `appsettings.json` file and pass it to the application as a provider. Setup the following exports before the application bootstrap.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const APP_SETTINGS = new InjectionToken<AppSettings>('APP_SETTINGS');
|
||||||
|
|
||||||
|
export const fetchAppSettings = async (): Promise<AppSettings> => {
|
||||||
|
const baseHref = document.querySelector('base')?.getAttribute('href');
|
||||||
|
const settingUrl = baseHref === '/' ? '/appsettings.json' : `${baseHref}/appsettings.json`;
|
||||||
|
const response = await fetch(settingUrl);
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const provideAppSettings = (appSettings: AppSettings): StaticProvider => {
|
||||||
|
return {
|
||||||
|
provide: APP_SETTINGS,
|
||||||
|
useValue: appSettings,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
In this we are creating a new `InjectionToken` called `APP_SETTINGS` that will be used to inject the configuration values into the application. We also have a function `fetchAppSettings` that will fetch the `appsettings.json` file and return the configuration values. Finally we have a function `provideAppSettings` that will create a provider for the configuration values.
|
||||||
|
|
||||||
|
Next we can update the bootstrap function to fetch the configuration values and pass them to the application.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
(async function () {
|
||||||
|
// Fetch the app settings and then bootstrap the application
|
||||||
|
const appSettings = await fetchAppSettings();
|
||||||
|
bootstrapApplication(AppComponent, {
|
||||||
|
providers: [
|
||||||
|
provideAppSettings(appSettings),
|
||||||
|
{ provide: appConfig, useValue: appSettings },
|
||||||
|
...appConfig.providers,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
If you have other configuration in the appConfig you will also need to ensure it's passed correctly to the `bootstrapApplication()` function.
|
||||||
|
:::
|
||||||
|
|
||||||
|
This will fetch the configuration values and pass them to the application as a provider.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Using settings is now as simple as injecting the `APP_SETTINGS` token into a service or component.
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```typescript [app.component.ts]
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
import { APP_SETTINGS } from '../main';
|
||||||
|
import { AppSettings } from './common/models/config';
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [RouterOutlet],
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrl: './app.component.scss'
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
appSettings: AppSettings = inject(APP_SETTINGS);
|
||||||
|
title = 'app-settings';
|
||||||
|
someSetting = this.appSettings.someSetting
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```html [app.component.html]
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<p>The value of someSettings is: {{ someSetting }}</p>
|
||||||
|
```
|
||||||
|
:::
|
64
docs/angular/base-components.md
Normal file
64
docs/angular/base-components.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Base Components
|
||||||
|
|
||||||
|
This is a small collection of handy base angular components.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```ts:line-numbers [vstack.component.ts]
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-vstack',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
template: `
|
||||||
|
<div class="vstack" [ngStyle]="{ gap: gap }">
|
||||||
|
<ng-content />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
.vstack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class VStackComponent {
|
||||||
|
@Input() gap: string = '5px';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```ts:line-numbers [hstack.component.ts]
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-hstack',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
template: `
|
||||||
|
<div class="hstack" [ngStyle]="{ gap: gap }">
|
||||||
|
<ng-content />
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: `
|
||||||
|
.hstack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class HStackComponent {
|
||||||
|
@Input() gap: string = '5px';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
4
docs/angular/index.md
Normal file
4
docs/angular/index.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Angular Snippets and Musings
|
||||||
|
|
||||||
|
#### [Application Settings](./application-settings.md)
|
||||||
|
#### [Base Components](./base-components.md)
|
7
docs/docker/exec-into-container.md
Normal file
7
docs/docker/exec-into-container.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Exec Into Container
|
||||||
|
|
||||||
|
Often it's handy to exec into a container to poke around or run commands. Here's how you can do that:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -it --entrypoint /bin/bash <container-name>
|
||||||
|
```
|
5
docs/docker/index.md
Normal file
5
docs/docker/index.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Docker Snippets and Musings
|
||||||
|
|
||||||
|
#### [Exec Into Container](./exec-into-container.md)
|
||||||
|
#### [Local Database With Scripts (MSSQL)](./local-db-mssql.md)
|
||||||
|
#### [Local Database With Scripts (PostgreSQL)](./local-db-pg.md)
|
174
docs/docker/local-db-mssql.md
Normal file
174
docs/docker/local-db-mssql.md
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# Local Database With Scripts (MSSQL)
|
||||||
|
|
||||||
|
When developing apps locally it can be really useful to have a dockerised database unique to the application.
|
||||||
|
|
||||||
|
Often rather than just running a pre-built image, you'll want to run a database with some initial data, tables, or a schema.
|
||||||
|
|
||||||
|
For this purpose we can create our own image that extends the base image and adds our own scripts.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
For most applications the directory structure will look something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
database/
|
||||||
|
Dockerfile
|
||||||
|
scripts/
|
||||||
|
01-create-database.sql
|
||||||
|
02-create-tables.sql
|
||||||
|
03-seed-data.sql
|
||||||
|
development/
|
||||||
|
compose.yml
|
||||||
|
src/
|
||||||
|
...
|
||||||
|
tests/
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dockerfile
|
||||||
|
|
||||||
|
Create a dockerfile in the `database/` directory:
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```dockerfile [Dockerfile]
|
||||||
|
FROM mcr.microsoft.com/mssql/server:2022-latest
|
||||||
|
|
||||||
|
# Set the SQL Server environment variables
|
||||||
|
ENV ACCEPT_EULA="Y"
|
||||||
|
ENV SA_PASSWORD="Password123"
|
||||||
|
|
||||||
|
# Setup port
|
||||||
|
EXPOSE 1433
|
||||||
|
|
||||||
|
# Create a temp directory
|
||||||
|
RUN mkdir -p /tmp/init
|
||||||
|
|
||||||
|
# Copy all the scripts into the container
|
||||||
|
COPY ./scripts/ /tmp/init
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/tmp/init/entrypoint.sh" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: danger
|
||||||
|
|
||||||
|
As this is a local development database, we're using the `sa` user with a simple password. **Do not use this in production**.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
Create the scripts in the `database/scripts/` directory:
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```bash [entrypoint.sh]
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Run the sql scripts and start sql server
|
||||||
|
/tmp/init/run-scripts.sh & /opt/mssql/bin/sqlservr
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```bash [run-scripts.sh]
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Wait for the mssql database to be ready
|
||||||
|
while ! /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -C -Q "SELECT 1" > /dev/null; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "SQL Server is up and running"
|
||||||
|
|
||||||
|
# Check if the setup has already been executed
|
||||||
|
SETUP_DONE=$(/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -C -Q "IF EXISTS (SELECT 1 FROM master.sys.tables WHERE name = 'setup_marker' AND schema_id = SCHEMA_ID('dbo')) SELECT 1 ELSE SELECT 0" -h -1 -W -r 1 | grep -oE '^[0-9]+' | tr -d '[:space:]')
|
||||||
|
|
||||||
|
if [[ "$SETUP_DONE" == "1" ]]; then
|
||||||
|
echo "Setup has already been completed. Skipping initialization."
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Setup has not been completed. Running initialization."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run all scripts in the scripts folder
|
||||||
|
for entry in /tmp/init/*.sql;
|
||||||
|
do
|
||||||
|
echo "Running script $entry"
|
||||||
|
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -C -i $entry
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create a marker table to indicate setup completion
|
||||||
|
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -C -Q "CREATE TABLE master.dbo.setup_marker (id INT PRIMARY KEY IDENTITY, created_at DATETIME DEFAULT GETDATE())"
|
||||||
|
|
||||||
|
echo "All scripts have been run"
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
The above script waits for the database to be ready, then checks if the setup has already run. If not it will run all the scripts in the `scripts/` directory and create a marker table to indicate that the setup has been completed.
|
||||||
|
|
||||||
|
Create any scripts that you need in the `database/scripts/` directory.
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
|
||||||
|
See below for an example of the scripts you might want to run.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```sql [01-create-database.sql]
|
||||||
|
CREATE DATABASE MyDatabase
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql [02-create-tables.sql]
|
||||||
|
USE MyDatabase
|
||||||
|
|
||||||
|
CREATE TABLE MyTable (
|
||||||
|
id INT PRIMARY KEY,
|
||||||
|
name NVARCHAR(50)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql [03-seed-data.sql]
|
||||||
|
USE MyDatabase
|
||||||
|
|
||||||
|
INSERT INTO MyTable (id, name) VALUES (1, 'Alice')
|
||||||
|
INSERT INTO MyTable (id, name) VALUES (2, 'Bob')
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql [04-create-user.sql]
|
||||||
|
USE MyDatabase
|
||||||
|
|
||||||
|
CREATE LOGIN MyUser WITH PASSWORD = 'MyPassword'
|
||||||
|
CREATE USER MyUser FOR LOGIN MyUser
|
||||||
|
ALTER ROLE db_owner ADD MEMBER MyUser;
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Compose
|
||||||
|
|
||||||
|
Lastly we need to create a `docker-compose.yml` file in the `development/` directory:
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```yaml [compose.yml]
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
build:
|
||||||
|
context: ../database
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
volumes:
|
||||||
|
- db-data:/var/opt/mssql
|
||||||
|
ports:
|
||||||
|
- "1433:1433"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db-data:
|
||||||
|
```
|
89
docs/docker/local-db-pg.md
Normal file
89
docs/docker/local-db-pg.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# Local Database With Scripts (PostgreSQL)
|
||||||
|
|
||||||
|
When developing apps locally it can be really useful to have a dockerised database unique to the application.
|
||||||
|
|
||||||
|
Often rather than just running a pre-built image, you'll want to run a database with some initial data, tables, or a schema.
|
||||||
|
|
||||||
|
For this purpose we can create our own image that extends the base image and adds our own scripts.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
For most applications the directory structure will look something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
database/
|
||||||
|
Dockerfile
|
||||||
|
scripts/
|
||||||
|
01-create-tables.sql
|
||||||
|
development/
|
||||||
|
compose.yml
|
||||||
|
src/
|
||||||
|
...
|
||||||
|
tests/
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dockerfile
|
||||||
|
|
||||||
|
Create a dockerfile in the `database/` directory:
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```dockerfile [Dockerfile]
|
||||||
|
FROM postgres:17
|
||||||
|
|
||||||
|
# Setup the postgres environment variables
|
||||||
|
ENV POSTGRES_USER=myuser
|
||||||
|
ENV POSTGRES_PASSWORD=mypassword
|
||||||
|
ENV POSTGRES_DB=mydatabase
|
||||||
|
|
||||||
|
# Setup port
|
||||||
|
EXPOSE 5432
|
||||||
|
|
||||||
|
# Copy all the scripts into the container
|
||||||
|
COPY ./scripts /docker-entrypoint-initdb.d/
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: danger
|
||||||
|
|
||||||
|
As this is a local development database, we're using the a simple username and password. **Do not use this in production**.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
Create any scripts you need in the `database/scripts/` directory. PostgreSQL will run these scripts in alphabetical order against the database specified in the `POSTGRES_DB` environment variable.
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```sql [01-create-tables.sql]
|
||||||
|
CREATE TABLE MyTable (
|
||||||
|
Id INT NOT NULL PRIMARY KEY,
|
||||||
|
Name VARCHAR(50) NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Compose
|
||||||
|
|
||||||
|
Lastly we need to create a `docker-compose.yml` file in the `development/` directory:
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```yaml [compose.yml]
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
build:
|
||||||
|
context: ../database
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
volumes:
|
||||||
|
- db-data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db-data:
|
||||||
|
```
|
136
docs/dotnet/api-key-auth.md
Normal file
136
docs/dotnet/api-key-auth.md
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# API Key Auth
|
||||||
|
|
||||||
|
Simple API Key authentication is a great option when building public facing APIs without strict security requirements, but you would rather not leave open. Think syncs, long running jobs or other non-critical operations.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
This example stores the ApiKey in the `appsettings.json` file. You can also store it in a database, environment variable, or any other configuration source.
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```json[appsettings.json]
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*",
|
||||||
|
"ApiKey": "ThisIsMySecretKey",
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Filter
|
||||||
|
|
||||||
|
The logic for the api key authentication is a simple Authorization filter. It checks the `ApiKey` header against the configured value.
|
||||||
|
|
||||||
|
Start by storing the header name in a constants file or similar:
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```csharp[Constants.cs]
|
||||||
|
namespace ApiKeyAuthDemo.Core
|
||||||
|
{
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public const string API_KEY_HEADER_NAME = "X-API-KEY";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Then create the filter:
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```csharp[ApiKeyAuthorizeAttribute.cs]
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace ApiKeyAuthDemo.Core.Filters
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||||
|
public class ApiKeyAuthorizeAttribute() : Attribute, IAuthorizationFilter
|
||||||
|
{
|
||||||
|
public void OnAuthorization(AuthorizationFilterContext context)
|
||||||
|
{
|
||||||
|
// Get the API key from the request headers
|
||||||
|
string? apiKeyValue = context.HttpContext.Request.Headers[Constants.API_KEY_HEADER_NAME];
|
||||||
|
|
||||||
|
// Get the API key from the configuration
|
||||||
|
IConfiguration configuration = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
|
||||||
|
string? apiKey = configuration.GetValue<string>("ApiKey");
|
||||||
|
|
||||||
|
// Check if the API key is valid and set
|
||||||
|
if (apiKeyValue == null || apiKeyValue != apiKey)
|
||||||
|
{
|
||||||
|
context.Result = new UnauthorizedResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
See below for example usage (on the second GET method):
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```csharp[WeatherForecastController.cs]
|
||||||
|
using ApiKeyAuthDemo.Core.Filters;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ApiKeyAuthDemo.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("[controller]")]
|
||||||
|
public class WeatherForecastController : ControllerBase
|
||||||
|
{
|
||||||
|
private static readonly string[] Summaries = new[]
|
||||||
|
{
|
||||||
|
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly ILogger<WeatherForecastController> _logger;
|
||||||
|
|
||||||
|
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IEnumerable<WeatherForecast> Get()
|
||||||
|
{
|
||||||
|
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||||
|
{
|
||||||
|
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||||
|
TemperatureC = Random.Shared.Next(-20, 55),
|
||||||
|
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
[ApiKeyAuthorize]
|
||||||
|
[HttpGet("auth")]
|
||||||
|
public IEnumerable<WeatherForecast> GetAuth()
|
||||||
|
{
|
||||||
|
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||||
|
{
|
||||||
|
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||||
|
TemperatureC = Random.Shared.Next(-20, 55),
|
||||||
|
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
57
docs/dotnet/background-service.md
Normal file
57
docs/dotnet/background-service.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Background Services
|
||||||
|
|
||||||
|
Often in application you will want logic to execute in the background. This could be for a variety of reasons, such as:
|
||||||
|
* Processing data
|
||||||
|
* Sending emails
|
||||||
|
* Performing maintenance tasks
|
||||||
|
* Running scheduled jobs
|
||||||
|
|
||||||
|
In .NET Core, you can create a background service by creating a class that inherits from the `BackgroundService` class. This class is part of the `Microsoft.Extensions.Hosting` namespace.
|
||||||
|
|
||||||
|
::: warn
|
||||||
|
|
||||||
|
The ability of a background service to run is dependent on the lifetime of the host. If the host is stopped, the background service will also stop. This means for software running in IIS you will need to ensure the site is always running. For this reason docker deployments are usually preferred
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Creating a Background Service
|
||||||
|
|
||||||
|
To create a background service, you need to create a class that inherits from the `BackgroundService` class. This class has a single method that you need to override, called `ExecuteAsync`. This method is called when the background service is started, and it should contain the logic that you want to run in the background.
|
||||||
|
|
||||||
|
Here is an example of a simple background service that writes to the console every second:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace BackgroundServiceExample
|
||||||
|
{
|
||||||
|
public class MyBackgroundService : BackgroundService
|
||||||
|
{
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Background service is running...");
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Registering the Background Service
|
||||||
|
|
||||||
|
The next step is to register the background service with the dependency injection container. You can do this in `Program.cs` when you are building the host.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ...
|
||||||
|
builder.Services.AddHostedService<MyBackgroundService>();
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This is just a simple way to use background services, for more complex scenarios you may want to look at writing a console app that runs as a service which can be triggered by a scheduler or a message queue. This will also give you flexibility to run the service on a different machine or in a container and have separate configuration and so on.
|
@ -1,5 +1,6 @@
|
|||||||
# .NET Snippets and Musings
|
# .NET Snippets and Musings
|
||||||
|
|
||||||
|
#### [Background Services](./background-service.md)
|
||||||
#### [Blazor with an inbuilt API](./blazor-with-api.md)
|
#### [Blazor with an inbuilt API](./blazor-with-api.md)
|
||||||
#### [Database Seeding](./database-seed.md)
|
#### [Database Seeding](./database-seed.md)
|
||||||
#### [Dockerising a Blazor Web App](./dockerising-blazor.md)
|
#### [Dockerising a Blazor Web App](./dockerising-blazor.md)
|
||||||
@ -10,4 +11,5 @@
|
|||||||
#### [JWT Authentication with Cookie](./jwt-authentication-cookie.md)
|
#### [JWT Authentication with Cookie](./jwt-authentication-cookie.md)
|
||||||
#### [Google Sign in Without Identity](./google-sign-in-without-identity.md)
|
#### [Google Sign in Without Identity](./google-sign-in-without-identity.md)
|
||||||
#### [Service Testing](./service-testing.md)
|
#### [Service Testing](./service-testing.md)
|
||||||
#### [Controller Testing](./controller-testing.md)
|
#### [Controller Testing](./controller-testing.md)
|
||||||
|
#### [API Key Authentication](./api-key-auth.md)
|
@ -10,6 +10,10 @@ hero:
|
|||||||
text: .NET
|
text: .NET
|
||||||
link: /dotnet/
|
link: /dotnet/
|
||||||
|
|
||||||
|
- theme: alt
|
||||||
|
text: Angular
|
||||||
|
link: /angular/
|
||||||
|
|
||||||
- theme: alt
|
- theme: alt
|
||||||
text: Ansible
|
text: Ansible
|
||||||
link: /ansible/
|
link: /ansible/
|
||||||
@ -18,6 +22,10 @@ hero:
|
|||||||
text: CSS
|
text: CSS
|
||||||
link: /css/
|
link: /css/
|
||||||
|
|
||||||
|
- theme: alt
|
||||||
|
text: Docker
|
||||||
|
link: /docker/
|
||||||
|
|
||||||
- theme: alt
|
- theme: alt
|
||||||
text: EF Core
|
text: EF Core
|
||||||
link: /ef-core/
|
link: /ef-core/
|
||||||
|
Reference in New Issue
Block a user