Skip to content

Add PowerShell scripting support in Aspire AppHost #706

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3c0d25c
rebase main
Jun 2, 2025
fa49eaf
add tests
May 31, 2025
5783059
fix up directory.packages.props, oops
May 31, 2025
3a4fca9
Update src/CommunityToolkit.Aspire.Hosting.PowerShell/DistributedAppl…
oising Jun 3, 2025
a71bec5
Update src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellRunsp…
oising Jun 3, 2025
a0ffe83
Update src/CommunityToolkit.Aspire.Hosting.PowerShell/DistributedAppl…
oising Jun 3, 2025
73a2446
Merge branch 'main' into powershell
oising Jun 10, 2025
8c18cde
address nits and completely restructure lifecycle management
Jun 10, 2025
194165b
conditionally use az cli in apphost sample; remove dead code
Jun 10, 2025
32ec6dc
Update src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellScrip…
oising Jun 10, 2025
8306f64
Merge branch 'main' into powershell
oising Jun 11, 2025
0947edf
rebase main
Jun 2, 2025
8c039a7
add tests
May 31, 2025
bb34f88
fix up directory.packages.props, oops
May 31, 2025
a9d3252
Update src/CommunityToolkit.Aspire.Hosting.PowerShell/DistributedAppl…
oising Jun 3, 2025
e68d7ce
Update src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellRunsp…
oising Jun 3, 2025
84529c6
Update src/CommunityToolkit.Aspire.Hosting.PowerShell/DistributedAppl…
oising Jun 3, 2025
35396da
address nits and completely restructure lifecycle management
Jun 10, 2025
9afd47b
conditionally use az cli in apphost sample; remove dead code
Jun 10, 2025
f4b6c05
Update src/CommunityToolkit.Aspire.Hosting.PowerShell/PowerShellScrip…
oising Jun 10, 2025
cf0dfd4
address aaron's comments
Jun 11, 2025
45a2dec
fix git dumbness
Jun 11, 2025
dd67233
what
Jun 11, 2025
ee58e7e
drop some informational messages to debug level to reduce noise
Jun 11, 2025
6784a3f
Apply suggestions from code review
aaronpowell Jun 11, 2025
c656736
Merge branch 'main' into powershell
oising Jun 16, 2025
1389a65
Update tests.yaml
oising Jun 16, 2025
d11ae45
Update src/CommunityToolkit.Aspire.Hosting.PowerShell/CommunityToolki…
aaronpowell Jun 17, 2025
a77e240
Merge branch 'main' into powershell
aaronpowell Jun 17, 2025
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
1 change: 1 addition & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
Hosting.Ollama.Tests,
Hosting.PapercutSmtp.Tests,
Hosting.PostgreSQL.Extensions.Tests,
Hosting.PowerShell.Tests,
Hosting.Python.Extensions.Tests,
Hosting.RavenDB.Tests,
Hosting.Redis.Extensions.Tests,
Expand Down
474 changes: 235 additions & 239 deletions CommunityToolkit.Aspire.slnx

Large diffs are not rendered by default.

12 changes: 5 additions & 7 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
<!-- Aspire packages -->
<PackageVersion Include="Aspire.Hosting" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Azure.Storage" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Dapr" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Azure.AppContainers" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Azure.AppContainers" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Azure.Redis" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.NodeJS" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="$(AspireVersion)" />
Expand All @@ -16,7 +17,7 @@
<PackageVersion Include="Aspire.Hosting.Redis" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.MongoDB" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.MySql" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="$(AspireVersion)" />
</ItemGroup>
<ItemGroup Label="Core Packages">
<!-- AspNetCore packages -->
Expand Down Expand Up @@ -54,11 +55,9 @@
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="$(OpenTelemetryVersion)" />
</ItemGroup>

<ItemGroup Label="Build Dependencies">
<PackageVersion Include="Microsoft.DotNet.GenAPI.Task" Version="9.0.103-servicing.25071.13" />
</ItemGroup>

<ItemGroup Label="Integration Packages">
<PackageVersion Include="Azure.Provisioning.AppContainers" Version="1.0.0" />
<PackageVersion Include="JsonSchema.Net" Version="7.3.4" />
Expand All @@ -83,8 +82,8 @@
<PackageVersion Include="AspNetCore.HealthChecks.RavenDB" Version="9.0.0" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.1" />
<PackageVersion Include="Microsoft.PowerShell.SDK" Version="7.4.10" />
</ItemGroup>

