post: add initial notes
This commit is contained in:
+292
@@ -0,0 +1,292 @@
|
||||
```json meta
|
||||
{
|
||||
"title": "Providing Real-Time Feedback About Long-Running Task with SignalR",
|
||||
"lead": "",
|
||||
"isPublished": true,
|
||||
"publishedAt": "2024-04-15",
|
||||
"openGraphImage": "posts/providing-real-time-feedback-about-long-running-task-with-signal-r/og-image.png",
|
||||
}
|
||||
```
|
||||
|
||||
There are times when a user takes an action in your system that requires it to run in the background.
|
||||
|
||||
Things like:
|
||||
|
||||
- Generating a report
|
||||
- Processing a large file
|
||||
- Running a complex algorithm
|
||||
- Sending a large number of emails
|
||||
|
||||
In these cases you don't want to hold your user hostage by making them wait for the task to complete before giving a response.
|
||||
|
||||
Instead you'd rather immediately acknowledge the user's request, add the task to some kind of queue, and then give the user feedback about how that task is progressing.
|
||||
|
||||
This leaves it up to the user if they want to wait for the task to complete or if they want to navigate away and come back later.
|
||||
|
||||
Traditionally you could provide this feedback with long polling, but websockets offer a way to open a persistent connection between the client and the server. The server then can push updates to the client in real-time.
|
||||
|
||||
SignalR is a library that makes it easy to add real-time web functionality to your applications. It's built on top of websockets and abstracts away the complexity of managing connections. Plus as a fallback it will use polling if websockets aren't available.
|
||||
|
||||
How to set this up?
|
||||
|
||||
- Let's build a simple client using vue
|
||||
- Let's build a simple server using a .NET web api
|
||||
|
||||
The web api will have a singleton service that manages an in-memory queue.
|
||||
The web api will have a hosted background service that processes the queue and sends updates to the clients.
|
||||
The web api will have an endpoint to add a task to the queue.
|
||||
The web api will have a hub that the clients can connect to to receive updates.
|
||||
The client will have a form to add a task to the queue.
|
||||
The client will then redirect to an edit page where it will connect to the hub and receive updates.
|
||||
The task will have the following states:
|
||||
|
||||
- Pending
|
||||
- Processing
|
||||
- Completed
|
||||
- Failed
|
||||
|
||||
The client will display the current state of the task and any messages that are sent with the updates.
|
||||
|
||||
## Setting up client
|
||||
|
||||
```sh
|
||||
npm create vue@latest
|
||||
```
|
||||
|
||||
Answer the questions:
|
||||
|
||||
```sh
|
||||
✔ Project name: … client
|
||||
✔ Add TypeScript? … Yes
|
||||
✔ Add JSX Support? … No
|
||||
✔ Add Vue Router for Single Page Application development? … Yes
|
||||
✔ Add Pinia for state management? … Yes
|
||||
✔ Add Vitest for Unit testing? … Yes
|
||||
✔ Add an End-to-End Testing Solution? … Playwright
|
||||
✔ Add ESLint for code quality? … Yes
|
||||
✔ Add Prettier for code formatting? … Yes
|
||||
✔ Add Vue DevTools 7 extension for debugging? (experimental) … Yes
|
||||
```
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```sh
|
||||
cd client
|
||||
npm install
|
||||
```
|
||||
|
||||
Setup https in development:
|
||||
|
||||
```sh
|
||||
npm install --save-dev vite-plugin-mkcert
|
||||
```
|
||||
|
||||
Edit vite config:
|
||||
|
||||
```ts
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import mkcert from'vite-plugin-mkcert'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
https: true,
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
VueDevTools(),
|
||||
mkcert(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Run client in dev
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Setting up server
|
||||
|
||||
```sh
|
||||
mkdir server
|
||||
cd server
|
||||
dotnet new webapi -o Server.API
|
||||
dotnet new xunit -o Server.Tests
|
||||
dotnet add Server.API reference Server.Tests
|
||||
dotnet new sln -n Server
|
||||
dotnet sln add Server.API
|
||||
dotnet sln add Server.Tests
|
||||
```
|
||||
|
||||
Update launchSettings.json so it runs on https:
|
||||
|
||||
```json
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:7138;http://localhost:5031",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Update Program.cs so swagger ui launches on root:
|
||||
|
||||
```csharp
|
||||
app.UseSwaggerUI(config =>
|
||||
{
|
||||
config.SwaggerEndpoint("/swagger/v1/swagger.json", "Server API");
|
||||
config.RoutePrefix = string.Empty;
|
||||
});
|
||||
```
|
||||
|
||||
Update program.cs so client can make CORS requests:
|
||||
|
||||
```csharp
|
||||
...
|
||||
builder.Services.AddCors(
|
||||
options =>
|
||||
options.AddDefaultPolicy(
|
||||
builder => builder
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
)
|
||||
);
|
||||
...
|
||||
app.UseCors();
|
||||
```
|
||||
|
||||
Run server in watch mode for development:
|
||||
|
||||
```sh
|
||||
dotnet watch --project Server.API
|
||||
```
|
||||
|
||||
## Setup debugging
|
||||
|
||||
If you want to debug either the client or the server you most definitely can. I've set the my repo to do this using visual studio code.
|
||||
|
||||
## Add client code to add task to queue
|
||||
|
||||
Just some styling to make things centered.
|
||||
|
||||
In base.css add display flex to body
|
||||
|
||||
Replace app.css with:
|
||||
|
||||
```css
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
```
|
||||
|
||||
Replace App.vue with:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
|
||||
const addTaskStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||
|
||||
async function addTask() {
|
||||
try {
|
||||
addTaskStatus.value = 'pending';
|
||||
const result = await fetch('https://localhost:7138/add-task', { method: 'POST' });
|
||||
|
||||
if (result.ok === false) {
|
||||
addTaskStatus.value = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
addTaskStatus.value = 'success';
|
||||
} catch (error) {
|
||||
addTaskStatus.value = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div class="add-task-container">
|
||||
<button @click="addTask" type="button">Add Task</button>
|
||||
<Transition mode="out-in">
|
||||
<div v-if="addTaskStatus === 'pending'">Adding task...</div>
|
||||
<div v-else-if="addTaskStatus === 'success'">Task added!</div>
|
||||
<div v-else-if="addTaskStatus === 'error'">Failed to add task</div>
|
||||
<div v-else>Click the button to add a task</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-task-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Add server code to add task to queue
|
||||
|
||||
Update program.cs to remove all references to weather forecast.
|
||||
|
||||
Change weatherforecast endpoint to add-task endpoint. Map as post method That just returns new anonymous object with id property set to new guid.
|
||||
|
||||
```csharp
|
||||
app
|
||||
.MapPost("/add-task", () =>
|
||||
{
|
||||
return new { Id = Guid.NewGuid().ToString() };
|
||||
})
|
||||
.WithName("AddTask")
|
||||
.WithDisplayName("Add Task")
|
||||
.WithDescription("Add a new task to the queue")
|
||||
.WithOpenApi();
|
||||
```
|
||||
|
||||
Run client and server. Click the add task button. You should see a success message with id of the task just added.
|
||||
|
||||
### Implement the task queue
|
||||
|
||||
### Implement the task service
|
||||
|
||||
### Update add task endpoint to add task to queue
|
||||
|
||||
### Let's add signal r hub
|
||||
|
||||
### Add client code to establish connection to server hub
|
||||
|
||||
```sh
|
||||
npm install @microsoft/signalr
|
||||
```
|
||||
Reference in New Issue
Block a user