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..79f30a142 100644 --- a/samples/ChatSample/ChatSample.CSharpClient/Program.cs +++ b/samples/ChatSample/ChatSample.CSharpClient/Program.cs @@ -16,33 +16,68 @@ sealed class Program { static async Task Main(string[] args) { - var url = "http://localhost:5050"; + var url = Environment.GetEnvironmentVariable("ServerEndpoint") ?? "http://localhost:5050"; + 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"); - 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 - 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) { - 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("Auto mode cancelled."); } + } + 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 +140,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..5b0ed1c05 --- /dev/null +++ b/samples/Samples.AppHost/Program.cs @@ -0,0 +1,20 @@ +// 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); + +// 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. + +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..3c6400b6a --- /dev/null +++ b/samples/Samples.AppHost/Samples.AppHost.csproj @@ -0,0 +1,23 @@ + + + + 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" + } + } +}