<ItemGroup Label="Testing">
<!-- Testing packages -->
<PackageVersion Include="Aspire.Hosting.Testing" Version="$(AspireVersion)" />
Expand All @@ -100,9 +99,8 @@
<PackageVersion Include="Testcontainers" Version="$(TestContainersVersion)" />
<PackageVersion Include="Testcontainers.MsSql" Version="$(TestContainersVersion)" />
</ItemGroup>

<ItemGroup Label=".NET 9 Overrides" Condition="'$(TargetFramework)' == 'net9.0'">
<PackageVersion Update="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.4" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="$(AspireAppHostSdkVersion)" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>bc193f31-c9f7-4e3d-b70a-0dc39ec3047f</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" />
<PackageReference Include="Aspire.Hosting.AppHost" />
<PackageReference Include="Aspire.Hosting.Azure.Storage" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\CommunityToolkit.Aspire.Hosting.PowerShell\CommunityToolkit.Aspire.Hosting.PowerShell.csproj" IsAspireProjectResource="false" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using CommunityToolkit.Aspire.Hosting.PowerShell;

var builder = DistributedApplication.CreateBuilder(args);

var storage = builder.AddAzureStorage("storage").RunAsEmulator();
var blob = storage.AddBlobs("myblob");

var ps = builder.AddPowerShell("ps")
.WithReference(blob)
.WaitFor(storage);

// uploads the script in scripts/
var script1 = ps.AddScript("script1", """
param($name)

write-information "Hello, $name"

# uncommenting this will hang the script if you don't attach the pwsh debugger
# wait-debugger

write-information "`$myblob is $myblob"

# only run this if Azure CLI is installed
if ((gcm az -ErrorAction SilentlyContinue) -ne $null) {

az storage container create --connection-string $myblob -n demo
az storage blob upload --connection-string $myblob -c demo --file ./scripts/script.ps1
write-information "Blob uploaded"

} else {

write-warning "Azure CLI not found, skipping blob upload"

}
write-information $pwd


""").WithArgs("world");

// outputs "the sum of 2 and 3 is 5"
var script2 = ps.AddScript("script2", """
& ./scripts/script.ps1 @args
""")
.WithArgs(2, 3)
.WaitForCompletion(script1);

builder.Build().Run();

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17118;http://localhost:15215",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21165",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22038"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15215",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19175",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20201"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
param($a, $b)

Write-Information "The sum of $a and $b is: $($a + $b)"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AdditionalPackageTags>powershell; pwsh; scripting; script; hosting</AdditionalPackageTags>
<Description>Run powershell scripts in-process with your Aspire AppHost, injecting aspire resources and/or object instances as variables, using the command lines tools of your choice like azure cli, azd, or any other terminal tools.</Description>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" />
<PackageReference Include="Microsoft.PowerShell.SDK" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="CommunityToolkit.Aspire.Hosting.PowerShell.Tests" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace CommunityToolkit.Aspire.Hosting.PowerShell;

/// <summary>
/// Extensions for the <see cref="IDistributedApplicationBuilder"/> to add PowerShell runspace pool resources.
/// </summary>
public static class DistributedApplicationBuilderExtensions
{
/// <summary>
/// Adds a PowerShell runspace pool resource to the distributed application.
/// </summary>
/// <param name="builder"></param>
/// <param name="name"></param>
/// <param name="languageMode"></param>
/// <param name="minRunspaces"></param>
/// <param name="maxRunspaces"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="DistributedApplicationException"></exception>
public static IResourceBuilder<PowerShellRunspacePoolResource> AddPowerShell(
this IDistributedApplicationBuilder builder,
[ResourceName] string name,
PSLanguageMode languageMode = PSLanguageMode.ConstrainedLanguage,
int minRunspaces = 1,
int maxRunspaces = 5)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);

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


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

Debug.Assert(poolResource is not null);

var loggerService = e.Services.GetRequiredService<ResourceLoggerService>();
var notificationService = e.Services.GetRequiredService<ResourceNotificationService>();

var sessionState = InitialSessionState.CreateDefault();

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

foreach (var annotation in poolResource.Annotations.OfType<PowerShellVariableReferenceAnnotation<ConnectionStringReference>>())
{
if (annotation is { } reference)
{
var connectionString = await reference.Value.Resource.GetConnectionStringAsync(ct);
sessionState.Variables.Add(
new SessionStateVariableEntry(reference.Name, connectionString,
$"ConnectionString for {reference.Value.Resource.GetType().Name} '{reference.Name}'",
ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope));
}
}

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

_ = poolResource.StartAsync(sessionState, notificationService, poolLogger, 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();
}
}
Loading
Loading