Skip to content

Commit

Permalink
rename project and package prefix #2
Browse files Browse the repository at this point in the history
  • Loading branch information
oising committed Dec 26, 2024
1 parent 663030c commit e49c1f2
Show file tree
Hide file tree
Showing 11 changed files with 450 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;

namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;

/// <summary>
/// Extension methods for configuring the Aspire Project Commander resource.
/// </summary>
public static class DistributedApplicationBuilderExtensions
{
/// <summary>
/// Adds the Aspire Project Commander resource to the application model.
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IResourceBuilder<ProjectCommanderHubResource> AddAspireProjectCommander(this IDistributedApplicationBuilder builder)
{
return AddAspireProjectCommander(builder, new ProjectCommanderHubOptions());
}

/// <summary>
/// Adds the Aspire Project Commander resource to the application model.
/// </summary>
/// <param name="builder"></param>
/// <param name="options"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="ArgumentException"></exception>
public static IResourceBuilder<ProjectCommanderHubResource> AddAspireProjectCommander(this IDistributedApplicationBuilder builder, ProjectCommanderHubOptions options)
{
if (builder.Resources.Any(r => r.Name == "project-commander"))
{
throw new InvalidOperationException("project-commander resource already exists in the application model");
}

if (options == null) throw new ArgumentNullException(nameof(options));

// ensure options.HubPort is > 1024 and < 65535
if (options.HubPort < 1024 || options.HubPort > 65535)
{
throw new ArgumentOutOfRangeException(nameof(options.HubPort), "HubPort must be > 1024 and < 65535");
}

if (string.IsNullOrWhiteSpace(options.HubPath) || options.HubPath.Length < 2)
{
throw new ArgumentException("HubPath must be a valid path", nameof(options.HubPath));
}

builder.Services.TryAddLifecycleHook<ProjectCommanderHubLifecycleHook>();

var resource = new ProjectCommanderHubResource("project-commander", options);

return builder.AddResource(resource)
.WithInitialState(new()
{
ResourceType = "ProjectCommander",
State = "Stopped",
Properties = [
new(CustomResourceKnownProperties.Source, "Project Commander"),
]
})
.ExcludeFromManifest();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Title>Aspire Project Commander Hosting</Title>
<PackageIcon>icon.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<None Include="..\icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting" Version="9.0.0" />
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
14 changes: 14 additions & 0 deletions Src/Nivot.Aspire.Hosting.ProjectCommander/ProjectCommanderHub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;

namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;

internal sealed class ProjectCommanderHub(ILogger logger) : Hub
{
public async Task Identify(string resourceName)
{
logger.LogInformation("{ResourceName} connected to Aspire Project Commander Hub", resourceName);

await Groups.AddToGroupAsync(Context.ConnectionId, resourceName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;

namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;

internal sealed class ProjectCommanderHubLifecycleHook(ResourceNotificationService notificationService, ResourceLoggerService loggerService) : IDistributedApplicationLifecycleHook
{
public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
var hubResource = appModel.Resources.OfType<ProjectCommanderHubResource>().Single();

var logger = loggerService.GetLogger(hubResource);
hubResource.SetLogger(logger);

await notificationService.PublishUpdateAsync(hubResource, state => state with
{
State = KnownResourceStates.Starting,
CreationTimeStamp = DateTime.Now
});

try
{
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
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;

/// <summary>
/// Options for configuring the ProjectCommanderHub
/// </summary>
public class ProjectCommanderHubOptions
{
/// <summary>
/// Default port the hub will listen on.
/// </summary>
public const int DefaultHubPort = 27960;
/// <summary>
/// Default path the hub will listen on.
/// </summary>
public const string DefaultHubPath = "/projectcommander";

/// <summary>
/// Gets or sets the port the hub will listen on. Defaults to 27960.
/// </summary>
public int HubPort { get; set; } = DefaultHubPort;
/// <summary>
/// Gets or sets the path the hub will listen on. Defaults to "/projectcommander".
/// </summary>
public string? HubPath { get; set; } = DefaultHubPath;
/// <summary>
/// Gets or sets whether to use HTTPS for the hub. Defaults to true.
/// </summary>
public bool UseHttps { get; set; } = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Diagnostics;
using Aspire.Hosting.ApplicationModel;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace CommunityToolkit.Aspire.Hosting.ProjectCommander;

/// <summary>
///
/// </summary>
/// <param name="name"></param>
/// <param name="options"></param>
public sealed class ProjectCommanderHubResource([ResourceName] string name, ProjectCommanderHubOptions options)
: Resource(name), IResourceWithConnectionString, IAsyncDisposable
{
private WebApplication? _web;
private ILogger? _logger;

internal async Task StartHubAsync()
{
Hub = BuildHub();

await (_web!.StartAsync()).ConfigureAwait(false);

_logger?.LogInformation("Aspire Project Commander Hub started");
}

internal void SetLogger(ILogger logger) => _logger = logger;

internal IHubContext<ProjectCommanderHub>? Hub { get; set; }

private IHubContext<ProjectCommanderHub> BuildHub()
{
// we need the logger to be set before building the hub so we can inject it
Debug.Assert(_logger != null, "Logger must be set before building hub");

_logger?.LogInformation("Building SignalR Hub");

// signalr project command host setup
var host = WebApplication.CreateBuilder();

// proxy logging to AppHost logger
host.Services.AddSingleton(_logger!);

host.WebHost.UseUrls($"{(options.UseHttps ? "https" : "http")}://localhost:{options.HubPort}");

host.Services.AddSignalR();

_web = host.Build();
_web.UseRouting();
_web.MapGet("/", () => "Aspire Project Commander Host 1.0, powered by SignalR.");
_web.MapHub<ProjectCommanderHub>(options.HubPath!);

var hub = _web.Services.GetRequiredService<IHubContext<ProjectCommanderHub>>();

_logger?.LogInformation("SignalR Hub built");

return hub;
}

/// <summary>
/// Gets the connection string expression for the SignalR Hub endpoint
/// </summary>
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create(
$"{(options.UseHttps ? "https" : "http")}://localhost:{options.HubPort.ToString()}/{options.HubPath!.TrimStart('/')}");

/// <summary>
/// Disposes hosted resources
/// </summary>
/// <returns></returns>
public async ValueTask DisposeAsync()
{
if (_web != null) await _web.DisposeAsync();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Hosting.ProjectCommander;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;

// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;

/// <summary>
/// Extension methods for configuring the Aspire Project Commander.
/// </summary>
public static class ResourceBuilderProjectCommanderExtensions
{
/// <summary>
/// Adds project commands to a project resource.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="builder"></param>
/// <param name="commands"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static IResourceBuilder<T> WithProjectCommands<T>(
this IResourceBuilder<T> builder, params (string Name, string DisplayName)[] 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) =>
{
bool success = false;
string errorMessage = string.Empty;

try
{
var model = context.ServiceProvider.GetRequiredService<DistributedApplicationModel>();
var hub = model.Resources.OfType<ProjectCommanderHubResource>().Single().Hub!;

var groupName = context.ResourceName;
await hub.Clients.Group(groupName).SendAsync("ReceiveCommand", command.Name, context.CancellationToken);

success = true;
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
return new ExecuteCommandResult() { Success = success, ErrorMessage = errorMessage };
}, iconName: "DesktopSignal", iconVariant: IconVariant.Regular);
}

return builder;
}
}
Loading

0 comments on commit e49c1f2

Please sign in to comment.