Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ProjectCommander.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions Sample/sequenceDiagram.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
sequenceDiagram
participant AppHost as Aspire AppHost
participant Hub as ProjectCommanderHub<br/>(SignalR Server)
participant DataGen as DataGenerator<br/>(AspireProjectCommanderClientWorker)
participant Worker as DataGeneratorWorker<br/>(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<br/>'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<string>("ReceiveCommand", handler)

Note over AppHost,Worker: Command Execution Flow
AppHost->>AppHost: User clicks "Go Slow" or "Go Fast"<br/>in Aspire Dashboard

AppHost->>Hub: Execute command via<br/>WithCommand callback
Note over AppHost: Resolves ProjectCommanderHubResource<br/>and gets IHubContext

Hub->>DataGen: Clients.Group(resourceName)<br/>.SendAsync("ReceiveCommand", commandName)
Note over Hub: commandName = "slow" or "fast"

DataGen->>DataGen: Trigger registered handler
DataGen->>Worker: Fire CommandReceived event<br/>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<br/>with new timing period
11 changes: 11 additions & 0 deletions Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;
/// <param name="model"></param>
internal sealed class ProjectCommanderHub(ILogger logger, ResourceLoggerService loggerService, DistributedApplicationModel model) : Hub
{
/// <summary>
/// Identifies the connecting client by adding it to a group named after the resource.
/// </summary>
/// <param name="resourceName"></param>
/// <returns></returns>
[UsedImplicitly]
public async Task Identify([ResourceName] string resourceName)
{
Expand All @@ -21,6 +26,12 @@ public async Task Identify([ResourceName] string resourceName)
await Groups.AddToGroupAsync(Context.ConnectionId, resourceName);
}

/// <summary>
/// Allows remote clients to watch logs for a specific resource.
/// </summary>
/// <param name="resourceName"></param>
/// <param name="take"></param>
/// <returns></returns>
[UsedImplicitly]
public async IAsyncEnumerable<IReadOnlyList<LogLine>> WatchResourceLogs([ResourceName] string resourceName, int? take = null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public static IResourceBuilder<T> WithProjectCommands<T>(
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) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

namespace CommunityToolkit.Aspire.ProjectCommander
{
/// <summary>
/// Background service that connects to the Aspire Project Commander SignalR Hub and listens for commands.
/// </summary>
/// <param name="configuration"></param>
/// <param name="serviceProvider"></param>
/// <param name="logger"></param>
internal sealed class AspireProjectCommanderClientWorker(IConfiguration configuration, IServiceProvider serviceProvider, ILogger<AspireProjectCommanderClientWorker> logger)
: BackgroundService, IAspireProjectCommanderClient
{
Expand All @@ -27,6 +33,7 @@ await Task.Run(async () =>
.WithAutomaticReconnect()
.Build();

// Wire up a command handler
hub.On<string>("ReceiveCommand", async (command) =>
{
logger.LogDebug("Received command: {CommandName}", command);
Expand All @@ -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);
}
}
});
Expand All @@ -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];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static class ServiceCollectionAspireProjectCommanderExtensions
/// Adds the Aspire Project Commander client to the service collection.
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
/// <returns>Returns the updated service collection.</returns>
public static IServiceCollection AddAspireProjectCommanderClient(this IServiceCollection services)
{
var sp = services.BuildServiceProvider();
Expand Down