diff --git a/Sample/Consumer/Consumer.csproj b/Sample/Consumer/Consumer.csproj
index 1eb8ccf..66f4392 100644
--- a/Sample/Consumer/Consumer.csproj
+++ b/Sample/Consumer/Consumer.csproj
@@ -12,6 +12,7 @@
+
diff --git a/Sample/Consumer/Program.cs b/Sample/Consumer/Program.cs
index 6e01dce..7f9b058 100644
--- a/Sample/Consumer/Program.cs
+++ b/Sample/Consumer/Program.cs
@@ -7,7 +7,9 @@
builder.AddServiceDefaults();
-builder.AddAzureEventHubConsumerClient("hub");
+builder.AddAzureEventHubConsumerClient("client");
+
+builder.Services.AddAspireProjectCommanderClient();
builder.Services.AddHostedService();
diff --git a/Sample/ProjectCommander.AppHost/Program.cs b/Sample/ProjectCommander.AppHost/Program.cs
index 1b60b9d..243a80b 100644
--- a/Sample/ProjectCommander.AppHost/Program.cs
+++ b/Sample/ProjectCommander.AppHost/Program.cs
@@ -6,17 +6,26 @@
var datahub = builder.AddAzureEventHubs("data")
.RunAsEmulator()
- .AddHub("hub");
+ .AddHub("hub")
+ .WithProperties(configure =>
+ {
+ configure.PartitionCount = 2;
+ });
+
+var client = datahub.AddConsumerGroup("client");
builder.AddProject("datagenerator")
.WithReference(datahub)
.WithReference(commander)
.WaitFor(commander)
.WaitFor(datahub)
- .WithProjectCommands(("slow", "Go Slow"), ("fast", "Go Fast"));
+ .WithProjectCommands(
+ new("slow", "Go Slow"),
+ new("fast", "Go Fast"));
builder.AddProject("consumer")
- .WithReference(datahub)
+ .WithReference(commander)
+ .WithReference(client)
.WaitFor(datahub);
builder.Build().Run();
diff --git a/Src/Nivot.Aspire.Hosting.ProjectCommander/DistributedApplicationBuilderExtensions.cs b/Src/Nivot.Aspire.Hosting.ProjectCommander/DistributedApplicationBuilderExtensions.cs
index 40927e1..1e2aecb 100644
--- a/Src/Nivot.Aspire.Hosting.ProjectCommander/DistributedApplicationBuilderExtensions.cs
+++ b/Src/Nivot.Aspire.Hosting.ProjectCommander/DistributedApplicationBuilderExtensions.cs
@@ -1,6 +1,7 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
-using Aspire.Hosting.Lifecycle;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;
@@ -31,9 +32,9 @@ public static IResourceBuilder AddAspireProjectComm
///
public static IResourceBuilder AddAspireProjectCommander(this IDistributedApplicationBuilder builder, ProjectCommanderHubOptions options)
{
- if (builder.Resources.Any(r => r.Name == "project-commander"))
+ if (builder.Resources.Any(r => r.Name == ProjectCommanderHubResource.ResourceName))
{
- throw new InvalidOperationException("project-commander resource already exists in the application model");
+ throw new InvalidOperationException("ProjectCommanderHubResource already exists in the application model");
}
if (options == null) throw new ArgumentNullException(nameof(options));
@@ -48,10 +49,38 @@ public static IResourceBuilder AddAspireProjectComm
{
throw new ArgumentException("HubPath must be a valid path", nameof(options.HubPath));
}
+
+ var resource = new ProjectCommanderHubResource(options);
- builder.Services.TryAddLifecycleHook();
-
- var resource = new ProjectCommanderHubResource("project-commander", options);
+ builder.Eventing.Subscribe(resource, async (e, ct) =>
+ {
+ var notify = e.Services.GetRequiredService();
+ await notify.PublishUpdateAsync(resource, state => state with
+ {
+ State = KnownResourceStates.Starting,
+ CreationTimeStamp = DateTime.Now
+ });
+
+ var logger = e.Services.GetRequiredService().GetLogger(resource);
+ logger.LogInformation("Initializing Aspire Project Commander Resource");
+
+ await builder.Eventing.PublishAsync(
+ new BeforeResourceStartedEvent(resource, e.Services), ct);
+
+ var loggerFactory = e.Services.GetRequiredService();
+ var model = e.Services.GetRequiredService();
+
+ await resource.StartHubAsync(loggerFactory, model).ConfigureAwait(false);
+
+ var hubUrl = await resource.ConnectionStringExpression.GetValueAsync(ct);
+
+ await notify.PublishUpdateAsync(resource, state => state with
+ {
+ State = KnownResourceStates.Running,
+ StartTimeStamp = DateTime.Now,
+ Properties = [.. state.Properties, new("hub.url", hubUrl)]
+ });
+ });
return builder.AddResource(resource)
.WithInitialState(new()
@@ -59,8 +88,13 @@ public static IResourceBuilder AddAspireProjectComm
ResourceType = "ProjectCommander",
State = "Stopped",
Properties = [
- new(CustomResourceKnownProperties.Source, "Project Commander"),
- ]
+ new(
+ CustomResourceKnownProperties.Source,
+ "Project Commander Host"),
+ ],
+#if !DEBUG
+ IsHidden = true
+#endif
})
.ExcludeFromManifest();
}
diff --git a/Src/Nivot.Aspire.Hosting.ProjectCommander/Nivot.Aspire.Hosting.ProjectCommander.csproj b/Src/Nivot.Aspire.Hosting.ProjectCommander/Nivot.Aspire.Hosting.ProjectCommander.csproj
index 137da02..df83b10 100644
--- a/Src/Nivot.Aspire.Hosting.ProjectCommander/Nivot.Aspire.Hosting.ProjectCommander.csproj
+++ b/Src/Nivot.Aspire.Hosting.ProjectCommander/Nivot.Aspire.Hosting.ProjectCommander.csproj
@@ -21,5 +21,6 @@
all
runtime; build; native; contentfiles; analyzers
+
diff --git a/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHub.cs b/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHub.cs
index 8bfda9a..a3e3458 100644
--- a/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHub.cs
+++ b/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHub.cs
@@ -1,12 +1,19 @@
using Aspire.Hosting.ApplicationModel;
-using Microsoft.AspNetCore.Razor.Hosting;
+using JetBrains.Annotations;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;
-internal sealed class ProjectCommanderHub(ILogger logger, ResourceLoggerService resourceLogger, DistributedApplicationModel appModel) : Hub
+///
+/// Represents the Aspire Project Commander SignalR Hub implementation.
+///
+///
+///
+///
+internal sealed class ProjectCommanderHub(ILogger logger, ResourceLoggerService loggerService, DistributedApplicationModel model) : Hub
{
+ [UsedImplicitly]
public async Task Identify([ResourceName] string resourceName)
{
logger.LogInformation("{ResourceName} connected to Aspire Project Commander Hub", resourceName);
@@ -14,21 +21,23 @@ public async Task Identify([ResourceName] string resourceName)
await Groups.AddToGroupAsync(Context.ConnectionId, resourceName);
}
+ [UsedImplicitly]
public async IAsyncEnumerable> WatchResourceLogs([ResourceName] string resourceName, int? take = null)
{
- logger.LogInformation("Getting {LinesWanted} logs for resource {ResourceName}", take?.ToString() ?? "all", resourceName);
+ logger.LogTrace("Getting {LinesWanted} logs for resource {ResourceName}", take?.ToString() ?? "all", resourceName);
int taken = 0;
// resolve IResource from resource name
- var resource = appModel.Resources.SingleOrDefault(r => r.Name == resourceName);
+ var resource = model.Resources.SingleOrDefault(r => r.Name == resourceName);
+
if (resource is null)
{
logger.LogWarning("Resource {ResourceName} not found", resourceName);
yield break; // No matching resource found, exit
}
- await foreach (var logs in resourceLogger.WatchAsync(resource)
+ await foreach (var logs in loggerService.WatchAsync(resource)
.WithCancellation(Context.ConnectionAborted)
.ConfigureAwait(false))
{
diff --git a/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHubLifecycleHook.cs b/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHubLifecycleHook.cs
deleted file mode 100644
index 721ff57..0000000
--- a/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHubLifecycleHook.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using Aspire.Hosting.ApplicationModel;
-using Aspire.Hosting.Eventing;
-using Aspire.Hosting.Lifecycle;
-using Microsoft.Extensions.Logging;
-
-namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;
-
-internal sealed class ProjectCommanderHubLifecycleHook(
- ResourceNotificationService notificationService,
- ResourceLoggerService loggerService,
- IDistributedApplicationEventing eventing,
- IServiceProvider services) : IDistributedApplicationLifecycleHook
-{
- public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
- {
- var hubResource = appModel.Resources.OfType().Single();
-
- var logger = loggerService.GetLogger(hubResource);
- hubResource.SetLogger(loggerService);
- hubResource.SetModel(appModel);
-
- await notificationService.PublishUpdateAsync(hubResource, state => state with
- {
- State = KnownResourceStates.Starting,
- CreationTimeStamp = DateTime.Now
- });
-
- try
- {
- await eventing.PublishAsync(
- new BeforeResourceStartedEvent(hubResource, services), cancellationToken);
-
- await hubResource.StartHubAsync();
-
- var hubUrl = await hubResource.ConnectionStringExpression.GetValueAsync(cancellationToken);
-
- await notificationService.PublishUpdateAsync(hubResource, state => state with
- {
- State = KnownResourceStates.Running,
- StartTimeStamp = DateTime.Now,
- Properties = [.. state.Properties, new("HubUrl", hubUrl)]
- });
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "Failed to start Project Commands Hub: {Message}", ex.Message);
-
- await notificationService.PublishUpdateAsync(hubResource, state => state with
- {
- State = KnownResourceStates.FailedToStart
- });
- }
- }
-}
\ No newline at end of file
diff --git a/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHubResource.cs b/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHubResource.cs
index ffe0d05..fedfcde 100644
--- a/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHubResource.cs
+++ b/Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHubResource.cs
@@ -9,40 +9,32 @@
namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;
///
-///
+/// Represents the Aspire Project Commander SignalR Hub Aspire resource.
///
-///
///
-public sealed class ProjectCommanderHubResource([ResourceName] string name, ProjectCommanderHubOptions options)
- : Resource(name), IResourceWithConnectionString, IAsyncDisposable
+public sealed class ProjectCommanderHubResource(ProjectCommanderHubOptions options)
+ : Resource(ResourceName), IResourceWithConnectionString, IAsyncDisposable
{
+ internal const string ResourceName = "project-commander";
+
private WebApplication? _web;
private ILogger? _logger;
- private ResourceLoggerService? _resourceLogger;
- private DistributedApplicationModel? _appModel;
- internal async Task StartHubAsync()
+ internal async Task StartHubAsync(ResourceLoggerService loggerService, DistributedApplicationModel model)
{
- Hub = BuildHub();
+ Hub = BuildHub(loggerService, model);
await (_web!.StartAsync()).ConfigureAwait(false);
_logger?.LogInformation("Aspire Project Commander Hub started");
}
- internal void SetLogger(ResourceLoggerService logger) => _resourceLogger = logger;
-
- internal void SetModel(DistributedApplicationModel appModel) => _appModel = appModel;
-
internal IHubContext? Hub { get; set; }
- private IHubContext BuildHub()
+ private IHubContext BuildHub(ResourceLoggerService loggerService, DistributedApplicationModel model)
{
- // we need the logger to be set before building the hub so we can inject it
- Debug.Assert(_resourceLogger != null, "ResourceLoggerService must be set before building hub");
- _logger = _resourceLogger.GetLogger(this);
-
- Debug.Assert(_appModel != null, "DistributedApplicationModel must be set before building hub");
+ // Get logger for this resource
+ _logger = loggerService.GetLogger(this);
_logger.LogInformation("Building SignalR Hub");
@@ -50,10 +42,10 @@ private IHubContext BuildHub()
var host = WebApplication.CreateBuilder();
// used for streaming logs to clients
- host.Services.AddSingleton(_resourceLogger);
+ host.Services.AddSingleton(loggerService);
// require to resolve IResource from resource names
- host.Services.AddSingleton(_appModel);
+ host.Services.AddSingleton(model);
// proxy logging to AppHost logger
host.Services.AddSingleton(_logger);
@@ -65,7 +57,7 @@ private IHubContext BuildHub()
_web = host.Build();
_web.UseRouting();
- _web.MapGet("/", () => "Aspire Project Commander Host 1.0, powered by SignalR.");
+ _web.MapGet("/", () => "Aspire Project Commander Host, powered by SignalR.");
_web.MapHub(options.HubPath!);
var hub = _web.Services.GetRequiredService>();
diff --git a/Src/Nivot.Aspire.Hosting.ProjectCommander/ResourceBuilderProjectCommanderExtensions.cs b/Src/Nivot.Aspire.Hosting.ProjectCommander/ResourceBuilderProjectCommanderExtensions.cs
index c4486e2..cee2e05 100644
--- a/Src/Nivot.Aspire.Hosting.ProjectCommander/ResourceBuilderProjectCommanderExtensions.cs
+++ b/Src/Nivot.Aspire.Hosting.ProjectCommander/ResourceBuilderProjectCommanderExtensions.cs
@@ -6,6 +6,14 @@
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
+///
+/// Represents a command associated with a project, including its name and display name.
+///
+/// The unique name of the command. This value is typically used as an identifier.
+/// The user-friendly name of the command, intended for display in UI or logs.
+/// Optional arguments to pass to the command.
+public record ProjectCommand(string Name, string DisplayName, params string[] Arguments);
+
///
/// Extension methods for configuring the Aspire Project Commander.
///
@@ -20,14 +28,14 @@ public static class ResourceBuilderProjectCommanderExtensions
///
///
public static IResourceBuilder WithProjectCommands(
- this IResourceBuilder builder, params (string Name, string DisplayName)[] commands)
+ this IResourceBuilder builder, params ProjectCommand[] commands)
where T : ProjectResource
{
if (commands.Length == 0)
{
throw new ArgumentException("You must supply at least one command.");
}
-
+
foreach (var command in commands)
{
builder.WithCommand(command.Name, command.DisplayName, async (context) =>
@@ -39,7 +47,7 @@ public static IResourceBuilder WithProjectCommands(
{
var model = context.ServiceProvider.GetRequiredService();
var hub = model.Resources.OfType().Single().Hub!;
-
+
var groupName = context.ResourceName;
await hub.Clients.Group(groupName).SendAsync("ReceiveCommand", command.Name, context.CancellationToken);
@@ -50,7 +58,9 @@ public static IResourceBuilder WithProjectCommands(
errorMessage = ex.Message;
}
return new ExecuteCommandResult() { Success = success, ErrorMessage = errorMessage };
- }, new CommandOptions {
+ },
+ new CommandOptions
+ {
IconName = "DesktopSignal",
IconVariant = IconVariant.Regular
});
diff --git a/Src/Nivot.Aspire.ProjectCommander/ServiceCollectionAspireProjectCommanderExtensions.cs b/Src/Nivot.Aspire.ProjectCommander/ServiceCollectionAspireProjectCommanderExtensions.cs
index 82d53fe..36c42ea 100644
--- a/Src/Nivot.Aspire.ProjectCommander/ServiceCollectionAspireProjectCommanderExtensions.cs
+++ b/Src/Nivot.Aspire.ProjectCommander/ServiceCollectionAspireProjectCommanderExtensions.cs
@@ -18,7 +18,7 @@ public static IServiceCollection AddAspireProjectCommanderClient(this IServiceCo
{
var sp = services.BuildServiceProvider();
- if (sp.GetService() == null)
+ if (sp.GetService() is null)
{
var worker = ActivatorUtilities.CreateInstance(sp);
services.AddSingleton(worker);