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
17 changes: 17 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
**/bin/
**/obj/
**/.vs/
**/.weave/
**/node_modules/
**/*.user
**/*.suo
**/TestResults/
.git/
docs/
tests/
*.md
*.txt
*.ncrunchsolution
*.ncrunchsolution.user
*.DotSettings.user
assets/
9 changes: 1 addition & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,7 @@ jobs:
run: dotnet build MicroStack.slnx -c Release --no-restore

- name: Test
run: dotnet test MicroStack.slnx -c Release --no-build --logger "trx;LogFileName=results.trx"

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: '**/results.trx'
run: dotnet test --project tests/MicroStack.Tests/MicroStack.Tests.csproj -c Release --no-build --filter-not-class MicroStack.Tests.ContainerSmokeTests

- name: Pack NuGet packages
run: dotnet pack src/MicroStack.Aspire.Hosting/MicroStack.Aspire.Hosting.csproj -c Release --no-build --output ./packages
Expand Down
2 changes: 1 addition & 1 deletion .weave/learnings/port-ministack.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
- `ApplyResultPath` unwraps both `__scalar__` and `__list__` before applying to paths (so `ResultPath: "$.status"` with scalar `"enriched"` sets `output["status"] = "enriched"` directly)
- `ApplyResultPathRaw("$", ...)` re-wraps scalars as `__scalar__` and lists as `__list__` to maintain the `Dictionary<string, object?>` return type
- **PostAsJsonAsync camelCase**: `HttpClient.PostAsJsonAsync` uses `JsonSerializerDefaults.Web` in .NET 10 which camelCases property names. Mock config endpoint expects PascalCase keys (StateMachines, TestCases, MockedResponses). Solution: send raw JSON strings with explicit property names instead of using anonymous types.
- **Config endpoint nested properties**: The `/_ministack/config` endpoint receives `{"stepfunctions": {"_sfn_mock_config": {...}}}` as a nested object, NOT a dotted property name. Must navigate `doc.RootElement.TryGetProperty("stepfunctions", out var sfnEl)` then `sfnEl.TryGetProperty("_sfn_mock_config", out var mockEl)`.
- **Config endpoint nested properties**: The `/_microstack/config` endpoint receives `{"stepfunctions": {"_sfn_mock_config": {...}}}` as a nested object, NOT a dotted property name. Must navigate `doc.RootElement.TryGetProperty("stepfunctions", out var sfnEl)` then `sfnEl.TryGetProperty("_sfn_mock_config", out var mockEl)`.
- **SNS protocol mismatch**: SFN's `DispatchToService` sends JSON body with `x-amz-target: SNS.Publish`, but the SNS handler expects query/XML (form-encoded) protocol. Integration test skipped.
- **AWS SDK v4 type ambiguity**: `Tag`, `TagResourceRequest`, `UntagResourceRequest` are ambiguous between StepFunctions and DynamoDBv2 namespaces — use type aliases (`SfnTag`, `SfnTagResourceRequest`, `SfnUntagResourceRequest`).
- **xUnit2012**: Don't use `Assert.True(collection.Any(...))` — use `Assert.Contains(collection, predicate)` instead.
6 changes: 3 additions & 3 deletions .weave/plans/aot-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Make MicroStack (.NET 10 ASP.NET Core app emulating 39 AWS services) compatible

7. **Program.cs** — 3 anonymous types in `Results.Ok(new { ... })`.

8. **Program.cs `/_ministack/config`** — `JsonElementToDict()` / `JsonElementToObject()` helper converts `JsonElement` → `Dictionary<string, object?>` → passed to `sfnHandler.SetMockConfig()`.
8. **Program.cs `/_microstack/config`** — `JsonElementToDict()` / `JsonElementToObject()` helper converts `JsonElement` → `Dictionary<string, object?>` → passed to `sfnHandler.SetMockConfig()`.

**Critical insight about the `Dictionary<string, object?>` pattern:**

Expand Down Expand Up @@ -329,10 +329,10 @@ Enable Native AOT publishing for MicroStack with zero trim warnings and all 1,17
- [x] 17. Verify AOT binary runs correctly
**What**: Run the AOT-compiled binary and execute a smoke test:
1. Start the binary
2. Hit `/_ministack/health` endpoint
2. Hit `/_microstack/health` endpoint
3. Create an SQS queue via AWS SDK
4. Send/receive a message
5. Hit `/_ministack/reset`
5. Hit `/_microstack/reset`
6. Verify health returns clean state
**Acceptance**: AOT binary starts, serves requests, returns correct responses

