From d6b0a6619cb207b9fe83a934b43cf80f5dac482b Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Tue, 16 Apr 2024 20:53:30 -0500 Subject: [PATCH] post: add initial notes --- .../index.md | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 src/Blog/wwwroot/posts/providing-real-time-feedback-about-long-running-task-with-signal-r/index.md diff --git a/src/Blog/wwwroot/posts/providing-real-time-feedback-about-long-running-task-with-signal-r/index.md b/src/Blog/wwwroot/posts/providing-real-time-feedback-about-long-running-task-with-signal-r/index.md new file mode 100644 index 0000000..a425cc3 --- /dev/null +++ b/src/Blog/wwwroot/posts/providing-real-time-feedback-about-long-running-task-with-signal-r/index.md @@ -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 + + + + + +``` + +## 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 +```