This content originally appeared on DEV Community and was authored by Nhan Nguyen
Introduction
As modern applications become increasingly distributed, performance testing becomes both more critical and more complex. Traditional approaches often involve setting up separate environments, managing extra tools, and fighting configuration bloat.
But what if load testing could be built directly into your orchestration layer?
In this post, I’ll show you how to integrate K6 load testing seamlessly into a .NET Aspire application. The result is a zero-friction setup where performance tests run alongside your microservices with just:
dotnet aspire run
The Challenge: Testing Distributed Systems
While building the BookWorm platform—an e-commerce app composed of multiple services like Catalog
, Basket
, Ordering
, and Chat
— I faced these typical challenges:
Environment Complexity: Multiple services, databases, and message brokers
Configuration Overhead: Load testing tools require their own pipelines and configs
Developer Friction: Setting up test scenarios slows down iteration
Real-World Simulation: Tests must mimic true user behavior across services
Enter .NET Aspire + K6: A Perfect Match
.NET Aspire provides service orchestration, while K6 offers modern, scriptable load testing.
This combo brings:
Zero Setup: Run tests with dotnet aspire run
Service Discovery: Automatically wires service endpoints
TypeScript Authoring: Full IDE support and intellisense
Integrated Dashboards: K6 dashboard lives inside Aspire UI
Realistic Scenarios: Simulates complex user flows across services
Build Process Overview
The integration seamlessly handles the TypeScript-to-JavaScript build pipeline during the Aspire build process:
Step-by-Step Integration
1.
Install the K6 Hosting Package
Install the Aspire-compatible K6 extension:
dotnet add package CommunityToolkit.Aspire.Hosting.k6
Or add to your .csproj
:
<PackageReference Include="CommunityToolkit.Aspire.Hosting.k6" Version="*" />
2.
MSBuild Integration for TypeScript Compilation
Create a K6Integration.targets
file in your AppHost project:
<Project>
<Target Name="BuildK6Container" BeforeTargets="Build">
<Message Text="Installing node packages" Importance="high" />
<Exec Command="npm install" WorkingDirectory="Container/k6" Condition="Exists('Container/k6/package.json')" />
<Message Text="Formatting K6 code" Importance="high" />
<Exec Command="npm run format:check" WorkingDirectory="Container/k6" Condition="Exists('Container/k6/package.json')" ContinueOnError="true" />
<Message Text="Bundle k6 scripts" Importance="high" />
<Exec Command="npm run build" WorkingDirectory="Container/k6" Condition="Exists('Container/k6/package.json')" />
</Target>
</Project>
Then import it in your AppHost .csproj
:
<Import Project="K6Integration.targets" />
3.
Aspire Extension for K6
public static void AddK6(
this IDistributedApplicationBuilder builder,
IResourceBuilder<YarpResource> entryPoint,
int vus = 10)
{
builder
.AddK6("k6")
.WithImagePullPolicy(ImagePullPolicy.Always)
.WithBindMount("Container/k6", "/scripts", true)
.WithBindMount("Container/k6/dist", "/home/k6")
.WithScript("/scripts/dist/main.js", vus)
.WithReference(entryPoint.Resource.GetEndpoint(Protocol.Http))
.WithEnvironment("K6_WEB_DASHBOARD", "true")
.WithEnvironment("K6_WEB_DASHBOARD_EXPORT", "dashboard-report.html")
.WithHttpEndpoint(targetPort: 5665, name: "k6-dashboard")
.WithUrlForEndpoint("k6-dashboard", url => url.DisplayText = "K6 Dashboard")
.WithK6OtlpEnvironment()
.WaitFor(entryPoint);
}
4.
Integrate K6 into Your AppHost
var builder = DistributedApplication.CreateBuilder(args);
// ... configure your services (databases, APIs, etc.)
var gateway = builder
.AddYarp("gateway")
.LoadFromConfiguration("ReverseProxy")
.WithReference("catalog-api")
.WithReference("basket-api")
// ... other service references
// Add K6 only in run mode
if (builder.ExecutionContext.IsRunMode)
{
builder.AddK6(gateway); // 🎯 That's it!
}
builder.Build().Run();
5.
Authoring Scenarios in TypeScript
The real power comes from structured, TypeScript-based test scenarios. Here’s the project structure:
Container/k6/
├── src/
│ ├── main.ts
│ ├── scenarios/
│ │ ├── browse-catalog.ts
│ │ ├── search-filter.ts
│ │ └── api-comprehensive.ts
│ ├── utils/
│ └── types/
├── package.json
├── tsconfig.json
└── webpack.config.js
You can view the full working K6 test code in the BookWorm repository
BookWorm K6 Source Code
Example:
group("Browse Catalog", () => {
let res = http.get(`${BASE_URL}/`);
check(res, { "homepage loaded": (r) => r.status === 200 });
const books = JSON.parse(res.body);
if (books.length) {
const bookId = books[0].id;
res = http.get(`${BASE_URL}/catalog/api/v1/books/${bookId}`);
}
sleep(1 + Math.random() * 2);
});
6.
Smart Test Configuration
export const options = {
scenarios: {
browse_catalog: {
executor: "ramping-vus",
stages: [
{ duration: "30s", target: 3 },
{ duration: "1m", target: 5 },
{ duration: "30s", target: 0 },
],
env: { scenario: "browse_catalog" },
},
},
thresholds: {
http_req_duration: ["p(95)<1000"],
http_req_failed: ["rate<0.1"],
},
};
7.
Simulating Real User Journeys
export function browseCatalogScenario() {
const res = http.get(`${BASE_URL}/`);
sleep(random(1, 3));
}
Developer Experience
dotnet aspire run
This command:
Starts all services
Runs K6 scenarios
Launches Aspire dashboard
Opens integrated K6 UI
Observability and Reporting
Real-Time Dashboard
Live insights into:
- Requests per second
- P95 response times
- Active users per scenario
- Failures and retries
Test Summary Reports
export function handleSummary(data) {
return {
"summary.html": htmlReport(data),
"summary.json": JSON.stringify(data, null, 2),
stdout: textSummary(data, { indent: " ", enableColors: true }),
};
}
Example output:
Benefits and Results
For Developers
Zero-setup performance tests
Full TypeScript IDE support
Instant feedback from local testing
For Teams
Shift-left testing in dev environments
Reproducible results with seeded randomness
Realistic test coverage across services
Real-World Results
Caught a 300ms latency bug in Catalog search
Optimized DB queries reducing P95 by 40%
Verified gateway can handle 50+ concurrent users
Detected a memory leak in Basket service under load
Conclusion
With .NET Aspire and K6 working together, performance testing becomes just another part of your development loop—not an afterthought.
You can:
- Catch performance regressions early
- Simulate real user flows
- Simplify your testing infrastructure
- Empower every dev to think about performance
Resources
This content originally appeared on DEV Community and was authored by Nhan Nguyen