Expand Down
16 changes: 8 additions & 8 deletions .weave/plans/port-ministack.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Create a fully functional .NET 10 port of MiniStack that passes equivalent integ
### Definition of Done
- [ ] `dotnet build` succeeds with zero warnings
- [ ] `dotnet test` passes all integration tests
- [ ] Health endpoint returns service list at `http://localhost:4566/_ministack/health`
- [ ] Health endpoint returns service list at `http://localhost:4566/_microstack/health`
- [ ] AWS SDK for .NET clients can connect and perform CRUD operations on all ported services

### Guardrails (Must NOT)
Expand Down Expand Up @@ -123,19 +123,19 @@ Create a fully functional .NET 10 port of MiniStack that passes equivalent integ
Source reference: `ministack/app.py` lines 193-552 (ASGI app function)

- [x] 6 Special Route Handling (Pre-Router)
**What**: Implement the special routing paths that are handled before general service dispatch in `app.py`: (a) S3 virtual-hosted style (`{bucket}.localhost` or `{bucket}.s3.localhost`), (b) API Gateway execute-api data plane (`{apiId}.execute-api.localhost`), (c) ALB data plane (`{lb}.alb.localhost` or `/_alb/{lb}/...`), (d) S3 Control API (`/v20180820/...`), (e) RDS Data API REST paths (`/Execute`, `/BeginTransaction`, etc.), (f) SES v2 REST paths (`/v2/email/...`), (g) Cognito well-known endpoints (`/.well-known/jwks.json`, `/.well-known/openid-configuration`), (h) Lambda layer content download (`/_ministack/lambda-layers/...`). For Phase 1, stub these routes to return 501 — they'll be implemented when their respective services are ported.
**What**: Implement the special routing paths that are handled before general service dispatch in `app.py`: (a) S3 virtual-hosted style (`{bucket}.localhost` or `{bucket}.s3.localhost`), (b) API Gateway execute-api data plane (`{apiId}.execute-api.localhost`), (c) ALB data plane (`{lb}.alb.localhost` or `/_alb/{lb}/...`), (d) S3 Control API (`/v20180820/...`), (e) RDS Data API REST paths (`/Execute`, `/BeginTransaction`, etc.), (f) SES v2 REST paths (`/v2/email/...`), (g) Cognito well-known endpoints (`/.well-known/jwks.json`, `/.well-known/openid-configuration`), (h) Lambda layer content download (`/_microstack/lambda-layers/...`). For Phase 1, stub these routes to return 501 — they'll be implemented when their respective services are ported.
**Files**:
- `src/MicroStack/Internal/AwsRequestMiddleware.cs` (extend)
**Acceptance**: Virtual-hosted S3 request to `mybucket.localhost:4566/key` is recognized and routed (to stub). Execute-api host header is recognized.

Source reference: `ministack/app.py` lines 263-513 (pre-router special cases)

- [x] 7 Admin Endpoints (Health, Reset, Config)
**What**: Implement the three admin endpoints: (a) `GET /_ministack/health` (and aliases `/health`, `/_localstack/health`) returns JSON `{"services": {...}, "edition": "light", "version": "0.1.0"}` listing all registered service handlers, (b) `POST /_ministack/reset` calls `Reset()` on all registered service handlers and wipes persistence files, (c) `POST /_ministack/config` accepts JSON body with allowed config keys (stub for now). Register these as minimal API endpoints or handle in middleware before service dispatch.
**What**: Implement the three admin endpoints: (a) `GET /_microstack/health` (and aliases `/health`, `/_localstack/health`) returns JSON `{"services": {...}, "edition": "light", "version": "0.1.0"}` listing all registered service handlers, (b) `POST /_microstack/reset` calls `Reset()` on all registered service handlers and wipes persistence files, (c) `POST /_microstack/config` accepts JSON body with allowed config keys (stub for now). Register these as minimal API endpoints or handle in middleware before service dispatch.
**Files**:
- `src/MicroStack/Internal/AdminEndpoints.cs`
- `src/MicroStack/Program.cs` (wire up)
**Acceptance**: `curl http://localhost:4566/_ministack/health` returns 200 with services JSON. `curl -X POST http://localhost:4566/_ministack/reset` returns 200 with `{"reset":"ok"}`.
**Acceptance**: `curl http://localhost:4566/_microstack/health` returns 200 with services JSON. `curl -X POST http://localhost:4566/_microstack/reset` returns 200 with `{"reset":"ok"}`.

Source reference: `ministack/app.py` lines 289-321, 399-408, 750-819

