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
3 changes: 2 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
Hosting.Flagd.Tests,
Hosting.GoFeatureFlag.Tests,
Hosting.Golang.Tests,
Hosting.JavaScript.Extensions.Tests,
Hosting.Java.Tests,
Hosting.k6.Tests,
Hosting.Keycloak.Extensions.Tests,
Expand All @@ -44,7 +45,6 @@ jobs:
Hosting.MongoDB.Extensions.Tests,
Hosting.MySql.Extensions.Tests,
Hosting.Ngrok.Tests,
Hosting.JavaScript.Extensions.Tests,
Hosting.Ollama.Tests,
Hosting.OpenTelemetryCollector.Tests,
Hosting.PapercutSmtp.Tests,
Expand All @@ -58,6 +58,7 @@ jobs:
Hosting.SqlDatabaseProjects.Tests,
Hosting.Sqlite.Tests,
Hosting.SqlServer.Extensions.Tests,
Hosting.Stripe.Tests,
Hosting.SurrealDb.Tests,

# Client integration tests
Expand Down
6 changes: 6 additions & 0 deletions CommunityToolkit.Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@
<Folder Name="/examples/sqlserver-ext/">
<Project Path="examples/sqlserver-ext/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.AppHost/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.AppHost.csproj" />
</Folder>
<Folder Name="/examples/stripe/">
<Project Path="examples/stripe/CommunityToolkit.Aspire.Hosting.Stripe.Api/CommunityToolkit.Aspire.Hosting.Stripe.Api.csproj" />
<Project Path="examples/stripe/CommunityToolkit.Aspire.Hosting.Stripe.AppHost/CommunityToolkit.Aspire.Hosting.Stripe.AppHost.csproj" />
</Folder>
<Folder Name="/examples/surrealdb/">
<Project Path="examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService.csproj" />
<Project Path="examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost.csproj" />
Expand Down Expand Up @@ -198,6 +202,7 @@
<Project Path="src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Sqlite/CommunityToolkit.Aspire.Hosting.Sqlite.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Stripe/CommunityToolkit.Aspire.Hosting.Stripe.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.SurrealDb/CommunityToolkit.Aspire.Hosting.SurrealDb.csproj" />
<Project Path="src/CommunityToolkit.Aspire.KurrentDB/CommunityToolkit.Aspire.KurrentDB.csproj" />
<Project Path="src/CommunityToolkit.Aspire.MassTransit.RabbitMQ/CommunityToolkit.Aspire.MassTransit.RabbitMQ.csproj" />
Expand Down Expand Up @@ -250,6 +255,7 @@
<Project Path="tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Sqlite.Tests/CommunityToolkit.Aspire.Hosting.Sqlite.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Stripe.Tests/CommunityToolkit.Aspire.Hosting.Stripe.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.KurrentDB.Tests/CommunityToolkit.Aspire.KurrentDB.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Keycloak.Extensions.Tests/CommunityToolkit.Aspire.Hosting.Keycloak.Extensions.Tests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () => "Stripe Webhook API");

app.MapPost("/payments/stripe-webhook", async (HttpContext context, IConfiguration configuration) =>
{
var webhookSecret = configuration["STRIPE_WEBHOOK_SECRET"];

using var reader = new StreamReader(context.Request.Body);
var json = await reader.ReadToEndAsync();

// In a real application, you would verify the webhook signature here
// using the STRIPE_WEBHOOK_SECRET

return Results.Ok(new { received = true, hasSecret = !string.IsNullOrEmpty(webhookSecret) });
});

app.MapGet("/health", () => Results.Ok());

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17227;http://localhost:15019",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15019",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Aspire.AppHost.Sdk/13.0.0">

<PropertyGroup>
<OutputType>Exe</OutputType>
<UserSecretsId>7e518d7d-87e8-4337-8806-1c99acce5e01</UserSecretsId>
</PropertyGroup>

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

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
var builder = DistributedApplication.CreateBuilder(args);

var api = builder.AddProject<Projects.CommunityToolkit_Aspire_Hosting_Stripe_Api>("api");

// Provide a development default; override via configuration in real projects.
var stripeApiKey = builder.AddParameter("stripe-api-key", secret: true);

// Forward Stripe webhooks to the API's webhook endpoint
var stripe = builder.AddStripe("stripe", stripeApiKey)
.WithListen(api);

// The API will receive the webhook signing secret via STRIPE_WEBHOOK_SECRET environment variable
api.WithReference(stripe);

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:17217;http://localhost:15269",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:22180",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22179"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15269",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19031",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20128"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AdditionalPackageTags>hosting stripe payments webhooks</AdditionalPackageTags>
<Description>A .NET Aspire integration for the Stripe CLI for local webhook forwarding and testing.</Description>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" />
</ItemGroup>

</Project>
123 changes: 123 additions & 0 deletions src/CommunityToolkit.Aspire.Hosting.Stripe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# CommunityToolkit.Aspire.Hosting.Stripe library

Provides extension methods and resource definitions for a .NET Aspire AppHost to configure the Stripe CLI for local webhook forwarding and testing.

## Getting Started

### Prerequisites

