Add connection id related apis (#68)

* Add connection id related apis

* Update to resolve parameter order

* Add sample with connectionId APIs

* Encode the connectonId
This commit is contained in:
Chenyang Liu 2019-07-22 15:18:54 +08:00 коммит произвёл GitHub
Родитель bcc4e549f8
Коммит 415e35d9cb
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 410 добавлений и 132 удалений

Просмотреть файл

@ -26,12 +26,18 @@ public class SignalRGroupAction {
* @param groupName Group to add user to or remove user from * @param groupName Group to add user to or remove user from
* @param userId User to add to or remove from group * @param userId User to add to or remove from group
*/ */
public SignalRGroupAction(String action, String groupName, String userId) { public SignalRGroupAction(String action, String groupName, String userId, String connectionId) {
this.action = action; this.action = action;
this.groupName = groupName; this.groupName = groupName;
this.userId = userId; this.userId = userId;
this.connectionId = connectionId;
} }
/**
* Connection to add to or remove from group
*/
public String connectionId = "";
/** /**
* User to add to or remove from group * User to add to or remove from group
*/ */

Просмотреть файл

@ -35,6 +35,11 @@ public class SignalRMessage {
this.arguments.addAll(Arrays.asList(arguments)); this.arguments.addAll(Arrays.asList(arguments));
} }
/**
* ConnectionId to send the message to
*/
public String connectionId = "";
/** /**
* User to send the message to * User to send the message to
*/ */

Просмотреть файл

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Package Versions"> <PropertyGroup Label="Package Versions">
<MicroBuildCorePackageVersion>0.3.0</MicroBuildCorePackageVersion> <MicroBuildCorePackageVersion>0.3.0</MicroBuildCorePackageVersion>
<MicrosoftAzureSignalRManagement>1.0.0-preview1-10420</MicrosoftAzureSignalRManagement> <MicrosoftAzureSignalRManagement>1.0.0-preview1-10449</MicrosoftAzureSignalRManagement>
<MicrosoftAzureWebJobsPackageVersion>3.0.4</MicrosoftAzureWebJobsPackageVersion> <MicrosoftAzureWebJobsPackageVersion>3.0.4</MicrosoftAzureWebJobsPackageVersion>
<MicrosoftNETTestSdkPackageVersion>15.8.0</MicrosoftNETTestSdkPackageVersion> <MicrosoftNETTestSdkPackageVersion>15.8.0</MicrosoftNETTestSdkPackageVersion>
<MoqPackageVersion>4.9.0</MoqPackageVersion> <MoqPackageVersion>4.9.0</MoqPackageVersion>

Просмотреть файл

@ -6,6 +6,7 @@
<RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'"> <RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'">
$(RestoreSources); $(RestoreSources);
https://api.nuget.org/v3/index.json; https://api.nuget.org/v3/index.json;
https://www.myget.org/F/azure-signalr-dev/api/v3/index.json
</RestoreSources> </RestoreSources>
<RestoreSources Condition="'$(UseLocalFeed)' == 'true'"> <RestoreSources Condition="'$(UseLocalFeed)' == 'true'">
$(LocalFeed); $(LocalFeed);

Просмотреть файл

@ -1,4 +1,5 @@
<html> <html>
<head> <head>
<title>Serverless Chat</title> <title>Serverless Chat</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css">
@ -6,10 +7,13 @@
window.apiBaseUrl = 'http://localhost:7071'; window.apiBaseUrl = 'http://localhost:7071';
</script> </script>
<style> <style>
.slide-fade-enter-active, .slide-fade-leave-active { .slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 1s ease; transition: all 1s ease;
} }
.slide-fade-enter, .slide-fade-leave-to {
.slide-fade-enter,
.slide-fade-leave-to {
height: 0px; height: 0px;
overflow-y: hidden; overflow-y: hidden;
opacity: 0; opacity: 0;
@ -29,7 +33,8 @@
<label for="checkbox">Send To Default Group: {{ this.defaultgroup }}</label> <label for="checkbox">Send To Default Group: {{ this.defaultgroup }}</label>
</div> </div>
<form v-on:submit.prevent="sendNewMessage(checked)"> <form v-on:submit.prevent="sendNewMessage(checked)">
<input type="text" v-model="newMessage" id="message-box" class="form-control" placeholder="Type message here..." /> <input type="text" v-model="newMessage" id="message-box" class="form-control"
placeholder="Type message here..." />
</form> </form>
</div> </div>
</div> </div>
@ -49,13 +54,21 @@
<a href="#" v-on:click.prevent="sendPrivateMessage(message.Sender)"> <a href="#" v-on:click.prevent="sendPrivateMessage(message.Sender)">
<span class="text-info small"><strong>{{ message.Sender || message.sender }}</strong></span> <span class="text-info small"><strong>{{ message.Sender || message.sender }}</strong></span>
</a> </a>
<a href="#" v-on:click.prevent="addToGroup(message.Sender || message.sender)"> <span v-if="message.ConnectionId || message.connectionId" class="badge badge-secondary">Connection: {{ message.ConnectionId || message.connectionId }}</span>
<a href="#" v-on:click.prevent="addToGroup(null, message.Sender || message.sender)">
<span class="badge badge-primary">AddGroup</span> <span class="badge badge-primary">AddGroup</span>
</a> </a>
<a href="#" v-on:click.prevent="removeFromGroup(message.Sender || message.sender)"> <a href="#" v-on:click.prevent="removeFromGroup(null, message.Sender || message.sender)">
<span class="badge badge-primary">RemoveGroup</span> <span class="badge badge-primary">RemoveGroup</span>
</a> </a>
<span v-if="message.IsPrivate || message.isPrivate" class="badge badge-secondary">private message</span> <a href="#" v-on:click.prevent="addToGroup(message.ConnectionId || message.connectionId, message.Sender || message.sender)">
<span v-if="message.ConnectionId || message.connectionId" class="badge badge-primary">AddConnectionToGroup</span>
</a>
<a href="#" v-on:click.prevent="removeFromGroup(message.ConnectionId || message.connectionId, message.Sender || message.sender)">
<span v-if="message.ConnectionId || message.connectionId" class="badge badge-primary">RemoveConnectionFromGroup</span>
</a>
<span v-if="message.IsPrivate || message.isPrivate" class="badge badge-secondary">private
message</span>
</div> </div>
<div> <div>
{{ message.Text || message.text }} {{ message.Text || message.text }}
@ -64,123 +77,146 @@
</div> </div>
</div> </div>
</div> </div>
</transition-group> </transition-group>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@aspnet/signalr@1.0.3/dist/browser/signalr.js"></script> <script src="https://cdn.jsdelivr.net/npm/@aspnet/signalr@1.0.3/dist/browser/signalr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script>
<script> <script>
const data = { const data = {
username: '', username: '',
defaultgroup: 'AzureSignalR', defaultgroup: 'AzureSignalR',
checked: false, checked: false,
newMessage: '', newMessage: '',
messages: [], messages: [],
ready: false myConnectionId: '',
}; ready: false
const app = new Vue({ };
el: '#app', const app = new Vue({
data: data, el: '#app',
methods: { data: data,
sendNewMessage: function (isToGroup) { methods: {
if(isToGroup) { sendNewMessage: function (isToGroup) {
sendMessage(this.username, null, this.defaultgroup, this.newMessage); if (isToGroup) {
} sendMessage(this.username, null, this.defaultgroup, this.newMessage);
else { }
sendMessage(this.username, null, null, this.newMessage); else {
} sendMessage(this.username, null, null, this.newMessage);
this.newMessage = ''; }
}, this.newMessage = '';
sendPrivateMessage: function (recipient) { },
const messageText = prompt('Send private message to ' + recipient); sendPrivateMessage: function (recipient) {
if (messageText) { const messageText = prompt('Send private message to ' + recipient);
sendMessage(this.username, recipient, null, messageText);
} if (messageText) {
}, sendMessage(this.username, recipient, null, messageText);
addToGroup: function (recipient) { }
var r = confirm('Add user ' + recipient + ' to group: ' + this.defaultgroup); },
if(r) { addToGroup: function (connectionId, recipient) {
addGroup(this.username, recipient, this.defaultgroup); var r;
} if (connectionId) {
}, r = confirm('Add connection ' + connectionId + ' to group: ' + this.defaultgroup);
removeFromGroup: function (recipient) { } else {
var r = confirm('Remove user ' + recipient + ' from group: ' + this.defaultgroup); r = confirm('Add user ' + recipient + ' to group: ' + this.defaultgroup);
if(r) { }
removeGroup(this.username, recipient, this.defaultgroup);
if (r) {
addGroup(this.username, recipient, connectionId, this.defaultgroup);
}
},
removeFromGroup: function (connectionId, recipient) {
var r;
if (connectionId) {
r = confirm('Remove connection ' + connectionId + ' from group: ' + this.defaultgroup);
} else {
r = confirm('Remove user ' + recipient + ' from group: ' + this.defaultgroup);
}
if (r) {
removeGroup(this.username, recipient, connectionId, this.defaultgroup);
}
} }
} }
});
const apiBaseUrl = prompt('Enter the Azure Function app base URL', window.apiBaseUrl);
data.username = prompt("Enter your username");
if (!data.username) {
alert("No username entered. Reload page and try again.");
throw "No username entered";
} }
}); getConnectionInfo().then(info => {
const apiBaseUrl = prompt('Enter the Azure Function app base URL', window.apiBaseUrl); // make compatible with old and new SignalRConnectionInfo
data.username = prompt("Enter your username"); info.accessToken = info.accessToken || info.accessKey;
if (!data.username) { info.url = info.url || info.endpoint;
alert("No username entered. Reload page and try again."); data.ready = true;
throw "No username entered"; const options = {
} accessTokenFactory: () => info.accessToken
getConnectionInfo().then(info => { };
// make compatible with old and new SignalRConnectionInfo const connection = new signalR.HubConnectionBuilder()
info.accessToken = info.accessToken || info.accessKey; .withUrl(info.url, options)
info.url = info.url || info.endpoint; .configureLogging(signalR.LogLevel.Information)
data.ready = true; .build();
const options = { connection.on('newMessage', newMessage);
accessTokenFactory: () => info.accessToken connection.on('newConnection', newConnection)
}; connection.onclose(() => console.log('disconnected'));
const connection = new signalR.HubConnectionBuilder() console.log('connecting...');
.withUrl(info.url, options) connection.start()
.configureLogging(signalR.LogLevel.Information) .then(() => console.log('connected!'))
.build(); .catch(console.error);
connection.on('newMessage', newMessage); }).catch(alert);
connection.onclose(() => console.log('disconnected')); function getAxiosConfig() {
console.log('connecting...'); const config = {
connection.start() headers: { 'x-ms-signalr-userid': data.username }
.then(() => console.log('connected!')) };
.catch(console.error); return config;
}).catch(alert); }
function getAxiosConfig() { function getConnectionInfo() {
const config = { return axios.post(`${apiBaseUrl}/api/negotiate`, null, getAxiosConfig())
headers: {'x-ms-signalr-userid': data.username} .then(resp => resp.data);
}; }
return config; function sendMessage(sender, recipient, groupname, messageText) {
} return axios.post(`${apiBaseUrl}/api/messages`, {
function getConnectionInfo() { connectionId: data.myConnectionId,
return axios.post(`${apiBaseUrl}/api/negotiate`, null , getAxiosConfig()) recipient: recipient,
.then(resp => resp.data); isPrivate: recipient != null,
} groupname: groupname,
function sendMessage(sender, recipient, groupname, messageText) { sender: sender,
return axios.post(`${apiBaseUrl}/api/messages`, { text: messageText
recipient: recipient, }, getAxiosConfig()).then(resp => resp.data);
isPrivate: recipient != null, }
groupname: groupname, function addGroup(sender, recipient, connectionId, groupName) {
sender: sender, return axios.post(`${apiBaseUrl}/api/addToGroup`, {
text: messageText connectionId: connectionId,
}, getAxiosConfig()).then(resp => resp.data); recipient: recipient,
} groupname: groupName
function addGroup(sender, recipient, groupName) { }, getAxiosConfig()).then(resp => {
return axios.post(`${apiBaseUrl}/api/addToGroup`, { if (resp.status == 200) {
recipient: recipient,
groupname: groupName
}, getAxiosConfig()).then(resp => {
if(resp.status == 200) {
confirm("Add Successfully") confirm("Add Successfully")
}}); }
} });
function removeGroup(sender, recipient, groupName) { }
return axios.post(`${apiBaseUrl}/api/removeFromGroup`, { function removeGroup(sender, recipient, connectionId, groupName) {
recipient: recipient, return axios.post(`${apiBaseUrl}/api/removeFromGroup`, {
groupname: groupName connectionId: connectionId,
}, getAxiosConfig()).then(resp => { recipient: recipient,
if(resp.status == 200) { groupname: groupName
}, getAxiosConfig()).then(resp => {
if (resp.status == 200) {
confirm("Remove Successfully") confirm("Remove Successfully")
}}); }
} });
let counter = 0; }
function newMessage(message) { let counter = 0;
message.id = counter++; // vue transitions need an id function newMessage(message) {
data.messages.unshift(message); message.id = counter++; // vue transitions need an id
} data.messages.unshift(message);
</script> };
function newConnection(message) {
data.myConnectionId = message.ConnectionId;
}
</script>
</body> </body>
</html> </html>

Просмотреть файл

@ -4,6 +4,7 @@
<AzureFunctionsVersion>v2</AzureFunctionsVersion> <AzureFunctionsVersion>v2</AzureFunctionsVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.EventGrid" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.23" /> <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.23" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

Просмотреть файл

@ -3,14 +3,17 @@
using System; using System;
using System.IO; using System.IO;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Azure.EventGrid.Models;
using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.EventGrid;
using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Extensions.SignalRService; using Microsoft.Azure.WebJobs.Extensions.SignalRService;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace FunctionApp namespace FunctionApp
{ {
@ -39,7 +42,6 @@ namespace FunctionApp
[HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequest req, [HttpTrigger(AuthorizationLevel.Anonymous, "post")]HttpRequest req,
[SignalR(HubName = "simplechat")]IAsyncCollector<SignalRMessage> signalRMessages) [SignalR(HubName = "simplechat")]IAsyncCollector<SignalRMessage> signalRMessages)
{ {
var message = new JsonSerializer().Deserialize<ChatMessage>(new JsonTextReader(new StreamReader(req.Body))); var message = new JsonSerializer().Deserialize<ChatMessage>(new JsonTextReader(new StreamReader(req.Body)));
return signalRMessages.AddAsync( return signalRMessages.AddAsync(
@ -60,10 +62,12 @@ namespace FunctionApp
var message = new JsonSerializer().Deserialize<ChatMessage>(new JsonTextReader(new StreamReader(req.Body))); var message = new JsonSerializer().Deserialize<ChatMessage>(new JsonTextReader(new StreamReader(req.Body)));
var decodedfConnectionId = GetBase64DecodedString(message.ConnectionId);
return signalRGroupActions.AddAsync( return signalRGroupActions.AddAsync(
new SignalRGroupAction new SignalRGroupAction
{ {
ConnectionId = decodedfConnectionId,
UserId = message.Recipient, UserId = message.Recipient,
GroupName = message.Groupname, GroupName = message.Groupname,
Action = GroupAction.Add Action = GroupAction.Add
@ -78,23 +82,83 @@ namespace FunctionApp
var message = new JsonSerializer().Deserialize<ChatMessage>(new JsonTextReader(new StreamReader(req.Body))); var message = new JsonSerializer().Deserialize<ChatMessage>(new JsonTextReader(new StreamReader(req.Body)));
var decodedfConnectionId = GetBase64DecodedString(message.ConnectionId);
return signalRGroupActions.AddAsync( return signalRGroupActions.AddAsync(
new SignalRGroupAction new SignalRGroupAction
{ {
ConnectionId = message.ConnectionId,
UserId = message.Recipient, UserId = message.Recipient,
GroupName = message.Groupname, GroupName = message.Groupname,
Action = GroupAction.Remove Action = GroupAction.Remove
}); });
} }
private static string GetBase64EncodedString(string source)
{
if (string.IsNullOrEmpty(source))
{
return source;
}
return Convert.ToBase64String(Encoding.UTF8.GetBytes(source));
}
private static string GetBase64DecodedString(string source)
{
if (string.IsNullOrEmpty(source))
{
return source;
}
return Encoding.UTF8.GetString(Convert.FromBase64String(source));
}
public static class EventGridTriggerCSharp
{
[FunctionName("onConnection")]
public static Task EventGridTest([EventGridTrigger]EventGridEvent eventGridEvent,
[SignalR(HubName = "simplechat")]IAsyncCollector<SignalRMessage> signalRMessages)
{
if (eventGridEvent.EventType == "Microsoft.SignalRService.ClientConnectionConnected")
{
var message = ((JObject) eventGridEvent.Data).ToObject<SignalREvent>();
return signalRMessages.AddAsync(
new SignalRMessage
{
ConnectionId = message.ConnectionId,
Target = "newConnection",
Arguments = new[] { new ChatMessage
{
// ConnectionId is not recommand to send to client directly.
// Here's a simple encryption for an easier sample.
ConnectionId = GetBase64EncodedString(message.ConnectionId),
}}
});
}
return Task.CompletedTask;
}
}
public class ChatMessage public class ChatMessage
{ {
public string Sender { get; set; } public string Sender { get; set; }
public string Text { get; set; } public string Text { get; set; }
public string Groupname { get; set; } public string Groupname { get; set; }
public string Recipient { get; set; } public string Recipient { get; set; }
public string ConnectionId { get; set; }
public bool IsPrivate { get; set; } public bool IsPrivate { get; set; }
} }
public class SignalREvent
{
public DateTime Timestamp { get; set; }
public string HubName { get; set; }
public string ConnectionId { get; set; }
public string UserId { get; set; }
}
} }
} }

Просмотреть файл

@ -38,12 +38,11 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
Arguments = message.Arguments Arguments = message.Arguments
}; };
if (!string.IsNullOrEmpty(message.UserId) && !string.IsNullOrEmpty(message.GroupName)) if (!string.IsNullOrEmpty(message.ConnectionId))
{ {
throw new ArgumentException("GroupName and UserId can not be specified at the same time."); await client.SendToConnection(hubName, message.ConnectionId, data).ConfigureAwait(false);
} }
else if (!string.IsNullOrEmpty(message.UserId))
if (!string.IsNullOrEmpty(message.UserId))
{ {
await client.SendToUser(hubName, message.UserId, data).ConfigureAwait(false); await client.SendToUser(hubName, message.UserId, data).ConfigureAwait(false);
} }
@ -59,13 +58,32 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
else if (convertItem.GetType() == typeof(SignalRGroupAction)) else if (convertItem.GetType() == typeof(SignalRGroupAction))
{ {
SignalRGroupAction groupAction = convertItem as SignalRGroupAction; SignalRGroupAction groupAction = convertItem as SignalRGroupAction;
if (groupAction.Action == GroupAction.Add)
if (!string.IsNullOrEmpty(groupAction.ConnectionId))
{ {
await client.AddUserToGroup(hubName, groupAction.UserId, groupAction.GroupName).ConfigureAwait(false); if (groupAction.Action == GroupAction.Add)
{
await client.AddConnectionToGroup(hubName, groupAction.ConnectionId, groupAction.GroupName).ConfigureAwait(false);
}
else
{
await client.RemoveConnectionFromGroup(hubName, groupAction.ConnectionId, groupAction.GroupName).ConfigureAwait(false);
}
}
else if (!string.IsNullOrEmpty(groupAction.UserId))
{
if (groupAction.Action == GroupAction.Add)
{
await client.AddUserToGroup(hubName, groupAction.UserId, groupAction.GroupName).ConfigureAwait(false);
}
else
{
await client.RemoveUserFromGroup(hubName, groupAction.UserId, groupAction.GroupName).ConfigureAwait(false);
}
} }
else else
{ {
await client.RemoveUserFromGroup(hubName, groupAction.UserId, groupAction.GroupName).ConfigureAwait(false); throw new ArgumentException($"ConnectionId and UserId cannot be null or empty together");
} }
} }
else else

Просмотреть файл

@ -7,10 +7,19 @@ using System.Runtime.Serialization;
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
{ {
/// <summary>
/// Class that contains parameters needed for group operations.
/// Either the group operation on connectionId or userId is supported.
/// If connectionId and userId are both set, it will be resolved by the following order:
/// 1. ConnectionId
/// 2. UserId
/// </summary>
[JsonObject] [JsonObject]
public class SignalRGroupAction public class SignalRGroupAction
{ {
[JsonProperty("userId"), JsonRequired] [JsonProperty("connectionId")]
public string ConnectionId { get; set; }
[JsonProperty("userId")]
public string UserId { get; set; } public string UserId { get; set; }
[JsonProperty("groupName"), JsonRequired] [JsonProperty("groupName"), JsonRequired]
public string GroupName { get; set; } public string GroupName { get; set; }

Просмотреть файл

@ -5,9 +5,19 @@ using Newtonsoft.Json;
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
{ {
/// <summary>
/// Class that contains parameters needed for sending messages.
/// There are three kinds of scope to send, and if more than one
/// scopes are set, it will be resolved by the following order:
/// 1. ConnectionId
/// 2. UserId
/// 3. GroupName
/// </summary>
[JsonObject] [JsonObject]
public class SignalRMessage public class SignalRMessage
{ {
[JsonProperty("connectionId")]
public string ConnectionId { get; set; }
[JsonProperty("userId")] [JsonProperty("userId")]
public string UserId { get; set; } public string UserId { get; set; }
[JsonProperty("groupName")] [JsonProperty("groupName")]

Просмотреть файл

@ -60,6 +60,12 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
await serviceHubContext.Clients.All.SendCoreAsync(data.Target, data.Arguments); await serviceHubContext.Clients.All.SendCoreAsync(data.Target, data.Arguments);
} }
public async Task SendToConnection(string hubName, string connectionId, SignalRData data)
{
var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName);
await serviceHubContext.Clients.Client(connectionId).SendCoreAsync(data.Target, data.Arguments);
}
public async Task SendToUser(string hubName, string userId, SignalRData data) public async Task SendToUser(string hubName, string userId, SignalRData data)
{ {
if (string.IsNullOrEmpty(userId)) if (string.IsNullOrEmpty(userId))
@ -108,6 +114,34 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
await serviceHubContext.UserGroups.RemoveFromGroupAsync(userId, groupName); await serviceHubContext.UserGroups.RemoveFromGroupAsync(userId, groupName);
} }
public async Task AddConnectionToGroup(string hubName, string connectionId, string groupName)
{
if (string.IsNullOrEmpty(connectionId))
{
throw new ArgumentException($"{nameof(connectionId)} cannot be null or empty");
}
if (string.IsNullOrEmpty(groupName))
{
throw new ArgumentException($"{nameof(groupName)} cannot be null or empty");
}
var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName);
await serviceHubContext.Groups.AddToGroupAsync(connectionId, groupName);
}
public async Task RemoveConnectionFromGroup(string hubName, string connectionId, string groupName)
{
if (string.IsNullOrEmpty(connectionId))
{
throw new ArgumentException($"{nameof(connectionId)} cannot be null or empty");
}
if (string.IsNullOrEmpty(groupName))
{
throw new ArgumentException($"{nameof(groupName)} cannot be null or empty");
}
var serviceHubContext = await serviceHubContextStore.GetOrAddAsync(hubName);
await serviceHubContext.Groups.RemoveFromGroupAsync(connectionId, groupName);
}
private static IEnumerable<Claim> BuildJwtClaims(IEnumerable<Claim> customerClaims, string prefix) private static IEnumerable<Claim> BuildJwtClaims(IEnumerable<Claim> customerClaims, string prefix)
{ {
if (customerClaims != null) if (customerClaims != null)

Просмотреть файл

@ -9,9 +9,12 @@ namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
internal interface IAzureSignalRSender internal interface IAzureSignalRSender
{ {
Task SendToAll(string hubName, SignalRData data); Task SendToAll(string hubName, SignalRData data);
Task SendToConnection(string hubName, string connectionId, SignalRData data);
Task SendToUser(string hubName, string userId, SignalRData data); Task SendToUser(string hubName, string userId, SignalRData data);
Task SendToGroup(string hubName, string group, SignalRData data); Task SendToGroup(string hubName, string group, SignalRData data);
Task AddUserToGroup(string hubName, string userId, string groupName); Task AddUserToGroup(string hubName, string userId, string groupName);
Task RemoveUserFromGroup(string hubName, string userId, string groupName); Task RemoveUserFromGroup(string hubName, string userId, string groupName);
Task AddConnectionToGroup(string hubName, string connectionId, string groupName);
Task RemoveConnectionFromGroup(string hubName, string connectionId, string groupName);
} }
} }

Просмотреть файл

@ -135,17 +135,108 @@ namespace SignalRServiceExtension.Tests
} }
[Fact] [Fact]
public async Task AddAsync_SendMessage_WithBothUserIdAndGroupNameThrowException() public async Task AddAsync_SendMessage_WithBothUserIdAndGroupName_UsePriorityOrder()
{ {
var signalRSenderMock = new Mock<IAzureSignalRSender>(); var signalRSenderMock = new Mock<IAzureSignalRSender>();
var collector = new SignalRAsyncCollector<SignalRMessage>(signalRSenderMock.Object, "chathub"); var collector = new SignalRAsyncCollector<SignalRMessage>(signalRSenderMock.Object, "chathub");
var item = new SignalRMessage await collector.AddAsync(new SignalRMessage
{ {
UserId = "user1", UserId = "user1",
GroupName = "group1", GroupName = "group1",
Target = "newMessage", Target = "newMessage",
Arguments = new object[] { "arg1", "arg2" } Arguments = new object[] { "arg1", "arg2" }
});
signalRSenderMock.Verify(
c => c.SendToUser("chathub", "user1", It.IsAny<SignalRData>()),
Times.Once);
signalRSenderMock.VerifyNoOtherCalls();
var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[2];
Assert.Equal("newMessage", actualData.Target);
Assert.Equal("arg1", actualData.Arguments[0]);
Assert.Equal("arg2", actualData.Arguments[1]);
}
[Fact]
public async Task AddAsync_WithConnectionId_CallsSendToUser()
{
var signalRSenderMock = new Mock<IAzureSignalRSender>();
var collector = new SignalRAsyncCollector<SignalRMessage>(signalRSenderMock.Object, "chathub");
await collector.AddAsync(new SignalRMessage
{
ConnectionId = "connection1",
Target = "newMessage",
Arguments = new object[] { "arg1", "arg2" }
});
signalRSenderMock.Verify(
c => c.SendToConnection("chathub", "connection1", It.IsAny<SignalRData>()),
Times.Once);
signalRSenderMock.VerifyNoOtherCalls();
var actualData = (SignalRData)signalRSenderMock.Invocations[0].Arguments[2];
Assert.Equal("newMessage", actualData.Target);
Assert.Equal("arg1", actualData.Arguments[0]);
Assert.Equal("arg2", actualData.Arguments[1]);
}
[Fact]
public async Task AddAsync_WithConnectionId_CallsAddConnectionToGroup()
{
var signalRSenderMock = new Mock<IAzureSignalRSender>();
var collector = new SignalRAsyncCollector<SignalRGroupAction>(signalRSenderMock.Object, "chathub");
await collector.AddAsync(new SignalRGroupAction
{
ConnectionId = "connection1",
GroupName = "group1",
Action = GroupAction.Add
});
signalRSenderMock.Verify(
c => c.AddConnectionToGroup("chathub", "connection1", "group1"),
Times.Once);
signalRSenderMock.VerifyNoOtherCalls();
var actualData = signalRSenderMock.Invocations[0];
Assert.Equal("chathub", actualData.Arguments[0]);
Assert.Equal("connection1", actualData.Arguments[1]);
Assert.Equal("group1", actualData.Arguments[2]);
}
[Fact]
public async Task AddAsync_WithConnectionId_CallsRemoveConnectionFromGroup()
{
var signalRSenderMock = new Mock<IAzureSignalRSender>();
var collector = new SignalRAsyncCollector<SignalRGroupAction>(signalRSenderMock.Object, "chathub");
await collector.AddAsync(new SignalRGroupAction
{
ConnectionId = "connection1",
GroupName = "group1",
Action = GroupAction.Remove
});
signalRSenderMock.Verify(
c => c.RemoveConnectionFromGroup("chathub", "connection1", "group1"),
Times.Once);
signalRSenderMock.VerifyNoOtherCalls();
var actualData = signalRSenderMock.Invocations[0];
Assert.Equal("chathub", actualData.Arguments[0]);
Assert.Equal("connection1", actualData.Arguments[1]);
Assert.Equal("group1", actualData.Arguments[2]);
}
[Fact]
public async Task AddAsync_GroupOperation_WithoutParametersThrowException()
{
var signalRSenderMock = new Mock<IAzureSignalRSender>();
var collector = new SignalRAsyncCollector<SignalRGroupAction>(signalRSenderMock.Object, "chathub");
var item = new SignalRGroupAction
{
GroupName = "group1",
Action = GroupAction.Add
}; };
await Assert.ThrowsAsync<ArgumentException>(() => collector.AddAsync(item)); await Assert.ThrowsAsync<ArgumentException>(() => collector.AddAsync(item));