From c0d2f319f6b6647635a91d4ae2e56187e335c51d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 23:57:13 +0000 Subject: [PATCH 1/4] Initial plan From 6f4cfe781dadf0c81da6ae0ec62278bf23487977 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:05:16 +0000 Subject: [PATCH 2/4] Add Aspire AppHost project and update samples to use named connection strings with Aspire 13.1.0 Co-authored-by: vicancy <668244+vicancy@users.noreply.github.com> --- .github/workflows/osx.yml | 2 + .github/workflows/ubuntu.yml | 2 + .github/workflows/windows.yml | 2 + AzureSignalR.sln | 7 ++ build/dependencies.private.props | 4 ++ .../ChatSample.CSharpClient/Program.cs | 42 +++++++---- .../Properties/launchSettings.json | 14 ++++ samples/ChatSample/ChatSample/README.md | 69 ++++++++++++------- samples/ChatSample/ChatSample/Startup.cs | 29 ++++---- samples/Samples.AppHost/Program.cs | 17 +++++ .../Properties/launchSettings.json | 29 ++++++++ .../Samples.AppHost/Samples.AppHost.csproj | 26 +++++++ .../appsettings.Development.json | 8 +++ samples/Samples.AppHost/appsettings.json | 9 +++ 14 files changed, 204 insertions(+), 56 deletions(-) create mode 100644 samples/ChatSample/ChatSample.CSharpClient/Properties/launchSettings.json create mode 100644 samples/Samples.AppHost/Program.cs create mode 100644 samples/Samples.AppHost/Properties/launchSettings.json create mode 100644 samples/Samples.AppHost/Samples.AppHost.csproj create mode 100644 samples/Samples.AppHost/appsettings.Development.json create mode 100644 samples/Samples.AppHost/appsettings.json diff --git a/.github/workflows/osx.yml b/.github/workflows/osx.yml index 44a0375e5..b0a9c6d7a 100644 --- a/.github/workflows/osx.yml +++ b/.github/workflows/osx.yml @@ -42,6 +42,8 @@ jobs: dotnet-version: ${{ matrix.dotnet-version }} - name: Display dotnet version run: dotnet --version + - name: Install aspire workload + run: dotnet workload install aspire - name: Build with dotnet run: | dotnet build AzureSignalR.sln /p:DisableNet461Tests=true diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 5ccf1e013..4dce1d7e1 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -42,6 +42,8 @@ jobs: dotnet-version: ${{ matrix.dotnet-version }} - name: Display dotnet version run: dotnet --version + - name: Install aspire workload + run: dotnet workload install aspire - name: Build with dotnet run: "dotnet build AzureSignalR.sln /p:DisableNet461Tests=true" if: steps.filter.outputs.src == 'true' diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 379640711..37da66b4d 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -42,6 +42,8 @@ jobs: dotnet-version: ${{ matrix.dotnet-version }} - name: Display dotnet version run: dotnet --version + - name: Install aspire workload + run: dotnet workload install aspire - name: Build with dotnet run: "dotnet build AzureSignalR.sln" if: steps.filter.outputs.src == 'true' diff --git a/AzureSignalR.sln b/AzureSignalR.sln index 559b7f701..f65e42ba7 100644 --- a/AzureSignalR.sln +++ b/AzureSignalR.sln @@ -89,6 +89,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatSample.RazorPages", "sa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatSample.Cli", "samples\ChatSample.Cli\ChatSample.Cli.csproj", "{45EFE0AE-DE2F-EED2-55FC-9213D2AB9F31}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.AppHost", "samples\Samples.AppHost\Samples.AppHost.csproj", "{66AA925F-2FFC-4CBF-906F-5BEDC1010062}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -203,6 +205,10 @@ Global {45EFE0AE-DE2F-EED2-55FC-9213D2AB9F31}.Debug|Any CPU.Build.0 = Debug|Any CPU {45EFE0AE-DE2F-EED2-55FC-9213D2AB9F31}.Release|Any CPU.ActiveCfg = Release|Any CPU {45EFE0AE-DE2F-EED2-55FC-9213D2AB9F31}.Release|Any CPU.Build.0 = Release|Any CPU + {66AA925F-2FFC-4CBF-906F-5BEDC1010062}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66AA925F-2FFC-4CBF-906F-5BEDC1010062}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66AA925F-2FFC-4CBF-906F-5BEDC1010062}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66AA925F-2FFC-4CBF-906F-5BEDC1010062}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -237,6 +243,7 @@ Global {0F32E624-7AC8-4CA7-8ED9-E1A877442020} = {C965ED06-6A17-4329-B3C6-811830F4F4ED} {D7A38BB7-6416-4E15-AD87-D525F203F549} = {C965ED06-6A17-4329-B3C6-811830F4F4ED} {45EFE0AE-DE2F-EED2-55FC-9213D2AB9F31} = {C965ED06-6A17-4329-B3C6-811830F4F4ED} + {66AA925F-2FFC-4CBF-906F-5BEDC1010062} = {C4BC9889-B49F-41B6-806B-F84941B2549B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7945A4E4-ACDB-4F6E-95CA-6AC6E7C2CD59} diff --git a/build/dependencies.private.props b/build/dependencies.private.props index e62f55d68..892d1291a 100644 --- a/build/dependencies.private.props +++ b/build/dependencies.private.props @@ -21,5 +21,9 @@ 2.2.0 2.2.0 E2eTestUserSecret + + 13.1.0 + 13.1.0 + diff --git a/samples/ChatSample/ChatSample.CSharpClient/Program.cs b/samples/ChatSample/ChatSample.CSharpClient/Program.cs index 7f8c2d7b2..47f384dd2 100644 --- a/samples/ChatSample/ChatSample.CSharpClient/Program.cs +++ b/samples/ChatSample/ChatSample.CSharpClient/Program.cs @@ -16,33 +16,46 @@ sealed class Program { static async Task Main(string[] args) { - var url = "http://localhost:5050"; + var url = Environment.GetEnvironmentVariable("ServerEndpoint") ?? "http://localhost:5050"; + Enum.TryParse(Environment.GetEnvironmentVariable("MODE"), out var mode); var proxy = await ConnectAsync(url + "/chat", Console.Out).ConfigureAwait(false); var currentUser = Guid.NewGuid().ToString("N"); - Mode mode = Mode.Broadcast; if (args.Length > 0) { Enum.TryParse(args[0], true, out mode); } Console.WriteLine($"Logged in as user {currentUser}"); - var input = Console.ReadLine(); - while (!string.IsNullOrEmpty(input)) + if (mode == Mode.Auto) { - switch (mode) + // auto mode + while (true) { - case Mode.Broadcast: - await proxy.InvokeAsync("BroadcastMessage", currentUser, input).ConfigureAwait(false); - break; - case Mode.Echo: - await proxy.InvokeAsync("echo", input).ConfigureAwait(false); - break; - default: - break; + Console.WriteLine("Broadcasting..."); + await proxy.InvokeAsync("BroadcastMessage", currentUser, $"Current time: {DateTime.Now}").ConfigureAwait(false); + await Task.Delay(5000).ConfigureAwait(false); } + } + else + { + var input = Console.ReadLine(); + while (!string.IsNullOrEmpty(input)) + { + switch (mode) + { + case Mode.Broadcast: + await proxy.InvokeAsync("BroadcastMessage", currentUser, input).ConfigureAwait(false); + break; + case Mode.Echo: + await proxy.InvokeAsync("echo", input).ConfigureAwait(false); + break; + default: + break; + } - input = Console.ReadLine(); + input = Console.ReadLine(); + } } } private static async Task ConnectAsync(string url, TextWriter output, CancellationToken cancellationToken = default) @@ -105,6 +118,7 @@ private enum Mode { Broadcast, Echo, + Auto } } } diff --git a/samples/ChatSample/ChatSample.CSharpClient/Properties/launchSettings.json b/samples/ChatSample/ChatSample.CSharpClient/Properties/launchSettings.json new file mode 100644 index 000000000..13a6d3333 --- /dev/null +++ b/samples/ChatSample/ChatSample.CSharpClient/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "ChatSample.CSharpClient": { + "commandName": "Project" + }, + "auto": { + "commandName": "Project", + "environmentVariables": { + "MODE": "Auto" + }, + "distributionName": "" + } + } +} diff --git a/samples/ChatSample/ChatSample/README.md b/samples/ChatSample/ChatSample/README.md index 1b4e6e5f6..ca0400558 100644 --- a/samples/ChatSample/ChatSample/README.md +++ b/samples/ChatSample/ChatSample/README.md @@ -9,35 +9,48 @@ This sample demonstrates how to use Azure SignalR Service with ASP.NET Core Sign - Git (for submodule dependencies) - Docker (optional, for containerized deployment) -## Setup +## Running the Sample -1. Initialize the required submodules: +### Option 1: Run with .NET Aspire (Recommended) + +[.NET Aspire](https://learn.microsoft.com/dotnet/aspire/get-started/aspire-overview#orchestration) is used to orchestrate the samples. + +To work with .NET Aspire, you need the following installed locally: +- [.NET 8.0](https://dotnet.microsoft.com/download/dotnet/8.0) +- .NET Aspire workload: + - Installed with the [Visual Studio installer](https://learn.microsoft.com/dotnet/aspire/fundamentals/setup-tooling?tabs=visual-studio#install-net-aspire) or [the .NET CLI workload](https://learn.microsoft.com/dotnet/aspire/fundamentals/setup-tooling?tabs=dotnet-cli#install-net-aspire). +- An OCI compliant container runtime, such as: + - [Docker Desktop](https://www.docker.com/products/docker-desktop) or [Podman](https://podman.io/). + +In Visual Studio, set **samples/Samples.AppHost** project as the Startup Project. Right click **Connected Services** and select **Azure Resource Provisioning Settings** and select your Azure subscription, region and resource group to use. + +Alternatively, you could add Azure related configurations in the appsettings.json file: + ```json + { + "Azure": { + "SubscriptionId": "your subscription", + "Location": "your location" + } + } + ``` -```bash -git submodule update --init --recursive -``` +Run the project and use Aspire dashboard to navigate to different samples. -2. Configure your Azure SignalR Service connection string: +### Option 2: Run without Aspire - - Update `appsettings.json` by replacing the empty connection string: +Aspire helps you to automatically provision a new Azure SignalR resource and set the connection strings for the sample to use automatically. You could still use the traditional way to set the connection strings by yourself and run the sample directly. Samples now use named connection string `AddNamedAzureSignalR("signalr1")`. Set your connection string to `Azure:SignalR:signalr1:ConnectionString`, or `ConnectionStrings:signalr1`: - ```json - { - "Azure": { - "SignalR": { - "ConnectionString": "" - } - } - } - ``` +```bash +dotnet user-secrets set Azure:SignalR:signalr1:ConnectionString "" +``` - ⚠️ **Important**: Make sure to set your connection string before building the Docker image or running the application. +Or: -## Running the Sample - -### Option 1: Running Locally +```bash +dotnet user-secrets set ConnectionStrings:signalr1 "" +``` -1. Build and run the project: +Then build and run: ```bash dotnet build @@ -50,16 +63,22 @@ You can also specify a custom port: dotnet run --urls="http://localhost:5050" ``` -### Option 2: Running with Docker +### Option 3: Running with Docker + +1. Initialize the required submodules: + +```bash +git submodule update --init --recursive +``` -1. Build the Docker image: +2. Build the Docker image: ```bash docker build -t chat-app -f samples/ChatSample/ChatSample/Dockerfile . ``` -2. Run the container: +3. Run the container: ```bash -docker run -d -p 5050:5050 chat-app +docker run -d -p 5050:5050 -e "ConnectionStrings__signalr1=" chat-app ``` Additional Docker commands: diff --git a/samples/ChatSample/ChatSample/Startup.cs b/samples/ChatSample/ChatSample/Startup.cs index 2050bc0ad..df4cdce6a 100644 --- a/samples/ChatSample/ChatSample/Startup.cs +++ b/samples/ChatSample/ChatSample/Startup.cs @@ -61,24 +61,19 @@ private enum AuthTypes public void ConfigureServices(IServiceCollection services) { services.AddMvc(); - services.AddSignalR() - .AddAzureSignalR(option => + var builder = services.AddSignalR() + .AddNamedAzureSignalR("signalr1"); + builder.Services.Configure(option => + { + option.GracefulShutdown.Mode = GracefulShutdownMode.WaitForClientsClose; + option.GracefulShutdown.Timeout = TimeSpan.FromSeconds(30); + + option.GracefulShutdown.Add(async (c) => { - TokenCredential credential = AuthType switch - { - AuthTypes.VisualStudio => new VisualStudioCodeCredential(), - AuthTypes.ApplicationWithCertificate => new ClientCertificateCredential(TenantId, AppClientId, "path-to-cert-file"), - AuthTypes.ApplicationWithClientSecret => new ClientSecretCredential(TenantId, AppClientId, "client-secret-value"), - AuthTypes.ApplicationWithFederatedIdentity => GetClientAssertionCredential(TenantId, AppClientId, MsiClientId), - AuthTypes.SystemAssignedManagedIdentity => new ManagedIdentityCredential(), - AuthTypes.UserAssignedManagedIdentity => new ManagedIdentityCredential(MsiClientId), - _ => throw new NotImplementedException(), - }; - - option.Endpoints = [ - new ServiceEndpoint(new Uri(Endpoint), credential) - ]; - }) + await c.Clients.All.SendAsync("exit"); + }); + }); + builder .AddMessagePackProtocol(); } diff --git a/samples/Samples.AppHost/Program.cs b/samples/Samples.AppHost/Program.cs new file mode 100644 index 000000000..007bb4c9c --- /dev/null +++ b/samples/Samples.AppHost/Program.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); + +#pragma warning disable AZPROVISION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +var signalr = builder.AddAzureSignalR("signalr1", (_, _, k) => k.AssignProperty(i => i.Sku.Name, "'Standard_S1'")); +#pragma warning restore AZPROVISION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +var chatServer = builder.AddProject("chat").WithReference(signalr).WithExternalHttpEndpoints().WithHttpsEndpoint(); + +builder.AddProject("csharp-client-for-chat", "auto") + .WithEnvironment("ServerEndpoint", chatServer.GetEndpoint("https")); + +builder.Build().Run(); diff --git a/samples/Samples.AppHost/Properties/launchSettings.json b/samples/Samples.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..6bff53917 --- /dev/null +++ b/samples/Samples.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17064;http://localhost:15259", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21254", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22039" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15259", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19030", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20189" + } + } + } +} diff --git a/samples/Samples.AppHost/Samples.AppHost.csproj b/samples/Samples.AppHost/Samples.AppHost.csproj new file mode 100644 index 000000000..5c5de19ba --- /dev/null +++ b/samples/Samples.AppHost/Samples.AppHost.csproj @@ -0,0 +1,26 @@ + + + + Exe + net8.0 + enable + enable + true + 1d977abc-8856-44ed-a28a-f31caa2e7174 + + + + + + + + + + + + + + + + + diff --git a/samples/Samples.AppHost/appsettings.Development.json b/samples/Samples.AppHost/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/samples/Samples.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/Samples.AppHost/appsettings.json b/samples/Samples.AppHost/appsettings.json new file mode 100644 index 000000000..31c092aa4 --- /dev/null +++ b/samples/Samples.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} From 63253148e9fced747911dbd31f83144ecf8f79d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:07:55 +0000 Subject: [PATCH 3/4] Update Samples.AppHost to exclude incompatible .NET Framework projects Co-authored-by: vicancy <668244+vicancy@users.noreply.github.com> --- samples/Samples.AppHost/Samples.AppHost.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/samples/Samples.AppHost/Samples.AppHost.csproj b/samples/Samples.AppHost/Samples.AppHost.csproj index 5c5de19ba..3c6400b6a 100644 --- a/samples/Samples.AppHost/Samples.AppHost.csproj +++ b/samples/Samples.AppHost/Samples.AppHost.csproj @@ -15,9 +15,6 @@ - - - From 47b8254c2075e4aea5919627052c253c07fc17d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:12:35 +0000 Subject: [PATCH 4/4] Address code review feedback - improve error handling and add documentation Co-authored-by: vicancy <668244+vicancy@users.noreply.github.com> --- .../ChatSample.CSharpClient/Program.cs | 34 +++++++++++++++---- samples/Samples.AppHost/Program.cs | 3 ++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/samples/ChatSample/ChatSample.CSharpClient/Program.cs b/samples/ChatSample/ChatSample.CSharpClient/Program.cs index 47f384dd2..79f30a142 100644 --- a/samples/ChatSample/ChatSample.CSharpClient/Program.cs +++ b/samples/ChatSample/ChatSample.CSharpClient/Program.cs @@ -17,7 +17,15 @@ sealed class Program static async Task Main(string[] args) { var url = Environment.GetEnvironmentVariable("ServerEndpoint") ?? "http://localhost:5050"; - Enum.TryParse(Environment.GetEnvironmentVariable("MODE"), out var mode); + var mode = Mode.Broadcast; + + // Try to parse mode from environment variable + var modeEnv = Environment.GetEnvironmentVariable("MODE"); + if (!string.IsNullOrEmpty(modeEnv)) + { + Enum.TryParse(modeEnv, true, out mode); + } + var proxy = await ConnectAsync(url + "/chat", Console.Out).ConfigureAwait(false); var currentUser = Guid.NewGuid().ToString("N"); @@ -29,12 +37,26 @@ static async Task Main(string[] args) Console.WriteLine($"Logged in as user {currentUser}"); if (mode == Mode.Auto) { - // auto mode - while (true) + // auto mode - runs until process is terminated + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (s, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + + try + { + while (!cts.Token.IsCancellationRequested) + { + Console.WriteLine("Broadcasting..."); + await proxy.InvokeAsync("BroadcastMessage", currentUser, $"Current time: {DateTime.Now}").ConfigureAwait(false); + await Task.Delay(5000, cts.Token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) { - Console.WriteLine("Broadcasting..."); - await proxy.InvokeAsync("BroadcastMessage", currentUser, $"Current time: {DateTime.Now}").ConfigureAwait(false); - await Task.Delay(5000).ConfigureAwait(false); + Console.WriteLine("Auto mode cancelled."); } } else diff --git a/samples/Samples.AppHost/Program.cs b/samples/Samples.AppHost/Program.cs index 007bb4c9c..5b0ed1c05 100644 --- a/samples/Samples.AppHost/Program.cs +++ b/samples/Samples.AppHost/Program.cs @@ -5,6 +5,9 @@ var builder = DistributedApplication.CreateBuilder(args); +// Configure Azure SignalR resource with Standard S1 SKU +// Note: The AssignProperty API for Azure resource customization is currently in preview +// and may change in future Aspire updates. This is used here to demonstrate SKU configuration. #pragma warning disable AZPROVISION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var signalr = builder.AddAzureSignalR("signalr1", (_, _, k) => k.AssignProperty(i => i.Sku.Name, "'Standard_S1'")); #pragma warning restore AZPROVISION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.