Skip to content

Commit d65b2a2

Browse files
committed
Add backend-relayed chat mode and supporting API/services
Introduce "Chat via Backend" feature allowing chat messages to be sent via HTTP to the backend, which then relays them to all SignalR clients. Add ChatBackend.razor page, BackendChatApiClient service, ChatController API, and ChatMessageRequest model. Update navigation and refactor Chat.razor for improved hub URL handling and logging. Add example HTTP request for backend chat API. Enables both direct SignalR and HTTP-to-backend chat modes.
1 parent d376f62 commit d65b2a2

9 files changed

Lines changed: 241 additions & 2 deletions

File tree

Observability_WebAPI_Blazor/Observability_WebAPI_Blazor.Client/Pages/Chat.razor

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@
3838

3939
protected override async Task OnInitializedAsync()
4040
{
41+
var hubUri = GetHubUri();
4142
hubConnection = new HubConnectionBuilder()
42-
.WithUrl(Navigation.ToAbsoluteUri("/blazorChathub"))
43+
.WithUrl(hubUri)
4344
.ConfigureLogging(logging =>
4445
{
4546
logging.SetMinimumLevel(LogLevel.Information);
@@ -73,6 +74,7 @@
7374

7475
try
7576
{
77+
Logger.LogInformation("Hub url - {HubUri}", hubUri);
7678
await hubConnection.StartAsync();
7779
Logger.LogInformation(
7880
"SignalR connection started. ConnectionId: {ConnectionId}",
@@ -85,6 +87,18 @@
8587
}
8688
}
8789

90+
private Uri GetHubUri()
91+
{
92+
const string hubPath = "/blazorChatHub";
93+
94+
if (!OperatingSystem.IsBrowser())
95+
{
96+
return new Uri($"http://web:8080{hubPath}");
97+
}
98+
99+
return Navigation.ToAbsoluteUri(hubPath);
100+
}
101+
88102
private async Task Send()
89103
{
90104
if (hubConnection is not null)
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
@page "/chat-backend"
2+
@rendermode @(new InteractiveAutoRenderMode(prerender: true))
3+
@using Microsoft.AspNetCore.SignalR.Client
4+
@inject HttpClient HttpClient
5+
@inject Observability_WebAPI_Blazor.Client.Services.BackendChatApiClient ChatApi
6+
@inject ILogger<ChatBackend> Logger
7+
@implements IAsyncDisposable
8+
9+
<PageTitle>Chat (Backend)</PageTitle>
10+
11+
<h1>Chat (via Backend)</h1>
12+
13+
<div class="form-group">
14+
<label>
15+
User:
16+
<input @bind="userInput" />
17+
</label>
18+
</div>
19+
<div class="form-group">
20+
<label>
21+
Message:
22+
<input @bind="messageInput" size="50" />
23+
</label>
24+
</div>
25+
26+
<button @onclick="Send" disabled="@(!IsConnected || isSending)">Send</button>
27+
28+
<hr>
29+
30+
<ul id="messagesList">
31+
@foreach (var message in messages)
32+
{
33+
<li>@message</li>
34+
}
35+
</ul>
36+
37+
@code {
38+
private HubConnection? hubConnection;
39+
private readonly List<string> messages = [];
40+
private string? userInput;
41+
private string? messageInput;
42+
private bool isSending;
43+
44+
protected override async Task OnInitializedAsync()
45+
{
46+
if (HttpClient.BaseAddress is null)
47+
{
48+
throw new InvalidOperationException("HttpClient BaseAddress is not configured.");
49+
}
50+
51+
var hubUri = new Uri(HttpClient.BaseAddress, "hubs/chat");
52+
53+
hubConnection = new HubConnectionBuilder()
54+
.WithUrl(hubUri)
55+
.ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Information))
56+
.Build();
57+
58+
hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
59+
{
60+
var encodedMsg = $"{user}: {message}";
61+
messages.Add(encodedMsg);
62+
63+
Logger.LogInformation(
64+
"Backend SignalR message received. User: {User}, MessageLength: {MessageLength}",
65+
user,
66+
message?.Length ?? 0);
67+
68+
InvokeAsync(StateHasChanged);
69+
});
70+
71+
hubConnection.Closed += exception =>
72+
{
73+
if (exception is null)
74+
{
75+
Logger.LogInformation("Backend SignalR connection closed.");
76+
}
77+
else
78+
{
79+
Logger.LogWarning(exception, "Backend SignalR connection closed with error.");
80+
}
81+
82+
return Task.CompletedTask;
83+
};
84+
85+
try
86+
{
87+
Logger.LogInformation("Hub url - {HubUri}", hubUri);
88+
89+
await hubConnection.StartAsync();
90+
91+
Logger.LogInformation(
92+
"Backend SignalR connection started. ConnectionId: {ConnectionId}",
93+
hubConnection.ConnectionId);
94+
}
95+
catch (Exception ex)
96+
{
97+
Logger.LogError(ex, "Backend SignalR connection failed to start.");
98+
throw;
99+
}
100+
}
101+
102+
private async Task Send()
103+
{
104+
if (isSending)
105+
{
106+
return;
107+
}
108+
109+
if (HttpClient.BaseAddress is null)
110+
{
111+
throw new InvalidOperationException("HttpClient BaseAddress is not configured.");
112+
}
113+
114+
isSending = true;
115+
116+
try
117+
{
118+
var response = await ChatApi.SendAsync(userInput, messageInput);
119+
120+
if (!response.IsSuccessStatusCode)
121+
{
122+
Logger.LogError(
123+
"Failed to send chat message via HTTP. StatusCode: {StatusCode}",
124+
response.StatusCode);
125+
}
126+
}
127+
finally
128+
{
129+
isSending = false;
130+
}
131+
}
132+
133+
public bool IsConnected => hubConnection?.State == HubConnectionState.Connected;
134+
135+
public async ValueTask DisposeAsync()
136+
{
137+
if (hubConnection is not null)
138+
{
139+
await hubConnection.DisposeAsync();
140+
}
141+
}
142+
}

Observability_WebAPI_Blazor/Observability_WebAPI_Blazor.Client/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
22
using Observability_WebAPI_Blazor.Client;
3+
using Observability_WebAPI_Blazor.Client.Services;
34

45
var builder = WebAssemblyHostBuilder.CreateDefault(args);
56

@@ -21,4 +22,6 @@
2122
return factory.CreateClient("Backend");
2223
});
2324

