Portfolio Manifest — Prepared 2026-08-20, updated 2026-08-31

Terrence Daniels

Full-Stack .NET Engineer

An independently modernized version of Microsoft's dotnet/eShop reference app — a .NET Aspire microservices e-commerce platform added one file at a time, each file evaluated and upgraded against actual current-latest versions rather than copied over wholesale. Still early — five projects done, sixteen still ahead — but every file that's landed has been reviewed, tested, and, where it mattered, proven wrong in Microsoft's own reference source before being fixed.

6 of 21projects done
304/304tests passing
23real bugs found & fixed
3decorator layers, one IEventBus
~50package versions researched
1Blazor → React pivot

Why this project

Microsoft's own reference architecture, verified rather than trusted

.NET Aspire microservices, event-driven integration, and a real transactional outbox pattern show up constantly on resumes and rarely get built end-to-end. dotnet/eShop is Microsoft's own teaching reference for exactly this shape — which made it a genuinely useful thing to rebuild file by file rather than fork wholesale: every package version re-researched against what's actually current, every file read closely enough to catch two real bugs sitting in Microsoft's own sample code, and a handful of deliberate design departures (a Decorator split, React instead of Blazor) made and recorded as this fork's own choices, not upstream's.

.NET 10 .NET Aspire 13.4 RabbitMQ EF Core Polly OpenTelemetry MSTest / Microsoft.Testing.Platform NSubstitute GitHub Actions CodeQL

Foundation first, five projects deep

Shared → EventBus → EventBusRabbitMQ → eShop.ServiceDefaults → the transactional outbox, then Identity.API

