Feat/portfolio owned position events - #31
Open
JacobChwastek wants to merge 47 commits into
Open
Conversation
Delete tests/WiSave.Portal.IntegrationTests and its solution entry. The WebApplicationFactory-based suite covered auth endpoints, admin access management, the YARP user-header transform, SignalR hubs, the Redis backplane, and Wolverine consumer fan-out, but its maintenance cost outweighed the signal it produced. Portal test coverage is now WiSave.Portal.UnitTests only. Behavior worth keeping is re-covered at unit level as it comes up; the header transform's stripping of client-supplied X-User-* headers is the most notable gap left behind.
Add .config/dotnet-tools.json pinning dotnet-ef to 10.0.9 with rollForward disabled, matching Microsoft.EntityFrameworkCore.Design in Directory.Packages.props. Generating a script with a mismatched tool writes the wrong ProductVersion into __EFMigrationsHistory, which is how the tree ended up with a 10.0.7-generated script against 10.0.9 packages. Delete scripts/generate-dbup-script.sh. It only wrapped a single dotnet ef invocation, and its --startup-project pointed at WiSave.Portal.EfTools, which no longer exists. The Portal project's DesignTimeDbContextFactory makes the direct command sufficient. Run `dotnet tool restore` once per clone before generating scripts.
Make AGENTS.md the single source of truth and reduce CLAUDE.md to an @AGENTS.md import, so Claude Code inlines the same content other agent tooling reads. The two files had drifted into contradicting each other. Correct the guidance against the current tree while merging: - drop WiSave.Portal.EfTools and tests/WiSave.Portal.Tests, both gone - document WiSave.Portal.Console and WiSave.Portal.Contracts - replace the plan-based permission model with the role-claim model that RolePermissionResolver actually implements; there are no Plans, Permissions, or PlanPermissions tables - record that identity keys are UUIDv7 Guids - add the expenses proxy route, per-route authorization policies, and the /api prefix strip; correct the stocks port from 5086 to 5300 - state that the integration suite is not to be recreated Surface AGENTS.md and Directory.Packages.props in solution items.
Record the design for two requested changes: extracting code from the WiSave.Portal web project into class libraries, and replacing the hand-written Dockerfile plus docker-compose with .NET Aspire. The investigation recommends extracting zero class libraries. The reuse driver is structurally blocked — WiSave.Portal.Contracts has had no consumer for 3.6 months because the publish feed is not configured in this repo or any sibling — and the testability driver is a code-shape problem, not a project-boundary one. The untestable paths are untestable because logic sits in YARP transform closures, non-virtual methods on concrete Identity-manager dependencies, and private statics inside 399-line endpoint files. Refactoring in place fixes all of it. Aspire is adopted last and additively. Its two headline arguments do not survive scrutiny: the private-feed fix is dotnet publish -t:PublishContainer, an SDK capability the csproj is already configured for, and startup ordering is depends_on: service_completed_successfully. What remains is a dashboard, a one-command dev loop, and a home for OTel tracing across the YARP hop. RabbitMQ never moves; it is the ecosystem's only broker and Aspire 13.4.6 cannot join a pre-existing Docker network in run mode. The design also documents three live defects found while investigating: db-rebuild-identity throws at runtime, ApplyPortalMigrations has no callers so Migrations__AutoApplyOnStartup is dead config, and the stocks proxy target points at the incomes service port. User decisions folded in: drop the stash line of work and delete the identity-rebuild path; nothing deploys Portal at 192.168.1.50:5100, so the Dockerfile can go and Phase 8 is dropped. Package reuse is treated as aspirational by default and is flagged as the one decision not explicitly confirmed.
Delete docs/superpowers and ignore it, along with .superpowers. The 22 specs and plans tracked here now live in the Obsidian vault at Development/superpowers/wisave-portal/, with YAML frontmatter carrying project, type, date and nested Obsidian tags. Design churn does not belong in the code history, and the documents are more useful somewhere they can be searched and cross-linked. Migrated content was verified byte-identical before deletion. Note this stops tracking going forward; earlier commits still contain these files.
Split src/WiSave.Portal into three projects with an enforced dependency
direction:
Core.Application -> nothing at all
Core.Infrastructure -> Contracts, Core.Application
WebApi -> Contracts, Core.Application, Core.Infrastructure,
Migrations
Core.Application holds pure policy — permission vocabulary, access-management
predicates, downstream-service options, realtime envelopes — and declares zero
ProjectReference, PackageReference and FrameworkReference. Core.Infrastructure
holds the Identity entities, PortalDbContext and its EF migrations, permission
resolution and the Redis ticket store. The web project is renamed to
WiSave.Portal.WebApi and keeps only HTTP, SignalR, YARP and Wolverine wiring;
RootNamespace is pinned to WiSave.Portal so no C# namespace changed.
No Core.Domain project: ApplicationUser and ApplicationRole are EF Identity
persistence entities and would drag Microsoft.Extensions.Identity.Stores into
any "domain" project holding them.
Behaviour pinned by tests before it moved. The YARP request-transform closure
is now an internal static method, so the stripping of client-supplied X-User-*
headers is asserted directly — the gap 49eaa0c named when the integration
suite was deleted. Permission resolution moved behind IPermissionResolver and
the admin access-management predicates into their own class, both with tests.
ProjectDependencyDirectionTests asserts the csproj reference sets, so a future
violation fails the build rather than passing review.
The three EF files moved as a byte-preserving text edit — namespace, using and
five ApplicationUser entity-name strings, verified as 1/7/7 changed lines with
no table.Column, HasColumnType or [Migration] change. Regenerating them here
would have smuggled a text->uuid schema conversion into a refactor; that
belongs to the migration-authority plan.
Also removes the db-rebuild-identity console command and its DbMigrator path,
which threw at runtime after 49eaa0c deleted the maintenance SQL it loaded,
and corrects the downstream proxy ports — incomes was pointed at the retired
5114 and stocks at the incomes port 5300 instead of 5301.
Tests: 62 (1 failing) -> 90 passing.
BREAKING: the built assembly is now WiSave.Portal.WebApi.dll and the project
lives at src/WiSave.Portal.WebApi. The Dockerfile and docker-compose are
updated; the published container repository name is unchanged. Anything
outside this repo that builds src/WiSave.Portal by path needs updating.
Move IPermissionResolver from Core.Infrastructure into Core.Application and change its signature from GetPermissionsAsync(ApplicationUser) to GetPermissionsAsync(Guid userId, CancellationToken). The port was declared beside its own implementation and named an EF entity, so the web project depended on the infrastructure layer purely to obtain an abstraction. The resolver now looks the user up itself and returns an empty set when there is none. PermissionResolutionMiddleware drops its UserManager dependency and resolves straight from the name-identifier claim. Behaviour is unchanged: PermissionHandler already treated an absent and an empty permission set identically, and a test now pins the unknown-user path.
Introduce IRealtimeNotifier in Core.Application, implemented by SignalRRealtimeNotifier in the web project. The event handlers translate an inbound integration event into a RealtimeEnvelope and hand it to the port; the hub, its group naming and its Redis backplane stay behind the boundary. Their tests lose a three-level IHubContext/IHubClients/IClientProxy substitute in favour of one fake notifier. Adds RealtimePayloadContractTests, which pins the payload fields wisave-ui declares in src/app/core/signalr/stocks-signalr.types.ts. The portal never reads these payloads — it relays downstream integration events verbatim — so a rename in WiSave.Stock.Contracts would otherwise reach the browser with nothing failing in either build. Presence is asserted rather than equality, so additive downstream changes stay compatible.
Microsoft.OpenApi 2.0.0 has a known high-severity vulnerability (GHSA-v5pm-xwqc-g5wc) and was reaching the build transitively through Microsoft.AspNetCore.OpenApi, which still pins exactly 2.0.0 even in its latest 10.0.10 release — so bumping the ASP.NET package does not help. Enable CentralPackageTransitivePinningEnabled and pin Microsoft.OpenApi to 2.11.0, the newest release in the same major, so the ASP.NET package keeps the API surface it compiled against. The advisory fired NU1903 on every restore. The build is now warning-free, which also unblocks adopting TreatWarningsAsErrors later.
Move the Microsoft.* packages from 10.0.9 to 10.0.10, Npgsql.EntityFrameworkCore .PostgreSQL to 10.0.3 and NSubstitute to 6.0.0. NSubstitute 6 annotates Arg.Is<T> for nullability, which surfaced CS8604 in the two event-handler test helpers added with the realtime port: the predicate can be invoked with null, so the helpers now take RealtimeEnvelope? and assert non-null. Build is warning-free again and all 98 tests pass on the new majors.
The notification handlers and the Wolverine listen definitions were never the web project's responsibility. Move both, so WiSave.Portal.WebApi is HTTP surface plus composition and nothing else. Handlers move to Core.Application, which gains the three integration-contract packages — the only references it is allowed. Their duplicated envelope construction becomes a shared RealtimeNotificationHandler base: build the envelope, drop events that carry no user, hand it to IRealtimeNotifier. The domain discriminators move into a RealtimeDomain constant class; they are wire contract, since the Angular client filters on them before dispatching to a feature service. The broker and listen configuration moves to Core.Infrastructure as MessagingHostBuilderExtensions, taking WolverineFx.RabbitMQ, WiSave.Framework.Messaging.Wolverine and the contracts packages with it. The web project's package list drops from 12 to 7. ProjectDependencyDirectionTests is retuned rather than relaxed: Core.Application still bans project and framework references outright, and its packages must match an explicit three-entry allowlist, so a fourth is a deliberate edit rather than drift.
Both handlers did the same thing to different upstream event families, so splitting them by source service only duplicated the translation. Merge them into RealtimeNotificationsEventHandler covering all sixteen events. It is named for what it produces rather than how the notification travels: the handler pushes a RealtimeEnvelope at IRealtimeNotifier and does not know SignalR exists. Putting the transport back in the name would undo that. Also restores the legacy RabbitMQ queue name, which the previous commit's namespace rewrite silently changed. It reads like a .NET namespace because Wolverine derived it from one, but it is a durable queue that already exists on the broker. Renaming it would have made the portal bind a different queue and stop consuming the legacy income, category and expense notifications, with nothing failing at build time. A test now pins the literal.
Produce the image with `dotnet publish src/WiSave.Portal.WebApi -c Release -t:PublishContainer`, using the ContainerRepository, ContainerPort and ContainerEnvironmentVariable settings the project already carried, and point Compose at wisave-portal:latest with pull_policy: never. This removes the hand-written Dockerfile in full, including the BuildKit secret that fed GitHub Packages credentials into the build, the throwaway NuGet.Build.Config and its trap-based cleanup, and the two additional_contexts that bind-mounted the developer's NuGet cache into the build. That last one was a real fragility: the container build only succeeded when the host cache already held the exact pinned WiSave.Expenses.Contracts and WiSave.Incomes.Contracts versions. The SDK restores through the repository's own NuGet.Config and local-packages instead. Verified rather than assumed: the published image was run under Compose against Postgres, Redis and RabbitMQ, and /api/capabilities returned 200 with a real payload. All three private contract packages resolve into /app. Replaces the five tests that asserted Dockerfile contents with three that assert the image-based Compose setup, and drops the two helpers only they used.
…ution in CI /health runs the ready-tagged Postgres and Redis checks; /alive runs none. They answer different questions: /alive says the process is up, so a restart would help nothing, while /health can report that the process is fine but a dependency is not — route traffic away rather than restart. Both are anonymous so an orchestrator can reach them before anyone authenticates. Aspire's WithHttpHealthCheck will poll /health. The checks themselves live in Core.Infrastructure, not the web project: they open sockets to Postgres and Redis, which is infrastructure, and only the endpoint mapping is HTTP surface. Registration performs no I/O — each check connects lazily on first probe — so an unreachable dependency delays readiness rather than preventing startup, and dependencies that are not configured are not registered at all, which keeps the in-memory test host legitimately ready. CI now runs dotnet build on the solution between restore and test. It previously compiled only what the test project referenced, so a project outside that graph could break unnoticed — which will matter once the AppHost exists. Verified against live dependencies rather than asserted: /alive returned 200 and /health returned 200 Healthy with Postgres and Redis actually probed.
Additive: docker-compose --profile portal still works and remains the supported stack until the remaining verification gates pass. Aspire owns Postgres and Redis; RabbitMQ stays in Compose permanently, because it is the ecosystem's only broker, three sibling stacks resolve it by the hostname "rabbitmq" over the external wisave-net network, and Aspire 13.4.6 run mode cannot join a pre-existing Docker network. Portal runs as a host process, so localhost reaches it. Migrations are not modelled, by policy: they are run manually and never automated. AppHost_NeverAutomatesMigrations enforces that no migrator, seeder or WaitForCompletion appears here, and AGENTS.md now carries the rule. Beyond the policy, gating the portal on a one-shot resource turns any migration failure into a silent hang rather than a visible error. The tests encode failures reproduced while designing this, each of which throws at AppHost startup rather than at build: resource names are globally unique across types, so the project is "portal-api" and not "portal" alongside the database; 13.4.6 defaults to postgres:18.3 and redis:8.6, so both tags are pinned; AddProject has no args: parameter, and passing one silently binds launchProfileName; WithHttpHealthCheck throws for non-http-scheme resources. Verified by running it: postgres:17 and redis:7-alpine came up on the pinned tags, the portal bound the fixed port 5100 with IsProxied disabled so the Angular proxy still matches, /alive returned 200 and /health returned 200 Healthy with Postgres and Redis actually probed.
Bring AGENTS.md in line with the code. The layout section still described the web project as owning messaging and event handling, both of which moved out, and did not mention the AppHost or the health checks at all. Adds a "Running the stack" section covering the two supported paths, why RabbitMQ stays in Compose under both — it is the ecosystem's only broker and three sibling stacks resolve it by hostname over the external wisave-net network, which Aspire 13.4.6 run mode cannot join — and why applying the schema is a manual step under either path. Adds a health-endpoints section distinguishing liveness from readiness, since the difference decides whether an orchestrator restarts a pod or routes around it. The plan's version of this task described an end state that does not exist: it referenced the one-shot migrator and seeder, which are not modelled by policy, and db-rebuild-identity, which was deleted. Documented what is actually here.
…finitions Model the broker as an Aspire resource and import infrastructure/rabbitmq/definitions.json into it, so `aspire run` is self-contained instead of requiring `docker compose up -d` first. AddRabbitMQ cannot load a definitions file itself, so the file is bind-mounted and imported through the management plugin via RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS — the same mechanism docker-compose.yml uses. That file is load-bearing: Wolverine auto-provisions exchanges and queues but not vhosts, so without it the portal's connections to the incomes, expenses and stocks vhosts would fail. Credentials are pinned to guest/guest rather than generated. The definitions file provisions exactly one user, guest, with permissions on all five vhosts, so a generated password would authenticate against a user that is never created. Verified by running it: all five vhosts (/, portal, expenses, incomes, stocks) were created with guest permissioned on each, the portal opened live connections on all three named-broker vhosts, and Wolverine provisioned 20 queues across them. Caveat for the sibling stacks: incomes, expenses and stock resolve the broker as the literal hostname "rabbitmq" over the external wisave-net network, which Aspire run mode cannot join. While the broker runs here rather than in Compose they need RABBITMQ_HOST=host.docker.internal, a variable they already read. Compose still declares its own rabbitmq service, so do not run both at once — they collide on 5672.
Replace the stale 20260426212559_Initial with 20260801231030_Initial, generated against the current Guid-keyed model, and regenerate the matching idempotent DbUp script. The previous baseline described text Identity keys while the model has used Guid since 535aa47. The model snapshot had to be deleted before regenerating: EF diffs against the snapshot rather than the database, so with a stale snapshot in place `migrations add` produced an empty migration and the generated script contained no Identity tables at all — `has-pending-model-changes` reported "no changes" while `migrations list` reported "no migrations". The new baseline creates all seven Identity tables with uuid keys. It is a reset baseline, not an upgrade path: existing databases must be recreated.
Two omissions kept Rider from producing a working run configuration for the AppHost, both invisible from the CLI. Add Properties/launchSettings.json. The scaffold never created one because `aspire run` supplies its own defaults, so nothing surfaced the gap until the IDE needed a launch profile. Dashboard and OTLP ports are chosen clear of 5100, 5432, 6379 and 5672. Declare the Aspire MSBuild SDK as Sdk="Microsoft.NET.Sdk;Aspire.AppHost.Sdk/13.4.6" instead of a nested <Sdk> element. Both are valid MSBuild and `dotnet msbuild -getProperty:TargetFramework` resolved net10.0 either way, but Rider could not evaluate the element form and left Target framework empty and unselectable. Verified the SDK still applies: the Projects.* type handles are still generated. This unblocks debugging the portal under `aspire run`, which is what the Compose profile was previously needed for.
"Enabled": truew is not valid JSON, so the configuration provider threw a FormatException on startup and the portal could not boot at all — the debugger broke inside FileConfigurationProvider before any application code ran. Correct it to true, which also enables the incomes downstream service. It was previously false, which left the incomes proxy route inert regardless of the address configured for it, so the assertion pinning that value moves with the config rather than against it. The test parses the file, so it now fails on a syntax error in it before the portal refuses to start.
Both verification gates passed: the Angular dev server reaches the portal and opens a SignalR WebSocket over the fixed port 5100, and a debugger attaches to portal-api under `aspire run`. The Compose profile existed for that debugging workflow, so it has no remaining purpose. Delete docker-compose.yml entirely rather than reducing it to the broker. Every service it hosted — portal, Postgres, Redis and RabbitMQ — is now modelled in the AppHost, and two orchestrators declaring the same broker collide on 5672. The wisave-net network was declared external, so removing this file does not affect the sibling stacks that share it. Replaces the six tests that asserted Compose file contents with AppHost equivalents, keeping the assertions that still carry meaning: the downstream addresses and the portal RabbitMQ vhost, both of which moved into the AppHost's environment variables. Cross-repo consequence, documented in AGENTS.md: incomes, expenses and stock resolve the broker as the hostname "rabbitmq" over wisave-net, which Aspire run mode cannot join, so they need RABBITMQ_HOST=host.docker.internal while it runs here. They already read that variable.
The portal had no OpenTelemetry wiring at all, so the Aspire dashboard could only
scrape stdout: the trace and metric views were empty and the "structured" logs had
no fields to filter on. Aspire was already injecting OTEL_EXPORTER_OTLP_ENDPOINT;
nothing was exporting to it.
AddPortalObservability registers ASP.NET Core, HttpClient and runtime
instrumentation plus the OTLP exporter. The HTTP client instrumentation is the one
that matters most here: the portal is a gateway, and without it there is no way to
tell whether latency came from the portal or from the service it proxied to. With
it, the inbound request and the outbound call share one trace.
Wolverine is wired in per its docs. Its meter is named Wolverine:{ApplicationName},
so AddMeter("Wolverine*") needs the wildcard — a bare "Wolverine" matches nothing
and the messaging metrics never arrive.
Automatic instrumentation stops at the edges, so the portal also gets its own
ActivitySource and Meter. Both types are in the shared framework, so they live in
Core.Application without costing it its zero-package guarantee, and the layer that
owns the behaviour owns its instrumentation. Publishing a realtime notification and
resolving a user's permissions are now spans; permission resolution runs on every
authenticated request and issues several identity-store round trips, which is the
most likely hidden cost inside a slow request.
Events that carry no user are counted rather than silently dropped: a rising
notifications_dropped count means an upstream service is emitting events the portal
cannot route to anyone.
Health probes are filtered out of tracing, since Aspire polls /health continuously
and would otherwise dominate the trace list.
The automatic ASP.NET Core instrumentation already spans every endpoint, so timing and status are covered. What it cannot show is why a request failed: login returns 401 for an unknown email, a locked-out account, a disallowed sign-in and a wrong password alike, and those four mean very different things operationally. Count each outcome instead. A rising LOCKED_OUT or USER_NOT_FOUND rate is what credential stuffing looks like from the outside, and neither is distinguishable from a legitimate typo in the request span. Registration gets the same treatment, including ROLE_ASSIGNMENT_FAILED — a user created without its plan role resolves to an empty permission set and is refused by every proxied route, which is worth separating from a rejected password. Tags carry the operation and outcome only, never the email or user id: those are personal data and would give the metric unbounded cardinality.
Containers now stop with the AppHost. ContainerLifetime.Persistent kept them running after shutdown, holding 5432, 6379 and 5672 and colliding with the next run. Storage is independent of lifetime, so every stateful resource keeps a named volume and stopping the stack costs no data. Redis gains a volume it did not have. It holds the data-protection key ring as well as session tickets, so starting on a fresh one silently signs every user out. Also trims the comments added over this work to the ones that carry a constraint you cannot see from the code — the Wolverine meter wildcard, the resource-name collision, the definitions file being load-bearing for vhosts — and drops the ones that narrated what the call already says.
WolverineFx and WolverineFx.RabbitMQ 6.16.0 to 6.24.2, MessagePack 3.1.8, Microsoft.NET.Test.Sdk 18.8.1 and Scalar.AspNetCore 2.16.17. Build is warning-free and all 108 tests pass on the new versions, including the messaging configuration and realtime handler coverage that exercise Wolverine.
Move the ActivitySource and meter names out of PortalObservability into WolverineTelemetry. They describe what a third-party framework publishes under rather than how the portal configures itself, and the meter wildcard in particular is a constraint worth finding on its own rather than buried in a private field.
Core.Application had accumulated two unrelated jobs: it held the portal's shared vocabulary — the ports other layers implement, the wire contracts the Angular client consumes, and the names that appear in claims, config and telemetry — alongside the policy and event handlers that actually use them. Every outer layer had to depend on the whole thing to name an interface. Split the vocabulary into a new innermost project, WiSave.Portal.Core.Abstractions: Authorization/ IPermissionResolver, PortalRoles, PortalClaimTypes Realtime/ IRealtimeNotifier, RealtimeEnvelope, RealtimeEventType, RealtimeDomain Gateway/ DownstreamServicesOptions, DownstreamServiceOptions, DownstreamServiceNames Observability/ PortalTelemetry It names only BCL types and declares no references of any kind — no projects, no packages, no shared framework — which is what makes it safe for every other layer to depend on. Core.Application keeps the three types that are policy rather than vocabulary: AccessManagementPolicy, the RealtimeNotificationHandler base, and RealtimeNotificationsEventHandler. Core.Abstractions becomes its only project reference; the three integration-contract packages stay, since the handlers necessarily name those event types. Behaviour is unchanged. Every moved file differs from its previous content only in its namespace declaration, so no claim type, role name, event type, domain discriminator or metric name changed value. RealtimeNotificationsEventHandler deliberately stays in WiSave.Portal.Core.Application.EventHandlers: the shared Wolverine helper derives the durable RabbitMQ queue name from the handler's full type name, so moving it would orphan a live queue. ProjectDependencyDirectionTests gains two facts pinning the new layer - CoreAbstractions_DeclaresNoReferencesOfAnyKind checks the csproj declares no project, framework or package reference, and CoreAbstractionsAssembly_- ReferencesNoOtherWiSaveAssembly checks the compiled output agrees. The three existing reference-shape assertions were updated to expect the new project.
c2de517 extracted Wolverine's telemetry identifiers into a WolverineTelemetry class, but PortalObservability kept its own private copies of the same two strings, so the extracted class was never read. Two definitions of an identifier that must match what Wolverine actually publishes is exactly the kind of pair that drifts silently — a stale copy produces no error, just missing metrics. Point the registrations at WolverineTelemetry and delete the private consts. The registered values are unchanged: the meter is still the wildcard "Wolverine*", which is required because Wolverine names its meter Wolverine:{ApplicationName} and a bare "Wolverine" matches nothing.
…n tests The AppHost had accumulated more comment than code: a running commentary on every resource, most of it restating the call it sat above. Drop the narration and unwrap the three ReverseProxy environment variables onto single lines. ProxyConfigurationTests.AppHost_TargetsTheCurrentLocalDownstreamPorts asserted on the wrapped form — an embedded newline plus eight spaces of indentation — so reformatting broke it even though the configuration is identical. It now collapses whitespace before matching, pinning the key/value pairing rather than the line wrapping. That is the property worth protecting: environment variables win over appsettings, so those three addresses are what the running portal actually proxies to. Also restores the trailing newline the reformat dropped.
The portal owns the only RabbitMQ in the ecosystem, but wisave-incomes,
wisave-expenses and wisave-stock run in their own Compose stacks and resolve it
by the literal hostname "rabbitmq" over the external wisave-net network. Since
the broker moved into the AppHost it has been unreachable by that name, and the
three stacks have been carrying a local RABBITMQ_HOST=host.docker.internal
override that is not in any of their commits.
Attach the broker to wisave-net once its container is ready, aliased "rabbitmq".
The sibling stacks then need no override and no code change — the broker answers
to the name their committed Compose files already use.
This has to be a lifecycle hook rather than a builder call. Aspire gives a
container exactly one network and exposes no API to add a second:
WithContainerRuntimeArgs("--network", "wisave-net") compiles but collides with
DCP's own "--network bridge" on the docker create line, and Docker refuses to
combine a non-user-defined network mode with a user-defined one, so the container
dies at create time with exit 125. WithContainerNetworkAlias only registers DNS
names inside Aspire's own network. DCP itself attaches with docker network
connect after create, so WithSharedNetworkAlias does the same one step later.
The container name is pinned to wisave-rabbitmq because DCP otherwise appends a
random suffix, leaving no handle to attach to — or to docker exec against, which
the stock runbook needs. A failed attach warns rather than throws: only
cross-stack messaging degrades, and failing the resource would take the whole
local stack down with it.
Verified on a running stack: the container is dual-homed on both networks, a
container on wisave-net resolves rabbitmq and completes an AMQP handshake, guest
authenticates from that non-loopback source, and all five vhosts still load.
`aspire run` fails immediately with an OptionsValidationException: "The 'applicationUrl' setting must be an https address unless the ASPIRE_ALLOW_UNSECURED_TRANSPORT environment variable is set to true." No resource starts. The http profile is plain HTTP throughout — dashboard on 15100, OTLP on 19100, resource service on 20100 — and `aspire run` takes the first profile. The profile was added so the AppHost could be launched from an IDE with a debugger attached, and until now only the IDE exercised it; the https profile starts fine, which is why this stayed hidden. Declare the opt-in in the profile itself, which is where the error message points. HttpLaunchProfile_OptsInToUnsecuredTransport asserts the URL is HTTP and the opt-in is present, so the two cannot drift apart. Verified with `env -u ASPIRE_ALLOW_UNSECURED_TRANSPORT aspire run` — the variable explicitly stripped so a stray shell export could not fake a pass. The dashboard comes up, all three containers start and the portal reports healthy.
Found by checking every factual claim in the file against the repository. Repository Layout listed a `docs` directory that does not exist. The `aspire run` comment contradicted the Running the stack section 30 lines below it: it said Aspire starts "Postgres and Redis" and that the RabbitMQ broker "must already be up" as a precondition. Both are left over from before the broker moved into the AppHost, and an agent reading top-down would have gone looking for a broker to start. The port list claimed the RabbitMQ management UI is on 15672. It is not published: the -management-alpine image carries the plugin, but only WithManagementPlugin() maps a host port and the AppHost does not call it. Verified on a running stack — lsof shows dcp listening on 5432, 5672 and 6379 and nothing on 15672, and the port is closed from the host while the same UI answers at rabbitmq:15672 from a container on wisave-net. Everything else in the file checked out: PermissionHandlerTests, Migrations/Seeds and Scripts, .config/dotnet-tools.json, the absent docker-compose.yml, /scalar/v1, HttpContext.Items["UserPermissions"], all three YARP route names, AppHost_NeverAutomatesMigrations, and port 5672.
…ndored nupkg WiSave.Stock.Contracts was pinned to 2.0.0-marketmic.1, a build that exists on no feed. Its cache metadata records where it came from: "source": "/Users/.../.config/superpowers/worktrees/wisave-portal/position-market/local-packages" so the only way any machine could restore it was the committed nupkg plus the local-packages source mapped to that exact package id. Move to 2.0.4, which is published — its metadata records source https://nuget.pkg.github.com/WiSave/index.json — and delete both the source and the vendored artifacts. That mapping was also actively harmful. An exact-id pattern outranks a prefix, so restore resolved WiSave.Stock.Contracts only from the local folder, while Renovate ignored the source entirely because it drops non-http registries. Renovate was therefore free to propose feed versions that the mapping forbade restore from fetching; the two tools disagreed about where the package came from. WiSave.Expenses.Contracts.0.1.0.nupkg goes with it. Nothing could consume it: the id matched WiSave.Expenses.* on the wisave feed rather than the local source, and Directory.Packages.props pins 1.0.9 regardless. Verified: build clean, 113 tests pass. The realtime payload contract tests pass unchanged, so the Stock event shapes did not drift across the bump.
fe67bec retired the Compose stack but left the file listed under Solution Items. MSBuild never resolves <File> entries so builds were unaffected, but the IDE showed a dangling entry.
Translate YARP forwarding failures into structured 503 and 504 Problem Details responses using the downstream service metadata. Replace expected connection stack traces with concise structured warnings while preserving exception diagnostics for unexpected proxy errors. Cover error mapping, successful response pass-through, response-start safety, middleware ordering, and YARP log filtering.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.