Skip to content

Commit e49c1f2

Browse files
committed
rename project and package prefix #2
1 parent 663030c commit e49c1f2

11 files changed

+450
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using Aspire.Hosting;
2+
using Aspire.Hosting.ApplicationModel;
3+
using Aspire.Hosting.Lifecycle;
4+
5+
namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;
6+
7+
/// <summary>
8+
/// Extension methods for configuring the Aspire Project Commander resource.
9+
/// </summary>
10+
public static class DistributedApplicationBuilderExtensions
11+
{
12+
/// <summary>
13+
/// Adds the Aspire Project Commander resource to the application model.
14+
/// </summary>
15+
/// <param name="builder"></param>
16+
/// <returns></returns>
17+
public static IResourceBuilder<ProjectCommanderHubResource> AddAspireProjectCommander(this IDistributedApplicationBuilder builder)
18+
{
19+
return AddAspireProjectCommander(builder, new ProjectCommanderHubOptions());
20+
}
21+
22+
/// <summary>
23+
/// Adds the Aspire Project Commander resource to the application model.
24+
/// </summary>
25+
/// <param name="builder"></param>
26+
/// <param name="options"></param>
27+
/// <returns></returns>
28+
/// <exception cref="InvalidOperationException"></exception>
29+
/// <exception cref="ArgumentNullException"></exception>
30+
/// <exception cref="ArgumentOutOfRangeException"></exception>
31+
/// <exception cref="ArgumentException"></exception>
32+
public static IResourceBuilder<ProjectCommanderHubResource> AddAspireProjectCommander(this IDistributedApplicationBuilder builder, ProjectCommanderHubOptions options)
33+
{
34+
if (builder.Resources.Any(r => r.Name == "project-commander"))
35+
{
36+
throw new InvalidOperationException("project-commander resource already exists in the application model");
37+
}
38+
39+
if (options == null) throw new ArgumentNullException(nameof(options));
40+
41+
// ensure options.HubPort is > 1024 and < 65535
42+
if (options.HubPort < 1024 || options.HubPort > 65535)
43+
{
44+
throw new ArgumentOutOfRangeException(nameof(options.HubPort), "HubPort must be > 1024 and < 65535");
45+
}
46+
47+
if (string.IsNullOrWhiteSpace(options.HubPath) || options.HubPath.Length < 2)
48+
{
49+
throw new ArgumentException("HubPath must be a valid path", nameof(options.HubPath));
50+
}
51+
52+
builder.Services.TryAddLifecycleHook<ProjectCommanderHubLifecycleHook>();
53+
54+
var resource = new ProjectCommanderHubResource("project-commander", options);
55+
56+
return builder.AddResource(resource)
57+
.WithInitialState(new()
58+
{
59+
ResourceType = "ProjectCommander",
60+
State = "Stopped",
61+
Properties = [
62+
new(CustomResourceKnownProperties.Source, "Project Commander"),
63+
]
64+
})
65+
.ExcludeFromManifest();
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0</TargetFramework>
4+
<ImplicitUsings>enable</ImplicitUsings>
5+
<Nullable>enable</Nullable>
6+
<Title>Aspire Project Commander Hosting</Title>
7+
<PackageIcon>icon.png</PackageIcon>
8+
</PropertyGroup>
9+
<ItemGroup>
10+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
11+
</ItemGroup>
12+
<ItemGroup>
13+
<None Include="..\icon.png">
14+
<Pack>True</Pack>
15+
<PackagePath>\</PackagePath>
16+
</None>
17+
</ItemGroup>
18+
<ItemGroup>
19+
<PackageReference Include="Aspire.Hosting" Version="9.0.0" />
20+
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.1.1">
21+
<PrivateAssets>all</PrivateAssets>
22+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
23+
</PackageReference>
24+
</ItemGroup>
25+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.AspNetCore.SignalR;
2+
using Microsoft.Extensions.Logging;
3+
4+
namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;
5+
6+
internal sealed class ProjectCommanderHub(ILogger logger) : Hub
7+
{
8+
public async Task Identify(string resourceName)
9+
{
10+
logger.LogInformation("{ResourceName} connected to Aspire Project Commander Hub", resourceName);
11+
12+
await Groups.AddToGroupAsync(Context.ConnectionId, resourceName);
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using Aspire.Hosting.ApplicationModel;
2+
using Aspire.Hosting.Lifecycle;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;
6+
7+
internal sealed class ProjectCommanderHubLifecycleHook(ResourceNotificationService notificationService, ResourceLoggerService loggerService) : IDistributedApplicationLifecycleHook
8+
{
9+
public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
10+
{
11+
var hubResource = appModel.Resources.OfType<ProjectCommanderHubResource>().Single();
12+
13+
var logger = loggerService.GetLogger(hubResource);
14+
hubResource.SetLogger(logger);
15+
16+
await notificationService.PublishUpdateAsync(hubResource, state => state with
17+
{
18+
State = KnownResourceStates.Starting,
19+
CreationTimeStamp = DateTime.Now
20+
});
21+
22+
try
23+
{
24+
await hubResource.StartHubAsync();
25+
26+
var hubUrl = await hubResource.ConnectionStringExpression.GetValueAsync(cancellationToken);
27+
28+
await notificationService.PublishUpdateAsync(hubResource, state => state with
29+
{
30+
State = KnownResourceStates.Running,
31+
StartTimeStamp = DateTime.Now,
32+
Properties = [.. state.Properties, new("HubUrl", hubUrl)]
33+
});
34+
}
35+
catch (Exception ex)
36+
{
37+
logger.LogError(ex, "Failed to start Project Commands Hub: {Message}", ex.Message);
38+
39+
await notificationService.PublishUpdateAsync(hubResource, state => state with
40+
{
41+
State = KnownResourceStates.FailedToStart
42+
});
43+
}
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;
2+
3+
/// <summary>
4+
/// Options for configuring the ProjectCommanderHub
5+
/// </summary>
6+
public class ProjectCommanderHubOptions
7+
{
8+
/// <summary>
9+
/// Default port the hub will listen on.
10+
/// </summary>
11+
public const int DefaultHubPort = 27960;
12+
/// <summary>
13+
/// Default path the hub will listen on.
14+
/// </summary>
15+
public const string DefaultHubPath = "/projectcommander";
16+
17+
/// <summary>
18+
/// Gets or sets the port the hub will listen on. Defaults to 27960.
19+
/// </summary>
20+
public int HubPort { get; set; } = DefaultHubPort;
21+
/// <summary>
22+
/// Gets or sets the path the hub will listen on. Defaults to "/projectcommander".
23+
/// </summary>
24+
public string? HubPath { get; set; } = DefaultHubPath;
25+
/// <summary>
26+
/// Gets or sets whether to use HTTPS for the hub. Defaults to true.
27+
/// </summary>
28+
public bool UseHttps { get; set; } = true;
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System.Diagnostics;
2+
using Aspire.Hosting.ApplicationModel;
3+
using Microsoft.AspNetCore.Builder;
4+
using Microsoft.AspNetCore.Hosting;
5+
using Microsoft.AspNetCore.SignalR;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Logging;
8+
9+
namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;
10+
11+
/// <summary>
12+
///
13+
/// </summary>
14+
/// <param name="name"></param>
15+
/// <param name="options"></param>
16+
public sealed class ProjectCommanderHubResource([ResourceName] string name, ProjectCommanderHubOptions options)
17+
: Resource(name), IResourceWithConnectionString, IAsyncDisposable
18+
{
19+
private WebApplication? _web;
20+
private ILogger? _logger;
21+
22+
internal async Task StartHubAsync()
23+
{
24+
Hub = BuildHub();
25+
26+
await (_web!.StartAsync()).ConfigureAwait(false);
27+
28+
_logger?.LogInformation("Aspire Project Commander Hub started");
29+
}
30+
31+
internal void SetLogger(ILogger logger) => _logger = logger;
32+
33+
internal IHubContext<ProjectCommanderHub>? Hub { get; set; }
34+
35+
private IHubContext<ProjectCommanderHub> BuildHub()
36+
{
37+
// we need the logger to be set before building the hub so we can inject it
38+
Debug.Assert(_logger != null, "Logger must be set before building hub");
39+
40+
_logger?.LogInformation("Building SignalR Hub");
41+
42+
// signalr project command host setup
43+
var host = WebApplication.CreateBuilder();
44+
45+
// proxy logging to AppHost logger
46+
host.Services.AddSingleton(_logger!);
47+
48+
host.WebHost.UseUrls($"{(options.UseHttps ? "https" : "http")}://localhost:{options.HubPort}");
49+
50+
host.Services.AddSignalR();
51+
52+
_web = host.Build();
53+
_web.UseRouting();
54+
_web.MapGet("/", () => "Aspire Project Commander Host 1.0, powered by SignalR.");
55+
_web.MapHub<ProjectCommanderHub>(options.HubPath!);
56+
57+
var hub = _web.Services.GetRequiredService<IHubContext<ProjectCommanderHub>>();
58+
59+
_logger?.LogInformation("SignalR Hub built");
60+
61+
return hub;
62+
}
63+
64+
/// <summary>
65+
/// Gets the connection string expression for the SignalR Hub endpoint
66+
/// </summary>
67+
public ReferenceExpression ConnectionStringExpression =>
68+
ReferenceExpression.Create(
69+
$"{(options.UseHttps ? "https" : "http")}://localhost:{options.HubPort.ToString()}/{options.HubPath!.TrimStart('/')}");
70+
71+
/// <summary>
72+
/// Disposes hosted resources
73+
/// </summary>
74+
/// <returns></returns>
75+
public async ValueTask DisposeAsync()
76+
{
77+
if (_web != null) await _web.DisposeAsync();
78+
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using Aspire.Hosting.ApplicationModel;
2+
using CommunityToolkit.Aspire.Hosting.ProjectCommander;
3+
using Microsoft.AspNetCore.SignalR;
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
// ReSharper disable once CheckNamespace
7+
namespace Aspire.Hosting;
8+
9+
/// <summary>
10+
/// Extension methods for configuring the Aspire Project Commander.
11+
/// </summary>
12+
public static class ResourceBuilderProjectCommanderExtensions
13+
{
14+
/// <summary>
15+
/// Adds project commands to a project resource.
16+
/// </summary>
17+
/// <typeparam name="T"></typeparam>
18+
/// <param name="builder"></param>
19+
/// <param name="commands"></param>
20+
/// <returns></returns>
21+
/// <exception cref="ArgumentException"></exception>
22+
public static IResourceBuilder<T> WithProjectCommands<T>(
23+
this IResourceBuilder<T> builder, params (string Name, string DisplayName)[] commands)
24+
where T : ProjectResource
25+
{
26+
if (commands.Length == 0)
27+
{
28+
throw new ArgumentException("You must supply at least one command.");
29+
}
30+
31+
foreach (var command in commands)
32+
{
33+
builder.WithCommand(command.Name, command.DisplayName, async (context) =>
34+
{
35+
bool success = false;
36+
string errorMessage = string.Empty;
37+
38+
try
39+
{
40+
var model = context.ServiceProvider.GetRequiredService<DistributedApplicationModel>();
41+
var hub = model.Resources.OfType<ProjectCommanderHubResource>().Single().Hub!;
42+
43+
var groupName = context.ResourceName;
44+
await hub.Clients.Group(groupName).SendAsync("ReceiveCommand", command.Name, context.CancellationToken);
45+
46+
success = true;
47+
}
48+
catch (Exception ex)
49+
{
50+
errorMessage = ex.Message;
51+
}
52+
return new ExecuteCommandResult() { Success = success, ErrorMessage = errorMessage };
53+
}, iconName: "DesktopSignal", iconVariant: IconVariant.Regular);
54+
}
55+
56+
return builder;
57+
}
58+
}

0 commit comments

Comments
 (0)