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();