Expand Down Expand Up @@ -630,12 +630,12 @@ Create a fully functional .NET 10 port of MiniStack that passes equivalent integ
Source reference: `ministack/tests/test_multitenancy.py`

- [x] 62 Persistence Integration Test
**What**: Port `test_ministack_persist.py` (272 lines). Verify that with `PERSIST_STATE=1`, service state survives server restart.
**What**: Port `test_microstack_persist.py` (272 lines). Verify that with `PERSIST_STATE=1`, service state survives server restart.
**Files**:
- `tests/MicroStack.Tests/PersistenceTests.cs`
**Acceptance**: Create resources, restart server, resources still exist.

Source reference: `ministack/tests/test_ministack_persist.py`
Source reference: `ministack/tests/test_microstack_persist.py`

- [ ] 63 Unicode Handling Test
**What**: Port `test_unicode.py` (64 lines). Verify Unicode characters in S3 keys, SQS messages, DynamoDB items, SNS messages are handled correctly.
Expand All @@ -650,8 +650,8 @@ Create a fully functional .NET 10 port of MiniStack that passes equivalent integ
- [ ] `dotnet build src/MicroStack/MicroStack.csproj` succeeds with zero warnings
- [ ] `dotnet build tests/MicroStack.Tests/MicroStack.Tests.csproj` succeeds with zero warnings
- [ ] `dotnet test tests/MicroStack.Tests/` — all integration tests pass
- [ ] Health endpoint at `http://localhost:4566/_ministack/health` returns all services
- [ ] Reset endpoint at `http://localhost:4566/_ministack/reset` clears all state
- [ ] Health endpoint at `http://localhost:4566/_microstack/health` returns all services
- [ ] Reset endpoint at `http://localhost:4566/_microstack/reset` clears all state
- [ ] AWS SDK for .NET clients with `ServiceURL=http://localhost:4566` can perform operations on all services
- [ ] Docker build succeeds and container runs correctly
- [ ] No public types in `*.Internal.*` namespaces
Expand Down
10 changes: 5 additions & 5 deletions .weave/plans/task-27-step-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ Test file: `test_sfn.py` (2,213 lines, 68 tests). We port ~55 tests initially (s
- For direct service integrations (SQS, SNS, DynamoDB), call handlers directly

### Mock Config
- Python uses `_ministack_config` endpoint to set `stepfunctions._sfn_mock_config` at runtime
- Python uses `_microstack_config` endpoint to set `stepfunctions._sfn_mock_config` at runtime
- C# approach: Expose internal `SetMockConfig(Dictionary<string, object?> config)` method
- Wire up `/_ministack/config` endpoint to route SFN mock config to the handler
- Wire up `/_microstack/config` endpoint to route SFN mock config to the handler
- Tests that use mock config: `test_sfn_mock_config_return`, `test_sfn_mock_config_throw` — these need the config endpoint enhanced

### Tests to Skip (dependencies not yet ported)
Expand All @@ -55,7 +55,7 @@ These tests depend on ECS or RDS handlers which are Phase 5/6:
- `test_sfn_aws_sdk_rds_not_found_error` (rds)
- `test_sfn_aws_sdk_query_acronym_param_mapping` (rds)

Also these use `_ministack_config` which needs special handling:
Also these use `_microstack_config` which needs special handling:
- `test_sfn_mock_config_return`
- `test_sfn_mock_config_throw`

Expand Down Expand Up @@ -227,7 +227,7 @@ These are pure unit tests of helper functions — port as C# tests:
- Build and verify 0 warnings

### Step 10: Mock Config Support
**What**: Wire up the `/_ministack/config` endpoint to set SFN mock config at runtime.
**What**: Wire up the `/_microstack/config` endpoint to set SFN mock config at runtime.

**Files**:
- `src/MicroStack/Services/StepFunctions/StepFunctionsServiceHandler.cs` — add `SetMockConfig` method
Expand All @@ -254,7 +254,7 @@ These are pure unit tests of helper functions — port as C# tests:
- For sync client, the SDK adds `sync-` prefix to hostname — need to disable. Check if `AmazonStepFunctionsConfig.DisableHostPrefixInjection` is available, or use custom `HttpClientFactory`
- `_sqs`, `_ddb`, `_sns`, `_lambda`, `_sm` (SecretsManager), `_ssm` clients for integration tests
- `WaitForExecution` polling helper
- `InitializeAsync` calls `/_ministack/reset`
- `InitializeAsync` calls `/_microstack/reset`
- Build and verify 0 warnings

### Step 12: Core Tests (State Machine CRUD + Execution)
Expand Down
17 changes: 17 additions & 0 deletions @progress.txt
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,20 @@ Actions: CreateFileSystem, GetFileSystem, ListFileSystems, DeleteFileSystem, Cre

**Build result:** 0 warnings, 0 errors
**Test result:** 1160 passed, 1 skipped (pre-existing), 0 failed (16 new tests: 9 multi-tenancy + 7 unicode)

### 2026-04-22 - Fix xunit v2→v3 breaking changes (44 files)

**Problem:** Package references were updated from xunit 2.9.3 to xunit.v3.core.mtp-v2 3.2.2, causing ~80+ compilation errors.

**Changes made:**
1. **Both test csproj files** — Added `<OutputType>Exe</OutputType>` (required by xunit v3)
2. **42 test .cs files** — Changed `IAsyncLifetime` methods from `Task` to `ValueTask`:
- `public async Task InitializeAsync()` → `public async ValueTask InitializeAsync()`
- `public async Task DisposeAsync()` → `public async ValueTask DisposeAsync()`
- `public Task DisposeAsync() => Task.CompletedTask;` → `public ValueTask DisposeAsync() => ValueTask.CompletedTask;`
- `return Task.CompletedTask;` → `return ValueTask.CompletedTask;` in DisposeAsync block bodies
3. **LambdaTests.cs** — Replaced incorrect `Assert.SkipUnless(...)` calls with conditional `TestContext.Current.CancelCurrentTest()` (Assert class not available in xunit.v3.core.mtp-v2 without xunit.v3.assert package)

**Files changed:** 44 (2 csproj + 42 .cs files including MicroStackSmokeTests.cs in Aspire project)

**Build result:** 0 warnings, 0 errors
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
RUN apk add --no-cache clang binutils musl-dev build-base zlib-dev zlib-static
WORKDIR /src

# Copy props/solution files first for layer caching
COPY Directory.Build.props ./
COPY src/MicroStack/MicroStack.csproj src/MicroStack/

RUN dotnet restore src/MicroStack/MicroStack.csproj -r linux-musl-x64

# Copy source and publish
COPY src/MicroStack/ src/MicroStack/
RUN dotnet publish src/MicroStack/MicroStack.csproj -c Release -r linux-musl-x64 -o /app --no-restore

FROM alpine:3.21
COPY --from=build /app /app
ENV ASPNETCORE_HTTP_PORTS=4566
EXPOSE 4566
ENTRYPOINT ["/app/MicroStack"]
2 changes: 2 additions & 0 deletions MicroStack.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
</Folder>
<Folder Name="/tests/">
<Project Path="tests/MicroStack.Tests/MicroStack.Tests.csproj" />
<Project Path="tests/MicroStack.Aspire.Tests.AppHost/MicroStack.Aspire.Tests.AppHost.csproj" />
<Project Path="tests/MicroStack.Aspire.Tests/MicroStack.Aspire.Tests.csproj" />
<Project Path="tests/TestLambdaFunctions/SimpleHandler/SimpleHandler.csproj" />
</Folder>
</Solution>
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ dotnet run --project src/MicroStack/MicroStack.csproj
**Verify:**

```bash
curl http://localhost:4566/_ministack/health
curl http://localhost:4566/_microstack/health
```

## Usage
Expand Down Expand Up @@ -107,13 +107,13 @@ See [Services Overview](https://damianh.github.io/microstack/services/overview)

```bash
# Health check — service status
curl http://localhost:4566/_ministack/health
curl http://localhost:4566/_microstack/health

# Reset all state — useful between test runs
curl -X POST http://localhost:4566/_ministack/reset
curl -X POST http://localhost:4566/_microstack/reset

# Runtime config — change settings without restart
curl -X POST http://localhost:4566/_ministack/config \
curl -X POST http://localhost:4566/_microstack/config \
-H "Content-Type: application/json" \
-d '{"stepfunctions._sfn_mock_config": "{...}"}'
```
Expand Down
8 changes: 4 additions & 4 deletions docs/Docs/Content/docs/architecture/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ The registry (`src/MicroStack/Internal/ServiceRegistry.cs`) maps service names t
- Handlers register at startup in `Program.cs`.
- The `SERVICES` environment variable filters which handlers are active.
- Service name aliases are supported (e.g., `cloudwatch` -> `monitoring`, `eventbridge` -> `events`).
- Provides `ResetAll()` for the `/_ministack/reset` endpoint and `GetServiceStatus()` for health checks.
- Provides `ResetAll()` for the `/_microstack/reset` endpoint and `GetServiceStatus()` for health checks.

### IServiceHandler

Expand Down Expand Up @@ -126,9 +126,9 @@ MicroStack handles four AWS API protocols:

Three admin endpoints are registered before the AWS middleware:

- `GET /_ministack/health` — Returns service status and version. Also available at `/health` and `/_localstack/health`.
- `POST /_ministack/reset` — Clears all in-memory state and deletes persisted state files.
- `POST /_ministack/config` — Applies runtime configuration (currently supports Step Functions mock config).
- `GET /_microstack/health` — Returns service status and version. Also available at `/health` and `/_localstack/health`.
- `POST /_microstack/reset` — Clears all in-memory state and deletes persisted state files.
- `POST /_microstack/config` — Applies runtime configuration (currently supports Step Functions mock config).

## Startup Flow

Expand Down
2 changes: 1 addition & 1 deletion docs/Docs/Content/docs/architecture/multi-tenancy.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,4 @@ When persistence is enabled, `AccountScopedDictionary.ToRaw()` serializes **all*

## Reset Behavior

The `POST /_ministack/reset` endpoint calls `Clear()` on all `AccountScopedDictionary` instances, which removes data for **all** accounts. There is no per-account reset.
The `POST /_microstack/reset` endpoint calls `Clear()` on all `AccountScopedDictionary` instances, which removes data for **all** accounts. There is no per-account reset.
2 changes: 1 addition & 1 deletion docs/Docs/Content/docs/architecture/persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The `StatePersistence` class (`src/MicroStack/Internal/StatePersistence.cs`) man

### Reset

The `POST /_ministack/reset` endpoint calls:
The `POST /_microstack/reset` endpoint calls:
- `registry.ResetAll()` — clears all in-memory state.
- `persistence.DeleteAll()` — deletes all `.json` files in `STATE_DIR`.

Expand Down
4 changes: 2 additions & 2 deletions docs/Docs/Content/docs/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ volumes:
## Health Check

```bash
curl http://localhost:4566/_ministack/health
curl http://localhost:4566/_microstack/health
```

Returns JSON with all available services:
Expand All @@ -65,7 +65,7 @@ Returns JSON with all available services:
## Resetting State

```bash
curl -X POST http://localhost:4566/_ministack/reset
curl -X POST http://localhost:4566/_microstack/reset
```

Clears all in-memory state across all services.
2 changes: 1 addition & 1 deletion docs/Docs/Content/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ docker run -p 4566:4566 ghcr.io/damianh/microstack:latest
Verify it's running:

```bash
curl http://localhost:4566/_ministack/health
curl http://localhost:4566/_microstack/health
```

## Quick Start with .NET
Expand Down
8 changes: 4 additions & 4 deletions docs/Docs/Content/docs/internal-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ MicroStack exposes internal endpoints for health checking, state management, and
## Health Check

```bash
curl http://localhost:4566/_ministack/health
curl http://localhost:4566/_microstack/health
```

Returns JSON with service availability, edition, and version:
Expand All @@ -39,7 +39,7 @@ curl http://localhost:4566/health
## Reset State

```bash
curl -X POST http://localhost:4566/_ministack/reset
curl -X POST http://localhost:4566/_microstack/reset
```

Wipes all in-memory state across every service. Returns `200 OK` on success.
Expand All @@ -50,7 +50,7 @@ This is the recommended way to get a clean environment between test runs without
public async Task InitializeAsync()
{
using var http = new HttpClient { BaseAddress = new Uri(connectionString) };
await http.PostAsync("/_ministack/reset", null);
await http.PostAsync("/_microstack/reset", null);
}
```

Expand All @@ -59,7 +59,7 @@ If state persistence is enabled (`PERSIST_STATE=1`), reset also deletes the pers
## Runtime Config

```bash
curl -X POST http://localhost:4566/_ministack/config \
curl -X POST http://localhost:4566/_microstack/config \
-H "Content-Type: application/json" \
-d '{"stepfunctions._sfn_mock_config": "{...}"}'
```
Expand Down
2 changes: 1 addition & 1 deletion docs/Docs/Content/docs/services/step-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ MicroStack includes a full AWS States Language (ASL) execution engine supporting
Configure mock responses for Task states via the config endpoint:

```bash
curl -X POST http://localhost:4566/_ministack/config \
curl -X POST http://localhost:4566/_microstack/config \
-H 'Content-Type: application/json' \
-d '{
"stepfunctions": {
Expand Down
Loading
Loading