Skip to content

feat!: target net9.0 and net10.0 with configurable frameworks - #214

Merged
JacobAtchley merged 10 commits into
mainfrom
forks/jatchley/dotnet10-multitarget
Oct 8, 2026
Merged

JacobAtchley merged 10 commits into
mainfrom
forks/jatchley/dotnet10-multitarget

Conversation

@JacobAtchley

@JacobAtchley JacobAtchley commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Target frameworks come from FirebendTargetFrameworks in Directory.Build.props (currently net9.0;net10.0). Samples and other runnable apps use FirebendAppTargetFramework (net10.0). Adding .NET 11 later means editing that list and adding one net11.0 block to Directory.Packages.props.
  • Package versions move to central package management in Directory.Packages.props. Microsoft framework packages are versioned per framework: 9.0.x for net9.0 and 10.0.x for net10.0. Other packages are updated to their latest stable versions.
  • A repo nuget.config restricts restore to nuget.org.
  • global.json pins SDK 10.0.401 with rollForward: latestPatch, so local builds and CI run the same analyzer rules. CI reads it through actions/setup-dotnet@v4 (global-json-file) and also installs the 9.0 SDK for the net9.0 test runtime. CI uses actions/checkout@v4.
  • The release workflow sets up .NET before installing Versionize, so the tool installs on the pinned SDK.
  • Dependabot opens monthly dotnet-sdk updates for global.json.
  • Packages now embed the icon and use PackageLicenseExpression (MIT) instead of the deprecated PackageIconUrl and PackageLicenseUrl (NU5048, NU5125).
  • FluentAssertions is pinned to 7.x, because v8 requires a commercial license.
  • The first commit is feat!: with a BREAKING CHANGE: footer, so Versionize will release a new major version.