25+
builder.Services.AddScoped<BackendChatApiClient>();
26+
2427
await builder.Build().RunAsync();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Net.Http.Json;
2+
3+
namespace Observability_WebAPI_Blazor.Client.Services;
4+
5+
public sealed class BackendChatApiClient
6+
{
7+
private readonly HttpClient _httpClient;
8+
9+
public BackendChatApiClient(HttpClient httpClient)
10+
{
11+
_httpClient = httpClient;
12+
}
13+
14+
public Task<HttpResponseMessage> SendAsync(string? user, string? message, CancellationToken cancellationToken = default)
15+
{
16+
return _httpClient.PostAsJsonAsync(
17+
"api/chat/send",
18+
new { User = user, Message = message },
19+
cancellationToken);
20+
}
21+
}

Observability_WebAPI_Blazor/Observability_WebAPI_Blazor/Components/Layout/NavMenu.razor

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@
4040

4141
<div class="nav-item px-3">
4242
<NavLink class="nav-link" href="chat">
43-
<span class="bi bi-chat-dots-fill-nav-menu" aria-hidden="true"></span> Blazor Chat
43+
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Blazor Chat
44+
</NavLink>
45+
</div>
46+
47+
<div class="nav-item px-3">
48+
<NavLink class="nav-link" href="chat-backend">
49+
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Chat via Backend
4450
</NavLink>
4551
</div>
4652
</nav>

Observability_WebAPI_Blazor/Observability_WebAPI_Blazor/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.AspNetCore.SignalR;
33
using Microsoft.Extensions.Options;
44
using Observability_WebAPI_Blazor.Client;
5+
using Observability_WebAPI_Blazor.Client.Services;
56
using Observability_WebAPI_Blazor.Components;
67
using Observability_WebAPI_Blazor.Hubs;
78
using OpenTelemetry.Resources;
@@ -87,6 +88,8 @@
8788
});
8889
});
8990

91+
builder.Services.AddScoped<BackendChatApiClient>();
92+
9093
var app = builder.Build();
9194

9295
app.UseResponseCompression();
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Microsoft.AspNetCore.SignalR;
3+
using Observability_WebApi_Blazor.Backend.Hubs;
4+
using Observability_WebApi_Blazor.Backend.Models;
5+
6+
namespace Observability_WebApi_Blazor.Backend.Controllers;
7+
8+
[ApiController]
9+
[Route("api/[controller]")]
10+
public sealed class ChatController : ControllerBase
11+
{
12+
private readonly IHubContext<ChatHub> _hubContext;
13+
private readonly ILogger<ChatController> _logger;
14+
15+
public ChatController(IHubContext<ChatHub> hubContext, ILogger<ChatController> logger)
16+
{
17+
_hubContext = hubContext;
18+
_logger = logger;
19+
}
20+
21+
[HttpPost("send")]
22+
public async Task<IActionResult> Send([FromBody] ChatMessageRequest request, CancellationToken cancellationToken)
23+
{
24+
_logger.LogInformation(
25+
"Chat message received via HTTP. User: {User}, MessageLength: {MessageLength}",
26+
request.User,
27+
request.Message?.Length ?? 0);
28+
29+
await _hubContext.Clients.All.SendAsync(
30+
"ReceiveMessage",
31+
request.User ?? string.Empty,
32+
request.Message ?? string.Empty,
33+
cancellationToken);
34+
35+
return Ok();
36+
}
37+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace Observability_WebApi_Blazor.Backend.Models;
2+
3+
public sealed record ChatMessageRequest(string? User, string? Message);

Observability_WebApi_Blazor.Backend/Observability_WebApi_Blazor.Backend.http

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,13 @@ GET {{Observability_WebApi_Blazor.Backend_HostAddress}}/weatherforecast/
44
Accept: application/json
55

66
###
7+
8+
POST {{Observability_WebApi_Blazor.Backend_HostAddress}}/api/chat/send
9+
Content-Type: application/json
10+
11+
{
12+
"user": "user1",
13+
"message": "hello from http"
14+
}
15+
16+
###

0 commit comments

Comments
 (0)