From be2599218d5d27453c7876ff8a993e9ac7ac08e6 Mon Sep 17 00:00:00 2001 From: Frederic Beckmann Date: Thu, 6 Feb 2025 22:21:53 +0100 Subject: [PATCH] Add SignalR integration for real-time weather updates Implemented SignalR to enable real-time communication between the server and connected clients. Added a new hub (`WeatherUpdateHub`), a background service (`SignalRSendService`), and modified both the API backend and React frontend for seamless message broadcasting and handling. --- .idea/.idea.ASPReactDemo/.idea/.gitignore | 13 ++++ .idea/.idea.ASPReactDemo/.idea/encodings.xml | 4 ++ .../.idea/git_toolbox_blame.xml | 6 ++ .../.idea.ASPReactDemo/.idea/indexLayout.xml | 8 +++ .idea/.idea.ASPReactDemo/.idea/vcs.xml | 6 ++ Api/Api.csproj | 1 + Api/Program.cs | 16 ++++- Api/SignalR/SignalRSendService.cs | 31 +++++++++ Api/SignalR/WeatherUpdateHub.cs | 14 ++++ react-client/package.json | 1 + react-client/src/App.tsx | 49 ++++++++++--- react-client/yarn.lock | 68 ++++++++++++++++++- 12 files changed, 202 insertions(+), 15 deletions(-) create mode 100644 .idea/.idea.ASPReactDemo/.idea/.gitignore create mode 100644 .idea/.idea.ASPReactDemo/.idea/encodings.xml create mode 100644 .idea/.idea.ASPReactDemo/.idea/git_toolbox_blame.xml create mode 100644 .idea/.idea.ASPReactDemo/.idea/indexLayout.xml create mode 100644 .idea/.idea.ASPReactDemo/.idea/vcs.xml create mode 100644 Api/SignalR/SignalRSendService.cs create mode 100644 Api/SignalR/WeatherUpdateHub.cs diff --git a/.idea/.idea.ASPReactDemo/.idea/.gitignore b/.idea/.idea.ASPReactDemo/.idea/.gitignore new file mode 100644 index 0000000..6075abc --- /dev/null +++ b/.idea/.idea.ASPReactDemo/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/projectSettingsUpdater.xml +/contentModel.xml +/modules.xml +/.idea.ASPReactDemo.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.ASPReactDemo/.idea/encodings.xml b/.idea/.idea.ASPReactDemo/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.ASPReactDemo/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.ASPReactDemo/.idea/git_toolbox_blame.xml b/.idea/.idea.ASPReactDemo/.idea/git_toolbox_blame.xml new file mode 100644 index 0000000..7dc1249 --- /dev/null +++ b/.idea/.idea.ASPReactDemo/.idea/git_toolbox_blame.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/.idea.ASPReactDemo/.idea/indexLayout.xml b/.idea/.idea.ASPReactDemo/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.ASPReactDemo/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.ASPReactDemo/.idea/vcs.xml b/.idea/.idea.ASPReactDemo/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/.idea.ASPReactDemo/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Api/Api.csproj b/Api/Api.csproj index e06d179..d41d42e 100644 --- a/Api/Api.csproj +++ b/Api/Api.csproj @@ -8,6 +8,7 @@ + diff --git a/Api/Program.cs b/Api/Program.cs index 30fb870..443aa8b 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -1,9 +1,13 @@ +using Api.SignalR; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApi(); +builder.Services.AddSignalR(); +builder.Services.AddHostedService(); builder.Services.AddOpenApiDocument(config => { config.Title = "NSwag Demo API"; @@ -12,9 +16,10 @@ builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { - policy.AllowAnyOrigin() + policy.WithOrigins("http://localhost:3000") // Specify the allowed origin .AllowAnyHeader() - .AllowAnyMethod(); + .AllowAnyMethod() + .AllowCredentials(); // Allow credentials }); }); @@ -36,7 +41,7 @@ var summaries = new[] "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; -app.MapGet("/weatherforecast", () => +app.MapGet("/weatherforecast", async () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast @@ -46,10 +51,15 @@ app.MapGet("/weatherforecast", () => summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); + return forecast; }) .WithName("GetWeatherForecast"); + +// Add SignalR hub +app.MapHub("/WeatherUpdateHub"); + app.Run(); record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) diff --git a/Api/SignalR/SignalRSendService.cs b/Api/SignalR/SignalRSendService.cs new file mode 100644 index 0000000..ef63451 --- /dev/null +++ b/Api/SignalR/SignalRSendService.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.SignalR; + +namespace Api.SignalR +{ + public class SignalRSendService(IHubContext hubContext, ILogger logger) : BackgroundService + { + + #region Override ExecuteAsync + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + // For example, wait 10 seconds between messages. + await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); + + // Log or do any work here. + logger.LogInformation("Background service sending message at: {Time}", DateTime.Now); + + // Send a message to all connected clients. + await hubContext.Clients.All.SendAsync( + "ReceiveMessage", // This is the client method name. + "Background Service", // Example sender. + "Hello from the background task!", // The message. + stoppingToken); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Api/SignalR/WeatherUpdateHub.cs b/Api/SignalR/WeatherUpdateHub.cs new file mode 100644 index 0000000..aeedd07 --- /dev/null +++ b/Api/SignalR/WeatherUpdateHub.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.SignalR; + +namespace Api.SignalR +{ + public class WeatherUpdateHub: Hub + { + // This method can be called by connected clients. + public async Task SendMessage(string user, string message) + { + // Broadcast the message to all connected clients. + await Clients.All.SendAsync("ReceiveMessage", user, message); + } + } +} \ No newline at end of file diff --git a/react-client/package.json b/react-client/package.json index def4713..efdb499 100644 --- a/react-client/package.json +++ b/react-client/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@microsoft/signalr": "^8.0.7", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.2.1", diff --git a/react-client/src/App.tsx b/react-client/src/App.tsx index a703036..c8467a5 100644 --- a/react-client/src/App.tsx +++ b/react-client/src/App.tsx @@ -2,26 +2,53 @@ import React, { useEffect, useState } from 'react'; import logo from './logo.svg'; import './App.css'; import { Client, WeatherForecast } from './ApiCient'; +import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr'; function App() { const [forecast, setForecast] = useState([]); - + const [connection, setConnection] = useState(null); useEffect(() => { - const client = new Client(); + const newConnection = new HubConnectionBuilder() + .withUrl('http://localhost:5175/WeatherUpdateHub') // adjust the URL/port as needed + .withAutomaticReconnect() + .build(); - const fetchForecast = async () => { - const forecast = await client.getWeatherForecast(); - setForecast(forecast); - }; - - fetchForecast(); - const intervalId = setInterval(fetchForecast, 1000); - - return () => clearInterval(intervalId); + setConnection(newConnection); }, []); + useEffect(() => { + if (connection) { + connection + .start() + .then(() => { + console.log('Connected to SignalR hub!'); + connection.on('ReceiveMessage', (user, message) => { + console.log('Received message:', user, message); + }); + }) + .catch(error => console.error('SignalR Connection Error: ', error)); + } + }, [connection]); + + + + + // useEffect(() => { + // const client = new Client(); + + // const fetchForecast = async () => { + // const forecast = await client.getWeatherForecast(); + // setForecast(forecast); + // }; + + // fetchForecast(); + // const intervalId = setInterval(fetchForecast, 1000); + + // return () => clearInterval(intervalId); + // }, []); + return (
diff --git a/react-client/yarn.lock b/react-client/yarn.lock index 5a4b992..9d39afb 100644 --- a/react-client/yarn.lock +++ b/react-client/yarn.lock @@ -1557,6 +1557,17 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== +"@microsoft/signalr@^8.0.7": + version "8.0.7" + resolved "https://registry.yarnpkg.com/@microsoft/signalr/-/signalr-8.0.7.tgz#94419ddbf9418753e493f4ae4c13990316ec2ea5" + integrity sha512-PHcdMv8v5hJlBkRHAuKG5trGViQEkPYee36LnJQx4xHOQ5LL4X0nEWIxOp5cCtZ7tu+30quz5V3k0b1YNuc6lw== + dependencies: + abort-controller "^3.0.0" + eventsource "^2.0.2" + fetch-cookie "^2.0.3" + node-fetch "^2.6.7" + ws "^7.4.5" + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -2639,6 +2650,13 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -4712,6 +4730,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -4722,6 +4745,11 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +eventsource@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-2.0.2.tgz#76dfcc02930fb2ff339520b6d290da573a9e8508" + integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA== + execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -4852,6 +4880,14 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fetch-cookie@^2.0.3: + version "2.2.0" + resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-2.2.0.tgz#01086b6b5b1c3e08f15ffd8647b02ca100377365" + integrity sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ== + dependencies: + set-cookie-parser "^2.4.8" + tough-cookie "^4.0.0" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -7069,6 +7105,13 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== +node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-forge@^1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -8769,6 +8812,11 @@ serve-static@1.16.2: parseurl "~1.3.3" send "0.19.0" +set-cookie-parser@^2.4.8: + version "2.7.1" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943" + integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== + set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -9480,6 +9528,11 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + tryer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" @@ -9799,6 +9852,11 @@ web-vitals@^2.1.0: resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c" integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -9950,6 +10008,14 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" @@ -10250,7 +10316,7 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@^7.4.6: +ws@^7.4.5, ws@^7.4.6: version "7.5.10" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==