Skip to content

Commit 2cbcfd0

Browse files
authored
[WebPubSub] Adding feature support (Azure#32462)
1 parent 12f8841 commit 2cbcfd0

File tree

9 files changed

+483
-25
lines changed

9 files changed

+483
-25
lines changed

sdk/webpubsub/Azure.Messaging.WebPubSub/CHANGELOG.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
# Release History
22

3-
## 1.3.0-beta.1 (Unreleased)
3+
## 1.3.0 (2022-11-20)
44

55
### Features Added
66

7-
### Breaking Changes
8-
9-
### Bugs Fixed
10-
11-
### Other Changes
7+
- Added method `serviceClient.RemoveConnectionFromAllGroups` to remove the connection from all the groups it is in.
8+
- Added a `groups` option in `serviceClient.GetClientAccessUri`, to enable connections join initial groups once it is connected.
9+
- Added a `filter` parameter when sending messages to connections in a hub/group/user to filter out the connections recieving message, details about `filter` syntax please see [OData filter syntax for Azure Web PubSub](https://aka.ms/awps/filter-syntax).
10+
- Provided a utility class `ClientConnectionFilter` to generate the `filter` parameter, e.g. `ClientConnectionFilter.Create($"{group1} in groups and not({group2} in groups)"))`
1211

1312
## 1.2.0 (2022-11-04)
1413

sdk/webpubsub/Azure.Messaging.WebPubSub/README.md

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,27 @@ When a client is connected, it can send messages to the upstream application, or
7070

7171
## Examples
7272

73-
### Broadcast a text message to all clients
73+
### Generate the full URI containing access token for the connection to use when connects the Azure Web PubSub
74+
75+
```C# Snippet:GetClientAccessUri
76+
// Generate client access URI for userA
77+
serviceClient.GetClientAccessUri(userId: "userA");
78+
// Generate client access URI with initial permissions
79+
serviceClient.GetClientAccessUri(roles: new string[] { "webpubsub.joinLeaveGroup.group1", "webpubsub.sendToGroup.group1" });
80+
// Generate client access URI with initial groups to join when the connection connects
81+
serviceClient.GetClientAccessUri(groups: new string[] { "group1", "group2" });
82+
```
83+
84+
### Send messages to the connections
85+
#### Broadcast a text message to all clients
7486

7587
```C# Snippet:WebPubSubHelloWorld
7688
var serviceClient = new WebPubSubServiceClient(connectionString, "some_hub");
7789

7890
serviceClient.SendToAll("Hello World!");
7991
```
8092

81-
### Broadcast a JSON message to all clients
93+
#### Broadcast a JSON message to all clients
8294

8395
```C# Snippet:WebPubSubSendJson
8496
var serviceClient = new WebPubSubServiceClient(connectionString, "some_hub");
@@ -92,7 +104,7 @@ serviceClient.SendToAll(RequestContent.Create(
92104
ContentType.ApplicationJson);
93105
```
94106

95-
### Broadcast a binary message to all clients
107+
#### Broadcast a binary message to all clients
96108

97109
```C# Snippet:WebPubSubSendBinary
98110
var serviceClient = new WebPubSubServiceClient(connectionString, "some_hub");
@@ -101,6 +113,54 @@ Stream stream = BinaryData.FromString("Hello World!").ToStream();
101113
serviceClient.SendToAll(RequestContent.Create(stream), ContentType.ApplicationOctetStream);
102114
```
103115

116+
#### Broadcast messages to clients using filter
117+
Azure Web PubSub supports OData filter syntax to filter out the connections to send messages to.
118+
119+
Details about `filter` syntax please see [OData filter syntax for Azure Web PubSub](https://aka.ms/awps/filter-syntax).
120+
121+
```C# Snippet:WebPubSubSendWithFilter
122+
var serviceClient = new WebPubSubServiceClient(connectionString, "some_hub");
123+
124+
// Use filter to send text message to anonymous connections
125+
serviceClient.SendToAll(
126+
RequestContent.Create("Hello World!"),
127+
ContentType.TextPlain,
128+
filter: ClientConnectionFilter.Create($"userId eq {null}"));
129+
130+
// Use filter to send JSON message to connections in groupA but not in groupB
131+
var group1 = "GroupA";
132+
var group2 = "GroupB";
133+
serviceClient.SendToAll(RequestContent.Create(
134+
new
135+
{
136+
Foo = "Hello World!",
137+
Bar = 42
138+
}),
139+
ContentType.ApplicationJson,
140+
filter: ClientConnectionFilter.Create($"{group1} in groups and not({group2} in groups)"));
141+
```
142+
143+
### Connection management
144+
145+
#### Add connections for some user to some group:
146+
```C# Snippet:WebPubSubAddUserToGroup
147+
client.AddUserToGroup("some_group", "some_user");
148+
149+
// Avoid sending messages to users who do not exist.
150+
if (client.UserExists("some_user").Value)
151+
{
152+
client.SendToUser("some_user", "Hi, I am glad you exist!");
153+
}
154+
155+
client.RemoveUserFromGroup("some_group", "some_user");
156+
```
157+
158+
#### Remove connection from all groups
159+
```C# Snippet:WebPubSubRemoveConnectionFromAllGroups
160+
var client = new WebPubSubServiceClient(connectionString, "some_hub");
161+
client.RemoveConnectionFromAllGroups("some_connection");
162+
```
163+
104164
## Troubleshooting
105165

106166
### Setting up console logging

sdk/webpubsub/Azure.Messaging.WebPubSub/api/Azure.Messaging.WebPubSub.netstandard2.0.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
namespace Azure.Messaging.WebPubSub
22
{
3+
public static partial class ClientConnectionFilter
4+
{
5+
public static string Create(System.FormattableString filter) { throw null; }
6+
public static string Create(System.FormattableString filter, System.IFormatProvider formatProvider) { throw null; }
7+
}
38
public enum WebPubSubPermission
49
{
510
SendToGroup = 1,
@@ -33,10 +38,14 @@ public WebPubSubServiceClient(System.Uri endpoint, string hub, Azure.Core.TokenC
3338
public virtual System.Threading.Tasks.Task<Azure.Response> CloseUserConnectionsAsync(string userId, System.Collections.Generic.IEnumerable<string> excluded = null, string reason = null, Azure.RequestContext context = null) { throw null; }
3439
public virtual Azure.Response<bool> ConnectionExists(string connectionId, Azure.RequestContext context = null) { throw null; }
3540
public virtual System.Threading.Tasks.Task<Azure.Response<bool>> ConnectionExistsAsync(string connectionId, Azure.RequestContext context = null) { throw null; }
36-
public virtual System.Uri GetClientAccessUri(System.DateTimeOffset expiresAt, string userId = null, System.Collections.Generic.IEnumerable<string> roles = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
37-
public virtual System.Uri GetClientAccessUri(System.TimeSpan expiresAfter = default(System.TimeSpan), string userId = null, System.Collections.Generic.IEnumerable<string> roles = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
38-
public virtual System.Threading.Tasks.Task<System.Uri> GetClientAccessUriAsync(System.DateTimeOffset expiresAt, string userId = null, System.Collections.Generic.IEnumerable<string> roles = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
39-
public virtual System.Threading.Tasks.Task<System.Uri> GetClientAccessUriAsync(System.TimeSpan expiresAfter = default(System.TimeSpan), string userId = null, System.Collections.Generic.IEnumerable<string> roles = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
41+
public virtual System.Uri GetClientAccessUri(System.DateTimeOffset expiresAt, string userId = null, System.Collections.Generic.IEnumerable<string> roles = null, System.Collections.Generic.IEnumerable<string> groups = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
42+
public virtual System.Uri GetClientAccessUri(System.DateTimeOffset expiresAt, string userId, System.Collections.Generic.IEnumerable<string> roles, System.Threading.CancellationToken cancellationToken) { throw null; }
43+
public virtual System.Uri GetClientAccessUri(System.TimeSpan expiresAfter = default(System.TimeSpan), string userId = null, System.Collections.Generic.IEnumerable<string> roles = null, System.Collections.Generic.IEnumerable<string> groups = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
44+
public virtual System.Uri GetClientAccessUri(System.TimeSpan expiresAfter, string userId, System.Collections.Generic.IEnumerable<string> roles, System.Threading.CancellationToken cancellationToken) { throw null; }
45+
public virtual System.Threading.Tasks.Task<System.Uri> GetClientAccessUriAsync(System.DateTimeOffset expiresAt, string userId = null, System.Collections.Generic.IEnumerable<string> roles = null, System.Collections.Generic.IEnumerable<string> groups = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
46+
public virtual System.Threading.Tasks.Task<System.Uri> GetClientAccessUriAsync(System.DateTimeOffset expiresAt, string userId, System.Collections.Generic.IEnumerable<string> roles, System.Threading.CancellationToken cancellationToken) { throw null; }
47+
public virtual System.Threading.Tasks.Task<System.Uri> GetClientAccessUriAsync(System.TimeSpan expiresAfter = default(System.TimeSpan), string userId = null, System.Collections.Generic.IEnumerable<string> roles = null, System.Collections.Generic.IEnumerable<string> groups = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
48+
public virtual System.Threading.Tasks.Task<System.Uri> GetClientAccessUriAsync(System.TimeSpan expiresAfter, string userId, System.Collections.Generic.IEnumerable<string> roles, System.Threading.CancellationToken cancellationToken) { throw null; }
4049
public virtual Azure.Response GrantPermission(Azure.Messaging.WebPubSub.WebPubSubPermission permission, string connectionId, string targetName = null, Azure.RequestContext context = null) { throw null; }
4150
public virtual System.Threading.Tasks.Task<Azure.Response> GrantPermissionAsync(Azure.Messaging.WebPubSub.WebPubSubPermission permission, string connectionId, string targetName = null, Azure.RequestContext context = null) { throw null; }
4251
public virtual Azure.Response<bool> GroupExists(string group, Azure.RequestContext context = null) { throw null; }

sdk/webpubsub/Azure.Messaging.WebPubSub/src/Azure.Messaging.WebPubSub.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<PropertyGroup>
33
<Description>Azure SDK client library for the WebPubSub service</Description>
44
<AssemblyTitle>Azure SDK for WebPubSub</AssemblyTitle>
5-
<Version>1.3.0-beta.1</Version>
5+
<Version>1.3.0</Version>
66
<!--The ApiCompatVersion is managed automatically and should not generally be modified manually.-->
77
<ApiCompatVersion>1.2.0</ApiCompatVersion>
88
<PackageTags>Azure, WebPubSub, SignalR</PackageTags>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Core.GeoJson;
5+
using Azure.Core;
6+
using System.Globalization;
7+
using System.IO;
8+
using System.Text;
9+
using System;
10+
11+
namespace Azure.Messaging.WebPubSub
12+
{
13+
/// <summary>
14+
/// The ClientConnectionFilter class is used to help construct valid OData filter
15+
/// parameter to be used by Send* APIs by automatically replacing, quoting, and escaping interpolated
16+
/// parameters.
17+
/// For more information, see <see href="https://aka.ms/awps/filter-syntax">Filters in Azure Web PubSub</see>.
18+
/// </summary>
19+
public static class ClientConnectionFilter
20+
{
21+
/// <summary>
22+
/// Create an OData filter expression from an interpolated string. The
23+
/// interpolated values will be quoted and escaped as necessary.
24+
/// </summary>
25+
/// <param name="filter">An interpolated filter string.</param>
26+
/// <returns>A valid OData filter expression.</returns>
27+
public static string Create(FormattableString filter) =>
28+
Create(filter, null);
29+
30+
/// <summary>
31+
/// Create an OData filter expression from an interpolated string. The
32+
/// interpolated values will be quoted and escaped as necessary.
33+
/// </summary>
34+
/// <param name="filter">An interpolated filter string.</param>
35+
/// <param name="formatProvider">
36+
/// Format provider used to convert values to strings.
37+
/// <see cref="CultureInfo.InvariantCulture"/> is used as a default.
38+
/// </param>
39+
/// <returns>A valid OData filter expression.</returns>
40+
public static string Create(FormattableString filter, IFormatProvider formatProvider)
41+
{
42+
if (filter == null)
43+
{ return null; }
44+
formatProvider ??= CultureInfo.InvariantCulture;
45+
46+
string[] args = new string[filter.ArgumentCount];
47+
for (int i = 0; i < filter.ArgumentCount; i++)
48+
{
49+
args[i] = filter.GetArgument(i) switch
50+
{
51+
// Null
52+
null => "null",
53+
54+
// Boolean
55+
bool x => x.ToString(formatProvider).ToLowerInvariant(),
56+
57+
// Numeric
58+
sbyte x => x.ToString(formatProvider),
59+
byte x => x.ToString(formatProvider),
60+
short x => x.ToString(formatProvider),
61+
ushort x => x.ToString(formatProvider),
62+
int x => x.ToString(formatProvider),
63+
uint x => x.ToString(formatProvider),
64+
long x => x.ToString(formatProvider),
65+
ulong x => x.ToString(formatProvider),
66+
67+
// Text
68+
string x => Quote(x),
69+
char x => Quote(x.ToString(formatProvider)),
70+
StringBuilder x => Quote(x.ToString()),
71+
72+
// Everything else
73+
object x => throw new ArgumentException(
74+
$"Unable to convert argument {i} from type {x.GetType()} to a suppported OData filter string.")
75+
};
76+
}
77+
string text = string.Format(formatProvider, filter.Format, args);
78+
return text;
79+
}
80+
81+
/// <summary>
82+
/// Quote and escape OData strings.
83+
/// </summary>
84+
/// <param name="text">The text to quote.</param>
85+
/// <returns>The quoted text.</returns>
86+
private static string Quote(string text)
87+
{
88+
if (text == null)
89+
{ return "null"; }
90+
91+
// Optimistically allocate an extra 5% for escapes
92+
StringBuilder builder = new StringBuilder(2 + (int)(text.Length * 1.05));
93+
builder.Append('\'');
94+
foreach (char ch in text)
95+
{
96+
builder.Append(ch);
97+
if (ch == '\'')
98+
{
99+
builder.Append(ch);
100+
}
101+
}
102+
builder.Append('\'');
103+
return builder.ToString();
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)