diff --git a/ProjectCommander.sln b/ProjectCommander.sln index d095851..6321566 100644 --- a/ProjectCommander.sln +++ b/ProjectCommander.sln @@ -10,6 +10,9 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataGenerator", "Sample\DataGenerator\DataGenerator.csproj", "{C8B82394-D38B-490E-9BF9-3E66579C1EC3}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sample", "Sample", "{A12F2526-69A6-444C-82CA-49DAAB8D0C7E}" + ProjectSection(SolutionItems) = preProject + Sample\sequenceDiagram.mmd = Sample\sequenceDiagram.mmd + EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectCommander.AppHost", "Sample\ProjectCommander.AppHost\ProjectCommander.AppHost.csproj", "{DECE34B3-8776-4684-A92F-8B9C8A1147A7}" EndProject diff --git a/Sample/sequenceDiagram.mmd b/Sample/sequenceDiagram.mmd new file mode 100644 index 0000000..7b825a8 --- /dev/null +++ b/Sample/sequenceDiagram.mmd @@ -0,0 +1,43 @@ +sequenceDiagram + participant AppHost as Aspire AppHost + participant Hub as ProjectCommanderHub
(SignalR Server) + participant DataGen as DataGenerator
(AspireProjectCommanderClientWorker) + participant Worker as DataGeneratorWorker
(BackgroundService) + + Note over AppHost,Worker: Application Startup + AppHost->>Hub: Start SignalR Hub on localhost + Hub-->>AppHost: Hub started successfully + + DataGen->>Hub: Connect to SignalR Hub + Note over DataGen: Using connection string from
'project-commander' config + + DataGen->>Hub: InvokeAsync("Identify", resourceName) + Note over DataGen: resourceName = "datagenerator-{suffix}" + Hub->>Hub: Groups.AddToGroupAsync(connectionId, resourceName) + Hub-->>DataGen: Connection established and grouped + + DataGen->>DataGen: Register "ReceiveCommand" handler + Note over DataGen: hub.On("ReceiveCommand", handler) + + Note over AppHost,Worker: Command Execution Flow + AppHost->>AppHost: User clicks "Go Slow" or "Go Fast"
in Aspire Dashboard + + AppHost->>Hub: Execute command via
WithCommand callback + Note over AppHost: Resolves ProjectCommanderHubResource
and gets IHubContext + + Hub->>DataGen: Clients.Group(resourceName)
.SendAsync("ReceiveCommand", commandName) + Note over Hub: commandName = "slow" or "fast" + + DataGen->>DataGen: Trigger registered handler + DataGen->>Worker: Fire CommandReceived event
with command name + + Worker->>Worker: Process command + alt Command is "slow" + Worker->>Worker: Set period = 1 second + Worker->>Worker: Log "Slow command received" + else Command is "fast" + Worker->>Worker: Set period = 10 milliseconds + Worker->>Worker: Log "Fast command received" + end + + Note over Worker: Worker continues data generation
with new timing period \ No newline at end of file diff --git a/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHub.cs b/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHub.cs index a3e3458..d786c60 100644 --- a/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHub.cs +++ b/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHub.cs @@ -13,6 +13,11 @@ namespace CommunityToolkit.Aspire.Hosting.ProjectCommander; /// internal sealed class ProjectCommanderHub(ILogger logger, ResourceLoggerService loggerService, DistributedApplicationModel model) : Hub { + /// + /// Identifies the connecting client by adding it to a group named after the resource. + /// + /// + /// [UsedImplicitly] public async Task Identify([ResourceName] string resourceName) { @@ -21,6 +26,12 @@ public async Task Identify([ResourceName] string resourceName) await Groups.AddToGroupAsync(Context.ConnectionId, resourceName); } + /// + /// Allows remote clients to watch logs for a specific resource. + /// + /// + /// + /// [UsedImplicitly] public async IAsyncEnumerable> WatchResourceLogs([ResourceName] string resourceName, int? take = null) { diff --git a/Src/Nivot.Aspire.Hosting.ProjectCommander/ResourceBuilderProjectCommanderExtensions.cs b/Src/Nivot.Aspire.Hosting.ProjectCommander/ResourceBuilderProjectCommanderExtensions.cs index cee2e05..503c2ae 100644 --- a/Src/Nivot.Aspire.Hosting.ProjectCommander/ResourceBuilderProjectCommanderExtensions.cs +++ b/Src/Nivot.Aspire.Hosting.ProjectCommander/ResourceBuilderProjectCommanderExtensions.cs @@ -36,6 +36,7 @@ public static IResourceBuilder WithProjectCommands( throw new ArgumentException("You must supply at least one command."); } + // Add command proxies to the dashboard foreach (var command in commands) { builder.WithCommand(command.Name, command.DisplayName, async (context) => diff --git a/Src/Nivot.Aspire.ProjectCommander/AspireProjectCommanderClientWorker.cs b/Src/Nivot.Aspire.ProjectCommander/AspireProjectCommanderClientWorker.cs index 63c3fdd..f3478ec 100644 --- a/Src/Nivot.Aspire.ProjectCommander/AspireProjectCommanderClientWorker.cs +++ b/Src/Nivot.Aspire.ProjectCommander/AspireProjectCommanderClientWorker.cs @@ -5,6 +5,12 @@ namespace CommunityToolkit.Aspire.ProjectCommander { + /// + /// Background service that connects to the Aspire Project Commander SignalR Hub and listens for commands. + /// + /// + /// + /// internal sealed class AspireProjectCommanderClientWorker(IConfiguration configuration, IServiceProvider serviceProvider, ILogger logger) : BackgroundService, IAspireProjectCommanderClient { @@ -27,6 +33,7 @@ await Task.Run(async () => .WithAutomaticReconnect() .Build(); + // Wire up a command handler hub.On("ReceiveCommand", async (command) => { logger.LogDebug("Received command: {CommandName}", command); @@ -40,7 +47,7 @@ await Task.Run(async () => } catch (Exception ex) { - logger.LogError(ex, "Error invocating handler for command: {CommandName}", command); + logger.LogError(ex, "Error invoking handler for command: {CommandName}", command); } } }); @@ -49,6 +56,7 @@ await Task.Run(async () => logger.LogInformation("Connected to Aspire Project Commands Hub: Registering identity..."); + // Grab my suffix from OTEL env vars so the AppHost signalr hub can correctly isolate this client (i.e. there may be replicas) var aspireResourceName = Environment.GetEnvironmentVariable("OTEL_SERVICE_NAME")!; var aspireResourceSuffix = Environment.GetEnvironmentVariable("OTEL_RESOURCE_ATTRIBUTES")!.Split("=")[1]; diff --git a/Src/Nivot.Aspire.ProjectCommander/ServiceCollectionAspireProjectCommanderExtensions.cs b/Src/Nivot.Aspire.ProjectCommander/ServiceCollectionAspireProjectCommanderExtensions.cs index 36c42ea..191cc01 100644 --- a/Src/Nivot.Aspire.ProjectCommander/ServiceCollectionAspireProjectCommanderExtensions.cs +++ b/Src/Nivot.Aspire.ProjectCommander/ServiceCollectionAspireProjectCommanderExtensions.cs @@ -13,7 +13,7 @@ public static class ServiceCollectionAspireProjectCommanderExtensions /// Adds the Aspire Project Commander client to the service collection. /// /// - /// + /// Returns the updated service collection. public static IServiceCollection AddAspireProjectCommanderClient(this IServiceCollection services) { var sp = services.BuildServiceProvider();