jobba specifics

  • Next version: 12.0.1 → 13.0.0.
  • MassTransit 8.5.3 → 8.5.11 (latest v8; v9 is commercial), MongoDB.Driver 3.5.0 → 3.12.0, and Cronos 0.11.1 → 0.13.0.
  • Swashbuckle 10 in Web.Sample, and dotnet-ef 10.0.12.
  • Behavior change with .NET 10 hosting: this follows the Microsoft.Extensions.Hosting version the app runs with (normally the runtime's), not the TFM of the Jobba package, so net9.0 consumers running on .NET 10 are affected too. With .NET 10 hosting, BackgroundService.StartAsync runs all of ExecuteAsync as a task, so a failing IJobbaReadyGate no longer makes JobbaHostedService.StartAsync throw.
    • The host starts every other hosted service, including Kestrel, then logs "BackgroundService failed".
    • Under the default BackgroundServiceExceptionBehavior.StopHost, it stops gracefully and RunAsync returns without rethrowing, so the exit code is 0 unless the app sets one.
    • With Ignore, the host keeps running with Jobba uninitialized, and the only sign is the error log.
    • With .NET 9 hosting, a gate that fails immediately still aborts host startup.
    • Should_Throw_When_Ready_Gate_Fails now asserts the exact behavior for each hosting version.
  • Some properties move to the C# 14 field keyword. JobRegistration.TimeZoneInfo keeps its backing field, with the warning suppressed on that line, because the fix would add a setter to a persisted entity.
  • The changelog header was corrected (it was copied as "Lit Redis Changelog").
  • No new EF migrations are needed for the Sqlite or SqlServer stores on either framework.

Test plan

  • dotnet build -c Release: 0 warnings, 0 errors (SDK 10.0.401)
  • dotnet format --verify-no-changes passes
  • Unit tests: 121/121 on net9.0 and 121/121 on net10.0. During development, the MassTransit_Job_Event_Publisher_Should_Pub_Sub_* tests were flaky on .NET 10. They started the receiver service a second time and published before its receive endpoints were connected, which .NET 10's StartAsync no longer waits for. They now wait on the service's ready gate. 25 full-solution runs and 40 loaded runs passed after the fix.
  • Each of the 8 packages contains lib/net9.0 and lib/net10.0, the embedded icon, and the MIT license expression
  • Integration tests were not run (they need external services)

Release order: this references LitRedis.Core 5.0.0. Bump it to 6.0.0 in a follow-up once lit-redis is released.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98

JacobAtchley and others added 5 commits October 7, 2026 13:58
Library and test projects now read their target frameworks from
FirebendTargetFrameworks in Directory.Build.props; the samples use
FirebendAppTargetFramework (net10.0). Package versions move to central
package management in Directory.Packages.props, with per-framework
versions for the Microsoft.Extensions and EF Core packages (9.0.20 on
net9.0, 10.0.12 on net10.0) and the other packages updated to their
latest stable releases. A repo nuget.config pins restore to nuget.org.
CI installs both the 9.0 and 10.0 SDKs, and the checkout and setup-dotnet
actions move to v4. Hosted service tests now await ExecuteTask, since
BackgroundService.StartAsync no longer runs ExecuteAsync inline on .NET 10.

BREAKING CHANGE: packages now target only net9.0 and net10.0. Each Jobba package ships lib/net9.0 and lib/net10.0, and net10.0 consumers now get Microsoft.Extensions.* and Microsoft.EntityFrameworkCore.* 10.0.x dependencies (9.0.20 on net9.0). Minimum dependency versions rise to MassTransit 8.5.11, MongoDB.Driver 3.12.0 and Cronos 0.13.0. On .NET 10, a failing IJobbaReadyGate no longer makes JobbaHostedService.StartAsync throw; the failure surfaces from the background task instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98
FluentAssertions 8 requires a commercial license.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98
Replaces the deprecated PackageLicenseUrl and PackageIconUrl (NU5125, NU5048)
with PackageLicenseExpression and an embedded nuget-image.png, packed from
Directory.Build.props for every project that sets PackageIcon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98
global.json pins SDK 10.0.401 with rollForward latestPatch, so local builds and
CI run the same analyzer rules and a new SDK feature band can no longer break
dotnet format without a code change. CI reads global.json through setup-dotnet
and still installs the 9.0 SDK for the net9.0 test runtime. The release workflow
now sets up .NET before installing Versionize, so the dotnet tool install runs
on the pinned SDK. Dependabot opens monthly dotnet-sdk updates for global.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98

@mcarey1590 mcarey1590 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff. Nothing blocking; leaning approve. CI green; integration tests not run (need external services).

Medium

  • The hosted-service change follows the .NET 10 runtime, not the library TFM: on any .NET 10 runtime a ready-gate failure no longer throws from StartAsync and instead goes through the host's BackgroundServiceExceptionBehavior. net9.0 consumers running on .NET 10 are affected too. The BREAKING CHANGE footer ties it to ".NET 10" only, so please make that explicit in the notes. The updated Should_Throw_When_Ready_Gate_Fails passes on both runtimes but no longer pins either behavior.
  • The PR notes one net10.0 failure in a full-solution run that didn't reproduce in 13 reruns. A flaky test in the release workflow would block a publish; worth identifying which one.

Low

  • JobRegistration.cs:93-102 / JobbaCoreOptions.cs:26-31: field keyword refactor. JobRegistration is a persisted entity, so this is the only change that could affect behavior beyond build config. EF should find the compiler-generated backing field by convention, but I didn't see a test of EF 9.0.20 on net9.0 with it.
  • global.json pins 10.0.401 with rollForward: latestPatch; anyone on another feature band can't build. latestFeature would be friendlier.
  • Per-TFM PackageVersion blocks may confuse Dependabot; watch the first monthly PRs.
  • Nit: dotnet-ef 10.0.12 vs EF 9.0.20 packages on net9.0.

Ordering: Jobba depends only on LitRedis.Core (pinned 5.0.0). Better to merge after lit-redis#151 releases, otherwise Jobba.Redis needs a second major bump later.

JacobAtchley and others added 4 commits October 7, 2026 16:15
Adds a Building from source section covering global.json and the
FirebendTargetFrameworks and FirebendAppTargetFramework properties. Dependabot
now ignores major updates to the framework-aligned Microsoft packages, which
are versioned per target framework in Directory.Packages.props.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98
Assert that StartAsync throws the gate's exception before .NET 10, and that
from .NET 10 StartAsync completes while ExecuteTask faults with it, instead of
accepting either path. Guard that the Microsoft.Extensions.Hosting.Abstractions
major version matches the runtime so the branch reflects the loaded
BackgroundService.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98
TimeZoneId moved to a field-backed property, which changes the backing
field EF discovers. Pin that the TimeZoneId column is still written and read,
that the UTC default survives persistence, and that re-registering a job
updates the stored zone and resets the cached TimeZoneInfo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98
The pub/sub tests started the receiver hosted service a second time after
harness.Start() had already started it, then published immediately. From
.NET 10 BackgroundService.StartAsync returns before ExecuteAsync connects the
receive endpoints, so the publish raced the endpoint and the message was
dropped, failing intermittently under load. Await the service's ready gate,
which completes once every endpoint reports ready, and drop the duplicate
start that tried to connect the same endpoint twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98
@JacobAtchley

Copy link
Copy Markdown
Contributor Author

Responses to the review:

Medium

  • Ready-gate behavior. Agreed, and the PR description now says so explicitly. It depends on the hosting version, not on Jobba's TFM. Strictly, it depends on which Microsoft.Extensions.Hosting.Abstractions is loaded: ASP.NET Core apps get the one matching their runtime, but a net9.0 console app that references the 10.x hosting packages also gets the new behavior.
    • With .NET 10 hosting, StartAsync runs ExecuteAsync via Task.Run and returns a completed task. A failing gate surfaces as a faulted ExecuteTask.
    • The host then logs "BackgroundService failed". Under the default StopHost it stops gracefully, and RunAsync returns without rethrowing (exit code 0 unless set). Under Ignore, Jobba stays uninitialized and the error log is the only sign.
    • I've added this to the PR description so it can go into the squash message and release notes when this merges.
  • The ready-gate test now asserts the exact behavior on each runtime. Below 10, StartAsync throws. On 10 and above, StartAsync completes and ExecuteTask faults with the gate's exception. It also asserts that the loaded hosting assembly matches the runtime, so the branch can't silently test the wrong behavior. I flipped the condition to confirm both branches execute: each framework then fails.
  • The flaky test is found and fixed. It was the 7 MassTransit_Job_Event_Publisher_Should_Pub_Sub_* tests. I reproduced it in 3 of 9 full-solution runs, and in almost every iteration under CPU load, always on net10.0.
    • Cause: the test helper started the receiver service a second time after harness.Start(), then published immediately. On .NET 10, StartAsync returns before the receive endpoints connect, so the message was dropped. The second start also tried to add the same endpoint key twice.
    • Fix: the duplicate start is removed, and the test waits on the service's own ready gate (30s timeout). No delays were raised.
    • Result: 25/25 full-solution runs and 40/40 runs under load pass.
    • Note for consumers: on .NET 10 the endpoints connect after host start. Code that publishes Jobba events before the ready gate opens could lose them on the in-memory transport.

Low

  • field keyword on JobRegistration. Only TimeZoneId changed; JobbaCoreOptions isn't persisted.
    • For both the Sqlite and SqlServer stores, the EF relational mapping and generated CREATE scripts are identical across main (EF 9.0.9), this branch on net9.0 (EF 9.0.20) and on net10.0 (EF 10.0.12).
    • The only model difference is the discovered backing field name (_timeZoneId becomes <TimeZoneId>k__BackingField). Access mode is still PreferField, and has-pending-model-changes reports nothing.
    • Added Sqlite round-trip tests: a custom zone, the UTC default, and re-registration resetting the cached TimeZoneInfo.
    • Mongo's BSON output and deserialization are identical; it maps through the property.
  • latestPatch vs latestFeature. I kept latestPatch on purpose. The point of pinning is that new analyzer rules come with SDK feature bands. 10.0.401 is what broke dotnet format on auto-crud#440 with IDE0270, which 10.0.101 didn't report. latestFeature would let a local 10.0.5xx SDK pass or fail format differently from CI. The README now states the SDK requirement, and Dependabot's dotnet-sdk updates will propose feature-band bumps as PRs.
  • Dependabot and the per-TFM blocks. dependabot.yml now ignores major updates to the framework-aligned Microsoft packages, so the 9.0.x block can't be moved to 10.x.
  • dotnet-ef 10.0.12 vs EF 9.0.20. Verified: migrations list and has-pending-model-changes work for both EF stores with --framework net9.0 and net10.0.

Ordering: agreed. I'll hold this until lit-redis#151 releases 6.0.0, then bump LitRedis.Core here so Jobba.Redis takes one major version, not two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvmzsCHKfHVMozvywp42FU

@mcarey1590 mcarey1590 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked after latest commits. All review items addressed: ready-gate behavior documented in PR description and pinned per runtime in tests, flaky MassTransit pub/sub tests root-caused and fixed (25/25 full runs), JobRegistration field-keyword EF mapping verified plus Sqlite round-trip tests, latestPatch pin rationale + README, Dependabot ignore, dotnet-ef verified, LitRedis.Core 6.0.0 bump (published on NuGet) so Jobba.Redis takes one major. CI green on head. Integration tests still unrun (need external services).

@JacobAtchley
JacobAtchley merged commit 6c0c35c into main Oct 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants