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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

var builder = DistributedApplication.CreateBuilder(args);

var storage = builder.AddAzureStorage("storage").RunAsEmulator();
var storage = builder.AddAzureStorage("storage")
// see: https://github.com/dotnet/aspire/issues/13811
.RunAsEmulator(azurite =>
{
azurite.WithArgs("--disableProductStyleUrl");
});

var blob = storage.AddBlobs("myblob");

var ps = builder.AddPowerShell("ps")
Expand Down Expand Up @@ -57,5 +63,6 @@ az storage blob upload --connection-string $myblob -c demo --file ./Scripts/scri
.WithArgs(2, 3)
.WaitForCompletion(script1);


Copilot AI Jan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an extra blank line at the end of the file. While this is a minor style issue, it's good practice to maintain consistent formatting without trailing blank lines unless required by the project's style guide.

Suggested change

Copilot uses AI. Check for mistakes.
builder.Build().Run();

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"launchBrowser": true,
"applicationUrl": "https://localhost:17118;http://localhost:15215",
"environmentVariables": {
"DCP_DIAGNOSTICS_LOG_LEVEL": "debug",
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21165",
Comment thread
oising marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Diagnostics;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
Expand Down Expand Up @@ -34,23 +35,34 @@ public static IResourceBuilder<PowerShellRunspacePoolResource> AddPowerShell(

var pool = new PowerShellRunspacePoolResource(name, languageMode, minRunspaces, maxRunspaces);

var poolBuilder = builder.AddResource(pool)
.WithInitialState(new()
{
ResourceType = "PowerShellRunspacePool",
State = KnownResourceStates.NotStarted,
Properties = [

builder.Eventing.Subscribe<InitializeResourceEvent>(pool, async (e, ct) =>
{
var poolResource = e.Resource as PowerShellRunspacePoolResource;

Debug.Assert(poolResource is not null);
new ("LanguageMode", pool.LanguageMode.ToString()),
new ("MinRunspaces", pool.MinRunspaces.ToString()),
new ("MaxRunspaces", pool.MaxRunspaces.ToString())
]
})
.ExcludeFromManifest();

poolBuilder.OnInitializeResource(async (res, e, ct) =>
{
var loggerService = e.Services.GetRequiredService<ResourceLoggerService>();
var notificationService = e.Services.GetRequiredService<ResourceNotificationService>();
var hostLifetime = e.Services.GetRequiredService<IHostApplicationLifetime>();

var sessionState = InitialSessionState.CreateDefault();
sessionState.UseFullLanguageModeInDebugger = true;

// This will block until explicit and implied WaitFor calls are completed
await builder.Eventing.PublishAsync(
new BeforeResourceStartedEvent(poolResource, e.Services), ct);
new BeforeResourceStartedEvent(res, e.Services), ct);

foreach (var annotation in poolResource.Annotations.OfType<PowerShellVariableReferenceAnnotation<ConnectionStringReference>>())
foreach (var annotation in res.Annotations.OfType<PowerShellVariableReferenceAnnotation<ConnectionStringReference>>())
{
if (annotation is { } reference)
{
Expand All @@ -62,24 +74,12 @@ await builder.Eventing.PublishAsync(
}
}

var poolName = poolResource.Name;
var poolName = res.Name;
var poolLogger = loggerService.GetLogger(poolName);

_ = poolResource.StartAsync(sessionState, notificationService, poolLogger, ct);
_ = res.StartAsync(sessionState, notificationService, poolLogger, hostLifetime, ct);
});

return builder.AddResource(pool)
.WithInitialState(new()
{
ResourceType = "PowerShellRunspacePool",
State = KnownResourceStates.NotStarted,
Properties = [

new ("LanguageMode", pool.LanguageMode.ToString()),
new ("MinRunspaces", pool.MinRunspaces.ToString()),
new ("MaxRunspaces", pool.MaxRunspaces.ToString())
]
})
.ExcludeFromManifest();
return poolBuilder;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Management.Automation;
Expand Down Expand Up @@ -37,12 +38,7 @@ public class PowerShellRunspacePoolResource(
/// </summary>
public RunspacePool? Pool { get; private set; }

private Dictionary<string, bool> scriptResourceCompletion = [];
internal void AddScriptResource(PowerShellScriptResource scriptResource) => scriptResourceCompletion.Add(scriptResource.Name, false);
internal void ScriptResourceCompleted(PowerShellScriptResource scriptResource) => scriptResourceCompletion[scriptResource.Name] = true;
internal bool ScriptsCompleted => scriptResourceCompletion.Any(r => r.Value == false);

internal Task StartAsync(InitialSessionState sessionState, ResourceNotificationService notificationService, ILogger logger, CancellationToken token = default)
internal Task StartAsync(InitialSessionState sessionState, ResourceNotificationService notificationService, ILogger logger, IHostApplicationLifetime hostLifetime, CancellationToken token = default)
{
logger.LogInformation(
"Starting PowerShell runspace pool '{PoolName}' with {MinRunspaces} to {MaxRunspaces} runspaces",
Expand All @@ -53,6 +49,13 @@ internal Task StartAsync(InitialSessionState sessionState, ResourceNotificationS
Pool = RunspaceFactory.CreateRunspacePool(MinRunspaces, MaxRunspaces, sessionState, new AspirePSHost(logger));

ConfigureStateChangeNotifications(notificationService, logger);

Comment thread
oising marked this conversation as resolved.
hostLifetime.ApplicationStopping.Register(() =>
{
// if we don't do this, xunit 3 will hang on exit because of open runspaces (foreground threads)
logger.LogInformation("Closing PowerShell runspace pool '{PoolName}'", Name);
Pool?.Close();
});

return Task.Factory.FromAsync(Pool.BeginOpen, Pool.EndOpen, null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Management.Automation;

namespace CommunityToolkit.Aspire.Hosting.PowerShell;
Expand Down Expand Up @@ -33,53 +32,7 @@ public static IResourceBuilder<PowerShellScriptResource> AddScript(

var scriptResource = new PowerShellScriptResource(name, scriptBlock, builder.Resource);

builder.ApplicationBuilder.Eventing.Subscribe<InitializeResourceEvent>(scriptResource, async (e, ct) =>
{
var loggerService = e.Services.GetRequiredService<ResourceLoggerService>();
var notificationService = e.Services.GetRequiredService<ResourceNotificationService>();

var scriptName = scriptResource.Name;
var scriptLogger = loggerService.GetLogger(scriptName);

try
{
// this will block until the runspace pool is started, which is implied by the WaitFor call
await builder.ApplicationBuilder.Eventing.PublishAsync(
new BeforeResourceStartedEvent(scriptResource, e.Services), ct);

scriptLogger.LogInformation("Starting script '{ScriptName}'", scriptName);

_ = scriptResource.StartAsync(scriptLogger, notificationService, ct);

await notificationService.WaitForResourceAsync(scriptResource.Name, KnownResourceStates.Finished, ct);

((IDisposable)scriptResource).Dispose();

builder.Resource.ScriptResourceCompleted(scriptResource);

if (builder.Resource.ScriptsCompleted)
{
await notificationService.PublishUpdateAsync(builder.Resource, state => state with
{
State = KnownResourceStates.Finished,
Properties = [
.. state.Properties,
],
StopTimeStamp = DateTime.Now,
});

((IDisposable)builder.Resource).Dispose();
}
}
catch (Exception ex)
{
scriptLogger.LogError(ex, "Failed to start script '{ScriptName}'", scriptName);
}
});

builder.Resource.AddScriptResource(scriptResource);

return builder.ApplicationBuilder
var scriptBuilder = builder.ApplicationBuilder
.AddResource(scriptResource)
.WithParentRelationship(builder.Resource)
.WaitFor(builder)
Expand Down Expand Up @@ -111,6 +64,32 @@ await notificationService.PublishUpdateAsync(builder.Resource, state => state wi
ResourceCommandState.Disabled :
ResourceCommandState.Enabled
});

scriptBuilder.OnInitializeResource(async (res, e, ct) =>
{
var loggerService = e.Services.GetRequiredService<ResourceLoggerService>();
var notificationService = e.Services.GetRequiredService<ResourceNotificationService>();

var scriptName = res.Name;
var scriptLogger = loggerService.GetLogger(scriptName);

try
{
// this will block until the runspace pool is started, which is implied by the WaitFor call
await builder.ApplicationBuilder.Eventing.PublishAsync(
new BeforeResourceStartedEvent(res, e.Services), ct);

scriptLogger.LogInformation("Starting script '{ScriptName}'", scriptName);

_ = res.StartAsync(scriptLogger, notificationService, ct);
}
catch (Exception ex)
{
scriptLogger.LogError(ex, "Failed to start script '{ScriptName}'", scriptName);
}
});

return scriptBuilder;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ public async Task ScriptsExecuteSuccessfully()
.WaitAsync(TimeSpan.FromSeconds(90));

await Task.WhenAll([ready1, ready2]);

Assert.True(ready1.IsCompletedSuccessfully);
Assert.True(ready2.IsCompletedSuccessfully);
}
}

Loading