The Stripe CLI must be installed on your machine. You can install it by following the [official Stripe CLI installation guide](https://stripe.com/docs/stripe-cli#install).

### Install the package

In your AppHost project, install the package using the following command:

```dotnetcli
dotnet add package CommunityToolkit.Aspire.Hosting.Stripe
```

### Example usage

Then, in the _Program.cs_ file of your AppHost project, add the Stripe CLI and configure it to forward webhooks to your API:

```csharp
var builder = DistributedApplication.CreateBuilder(args);

var stripeApiKey = builder.AddParameter("stripe-api-key", "sk_test_default", secret: true); // Override for real keys

var externalEndpoint = builder.AddExternalService("webhook-endpoint", "http://localhost:5082");
var stripe = builder.AddStripe("stripe", stripeApiKey)
.WithListen(externalEndpoint, webhookPath: "/payments/stripe-webhook");

var api = builder.AddProject<Projects.API>("api")
.WithReference(stripe);

builder.Build().Run();
```

This will:

1. Start the Stripe CLI listening for webhook events
2. Forward all webhook events to `http://localhost:5082/payments/stripe-webhook`
3. Provide the Stripe API key to the container via the `STRIPE_API_KEY` environment variable
4. Make the webhook signing secret available to the API project via the `STRIPE_WEBHOOK_SECRET` environment variable

### Forwarding to an Aspire endpoint

You can also construct URLs dynamically using Aspire endpoint references:

```csharp
var builder = DistributedApplication.CreateBuilder(args);

var api = builder.AddProject<Projects.API>("api")
.WithHttpEndpoint(port: 5082, name: "http");

var stripeApiKey = builder.AddParameter("stripe-api-key", "sk_test_default", secret: true);

var stripe = builder.AddStripe("stripe", stripeApiKey)
.WithListen(api, webhookPath: "/payments/stripe-webhook",
events: ["payment_intent.created", "charge.succeeded"]);

api.WithReference(stripe);

builder.Build().Run();
```

Note: When constructing URLs with paths, you need to use `ReferenceExpression.Create` to combine the endpoint URL with your webhook path.

### Using a custom environment variable for the webhook secret

By default, the webhook signing secret is exposed as `STRIPE_WEBHOOK_SECRET`. You can customize this:

```csharp
var api = builder.AddProject<Projects.API>("api")
.WithReference(stripe, webhookSigningSecretEnvVarName: "STRIPE_SECRET");
```

### Configuring API key

`AddStripe` requires an `IResourceBuilder<ParameterResource>` that supplies your Stripe API key. The value is exposed to the container as the `STRIPE_API_KEY` environment variable. You can optionally reuse the same parameter to add the `--api-key` command-line argument:

```csharp
var apiKey = builder.AddParameter("stripe-api-key", "sk_test_default", secret: true);

var webhookEndpoint = builder.AddExternalService("webhook-endpoint", "http://localhost:5082");
var stripe = builder.AddStripe("stripe", apiKey)
.WithListen(webhookEndpoint, webhookPath: "/webhooks")
.WithApiKey(apiKey); // optional: forwards the key to the CLI via --api-key
Comment on lines 85 to 87
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The example uses WithListen with a plain string URL, but there is no overload that accepts a string. The available overloads accept either IResourceBuilder<IResourceWithEndpoints> or IResourceBuilder<ExternalServiceResource>.

To fix this example:

var webhookEndpoint = builder.AddExternalService("webhook-endpoint", "http://localhost:5082");
var stripe = builder.AddStripe("stripe", apiKey)
    .WithListen(webhookEndpoint, webhookPath: "/webhooks")
    .WithApiKey(apiKey);

Copilot uses AI. Check for mistakes.
```

### Filtering events

You can filter which webhook events the Stripe CLI listens for:

```csharp
var stripeApiKey = builder.AddParameter("stripe-api-key", "sk_test_default", secret: true);
var webhookEndpoint = builder.AddExternalService("webhook-endpoint", "http://localhost:5082");
var stripe = builder.AddStripe("stripe", stripeApiKey)
.WithListen(webhookEndpoint, webhookPath: "/webhooks",
events: ["payment_intent.created", "charge.succeeded"]);
```

## How it works

The Stripe CLI integration:

- Runs `stripe listen --forward-to <url>` to listen for webhook events from Stripe's test environment
- Forwards those events to your local application endpoint
- Exposes the webhook signing secret so your application can verify webhook authenticity
- Provides a development-friendly way to test webhook integrations without deploying to production

## Additional Information

For more information about the Stripe CLI:

- [Stripe CLI Documentation](https://stripe.com/docs/stripe-cli)
- [Testing webhooks locally](https://stripe.com/docs/webhooks/test)

https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-stripe

## Feedback & contributing

https://github.com/CommunityToolkit/Aspire

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace CommunityToolkit.Aspire.Hosting.Stripe;

internal static class StripeContainerImageTags
{
/// <summary>docker.io</summary>
public const string Registry = "docker.io";
/// <summary>stripe/stripe-cli</summary>
public const string Image = "stripe/stripe-cli";
/// <summary>v1.33.0</summary>
public const string Tag = "v1.33.0";
}
Loading
Loading