This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
DotNetWorkQueue is a producer/distributed consumer library for .NET. It supports queueing POCOs, compiled LINQ expressions, and re-occurring job scheduling. Targets .NET 10.0 and .NET 8.0.
Always use Context7 MCP when I need library/API documentation or setup steps. Automatically resolve library IDs and retrieve docs without being asked.
# Build entire solution
dotnet build "Source\DotNetWorkQueue.sln" -c Debug
# Build without test projects
dotnet build "Source\DotNetWorkQueueNoTests.sln" -c Debug
# Build a specific project
dotnet build "Source\DotNetWorkQueue\DotNetWorkQueue.csproj"Release builds enable TreatWarningsAsErrors and XML documentation generation. For NuGet release builds, always pass -p:CI=true to enable deterministic Source Link paths:
# Release build for NuGet publishing
dotnet build "Source\DotNetWorkQueueNoTests.sln" -c Release -p:CI=true
# Pack Dashboard.Ui (not auto-packed by build)
dotnet pack "Source\DotNetWorkQueue.Dashboard.Ui\DotNetWorkQueue.Dashboard.Ui.csproj" -c Release -p:CI=trueReal releases are published by the tag-triggered .github/workflows/publish.yml workflow — do NOT invoke dotnet nuget push locally. Push a v<version> tag (matching Source/Directory.Build.props <Version>) to trigger the three-job pipeline (verify-gate → build-pack → publish). The existing local dotnet build -c Release -p:CI=true and dotnet pack commands shown above are for inspection / dry-run only. Operational dry-run: Actions → Publish → Run workflow → dry_run=true exercises the gate + pack jobs without publishing.
Tests use MSTest 3.x, NSubstitute for mocking, AutoFixture for test data, and FluentAssertions.
# Run all unit tests for a specific project
dotnet test "Source\DotNetWorkQueue.Tests\DotNetWorkQueue.Tests.csproj"
# Run a single test by fully qualified name
dotnet test "Source\DotNetWorkQueue.Tests\DotNetWorkQueue.Tests.csproj" --filter "FullyQualifiedName~MyTestClassName.MyTestMethod"
# Unit test projects (no external dependencies needed):
dotnet test "Source\DotNetWorkQueue.Tests\DotNetWorkQueue.Tests.csproj"
dotnet test "Source\DotNetWorkQueue.Transport.SqlServer.Tests\DotNetWorkQueue.Transport.SqlServer.Tests.csproj"
dotnet test "Source\DotNetWorkQueue.Transport.PostgreSQL.Tests\DotNetWorkQueue.Transport.PostgreSQL.Tests.csproj"
dotnet test "Source\DotNetWorkQueue.Transport.Redis.Tests\DotNetWorkQueue.Transport.Redis.Tests.csproj"
dotnet test "Source\DotNetWorkQueue.Transport.SQLite.Tests\DotNetWorkQueue.Transport.SQLite.Tests.csproj"
dotnet test "Source\DotNetWorkQueue.Transport.LiteDb.Tests\DotNetWorkQueue.Transport.LiteDb.Tests.csproj"
dotnet test "Source\DotNetWorkQueue.Transport.RelationalDatabase.Tests\DotNetWorkQueue.Transport.RelationalDatabase.Tests.csproj"
# Additional unit test projects:
dotnet test "Source\DotNetWorkQueue.Dashboard.Api.Tests\DotNetWorkQueue.Dashboard.Api.Tests.csproj"
dotnet test "Source\DotNetWorkQueue.Transport.Memory.Tests\DotNetWorkQueue.Transport.Memory.Tests.csproj"
# In-memory integration tests (no external services needed):
dotnet test "Source\DotNetWorkQueue.Transport.Memory.Integration.Tests\DotNetWorkQueue.Transport.Memory.Integration.Tests.csproj"
dotnet test "Source\DotNetWorkQueue.Transport.Memory.Linq.Integration.Tests\DotNetWorkQueue.Transport.Memory.Linq.Integration.Tests.csproj"
# Dashboard API integration tests (Memory/SQLite/LiteDb only, no external services):
dotnet test "Source\DotNetWorkQueue.Dashboard.Api.Integration.Tests\DotNetWorkQueue.Dashboard.Api.Integration.Tests.csproj" --filter "FullyQualifiedName~Memory|FullyQualifiedName~Sqlite|FullyQualifiedName~LiteDb"
# Dashboard API integration tests (all transports, requires running services):
dotnet test "Source\DotNetWorkQueue.Dashboard.Api.Integration.Tests\DotNetWorkQueue.Dashboard.Api.Integration.Tests.csproj"Integration tests for SQL Server, PostgreSQL, Redis, SQLite, and LiteDb require running instances of those services and connection strings configured in connectionstring.txt files within each integration test project.
The main library containing all abstractions, interfaces, and default implementations. Key namespaces:
Configuration- Queue configuration objects (QueueProducerConfiguration,QueueConsumerConfiguration,QueueConnection)IoC- DI abstractions (IContainer,IContainerFactory) using SimpleInjectorQueue- Queue implementations for producers and consumersMessages-IMessage<T>,IReceivedMessage<T>,ISentMessageJobScheduler- Recurring job scheduling using cron formatPolicies- Polly-based resilience/retry policiesTrace- OpenTelemetry distributed tracing integration
Transport.Shared- Base interfaces and Command/Query pattern (ICommandHandler<T>,IQueryHandler<T,TR>,IQueryHandlerAsync<T,TR>) for transport-independent data accessTransport.RelationalDatabase- SQL-specific abstractions built on Transport.Shared- Transport implementations - Each transport (SqlServer, PostgreSQL, SQLite, Redis, LiteDb) implements
ITransportInit(withSend/Receive/Duplexvariants) and registers its DI bindings viaRegisterImplementations()
IProducerQueue<T>/IConsumerQueue- POCO message queuesIProducerMethodQueue/IConsumerMethodQueue- Delegate-based queues- LINQ expression variants for compiled expressions
IJobSchedulerfor recurring scheduled jobs
SimpleInjector is the IoC container. Each transport has an init class implementing ITransportInit that registers its services. IContainerFactory provides root-level container access to avoid circular dependencies.
Projects target net10.0 and net8.0. Legacy conditional compilation symbols (NETFULL, NETSTANDARD2_0) have been removed.
- SimpleInjector 5.5.0 - DI container
- Polly 8.6.5 - Resilience/retry
- Newtonsoft.Json 13.0.4 - Serialization
- Microsoft.Data.SqlClient 6.1.3 - SQL Server (replaced System.Data.SqlClient)
- OpenTelemetry 1.14.0 - Distributed tracing
- System.Diagnostics.Metrics - Built-in metrics via
System.Diagnostics.DiagnosticSource(users add OpenTelemetry.Metrics exporters to collect) - Cronos - Cron expression parsing (5-field and 6-field with seconds)
- CronExpressionDescriptor - Human-readable cron schedule descriptions
- All source files include LGPL-2.1 license headers (see
DotNetWorkQueue.licenseheader) - Interface prefix:
I(e.g.,IQueue); Factory suffix:Factory; Config suffix:Configuration - Abstract base classes use prefix
Aor suffixBase - Thread-safe disposal via
Interlockedoperations throughout - CI: Jenkins is the local CI server (setup guide at
docs/jenkins-setup.md). It runs 14 parallel integration test stages on Docker agents (net10.0 only) with Coverlet code coverage uploaded to Codecov.io. The 14th stage (TaskScheduler Distributed) runs without Coverlet by design — it tests an external NuGet and the core DLLs it uses are already covered by the other 13 stages. GitHub Actions (.github/workflows/ci.yml) runs net10.0 unit tests + the TaskScheduler Distributed integration tests on ubuntu-latest for CI validation. Jenkins is PR-triggered, not branch-triggered — any feature-branch CI validation MUST open a (draft) PR to trigger a build.
- When multi-targeting to net10.0 for Linux, check for: case-sensitive file paths in .csproj references, native library dependencies (libsqlite3, libdl),
#if NETFULLguards on .NET Framework-only APIs (SoapFormatter, GetObjectData), and timer/clock resolution differences in tests. - Connection strings for
dotnet test --no-buildmust be written to the bin output directory, not just the source directory. - Jenkins agent JRE version must exactly match the master's Java version — class file version mismatch causes silent agent launch failures.
- When a change marked "out of scope" keeps causing friction in CI (e.g., hardcoded connection strings), just do it — the cost of workarounds exceeds the cost of the fix.
- Label-based Jenkins agents are simpler than Docker Pipeline plugin for pre-built images.
- Integration test metrics assertions can race: the handler callback signals completion before
CommitMessage.Commit()increments the counter. Poll the liveIMetricsobject instead of taking a single snapshot. - Enabling
--retry-failed-testsrequires migrating ALL test projects to Microsoft.Testing.Platform (EnableMSTestRunner+TestingPlatformDotnetTestSupportin Directory.Build.props) — partial migration breaks coverage collection. - Dockerfile COPY paths must match exact Linux filesystem casing:
LiteDb.csproj(notLiteDB.csproj),Directory.Build.propsis inSource/not the repo root. --no-restoreondotnet publishin Docker fails when a laterCOPY Source/invalidates the restore cache layer.- 13 parallel Jenkins stages need staggered startup (5s intervals) to avoid GitHub clone rate-limiting.
- SQL UPDATE tests that only assert parameter values can pass while the UPDATE is a silent no-op: a WHERE clause guard may exclude the very rows you're trying to fix. Capture and assert the actual
CommandTextto catch this — the parameter assertion alone is a false positive. - StackExchange.Redis
ConnectionMultiplexercannot be mocked with NSubstitute (sealed types + extension methods). Expose aprotected virtual GetDb()seam on Redis handlers for testability; keep classes internal to contain the scope. RedisValue.Nullcast to(int)yields0, not an exception. When comparing against enums where0is a valid member (e.g.,MessageHistoryStatus.Enqueued), always check.HasValuebefore casting to avoid null-value collisions.- NuGet version ordering:
0.9.3<0.9.19, so you can't go back to a lower version number after incrementing past it. - NuGet.org does not allow pushing
.snupkgseparately after the.nupkgis already published, and re-pushing the same version is blocked. The CLI's auto-match of.snupkgalongside.nupkgis unreliable on Windows (required 12 manual.snupkgpushes per release in the legacy flow). The.github/workflows/publish.ymlGH Actions workflow splits the push into two explicit commands (deploy/*.nupkgthendeploy/*.snupkg) onubuntu-latest, which is portable. Do not rundotnet nuget pushlocally for real releases — push thev<version>tag and let the workflow do it. - Release builds for NuGet must use
-p:CI=true(e.g.,dotnet build -c Release -p:CI=true) to enableContinuousIntegrationBuildin Directory.Build.props. Without it, Source Link paths aren't deterministic and NuGet.org shows red validation indicators. DotNetWorkQueue.IConfigurationshadowsMicrosoft.Extensions.Configuration.IConfigurationin any code under theDotNetWorkQueue.*namespace hierarchy. C# resolves via namespace walk-up BEFOREusingdirectives — evenusingaliases don't help. Useglobal::Microsoft.Extensions.Configuration.IConfigurationfor all MS config type references in Dashboard.Ui code and tests.- MudBlazor 9.x expansion panel property is
Expanded(notIsInitiallyExpanded). Blazor silently ignores unknown attributes — no build error, just non-functional. - NSubstitute indexer mocking fails on
IFeatureCollection— use realFeatureCollectionwithSet<T>()in tests instead of mocking. TraceExtensionsand trace decorator code paths show 0% coverage in tests unless anActivityListeneris registered for the matchingActivitySource. Without a listener,ActivitySource.StartActivity()returnsnulland the entire trace decorator chain short-circuits silently — no error, just silent skipping. To get trace coverage in integration tests, register a listener viaActivitySource.AddActivityListener()even if you don't need to assert on the activities.Metrics.Metricsnamespace walk-up shadowing: insideDotNetWorkQueue.IntegrationTests.*projects,new Metrics.Metrics(...)resolves toDotNetWorkQueue.IntegrationTests.Metrics.Metricsvia namespace walk-up. From a transport test project (e.g.,DotNetWorkQueue.Transport.Memory.Integration.Tests), the same expression binds to the non-existentDotNetWorkQueue.Metrics.Metrics. Use the fully-qualifiedDotNetWorkQueue.IntegrationTests.Metrics.Metricsto disambiguate. Same root cause as theIConfigurationshadowing lesson above.- Sync vs async handler mocking split: Sync
IQueryHandler<TQuery, TResult>handlers can be tested by mockingIDbConnection/IDbCommand/IDataReader(the interfaces). AsyncIQueryHandlerAsync<TQuery, TResult>handlers MUST mock the abstract base classesDbConnection/DbCommand/DbDataReaderbecauseOpenAsync/ExecuteReaderAsync/ReadAsyncare defined on the base classes, not the interfaces. Mocking the interface for an async handler compiles but the async methods silently no-op via NSubstitute defaults. - MSTest 3.x uses
Assert.ThrowsExactly<T>(notAssert.ThrowsException<T>from MSTest 2.x). When two concurrent edits mix old and new APIs, staleobj/+bin/cache can surface phantom compile errors against files that already use the correct API. After multi-file concurrent test edits,rm -rf obj binand rebuild before chasing down "compile errors" that don't match the source. - Async dashboard query handlers (
GetDashboardJobsQueryHandlerAsync,GetDashboardErrorRetriesQueryHandlerAsync, etc.) do NOT take aCancellationToken. TheIQueryHandlerAsync<TQuery, TResult>interface signature isHandleAsync(TQuery query)-- no token. Don't add cancellation tests for these handlers. - Casting
IDbConnectionto a sealed transport-specific type (NpgsqlConnection,SqliteConnection,Microsoft.Data.SqlClient.SqlConnection) inside a handler breaks NSubstitute / Castle DynamicProxy mocking withTypeLoadException: parent type is sealed. Keep handlers operating onIDbConnectionand use genericDbTypeenum values (DbType.AnsiString,DbType.Int64,DbType.DateTimeOffset) withIDbCommand.CreateParameter()+Parameters.Add(param). The PostgreSQLSetJobLastKnownEventCommandHandlerwas re-refactored mid-Phase-3 (commit9c77537d) for exactly this reason. IDbConnectionFactoryinjection is the correct test seam for transport command handlers. The mock chain isIDbConnectionFactory.Create() -> IDbConnection -> IDbCommand -> IDataParameterCollection, withArg.Do<IDbDataParameter>(p => list.Add(p))to capture parameters for assertion. Don't reach forSystem.ReflectionorTestable*subclass workarounds to expose protected methods if the underlying problem is a hardcodednew SqlConnection()-- inject the factory instead.- LiteDb handler unit tests use real in-memory
LiteDatabaseinstances viausing var db = new LiteDatabase("Filename=:memory:");rather than mockingLiteDbConnectionManager.LiteDatabaseis cheap to construct in-memory, gives real collection/indexing behavior, and disposes cleanly. Handlers that accept aLiteDatabasedirectly (or can be invoked via a reflection-reached protected method) are testable this way. Handlers that reach throughLiteDbConnectionManager.GetDatabase()insideHandle()are NOT -- see the companion lesson below. - Redis Lua handler unit tests use a
Testable{X}Luaprivate inner class that subclasses the concrete Lua class and overridesTryExecute(object)(andTryExecuteAsyncif needed) to return a scriptedRedisResultwithout a live Redis connection. The seam requiresTryExecute/TryExecuteAsyncto bevirtualonBaseLua-- they are as of commitc7a9dd80. Pattern: subclass, expose aNextResultproperty, overrideTryExecuteto set aTryExecuteCalledflag and returnNextResult, then assert on the handler's output.IRedisConnectionis mocked with NSubstitute (it is an interface);ConnectionMultiplexeris never touched. LiteDbConnectionManagerhas no injection seam: its constructor takesIConnectionInformation+ICreationScopeand builds theLiteDatabaseinternally. Any LiteDb command/query handler that callsGetDatabase()insideHandle()cannot have that path unit-tested -- constructor-null-guard tests and reflection-reached protected helpers are the only viable unit-level coverage. Handle()-level coverage for such handlers lives in the LiteDb integration test suite (Source/DotNetWorkQueue.Transport.LiteDB.IntegrationTests/for POCO handlers,Source/DotNetWorkQueue.Transport.LiteDB.Linq.Integration.Tests/JobScheduler/for job-scheduler paths). Don't try to mockLiteDbConnectionManager-- it has no seams for that.- ASP.NET Core
AddControllers(action)in a bareServiceCollectiondoes NOT reliably surface user-addedMvcOptions.ConventionsviaIOptions<MvcOptions>.Value-- filters added by the same action DO propagate, but conventions do not. Four debugging iterations in Phase 5 (PLAN-1.3) confirmed the contradiction:mvcOptions.Filterscontained every Dashboard filter correctly, yetmvcOptions.Conventionsshowed only the framework-internalControllerApplicationModelConvention. Root cause is ASP.NET Core's internalConfigureMvcOptions/AddApplicationPartpipeline, which behaves differently without a realIHostEnvironment. For any test that must assert anIControllerModelConventionwas registered, use an integration test with a realWebApplicationpipeline (orWebApplicationFactory) -- not a bareServiceCollection. The unit-test workaround is to test the convention'sApply()method directly, then cover end-to-end wiring with an integration test that exercises the full pipeline. DotNetWorkQueue.TaskScheduling.Distributed.TaskScheduler.SchedulerContainerdoes NOT exposeGetInstance<T>()in the 0.4.0 NuGet. The only way to resolveITaskSchedulerJobCountSync(or any other container-registered service) is the IContainer closure pattern: captureIContainerduring theSchedulerContainer(registerService)callback, trigger build viaCreateTaskScheduler(), then callcapturedContainer.GetInstance<T>(). Used byConcurrencyRegressionTestsandNodeDiscoveryTestsin the TaskScheduler Distributed integration test project. An earlier NodeDiscoveryTests draft used the nonexistentSchedulerContainer.GetInstance<>and produced 10 compile errors.ITaskSchedulerJobCountSync.Start()MUST be called before spawning threads that callIncreaseCurrentTaskCount/DecreaseCurrentTaskCount. WithoutStart(),_outboundis null and Phase 1's null-safe guard short-circuits every call, making any concurrency test a false positive that would pass even if the lock fix were reverted.ConcurrencyRegressionTestscarries an inline comment documenting this invariant.- DNQ queue names must be alphanumeric/underscore/dot — DNQ validation rejects hyphens.
Guid.NewGuid().ToString()produces hyphenated strings that fail withQueue name contains invalid characters. UseGuid.NewGuid().ToString("N")(no hyphens) or a sanitized format. The TaskScheduler Distributed EndToEnd test uses"q" + Guid.NewGuid().ToString("N"). - Memory transport storage is per-
QueueContainer<MemoryMessageQueueInit>instance — two separate containers do NOT share the underlyingIDataStorageviaRegisterNonScopedSingleton(scope)alone. A naive hand-rolled producer/consumer split across two containers will see the producer's messages stay pending while the consumer's store is empty. For Memory-transport integration tests that need both roles, use the single-container shared runner (DotNetWorkQueue.IntegrationTests.Shared.Consumer.Implementation.SimpleConsumer.Run<>) which internally uses one container — but note that shared runner'ssetOptionsparameter isAction<TTransportCreate>for transport options, NOTAction<IContainer>for container registration, so there's no seam to injectInjectDistributedTaskSchedulerthrough. The Phase 3 EndToEndSchedulingTests was scope-reduced to a SimpleInjectorVerify()smoke test because of this constraint. -p:CI=trueis a NuGet packaging flag (it enablesContinuousIntegrationBuildinDirectory.Build.propsfor deterministic Source Link paths duringdotnet build -c Release) — it has NO effect ondotnet testand should not appear on test invocations. All 14 Jenkins integration test stages use-c Debugwith no-p:CI=true. Only the pre-publish Release build uses-c Release -p:CI=true.- Jenkins Multibranch Pipeline is PR-triggered, not branch-triggered. A
git pushof a feature branch alone will NOT cause Jenkins to build — you must open a (draft) PR viagh pr create --draft --base master --head <branch>to trigger the Jenkinsfile discovery and pipeline run. This is the correct pattern for CI-sensitive feature branch validation. - Before starting a new Shipyard milestone on this repo, run
git fetch origin master && git log HEAD..origin/master --onelineto confirm local master is current. Origin may have unpulled work from a concurrent milestone (e.g., a dashboard-coverage PR merged while you were offline). Resolving the divergence at ship time via merge-and-rebase is recoverable but expensive — resolving it up front is free. - Jenkinsfile stagger formula is
(n-1) * 5seconds per stage (0, 5, 10, ..., 60, 65) implemented as inlinesleep(time: N, unit: 'SECONDS')calls at the start of each stage'ssteps { }block. With 14 stages, worst-case startup delay is 65s. Adding a 15th stage would push total startup delay past the current ceiling — either revisit the formula (shorter intervals) or batch subsequent stages into a nested parallel block. - Release flow (v tag → publish.yml):
Source/Directory.Build.propsline 4 carries<Version>(4-space indent, inside the existing<PropertyGroup>immediately after<ManagePackageVersionsCentrally>); the 12 packable csprojs inherit. Tag regex^v\d+\.\d+\.\d+(-[A-Za-z0-9\.-]+)?$is enforced byverify-gate. Tag version must equalDirectory.Build.props<Version>exactly (stripped ofv). Tag must land on a commit whose Jenkins status contextcontinuous-integration/jenkins/branchissuccess(the B2 gate). Operator dry-run: Actions → Publish → Run workflow →dry_run=trueexercisesverify-gate+build-packwithout publishing. Before the first real release, add theNUGET_API_KEYsecret in GitHub repo Settings → Secrets and variables → Actions. - GitHub status API — rollup vs history endpoints.
GET /repos/{owner}/{repo}/commits/{sha}/status(singular) is the rollup endpoint: returns.statuses[]containing the latest state per unique context.GET /commits/{sha}/statuses(plural) returns EVERY status update ever posted — typically 15+pendingrows followed by onesuccessrow. A naive jq filter against the plural endpoint (.[] | select(.context=="...") | .state) emits multi-line output that silently breaks bash[[ "$state" == "success" ]]comparisons. Thepublish.ymlB2 gate uses the singular rollup. If Jenkins ever changes its status context name, update the literalcontinuous-integration/jenkins/branchinpublish.ymlaccordingly. - Publish-workflow dry-run on a fresh master merge will fail verify-gate until Jenkins finishes. A master merge does trigger Jenkins' Multibranch Pipeline (same as PRs), but the 14-stage matrix takes ~50 min to post its
continuous-integration/jenkins/branchstatus. If you runworkflow_dispatchwithdry_run=trueimmediately after merging, verify-gate correctly fails withJenkins status on <sha> is 'missing'; required 'success'— fail-loud as designed. Check readiness first:gh api repos/blehnen/DotNetWorkQueue/commits/<master-sha>/status --jq '.statuses[] | select(.context=="continuous-integration/jenkins/branch") | .state'— wait forsuccessbefore triggering the dry-run. This is not a bug to fix; it's the gate working. - Microsoft.OpenApi 1→2 namespace/API restructure (hit via Swashbuckle 10).
Microsoft.OpenApi.Models.*types flattened to rootMicrosoft.OpenApinamespace.AddSecurityRequirement(...)signature changed toFunc<OpenApiDocument, OpenApiSecurityRequirement>(lambda); useOpenApiSecuritySchemeReference(..., hostDocument: doc)for scheme refs.OpenApiSecurityRequirementvalue type isList<string>, notstring[]. On this repo the migration was 5 edits acrossSource/DotNetWorkQueue.Dashboard.Api/Extensions/DashboardExtensions.cs+ its swagger tests — expect the same shape anywhere Swashbuckle's legacyMicrosoft.OpenApi.Modelsusing statements appear. - The
IDbConnectionabstraction pays off for transport major bumps. Npgsql 8→10 (2-major leap) and Microsoft.Data.SqlClient 6→7 both compiled clean with zero migration surface on this codebase, because the existing discipline of never casting to sealed transport types (NpgsqlConnection,SqlConnection,SqliteConnection) absorbed both jumps. Keep that discipline — any new handler that reintroduces a sealed-type cast is a future migration tax that will be paid the next time these transports jump majors. - CVE-fix plans must cite the advisory's explicit patched version, not "newer". Phase 3 PLAN-5.1 initial draft specified
System.Security.Cryptography.Xml 8.0.2— which IS the vulnerable version listed in GHSA-37gx-xxp4-5rgx. Caught in plan critique before build. Author CVE-fix plans against the advisory's "Patched versions" field verbatim, not a vague "bump to latest" — the vulnerable version often sits numerically close to the fix. - Aggressive one-pass dependency refresh is viable on this codebase — precedent, not default. 8 majors + 1 CVE fix on a single branch, zero reverts, Jenkins green first try (2026-04). The posture worked because multi-targeting caps (net8 compat) were pre-identified, landmines were enumerated from prior lessons, and
IDbConnectionabstractions absorbed the transport bumps. Future refreshes can use this as precedent when the same preconditions hold; fall back to per-major PRs when they don't. - Uncommitted
.shipyard/STATE.jsonsilently blocksgit pull. When a session ends mid-build, Shipyard mutates STATE.json without committing. The nextgit pullfails withYour local changes to the following files would be overwritten by merge: .shipyard/STATE.json— easy to misread as a remote/tag issue. Fix: commit state transitions promptly at session close, orgit stash push .shipyard/STATE.jsonthen pull. A pre-pull hook that auto-stashes.shipyard/STATE.jsonwould eliminate the foot-gun entirely. - CI filters inherited from prior CI servers are stale until proven otherwise. The
--filter "FullyQualifiedName!~JobScheduler"exclusion across all 13 Jenkins integration stages was carried over from the TeamCity era. PR #130 (issue #127, 2026-04-21) dropped all 13 exclusions in one commit and got two consecutive green runs (PR + post-merge master) with zero flakiness. Lesson: when migrating CI, treat inherited filters/exclusions as suspect — re-validate them against the new infrastructure rather than assuming the old reason still applies. One experimental PR is cheaper than indefinitely missing regression coverage. - Microsoft.Playwright.MSTest pins MSTest 2.x — incompatible with this repo's MSTest 4.x. PR #133 (issue #126, 2026-04-21) attempted to use the wrapper for the standard
[TestClass]+PageTestergonomics; central package management resolved MSTest to 4.2.1 but Playwright.MSTest'sPageTestwas built against MSTest 2.x and its tests silently failed to discover via VSTest (No test is available). Fix: use rawMicrosoft.Playwrightand hand-roll the assembly/class fixtures (single[AssemblyInitialize]static class forIPlaywright+IBrowser; per-test[TestInitialize]/[TestCleanup]forIBrowserContext+IPage). Don't reach forMicrosoft.Playwright.MSTesthere. - WebApplicationFactory + Playwright is a dead end on Blazor Server. PR #133 spent significant time fighting
WebApplicationFactory<Program>to expose a Kestrel URL: WAF'sConfigureWebHostforcesUseTestServer()after user setup runs, so the resolvedIServeris always TestServer (no real socket → no Playwright). The dual-host trick (build TestServer host, then a parallel Kestrel host from the same builder) ran into address-binding races andUseUrls(":0")literal-port issues. Working approach: skip WAF entirely, launch Dashboard.Ui as a child process viadotnet bin/.../DotNetWorkQueue.Dashboard.Ui.dll, set config overrides through env vars (Key:Subkey→Key__Subkey), parse Kestrel'sNow listening on:log line for the bound URL. SeeSource/DotNetWorkQueue.Dashboard.Ui.E2E.Tests/Fixtures/DashboardSubprocess.cs. - Blazor Server
OnAfterRender-driven redirects race the SignalR circuit attach in E2E tests.MainLayout.razor's "redirect to /login if unauthenticated" path runs fromOnAfterRender(firstRender)on the interactive render pass, which only happens after the browser opens the SignalR WebSocket. Playwright assertions can fire before that handshake completes, so the URL stays at/for 5+ seconds. PR #133 droppedRootRedirectsToLogin_WhenAuthEnabledAndUnauthenticatedE2E test for this reason — the identical assertion is covered by the bUnitMainLayoutTestswhich doesn't have the circuit timing dependency. Pattern: keep redirect/state-transition assertions in bUnit; reserve E2E for plain HTTP flows (form POSTs, static page renders). - Jenkins agent
dockerlabel means the agent IS a Docker container — not that it has the docker CLI. PR #133 first attemptedagent { docker { image 'mcr.microsoft.com/playwright/dotnet:...' } }for the E2E stage and gotdocker: not foundon line 1 of the Jenkins script. Thedocker-labeled agents in this repo can't launch nested containers. For stages needing extra tooling (Playwright browsers, etc.), install at stage time on the standard agent — for Playwright specifically:dotnet exec --runtimeconfig <test>.runtimeconfig.json bin/.../Microsoft.Playwright.dll install --with-deps chromium. Nopwshneeded (the docs default topwsh playwright.ps1 installbut that's not what the repo's agents run).
- Prefer correct, complete implementations over minimal ones.
- Use appropriate data structures and algorithms — don't brute-force what has a known better solution.
- When fixing a bug, fix the root cause, not the symptom.
- If something I asked for requires error handling or validation to work reliably, include it without asking.
- New and changed features should be covered by either unit or integration Tests
- Features that might vary by the transport implementation should have integration Tests; This has caused issues before with Redis History for example