Skip to content

Commit 24d171e

Browse files
authored
Merge pull request #184417 from JialinXin/awps-chatapp-middleware
[WebPubSub] Update c# sample to use middleware.
2 parents 086bc4e + b539e2a commit 24d171e

File tree

1 file changed

+69
-130
lines changed

1 file changed

+69
-130
lines changed

articles/azure-web-pubsub/tutorial-build-chat.md

Lines changed: 69 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ First let's create an empty ASP.NET Core app.
7676

7777
```bash
7878
dotnet new web
79-
dotnet add package Azure.Messaging.WebPubSub
79+
dotnet add package Microsoft.Azure.WebPubSub.AspNetCore --version 1.0.0-beta.3
8080
```
8181

8282
2. Then add `app.UseStaticFiles();` before `app.UseRouting();` in `Startup.cs` to support static files. Remove the default `endpoints.MapGet` inside `app.UseEndpoints`.
@@ -90,7 +90,6 @@ First let's create an empty ASP.NET Core app.
9090
}
9191

9292
app.UseStaticFiles();
93-
9493
app.UseRouting();
9594

9695
app.UseEndpoints(endpoints =>
@@ -114,25 +113,44 @@ You can test the server by running `dotnet run --urls http://localhost:8080` and
114113
115114
You may remember in the [publish and subscribe message tutorial](./tutorial-pub-sub-messages.md) the subscriber uses an API in Web PubSub SDK to generate an access token from connection string and use it to connect to the service. This is usually not safe in a real world application as connection string has high privilege to do any operation to the service so you don't want to share it with any client. Let's change this access token generation process to a REST API at server side, so client can call this API to request an access token every time it needs to connect, without need to hold the connection string.
116115
117-
1. Install dependencies
118-
Install dependencies and use [Secret Manager](/aspnet/core/security/app-secrets#secret-manager) tool for .NET Core to set the connection string. Run the below command, replacing `<connection_string>` with the one fetched in [previous step](#get-the-connectionstring-for-future-use)
116+
1. Install dependencies.
119117
120118
```bash
121119
dotnet add package Microsoft.Extensions.Azure
122120
```
123121
124-
2. DI the service client inside `ConfigureServices` and don't forget to replace `<connection_string>` with the one of your services.
122+
2. Add a `SampleChatHub` class to handle hub events. And DI the service middleware and service client inside `ConfigureServices()`. Don't forget to replace `<connection_string>` with the one of your services.
125123
126124
```csharp
127125
public void ConfigureServices(IServiceCollection services)
128126
{
129-
services.AddAzureClients(builder =>
127+
services.AddWebPubSub(o => o.ServiceEndpoint = new ServiceEndpoint("<connection_string>"))
128+
.AddWebPubSubServiceClient<SampleChatHub>();
129+
}
130+
131+
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
132+
{
133+
if (env.IsDevelopment())
134+
{
135+
app.UseDeveloperExceptionPage();
136+
}
137+
138+
app.UseStaticFiles();
139+
app.UseRouting();
140+
141+
app.UseEndpoints(endpoints =>
130142
{
131-
builder.AddWebPubSubServiceClient("<connection_string>", "chat");
132143
});
133144
}
145+
146+
private sealed class SampleChatHub : WebPubSubHub
147+
{
148+
}
134149
```
135-
2. Add a `/negotiate` API to the server inside `app.UseEndpoints` to generate the token
150+
151+
`AddWebPubSubServiceClient<THub>()` is used to inject the service client `WebPubSubServiceClient<THub>`, with which we can use in negotiation step to generate client connection token and in hub methods to invoke service REST APIs when hub events are triggered.
152+
153+
3. Add a `/negotiate` API to the server inside `app.UseEndpoints` to generate the token.
136154
137155
```csharp
138156
app.UseEndpoints(endpoints =>
@@ -146,7 +164,7 @@ You may remember in the [publish and subscribe message tutorial](./tutorial-pub-
146164
await context.Response.WriteAsync("missing user id");
147165
return;
148166
}
149-
var serviceClient = context.RequestServices.GetRequiredService<Azure.Messaging.WebPubSub.WebPubSubServiceClient>();
167+
var serviceClient = context.RequestServices.GetRequiredService<WebPubSubServiceClient<SampleChatHub>>();
150168
await context.Response.WriteAsync(serviceClient.GetClientAccessUri(userId: id).AbsoluteUri);
151169
});
152170
});
@@ -156,7 +174,7 @@ You may remember in the [publish and subscribe message tutorial](./tutorial-pub-
156174
157175
You can test this API by running `dotnet run --urls http://localhost:8080` and accessing `http://localhost:8080/negotiate?id=<user-id>` and it will give you the full url of the Azure Web PubSub with an access token.
158176
159-
3. Then update `index.html` to include the following script to get the token from server and connect to service
177+
4. Then update `index.html` to include the following script to get the token from server and connect to service.
160178
161179
```html
162180
<html>
@@ -176,8 +194,7 @@ You may remember in the [publish and subscribe message tutorial](./tutorial-pub-
176194
</html>
177195
```
178196
179-
If you are using Chrome, you can test it by opening the home page, input your user name. press F12 to open the Developer Tools window, switch to **Console** table and you'll see `connected` being printed in browser console.
180-
197+
If you are using Chrome, you can test it by opening the home page, input your user name. Press F12 to open the Developer Tools window, switch to **Console** table and you'll see `connected` being printed in browser console.
181198
182199
# [JavaScript](#tab/javascript)
183200
@@ -285,7 +302,7 @@ You may remember in the [publish and subscribe message tutorial](./tutorial-pub-
285302
</html>
286303
```
287304
288-
If you are using Chrome, you can test it by opening the home page, input your user name. press F12 to open the Developer Tools window, switch to **Console** table and you'll see `connected` being printed in browser console.
305+
If you are using Chrome, you can test it by opening the home page, input your user name. Press F12 to open the Developer Tools window, switch to **Console** table and you'll see `connected` being printed in browser console.
289306
290307
# [Java](#tab/java)
291308
@@ -459,7 +476,7 @@ You may remember in the [publish and subscribe message tutorial](./tutorial-pub-
459476
</html>
460477
```
461478
462-
If you are using Chrome, you can test it by opening the home page, input your user name. press F12 to open the Developer Tools window, switch to **Console** table and you'll see `connected` being printed in browser console.
479+
If you are using Chrome, you can test it by opening the home page, input your user name. Press F12 to open the Developer Tools window, switch to **Console** table and you'll see `connected` being printed in browser console.
463480
464481
---
465482
@@ -472,63 +489,35 @@ Events are delivered to server in the form of Webhook. Webhook is served and exp
472489
Azure Web PubSub follows [CloudEvents](./reference-cloud-events.md) to describe the event data.
473490
474491
# [C#](#tab/csharp)
475-
For now, you need to implement the event handler by your own in C#, the steps are straight forward following [the protocol spec](./reference-cloud-events.md) and illustrated below.
492+
Here we're using Web PubSub middleware SDK, there is already an implementation to parse and process CloudEvents schema, so we don't need to deal with these details. Instead, we can focus on the inner business logic in the hub methods.
476493
477494
1. Add event handlers inside `UseEndpoints`. Specify the endpoint path for the events, let's say `/eventhandler`.
478-
479-
2. First we'd like to handle the abuse protection OPTIONS requests, we check if the header contains `WebHook-Request-Origin` header, and we return the header `WebHook-Allowed-Origin`. For simplicity for demo purpose, we return `*` to allow all the origins.
480495
```csharp
481496
app.UseEndpoints(endpoints =>
482497
{
483-
// abuse protection
484-
endpoints.Map("/eventhandler/{*path}", async context =>
485-
{
486-
if (context.Request.Method == "OPTIONS")
487-
{
488-
if (context.Request.Headers["WebHook-Request-Origin"].Count > 0)
489-
{
490-
context.Response.Headers["WebHook-Allowed-Origin"] = "*";
491-
context.Response.StatusCode = 200;
492-
return;
493-
}
494-
}
495-
});
498+
endpoints.MapWebPubSubHub<SampleChatHub>("/eventhandler/{*path}");
496499
});
497500
```
498501
499-
3. Then we'd like to check if the incoming requests are the events we expect. Let's say we now care about the system `connected` event, which should contain the header `ce-type` as `azure.webpubsub.sys.connected`. We add the logic after abuse protection:
502+
2. Go the `SampleChatHub` we created in previous step. Add a constructor to work with `WebPubSubServiceClient<SampleChatHub>` so we can use to invoke service. And override `OnConnectedAsync()` method to respond when `connected` event is triggered.
500503
```csharp
501-
app.UseEndpoints(endpoints =>
504+
private sealed class SampleChatHub : WebPubSubHub
502505
{
503-
// abuse protection
504-
endpoints.Map("/eventhandler/{*path}", async context =>
506+
private readonly WebPubSubServiceClient<SampleChatHub> _serviceClient;
507+
508+
public SampleChatHub(WebPubSubServiceClient<SampleChatHub> serviceClient)
505509
{
506-
if (context.Request.Method == "OPTIONS")
507-
{
508-
if (context.Request.Headers["WebHook-Request-Origin"].Count > 0)
509-
{
510-
context.Response.Headers["WebHook-Allowed-Origin"] = "*";
511-
context.Response.StatusCode = 200;
512-
return;
513-
}
514-
}
515-
else if (context.Request.Method == "POST")
516-
{
517-
// get the userId from header
518-
var userId = context.Request.Headers["ce-userId"];
519-
if (context.Request.Headers["ce-type"] == "azure.webpubsub.sys.connected")
520-
{
521-
// the connected event
522-
Console.WriteLine($"{userId} connected");
523-
context.Response.StatusCode = 200;
524-
return;
525-
}
526-
}
527-
});
528-
});
510+
_serviceClient = serviceClient;
511+
}
512+
513+
public override async Task OnConnectedAsync(ConnectedEventRequest request)
514+
{
515+
await _serviceClient.SendToAllAsync($"[SYSTEM] {request.ConnectionContext.UserId} joined.");
516+
}
517+
}
529518
```
530519
531-
In the above code, we simply print a message to console when a client is connected. You can see we use `context.Request.Headers["ce-userId"]` so we can see the identity of the connected client.
520+
In the above code, we use the service client to broadcast a notification message to all of whom is joined.
532521
533522
# [JavaScript](#tab/javascript)
534523
@@ -629,50 +618,35 @@ Besides system events like `connected` or `disconnected`, client can also send m
629618
630619
# [C#](#tab/csharp)
631620
632-
The `ce-type` of `message` event is always `azure.webpubsub.user.message`, details see [Event message](./reference-cloud-events.md#message).
621+
Implement the `OnMessageReceivedAsync()` method in `SampleChatHub`.
633622
634-
1. Handle message event
623+
1. Handle message event.
635624
636625
```csharp
637-
app.UseEndpoints(endpoints =>
626+
private sealed class SampleChatHub : WebPubSubHub
638627
{
639-
// abuse protection
640-
endpoints.Map("/eventhandler/{*path}", async context =>
628+
private readonly WebPubSubServiceClient<SampleChatHub> _serviceClient;
629+
630+
public SampleChatHub(WebPubSubServiceClient<SampleChatHub> serviceClient)
641631
{
642-
var serviceClient = context.RequestServices.GetRequiredService<Azure.Messaging.WebPubSub.WebPubSubServiceClient>();
643-
if (context.Request.Method == "OPTIONS")
644-
{
645-
if (context.Request.Headers["WebHook-Request-Origin"].Count > 0)
646-
{
647-
context.Response.Headers["WebHook-Allowed-Origin"] = "*";
648-
context.Response.StatusCode = 200;
649-
return;
650-
}
651-
}
652-
else if (context.Request.Method == "POST")
653-
{
654-
// get the userId from header
655-
var userId = context.Request.Headers["ce-userId"];
656-
if (context.Request.Headers["ce-type"] == "azure.webpubsub.sys.connected")
657-
{
658-
// the connected event
659-
Console.WriteLine($"{userId} connected");
660-
context.Response.StatusCode = 200;
661-
return;
662-
}
663-
else if (context.Request.Headers["ce-type"] == "azure.webpubsub.user.message")
664-
{
665-
using var stream = new StreamReader(context.Request.Body);
666-
await serviceClient.SendToAllAsync($"[{userId}] {await stream.ReadToEndAsync()}");
667-
context.Response.StatusCode = 200;
668-
return;
669-
}
670-
}
671-
});
672-
});
632+
_serviceClient = serviceClient;
633+
}
634+
635+
public override async Task OnConnectedAsync(ConnectedEventRequest request)
636+
{
637+
await _serviceClient.SendToAllAsync($"[SYSTEM] {request.ConnectionContext.UserId} joined.");
638+
}
639+
640+
public override async ValueTask<UserEventResponse> OnMessageReceivedAsync(UserEventRequest request, CancellationToken cancellationToken)
641+
{
642+
await _serviceClient.SendToAllAsync($"[{request.ConnectionContext.UserId}] {request.Data}");
643+
644+
return request.CreateResponse($"[SYSTEM] ack."));
645+
}
646+
}
673647
```
674648
675-
This event handler uses `WebPubSubServiceClient.SendToAllAsync()` to broadcast the received message to all clients.
649+
This event handler uses `WebPubSubServiceClient.SendToAllAsync()` to broadcast the received message to all clients. You can see in the end we returned `UserEventResponse`, which contains a message directly to the caller and make the WebHook request success. If you have extra logic to validate and would like to break this call, you can throw an exception here. The middleware will deliver the exception message to service and service will drop current client connection.
676650
677651
2. Update `index.html` to add the logic to send message from user to server and display received messages in the page.
678652
@@ -713,41 +687,6 @@ The `ce-type` of `message` event is always `azure.webpubsub.user.message`, detai
713687
714688
You can see in the above code we use `WebSocket.send()` to send message and `WebSocket.onmessage` to listen to message from service.
715689
716-
3. Finally update the `onConnected` handler to broadcast the connected event to all clients so they can see who joined the chat room.
717-
718-
```csharp
719-
app.UseEndpoints(endpoints =>
720-
{
721-
var serviceClient = context.RequestServices.GetRequiredService<Azure.Messaging.WebPubSub.WebPubSubServiceClient>();
722-
// abuse protection
723-
endpoints.Map("/eventhandler/{*path}", async context =>
724-
{
725-
if (context.Request.Method == "OPTIONS")
726-
{
727-
if (context.Request.Headers["WebHook-Request-Origin"].Count > 0)
728-
{
729-
context.Response.Headers["WebHook-Allowed-Origin"] = "*";
730-
context.Response.StatusCode = 200;
731-
return;
732-
}
733-
}
734-
else if (context.Request.Method == "POST")
735-
{
736-
// get the userId from header
737-
var userId = context.Request.Headers["ce-userId"];
738-
if (context.Request.Headers["ce-type"] == "azure.webpubsub.sys.connected")
739-
{
740-
// the connected event
741-
Console.WriteLine($"{userId} connected");
742-
await serviceClient.SendToAllAsync($"[SYSTEM] {userId} joined.");
743-
context.Response.StatusCode = 200;
744-
return;
745-
}
746-
}
747-
});
748-
});
749-
```
750-
751690
Now run the server using `dotnet run --urls http://localhost:8080` and open multiple browser instances to access http://localhost:8080/index.html, then you can chat with each other.
752691
753692
The complete code sample of this tutorial can be found [here][code-csharp].

0 commit comments

Comments
 (0)