Migration order is deliberate: shared/foundation projects first, since everything else depends on them. src/Shared/'s two linked-source files, EventBus's abstractions, EventBusRabbitMQ's RabbitMQ implementation, eShop.ServiceDefaults's Aspire telemetry/health-check/resilience defaults, and IntegrationEventLogEF — the EF Core-backed transactional outbox every event-publishing service will write through — Identity.API (Duende IdentityServer), and Identity.WebApp (React, replacing Duende's Quickstart Razor UI) — are all done and reviewed. Identity.WebApp's six areas — Home, Account, Consent, Diagnostics, Grants, Device — are all built, wired, and verified end-to-end with a real browser, including a real Vite dev-proxy bug that broke every OIDC redirect Duende itself triggers. The other 15 projects don't exist on disk yet; see todo.md for the honest current state rather than assuming more is done.

Deliberate deviations, not defaults

A RabbitMQEventBus mixing transport plumbing with telemetry and Polly resilience split into a three-layer Decorator chain once upstream fidelity stopped being the constraint. WebApp is going React, not Blazor — a deliberate job-market call, not a technical necessity — which cascades into a new WebBFF project not in upstream at all. Both decisions, and the reasoning behind them, are recorded in docs/architecturedesign.md.

Verification over assumption

The Polly bug below was confirmed by reflecting the real Polly.Core 8.6.6 assembly, not read off documentation. The MTP test-runner's CLI flags were confirmed against a real scratch project before trusting them in CI. A repo-wide unused-using audit was verified by deliberately planting a known-dead import first and confirming the check actually caught it, before trusting a clean result anywhere else.

Real bugs, not just version bumps

Found by reading closely and verifying against real assemblies, not by assuming upstream is correct

Every entry below was present, verbatim, in the files as they came from Microsoft's own dotnet/eShop repo unless noted otherwise — this isn't a list of typos, it's what a genuine line-by-line review turned up in a widely-referenced Microsoft reference sample.

01
EventBusRabbitMQ — Polly retry pipeline never actually retried
PublishAsync ran its publish step through _pipeline.Execute(async () => {...}) — the synchronous Execute<TResult> overload with TResult inferred as Task, confirmed by reflecting the real Polly.Core 8.6.6 assembly rather than assumed. Any exception thrown after the lambda's first await — exactly where BrokerUnreachableException would occur — happened after Execute had already returned, so the retry pipeline never observed the failures it was configured to catch. Fixed by moving to the real ExecuteAsync overload as part of a full Decorator split.
high · reliability
02
eShop.ServiceDefaults — JWT audience validation silently disabled
TokenValidationParameters.ValidateAudience = false meant a token's aud claim was never actually checked, so a token issued for one downstream API could be replayed against any other API using this same authentication code. Removed the override — ValidateAudience defaults to true, and JwtBearerOptions.Audience already auto-populates the expected value. Flagged for re-verification against real issued tokens once Identity.API lands.
high · security
03
EventBusRabbitMQ — a null-conditional that made its own error path unreachable
(await _rabbitMQConnection?.CreateChannelAsync()) ?? throw new InvalidOperationException(...) looks like it handles a closed connection gracefully. It doesn't — the ?. short-circuits the whole parenthesized expression to a null Task, and awaiting a null task throws NullReferenceException immediately, so the ?? throw branch was dead code. A caller during startup got an opaque NRE instead of the intended message. Fixed with an explicit null check before the await.
medium · reliability
04
EventBusRabbitMQ — an unchecked cast could crash the whole message-receive path
ExtractTraceContextFromBasicProperties did value as byte[] then passed the result straight to Encoding.UTF8.GetString unchecked — a trace header present under the expected key but not actually a byte[] would throw ArgumentNullException outside OnMessageReceived's own try/catch, crashing the entire receive path over one malformed header. Rewritten to pattern-match (value is byte[] bytes) and fall through to the existing empty-result path instead.
medium · reliability
05
EventBusRabbitMQ — a cast that can return null, used unchecked
DeserializeMessage's as IntegrationEvent cast can genuinely return null for a malformed message body, but its return type and ProcessEvent's use of the result didn't account for that. Return type changed to IntegrationEvent?; ProcessEvent now logs and returns early on null, matching the existing pattern for an unresolvable event type. The exact same cast-can-return-null shape recurred later in IntegrationEventLogEntry.cs — caught the second time because the first fix was still fresh.
medium · reliability
06
eShop.ServiceDefaults — a hidden order-dependency between two unrelated files
ClaimsPrincipalExtensions.GetUserId's "sub" claim lookup only worked because a different file, AuthenticationExtensions, happens to remove "sub" from the framework's default claim-type map elsewhere — nothing enforced that ordering or documented the dependency. Now falls back to ClaimTypes.NameIdentifier regardless of whether that removal ran, so it's correct on its own.
medium · correctness
07
eShop.ServiceDefaults — an unproven ordering assumption picking the "default" API version
UseDefaultOpenApi picked the default API doc via descriptions[^1] — "take the last one" — assuming IApiVersionDescriptionProvider.ApiVersionDescriptions returns versions in ascending order, which is undocumented anywhere. Switched to descriptions.MaxBy(d => d.ApiVersion), correct regardless of provider ordering.
medium · correctness
08
IntegrationEventLogEF — a lazy IEnumerable wrapping a side-effecting Select
RetrieveEventLogsPendingToPublishAsync returned Task<IEnumerable<...>>, but the real implementation's .OrderBy().Select(e => e.DeserializeJsonContent(...)) chain is lazy, and DeserializeJsonContent mutates state as a side effect — returning it as IEnumerable risked that mutation re-running, or running late, on every enumeration. Changed to Task<IReadOnlyList<...>>, forcing materialization when the implementation lands.
medium · correctness
09
eShop.ServiceDefaults — inconsistent logic between two adjacent branches
BuildDescription's deprecation-notice branch checked for a trailing period before appending its message; the sunset-date branch immediately after it didn't do the same check — a real logic inconsistency, not a style nit. Extracted a shared AppendSentenceSeparator helper both branches now call.
low · correctness
10
eShop.ServiceDefaults — a magic port number in an emulator-only code path
The #if DEBUG Android-emulator issuer hardcoded https://10.0.2.2:5243 — 10.0.2.2 is a legitimate fixed emulator host alias, but :5243 was only correct if eShop.AppHost (not built yet) happens to pin Identity.API to that exact port. Now derived from identityUrl's real Uri.Port instead of a hardcoded guess.
low · correctness
11
Identity.API — external-login callback crashed instead of falling back, on a missing value
ExternalController read AuthenticationProperties.Items["scheme"]/["returnUrl"] through the plain IDictionary indexer, which throws KeyNotFoundException on an absent key instead of returning null — so the ?? throw new Exception(...) and ?? "~/" fallbacks the code was written to lean on never actually ran for a missing key, only for one present but explicitly null. A callback missing either value crashed with an opaque exception instead of the intended message or default. Fixed with TryGetValue at both call sites.
medium · correctness
12
Identity.API — a device-flow consent error vanished on redisplay
DeviceController.Callback's own doc comment promised the redisplay-with-error outcome would show "the rebuilt form with the validation error" — but the code only ever returned the bare view-model, never actually including the error in the response. A user who left every scope unchecked saw the form redisplayed with no explanation of what went wrong. Fixed with a new DeviceCallbackResult wrapper carrying the view-model and the validation error together.
medium · correctness
13
Identity.API — unsanitized client input written straight into a log message
ConsentController.BuildViewModelAsync interpolated a client-supplied returnUrl directly into _logger.LogError, unsanitized — CWE-117 log injection, caught by CodeQL rather than by manual review. A crafted return URL could inject fake lines or newlines into this service's own logs. Fixed with returnUrl?.ReplaceLineEndings("_") and a named log placeholder.
medium · security
14
Identity.API — claims built from user fields the type system says can be null
ProfileService.GetClaimsFromUser constructed Claims straight from user.UserName/user.Email with no null guard, but IdentityUser's own base properties are genuinely nullable (string?, reflection-confirmed against the real Microsoft.Extensions.Identity.Stores assembly) — and Claim's constructor doesn't accept a null value. A user record with a null email would have thrown at token-issuance time. Fixed with an ?? user.Id fallback and an added guard.
medium · reliability
15
Identity.API — LoginPostResult never carried the validation error it promised
Its own doc comment already promised "the redisplayed form with a validation error," but the type never actually included one. AccountController.Login's invalid-credentials path called ModelState.AddModelError(...), which has zero effect on a JSON response — [ApiController]'s automatic 400 only fires from pre-action binding validation, never a manual call inside the action. The client got 200 OK with a redisplayed form and no indication why. Fixed by adding a real ValidationError field and setting it directly.
medium · correctness
16
Identity.API — a real <form> POST to /Account/Logout came back 415
LogoutInputModel's JSON-body binding rejected application/x-www-form-urlencoded outright — a real HTML form can only send that or multipart, never JSON, so the external-IdP sign-out fallback (a genuine full-page POST, since fetch() can't follow a cross-origin redirect) would have failed every time. Fixed by switching logoutId to a plain query parameter, which then created a genuine C# overload conflict between the GET and POST Logout actions, resolved with [ActionName("Logout")].
medium · correctness
17
Identity.API — the logout confirmation prompt could never actually show
BuildLogoutViewModelAsync's own comment says it "prevents attacks where the user is automatically signed out by another malicious web page" — but ShowLogoutPrompt was only ever explicitly forced false, never set true anywhere in the method, in this fork and in Microsoft's own real shipped Quickstart source. Fixed with an explicit override so Duende's own context.ShowSignoutPrompt == true security signal can't be silently demoted by the site-wide convenience toggle.
low · security
18
Identity.WebApp — a successful login redirected the browser to a URL that doesn't exist
AccountController.Login/LoginCancel returned RedirectUrl: "~/" for the no-return-URL case — ASP.NET's server-side tilde-root syntax, meaningless to a JSON client doing window.location.href = redirectUrl. Confirmed live with a real browser: it navigated to a literal, nonexistent /Account/~/ path instead of the site root. Fixed with a plain "/" literal, after Url.Content("~/") itself proved fragile (a real NullReferenceException in one test context, silently empty in another) for a sub-path-hosting scenario this app has no evidence of needing.
high · correctness
19
Identity.WebApp — the site's own root path was a dead end
Nothing in the SPA's router was ever mapped to bare "/" — so the fix above (a genuine, correct redirect target) still hit React Router's own 404 the moment a real browser reached it. Confirmed live via Playwright. Fixed with a route redirecting "/" to /Home/Index, this SPA's one real landing page.
medium · correctness
20
Identity.API — Duende's own consent-screen redirect targeted a route that doesn't exist
IdentityServerOptions.UserInteraction.ConsentUrl's compiled-in default is /consent (reflection-confirmed), but ConsentController's real route is /Consent/Index — a missing segment, not a case difference. Confirmed live and load-bearing: with a real client's RequireConsent temporarily set to true, an authenticated /connect/authorize request genuinely redirected to /consent. Currently latent (no registered client requires consent today), but would have silently broken the instant one did. Fixed with an explicit ConsentUrl override.
medium · correctness
21
Identity.WebApp — the dev proxy forwarded the wrong Host header, breaking every Duende-triggered redirect
Vite's dev-server proxy defaults (changeOrigin: false) are documented to preserve the browser's original Host header, but that didn't hold up empirically: a temporary diagnostic middleware confirmed Identity.API genuinely received its own address instead of the SPA's real origin. Every real, top-level browser navigation Duende itself triggers — Login's cookie-auth challenge, Consent's own redirect, Logout's /connect/endsession — sent the browser off the SPA entirely, onto Identity.API's raw port. Fixed with a proxyReq hook forcing the outgoing Host header back to the original, verified live against all three redirect chains plus a full device-flow round trip ending in a real token issued to the device.
high · correctness
22
Identity.API — Device's consent screen silently dropped the parameter suffix on parameterized scopes
ConsentController and DeviceController each had their own near-verbatim private CreateScopeViewModel helper, and the copies had drifted: Consent's appended a scope's parsed parameter (e.g. resource1.scope1:42) to the display name, Device's silently didn't. Confirmed this exact inconsistency exists in real upstream Duende source too — this fork had faithfully reproduced an upstream bug, not introduced a new one. Fixed by extracting one shared ScopeViewModelFactory both controllers call, with the correct behavior, plus a direct regression test — the total lack of any prior test on this logic is exactly why the drift went unnoticed.
medium · correctness
23
Identity.WebApp — Device's confirm screen silently dropped a validation error on redisplay
Device's local Step union's confirm variant had no field to carry a validation error at all, so when DeviceCallbackResult.validationError came back on a failed submission, the form redisplayed with zero indication of what went wrong — unlike ConsentPage's equivalent redisplay path, which already showed the message correctly. Surfaced while extracting the shared ScopeConsentForm component both pages' confirm screens needed; fixed by adding error: string | null to the confirm step and threading the real value through instead of discarding it.
medium · correctness

