diff --git a/samples/ServerlessReloading/MessagePublisher/MessagePublisher.csproj b/samples/ServerlessReloading/MessagePublisher/MessagePublisher.csproj
new file mode 100644
index 00000000..f13caee8
--- /dev/null
+++ b/samples/ServerlessReloading/MessagePublisher/MessagePublisher.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ netcoreapp3.1
+ ReloadingSample
+
+
+
+
+
+
+
+
+
diff --git a/samples/ServerlessReloading/MessagePublisher/MessagePubulisher.cs b/samples/ServerlessReloading/MessagePublisher/MessagePubulisher.cs
new file mode 100644
index 00000000..4905beb3
--- /dev/null
+++ b/samples/ServerlessReloading/MessagePublisher/MessagePubulisher.cs
@@ -0,0 +1,126 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Threading.Tasks;
+using System.Net.Http;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Azure.SignalR.Management;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System.Text;
+using System.Net.Http.Headers;
+using Microsoft.IdentityModel.Tokens;
+using System.IdentityModel.Tokens.Jwt;
+
+namespace Microsoft.Azure.SignalR.Samples.Management
+{
+ public class MessagePublisher
+ {
+ private const string Target = "Target";
+ private const string HubName = "ManagementSampleHub";
+ private readonly string _connectionString;
+ private readonly ServiceTransportType _serviceTransportType;
+ private IServiceHubContext _hubContext;
+ // reload connection string
+ private readonly string connectionString = "Endpoint=http://localhost;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH;Port=8081;Version=1.0;";
+
+
+ public MessagePublisher(string connectionString, ServiceTransportType serviceTransportType)
+ {
+ _connectionString = connectionString;
+ _serviceTransportType = serviceTransportType;
+ }
+
+ public async Task InitAsync()
+ {
+ var serviceManager = new ServiceManagerBuilder().WithOptions(option =>
+ {
+ option.ConnectionString = _connectionString;
+ option.ServiceTransportType = _serviceTransportType;
+ }).Build();
+
+ _hubContext = await serviceManager.CreateHubContextAsync(HubName, new LoggerFactory());
+ }
+
+ public Task ManageUserGroup(string command, string userId, string groupName)
+ {
+ switch (command)
+ {
+ case "add":
+ return _hubContext.UserGroups.AddToGroupAsync(userId, groupName);
+ case "remove":
+ return _hubContext.UserGroups.RemoveFromGroupAsync(userId, groupName);
+ default:
+ Console.WriteLine($"Can't recognize command {command}");
+ return Task.CompletedTask;
+ }
+ }
+
+ public Task SendMessages(string command, string receiver, string message)
+ {
+ var jMsg = new
+ {
+ msgType = "0",
+ content = message
+ };
+ var uMsg = JsonConvert.SerializeObject(jMsg);
+ switch (command)
+ {
+ case "broadcast":
+ return _hubContext.Clients.All.SendAsync(Target, uMsg);
+ case "user":
+ var userId = receiver;
+ return _hubContext.Clients.User(userId).SendAsync(Target, uMsg);
+ case "users":
+ var userIds = receiver.Split(',');
+ return _hubContext.Clients.Users(userIds).SendAsync(Target, uMsg);
+ case "group":
+ var groupName = receiver;
+ return _hubContext.Clients.Group(groupName).SendAsync(Target, uMsg);
+ case "groups":
+ var groupNames = receiver.Split(',');
+ return _hubContext.Clients.Groups(groupNames).SendAsync(Target, uMsg);
+ case "reload":
+ HttpClient restClient = new HttpClient();
+ var msg = new
+ {
+ Target = Target,
+ Arguments = new string[] { connectionString }
+ };
+
+ // generate token
+ string key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH";
+ var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key))
+ {
+ KeyId = key.GetHashCode().ToString()
+ };
+ SigningCredentials credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
+
+ JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler();
+ var token = JwtTokenHandler.CreateJwtSecurityToken(
+ issuer: null,
+ audience: "http://localhost/api/v1/reload",
+ notBefore: DateTime.Now,
+ expires: DateTime.Now.AddHours(1),
+ issuedAt: DateTime.Now,
+ signingCredentials: credentials) ;
+ string tk = JwtTokenHandler.WriteToken(token);
+
+ restClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tk);
+ restClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+ string json = JsonConvert.SerializeObject(msg);
+ var data = new StringContent(json, Encoding.UTF8, "application/json");
+ // Call reload rest api on the old service to start reloading connection
+ string url = "http://localhost:8080/api/v1/reload";
+
+ return restClient.PostAsync(url, data);
+ default:
+ Console.WriteLine($"Can't recognize command {command}");
+ return Task.CompletedTask;
+ }
+ }
+
+ public Task DisposeAsync() => _hubContext?.DisposeAsync();
+ }
+}
\ No newline at end of file
diff --git a/samples/ServerlessReloading/MessagePublisher/Program.cs b/samples/ServerlessReloading/MessagePublisher/Program.cs
new file mode 100644
index 00000000..056cf967
--- /dev/null
+++ b/samples/ServerlessReloading/MessagePublisher/Program.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.Azure.SignalR.Management;
+using Microsoft.Extensions.CommandLineUtils;
+using Microsoft.Extensions.Configuration;
+
+namespace Microsoft.Azure.SignalR.Samples.Management
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ var app = new CommandLineApplication();
+ app.FullName = "Azure SignalR Management Sample: Message Publisher";
+ app.HelpOption("--help");
+ app.Description = "Message publisher using Azure SignalR Service Management SDK.";
+
+ var connectionStringOption = app.Option("-c|--connectionstring", "Set connection string.", CommandOptionType.SingleValue, true);
+ var serviceTransportTypeOption = app.Option("-t|--transport", "Set service transport type. Options: |. Default value: transient. Transient: calls REST API for each message. Persistent: Establish a WebSockets connection and send all messages in the connection.", CommandOptionType.SingleValue, true); // todo: description
+ var configuration = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddUserSecrets()
+ .AddEnvironmentVariables()
+ .Build();
+
+
+ app.OnExecute(async () =>
+ {
+ var connectionString = connectionStringOption.Value() ?? configuration["Azure:SignalR:ConnectionString"];
+
+ if (string.IsNullOrEmpty(connectionString))
+ {
+ MissOptions();
+ return -1;
+ }
+
+ ServiceTransportType serviceTransportType;
+ if (string.IsNullOrEmpty(serviceTransportTypeOption.Value()))
+ {
+ serviceTransportType = ServiceTransportType.Transient;
+ }
+ else
+ {
+ serviceTransportType = Enum.Parse(serviceTransportTypeOption.Value(), true);
+ }
+
+ var publisher = new MessagePublisher(connectionString, serviceTransportType);
+ await publisher.InitAsync();
+
+ await StartAsync(publisher);
+
+ return 0;
+ });
+
+ app.Execute(args);
+ }
+
+ private static async Task StartAsync(MessagePublisher publisher)
+ {
+ Console.CancelKeyPress += async (sender, e) =>
+ {
+ await publisher.DisposeAsync();
+ Environment.Exit(0);
+ };
+
+ ShowHelp();
+
+ try
+ {
+ while (true)
+ {
+ var argLine = Console.ReadLine();
+ if (argLine == null)
+ {
+ continue;
+ }
+ var args = argLine.Split(' ');
+
+ if (args.Length == 2 && args[0].Equals("broadcast"))
+ {
+ Console.WriteLine($"broadcast message '{args[1]}'");
+ await publisher.SendMessages(args[0], null, args[1]);
+ }
+ else if (args.Length == 4 && args[0].Equals("send"))
+ {
+ await publisher.SendMessages(args[1], args[2], args[3]);
+ Console.WriteLine($"{args[0]} message '{args[3]}' to '{args[2]}'");
+ }
+ else if (args.Length == 4 && args[0] == "usergroup")
+ {
+ await publisher.ManageUserGroup(args[1], args[2], args[3]);
+ var preposition = args[1] == "add" ? "to" : "from";
+ Console.WriteLine($"{args[1]} user '{args[2]}' {preposition} group '{args[3]}'");
+ }
+ else if (args.Length == 1 && args[0].Equals("reload"))
+ {
+ await publisher.SendMessages(args[0], null, null);
+ Console.WriteLine($"{args[0]}");
+ }
+ else
+ {
+ Console.WriteLine($"Can't recognize command {argLine}");
+ }
+ }
+ }
+ finally
+ {
+ await publisher.DisposeAsync();
+ }
+ }
+
+ private static void ShowHelp()
+ {
+ Console.WriteLine(
+ "*********Usage*********\n" +
+ "send user \n" +
+ "send users \n" +
+ "send group \n" +
+ "send groups \n" +
+ "usergroup add \n" +
+ "usergroup remove \n" +
+ "broadcast \n" +
+ "reload\n" +
+ "***********************");
+ }
+
+ private static void MissOptions()
+ {
+ Console.WriteLine("Miss required options: Connection string and Hub must be set");
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/ServerlessReloading/MessagePublisher/README.md b/samples/ServerlessReloading/MessagePublisher/README.md
new file mode 100644
index 00000000..b56f3005
--- /dev/null
+++ b/samples/ServerlessReloading/MessagePublisher/README.md
@@ -0,0 +1,90 @@
+Message Publisher
+=========
+
+This sample shows how to use [Microsoft.Azure.SignalR.Management](https://www.nuget.org/packages/Microsoft.Azure.SignalR.Management) to publish messages to SignalR clients that connect to Azure SignalR Service.
+
+## Build from Scratch
+
+### Add Management SDK to your project
+
+```
+dotnet add package Microsoft.Azure.SignalR.Management -v 1.0.0-*
+```
+
+### Create instance of `IServiceManager`
+
+The `IServiceManager` is able to manage your Azure SignalR Service from your connection string.
+
+```c#
+var serviceManager = new ServiceManagerBuilder()
+ .WithOptions(option =>
+ {
+ option.ConnectionString = "";
+ })
+ .Build();
+```
+
+### Create instance of `IServiceHubContext`
+
+The `IServiceHubContext` is used to publish messages to a specific hub.
+
+```C#
+var hubContext = await serviceManager.CreateHubContextAsync("");
+```
+
+### Publish messages to a specific hub
+
+Once you create the `hubContext`, you can use it to publish messages to a given hub.
+
+```C#
+// broadcast
+hubContext.Clients.All.SendAsync("", "", "", ...);
+
+// send to a user
+hubContext.Clients.User("").SendAsync("", "", "", ...);
+
+// send to users
+hubContext.Clients.Users().SendAsync("", "", "", ...);
+
+// send to a group
+hubContext.Clients.Group("").SendAsync("", "", "", ...);
+
+// send to groups
+hubContext.Clients.Group().SendAsync("", "", "", ...);
+
+// add a user to a group
+hubContext.UserGroups.AddToGroupAsync("", "");
+
+// remove a user from a group
+hubContext.UserGroups.RemoveFromGroupAsync("", "");
+
+...
+```
+
+All features can be found [here]().
+
+### Reload connection to another new service platform
+
+```C#
+// Use specialized Json Web Token with userId claim
+restClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tk);
+
+// Serialize reloading message for transimisstion
+string json = JsonConvert.SerializeObject(msg);
+
+// Call reload rest api on the old service to start reloading connection
+string url = "http://localhost:8080/api/v1/reload";
+
+// Post REST API to reload connections on a specific service instance
+restClient.PostAsync(url, data);
+```
+
+### Dispose the instance of `IServiceHubContext`
+
+```c#
+await hubContext.DisposeAsync();
+```
+
+## Full Sample
+
+The full message publisher sample can be found [here](.). The usage of this sample can be found [here]().
\ No newline at end of file
diff --git a/samples/ServerlessReloading/NegotiationServer/Controllers/NegotiateController.cs b/samples/ServerlessReloading/NegotiationServer/Controllers/NegotiateController.cs
new file mode 100644
index 00000000..d323311b
--- /dev/null
+++ b/samples/ServerlessReloading/NegotiationServer/Controllers/NegotiateController.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Azure.SignalR.Management;
+using Microsoft.Extensions.Configuration;
+
+namespace NegotiationServer.Controllers
+{
+ [ApiController]
+ public class NegotiateController : ControllerBase
+ {
+ private readonly IServiceManager _serviceManager;
+
+ public NegotiateController(IConfiguration configuration)
+ {
+ var connectionString = configuration["Azure:SignalR:ConnectionString"];
+ // var connectionString = "Endpoint=http://localhost;AccessKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGH;Port=8080;Version=1.0;"; for localhost reloading
+ _serviceManager = new ServiceManagerBuilder()
+ .WithOptions(o => o.ConnectionString = connectionString)
+ .Build();
+ }
+
+ [HttpPost("{hub}/negotiate")]
+ public ActionResult Index(string hub, string user)
+ {
+ if (string.IsNullOrEmpty(user))
+ {
+ return BadRequest("User ID is null or empty.");
+ }
+
+ IList cs = new List();
+ cs.Add(new Claim("isFirstConnection", "True"));
+
+ return new JsonResult(new Dictionary()
+ {
+ { "url", _serviceManager.GetClientEndpoint(hub) },
+ { "accessToken", _serviceManager.GenerateClientAccessToken(hub, user, cs) }
+ });
+ }
+ }
+
+
+}
diff --git a/samples/ServerlessReloading/NegotiationServer/NegotiationServer.csproj b/samples/ServerlessReloading/NegotiationServer/NegotiationServer.csproj
new file mode 100644
index 00000000..6ebb3623
--- /dev/null
+++ b/samples/ServerlessReloading/NegotiationServer/NegotiationServer.csproj
@@ -0,0 +1,13 @@
+
+
+
+ netcoreapp3.1
+ ReloadingSample
+
+
+
+
+
+
+
+
diff --git a/samples/ServerlessReloading/NegotiationServer/Program.cs b/samples/ServerlessReloading/NegotiationServer/Program.cs
new file mode 100644
index 00000000..fd4e60db
--- /dev/null
+++ b/samples/ServerlessReloading/NegotiationServer/Program.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+
+namespace NegotiationServer
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+ }
+}
diff --git a/samples/ServerlessReloading/NegotiationServer/Properties/launchSettings.json b/samples/ServerlessReloading/NegotiationServer/Properties/launchSettings.json
new file mode 100644
index 00000000..c25960c7
--- /dev/null
+++ b/samples/ServerlessReloading/NegotiationServer/Properties/launchSettings.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:31190",
+ "sslPort": 44372
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "negotiate",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "NegotiationServer": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "negotiate",
+ "applicationUrl": "https://localhost:5001;http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/samples/ServerlessReloading/NegotiationServer/README.md b/samples/ServerlessReloading/NegotiationServer/README.md
new file mode 100644
index 00000000..5e5f55ed
--- /dev/null
+++ b/samples/ServerlessReloading/NegotiationServer/README.md
@@ -0,0 +1,84 @@
+# Negotiation Server
+
+This sample shows how to use [Microsoft.Azure.SignalR.Management](https://www.nuget.org/packages/Microsoft.Azure.SignalR.Management) to host negotiation endpoint for SignalR clients.
+
+> You can use [Azure Functions]() or other similar product instead to provide a totally serverless environment.
+>
+> For details what is negotiation and why we need a negotiation endpoint can be found [here]().
+
+## Build from Scratch
+
+### create a webapi app
+
+```
+dotnet new webapi
+```
+
+### Add Management SDK to your project
+
+```
+dotnet add package Microsoft.Azure.SignalR.Management -v 1.0.0-*
+```
+
+### Create a controller for negotiation
+
+```C#
+namespace NegotiationServer.Controllers
+{
+ [ApiController]
+ public class NegotiateController : ControllerBase
+ {
+ ...
+ }
+}
+```
+
+### Create instance of `IServiceManger`
+
+`IServiceManager` provides methods to generate client endpoints and access tokens for SignalR clients to connect to Azure SignalR Service. Add this constructor to the `NegotiateController` class.
+
+```
+private readonly IServiceManager _serviceManager;
+
+public NegotiateController(IConfiguration configuration)
+{
+ var connectionString = configuration["Azure:SignalR:ConnectionString"];
+ _serviceManager = new ServiceManagerBuilder()
+ .WithOptions(o => o.ConnectionString = connectionString)
+ .Build();
+}
+```
+
+### Provide Negotiation Endpoint
+
+In the `NegotiateController` class, provide the negotiation endpoint `//negotiate?user=`.
+
+We use the `_serviceManager` to generate a client endpoint and an access token and return to SignalR client following [Negotiation Protocol](https://github.com/aspnet/SignalR/blob/master/specs/TransportProtocols.md#post-endpoint-basenegotiate-request), which will redirect the SignalR client to the service.
+
+Here we add a special claim to let service recognize is this connection is first connection for this client for the use of reloading feature.
+
+> You only need to provide a negotiation endpoint, since SignalR clients will reach the `//negotiate` endpoint for redirecting, if you provide a hub endpoint `/` to SignalR clients.
+
+```C#
+[HttpPost("{hub}/negotiate")]
+public ActionResult Index(string hub, string user)
+{
+ if (string.IsNullOrEmpty(user))
+ {
+ return BadRequest("User ID is null or empty.");
+ }
+
+ IList cs = new List();
+ cs.Add(new Claim("isFirstConnection", "True"));
+
+ return new JsonResult(new Dictionary()
+ {
+ { "url", _serviceManager.GetClientEndpoint(hub) },
+ { "accessToken", _serviceManager.GenerateClientAccessToken(hub, user) }
+ });
+}
+```
+
+## Full Sample
+
+The full negotiation server sample can be found [here](.). The usage of this sample can be found [here]().
\ No newline at end of file
diff --git a/samples/ServerlessReloading/NegotiationServer/Startup.cs b/samples/ServerlessReloading/NegotiationServer/Startup.cs
new file mode 100644
index 00000000..b743b6c9
--- /dev/null
+++ b/samples/ServerlessReloading/NegotiationServer/Startup.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace NegotiationServer
+{
+ public class Startup
+ {
+ public Startup(IConfiguration configuration)
+ {
+ Configuration = configuration;
+ }
+
+ public IConfiguration Configuration { get; }
+
+ // This method gets called by the runtime. Use this method to add services to the container.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddControllers();
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+
+ //app.UseHttpsRedirection();
+
+ app.UseRouting();
+
+ app.UseAuthorization();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ });
+ }
+ }
+}
diff --git a/samples/ServerlessReloading/NegotiationServer/appsettings.Development.json b/samples/ServerlessReloading/NegotiationServer/appsettings.Development.json
new file mode 100644
index 00000000..8983e0fc
--- /dev/null
+++ b/samples/ServerlessReloading/NegotiationServer/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/samples/ServerlessReloading/NegotiationServer/appsettings.json b/samples/ServerlessReloading/NegotiationServer/appsettings.json
new file mode 100644
index 00000000..d9d9a9bf
--- /dev/null
+++ b/samples/ServerlessReloading/NegotiationServer/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/samples/ServerlessReloading/README.md b/samples/ServerlessReloading/README.md
new file mode 100644
index 00000000..2070ea0c
--- /dev/null
+++ b/samples/ServerlessReloading/README.md
@@ -0,0 +1,71 @@
+Azure SignalR Service Connection Reloading Sample(Service Graceful Shutdown).
+=================================
+
+This sample shows the use of Azure SignalR Service Management SDK to simulate service graceful shutdown and client connection reloading under serverless mode.
+
+* Message Publisher: shows how to publish messages to SignalR clients or to simulate service's graceful shutdown with REST api using Management SDK.
+* Negotiation Server: shows how to negotiate client from you app server to Azure SignalR Service using Management SDK.
+* SignalR Client: is a tool to start multiple SignalR clients(supporting reloading feature) and these clients listen messages for this sample.
+
+## Run the sample
+
+### Start the negotiation server
+
+```
+cd NegotitationServer
+dotnet user-secrets set Azure:SignalR:ConnectionString ""
+dotnet run
+```
+
+### Start SignalR clients
+
+```
+cd SignalRClient
+dotnet run
+```
+
+> Parameters:
+>
+> - -h|--hubEndpoint: Set hub endpoint. Default value: "".
+> - -u|--user: Set user ID. Default value: "User". You can set multiple users like this: "-u user1 -u user2".
+
+### Start message publisher
+
+```
+cd MessagePublisher
+dotnet run
+
+```
+
+> Parameters:
+>
+> -c|--connectionstring: Set connection string.
+> -t|--transport: Set service transport type. Options: |. Default value: transient. Transient: calls REST API for each message. Persistent: Establish a WebSockets connection and send all messages in the connection.
+
+Once the message publisher get started, use the command to send message. If you want to switch all connections from one service instance to another, just enter 'reload'
+
+```
+send user
+send users
+send group
+send groups
+usergroup add
+usergroup remove
+broadcast
+reload
+```
+ For example, type `broadcast hello`, and press keyboard `enter` to publish messages.
+
+You will see `User: gets message from service: 'hello'` from your SignalR client tool.
+
+### Use `user-secrets` to specify Connection String
+
+You can run `dotnet user-secrets set Azure:SignalR:ConnectionString ""` in the root directory of the sample. After that, you don't need the option `-c ""` anymore.
+
+## Build Management Sample from Scratch
+
+The following links are guides for building 3 components of this management sample from scratch.
+
+* [Message Publisher](./MessagePublisher/README.md)
+* [Negotiation Server](./NegotiationServer/README.md)
+* [SignalR Client](./SignalRClient/README.md)
\ No newline at end of file
diff --git a/samples/ServerlessReloading/SignalRClient/Program.cs b/samples/ServerlessReloading/SignalRClient/Program.cs
new file mode 100644
index 00000000..a4fd26b7
--- /dev/null
+++ b/samples/ServerlessReloading/SignalRClient/Program.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.CommandLineUtils;
+
+namespace SignalRClient
+{
+ class Program
+ {
+ private const string DefaultHubEndpoint = "http://localhost:5000/ManagementSampleHub";
+ private const string Target = "Target";
+ private const string DefaultUser = "User";
+
+ static void Main(string[] args)
+ {
+ var app = new CommandLineApplication();
+ app.FullName = "Azure SignalR Management Sample: SignalR Client Tool";
+ app.HelpOption("--help");
+
+ var hubEndpointOption = app.Option("-h|--hubEndpoint", $"Set hub endpoint. Default value: {DefaultHubEndpoint}", CommandOptionType.SingleValue, true);
+ var userIdOption = app.Option("-u|--userIdList", "Set user ID list", CommandOptionType.MultipleValue, true);
+
+ app.OnExecute(async () =>
+ {
+ var hubEndpoint = hubEndpointOption.Value() ?? DefaultHubEndpoint;
+ var userIds = userIdOption.Values != null && userIdOption.Values.Count > 0 ? userIdOption.Values : new List() { "User" };
+
+ var connections = (from userId in userIds
+ select new StableConnection(hubEndpoint, userId)).ToList();
+
+ await Task.WhenAll(from conn in connections
+ select conn.StartAsync());
+
+ Console.WriteLine($"{connections.Count} Client(s) started...");
+ Console.ReadLine();
+
+ await Task.WhenAll(from conn in connections
+ select conn.StopAsync());
+ return 0;
+ });
+
+ app.Execute(args);
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/ServerlessReloading/SignalRClient/README.md b/samples/ServerlessReloading/SignalRClient/README.md
new file mode 100644
index 00000000..efc58afb
--- /dev/null
+++ b/samples/ServerlessReloading/SignalRClient/README.md
@@ -0,0 +1,62 @@
+# SignalR Client
+
+This sample shows how to use SignalR clients to connect Azure SignalR Service without using a web server that host a SignalR hub. The SignalrR client support reloading connection with the use of various types of messages in TypeClass.cs.
+
+## Build from Scratch
+
+### Add Management SDK to your project
+
+```
+dotnet add package Microsoft.Azure.SignalR.Management -v 1.0.0-*
+```
+
+### Connect SignalR clients to a hub endpoint with user ID
+
+Here because we need client to support reloading feature, so we actually build a more powerful StableConnection class on the basis of HubConnection class.
+
+```C#
+var connections = (from userId in userIds
+select new StableConnection(hubEndpoint, userId)).ToList();
+```
+
+### Handle connection closed event in StableConnection class
+
+Sometimes SignalR clients may be disconnected by Azure SignalR Service, the `Closed` event handler will be useful to figure out the reason.
+
+```C#
+connection.Closed += async ex =>
+{
+ // handle exception here
+ ...
+};
+```
+
+### Handle SignalR client callback in StableConnection class
+
+```C#
+connection.On("", ( , , ...) =>
+{
+ // handle received arguments
+ ...
+});
+```
+
+### Establish connection to Azure SignalR Service
+
+```C#
+await connection.StartAsync();
+```
+
+Once your SignalR clients connect to the service, it is able to listen messages.
+
+### Stop SignalR clients
+
+You can stop the client connection anytime you want.
+
+```C#
+await connection.StopAsync();
+```
+
+## Full Sample
+
+The full refined message publisher version(supporting reloading feature) sample can be found [here](.). The usage of this sample can be found [here]().
\ No newline at end of file
diff --git a/samples/ServerlessReloading/SignalRClient/SignalRClient.csproj b/samples/ServerlessReloading/SignalRClient/SignalRClient.csproj
new file mode 100644
index 00000000..e5c3ab5e
--- /dev/null
+++ b/samples/ServerlessReloading/SignalRClient/SignalRClient.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ netcoreapp3.1
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ServerlessReloading/SignalRClient/StableConnection.cs b/samples/ServerlessReloading/SignalRClient/StableConnection.cs
new file mode 100644
index 00000000..be5c8854
--- /dev/null
+++ b/samples/ServerlessReloading/SignalRClient/StableConnection.cs
@@ -0,0 +1,158 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.SignalR.Client;
+using Newtonsoft.Json;
+
+namespace SignalRClient
+{
+ internal class BufferConnection
+ {
+ internal HubConnection conn;
+ internal Queue buffer; // Used to store message received from new connection before old connection ending
+ internal bool active; // Is this connection has already been active or not.
+ internal string addr; // Connected service's url
+ internal int finish; // A flag represents finish state, 2 means both two sides have received barrier msg
+
+ internal BufferConnection(string url)
+ {
+ addr = url;
+ conn = new HubConnectionBuilder().WithUrl(url).Build();
+ buffer = new Queue();
+ active = true;
+ finish = 0;
+ }
+
+ internal BufferConnection(string url, Action opt)
+ {
+ addr = url;
+ conn = new HubConnectionBuilder().WithUrl(url, opt).WithAutomaticReconnect().Build();
+ buffer = new Queue();
+ active = false;
+ finish = 0;
+ }
+
+ }
+ internal class StableConnection
+ {
+ private BufferConnection curconn;
+ private Queue conns; // Backup connections, there may be consecutive reloading event
+ private Channel chan;
+ private Thread t;
+ private const string DefaultHubEndpoint = "http://localhost:5000/ManagementSampleHub";
+ private const string Target = "Target";
+ private const string DefaultUser = "User";
+
+ internal StableConnection(string hubEndpoint = DefaultHubEndpoint, string userId = DefaultUser)
+ {
+ var url = hubEndpoint.TrimEnd('/') + $"?user={userId}";
+
+ curconn = new BufferConnection(url);
+
+ Bind(curconn);
+
+ conns = new Queue();
+ chan = Channel.CreateUnbounded();
+
+ t = new Thread(() => readFromChannel(DefaultUser));
+ t.Start();
+ }
+
+ private async void readFromChannel(string userId)
+ {
+ // print received messages if any
+ while (await chan.Reader.WaitToReadAsync())
+ while (chan.Reader.TryRead(out string item))
+ Console.WriteLine($"{userId}: gets message from {curconn.addr}: '{item}'");
+ }
+
+ private void Bind(BufferConnection bc)
+ {
+ bc.conn.On(Target, async (string message) =>
+ {
+ Message msg = JsonConvert.DeserializeObject(message);
+
+ // 0 : Messages
+ // 1 : ReloadMessage
+ // 2 : FinMessage
+ // 3 : AckMessage
+ if (msg.msgType == "0" && bc.active)
+ {
+ if (bc.active) chan.Writer.TryWrite(msg.content);
+ else
+ {
+ // If not active yet, put message to the buffer
+ bc.buffer.Enqueue(msg.content);
+ }
+ }
+ else if (msg.msgType == "1")
+ {
+ ReloadMessage rmsg = JsonConvert.DeserializeObject(msg.content);
+ Console.WriteLine("My url is" + rmsg.url);
+ BufferConnection backup_conn = new BufferConnection(rmsg.url, options =>
+ {
+ options.AccessTokenProvider = () => Task.FromResult(rmsg.token);
+ });
+ conns.Enqueue(backup_conn);
+
+ Bind(backup_conn);
+
+ await backup_conn.conn.StartAsync();
+ // Send barrier msg with endConnID to both old and new conns
+ await bc.conn.SendAsync("Barrier", bc.conn.ConnectionId, backup_conn.conn.ConnectionId);
+ await backup_conn.conn.SendAsync("Barrier", bc.conn.ConnectionId, backup_conn.conn.ConnectionId);
+ } else if (msg.msgType == "2")
+ {
+ bc.active = false;
+ FinMessage fmsg = JsonConvert.DeserializeObject(msg.content);
+
+ if (fmsg.from == bc.conn.ConnectionId)
+ {
+ bc.finish++;
+ }
+ } else if (msg.msgType == "3")
+ {
+ Console.WriteLine(bc.conn.ConnectionId + " Received message 3");
+ AckMessage amsg = JsonConvert.DeserializeObject(msg.content);
+ if (amsg.connID == bc.conn.ConnectionId)
+ {
+ bc.finish++;
+ }
+ }
+
+ // If both client and old service have received each other's barrier message
+ if (bc.finish == 2)
+ {
+ if (conns.Count == 0) return;
+ curconn = conns.Dequeue();
+ // Write new connection's buffering messages to the channel
+ while (curconn.buffer.Count>0) chan.Writer.TryWrite(curconn.buffer.Dequeue());
+ curconn.active = true;
+ await bc.conn.StopAsync();
+
+ }
+ });
+
+ bc.conn.Closed += ex =>
+ {
+ Console.WriteLine(ex);
+ return Task.CompletedTask;
+ };
+ }
+
+ internal async Task StartAsync()
+ {
+ await curconn.conn.StartAsync();
+ }
+
+ internal async Task StopAsync()
+ {
+ await curconn.conn.StopAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/ServerlessReloading/SignalRClient/TypeClass.cs b/samples/ServerlessReloading/SignalRClient/TypeClass.cs
new file mode 100644
index 00000000..c17df858
--- /dev/null
+++ b/samples/ServerlessReloading/SignalRClient/TypeClass.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+namespace SignalRClient
+{
+ public class Message
+ {
+ public string msgType { get; set; }
+ // 0 : just messages
+ // 1 : ReloadMessage
+ // 2 : FinMessage
+ // 3 : AckMessage
+
+ public string content { get; set; }
+ }
+
+ // Used to notify client that you need to reload connection with new url and token
+ public class ReloadMessage
+ {
+ public string url { get; set; }
+ public string token { get; set; }
+ }
+
+ // Used as Barrier Message
+ public class FinMessage
+ {
+ public string from { get; set; }
+ public string to { get; set; }
+ }
+
+ // When old service has received the Barrier message sent by the client, it will send this AckMessage to the client
+ public class AckMessage
+ {
+ // This connection is over.
+ public string connID { get; set; }
+ }
+}