Verifying it actually works

186 tests, and a Decorator split that finally let the flagship fix get proven

MSTest.Sdk on .NET's newer Microsoft.Testing.Platform runner — a genuinely different CLI surface from legacy VSTest, confirmed against a real scratch project before trusting its coverage/TRX flags in CI. Testing isn't deferred to a batched end-of-migration phase here: every project that's done gets full test coverage in the same unit of work, before the source migration moves past it.

186/186 tests, decorators make it possible

ResilientEventBusDecoratorTests.cs verifies bug #01 end-to-end for the first time in this project's history — a fake inner IEventBus that fails once then succeeds, no real RabbitMQ broker needed, since decorators wrap any IEventBus. Closes a gap that had sat unproven since the fix landed. See Testing Strategy.

Honest gap, tracked not solved

Wanted one combined HTML report across every test project — MTP's --report-html produces one file per project instead, confirmed via a real 2-project scratch solution. The actual fix merged upstream 2026-08-09 but hasn't shipped in a NuGet release yet; Dependabot already tracks the package individually, so no new tooling is needed to know when it does.

Judgment calls

Where the interesting decisions actually happened

The review posture changed partway through: early files were kept close to upstream's architecture on principle, then that constraint was explicitly dropped — "this is going to self-hosted" — in favor of treating inconsistencies across upstream's own multi-contributor codebase as things to resolve under one deliberate design, not inherit silently. That's what produced the Decorator split above, and it's why later reviews stopped citing "matches upstream" as a reason to leave something as-is. Separately, WebApp going React instead of Blazor wasn't a technical necessity — Blazor Server can call Basket.API over gRPC natively from the browser tier, which a React SPA can't — so that decision now cascades into a new WebBFF project and a Grpc.AspNetCore.Web middleware addition to Basket.API, both decided ahead of being built and recorded in todo.md as they happened.