Skip to content

Upgrade to .NET 10 - #18

Open
Hamad-canCode wants to merge 1 commit into
feature23:mainfrom
Hamad-canCode:upgrade-to-dotnet-10
Open

Upgrade to .NET 10#18
Hamad-canCode wants to merge 1 commit into
feature23:mainfrom
Hamad-canCode:upgrade-to-dotnet-10

Conversation

@Hamad-canCode

Copy link
Copy Markdown

Retarget all projects from net8.0 to net10.0 and move dependencies to the 10.0.x band.

  • global.json: pin the SDK to 10.0.100 (rollForward: latestMinor)
  • All six projects: net8.0 -> net10.0
  • Microsoft.Extensions.* and EntityFrameworkCore.Relational: 8.0.x -> 10.0.10
  • Test stack: Test.Sdk 18.8.1, xunit 2.9.3, xunit.runner.visualstudio 3.1.5, coverlet.collector 10.0.1
  • Nerdbank.GitVersioning: 3.7.115 -> 3.10.91 for SDK 10 compatibility
  • CI: actions/setup-dotnet 8.0.x -> 10.0.x

Verified with clean Debug and Release builds (0 warnings), 92/92 tests passing in both configurations, dotnet pack producing all three packages, and both example projects running correctly.

Note: this drops support for consumers targeting .NET 8 and 9, since the published packages no longer target net8.0. Happy to multi-target net8.0;net10.0 instead if that is preferred.

Fixes #17

Co-Authored-By: Claude

Retarget all projects from net8.0 to net10.0 and move dependencies to the
10.0.x band.

- global.json: pin the SDK to 10.0.100 (rollForward: latestMinor)
- All six projects: net8.0 -> net10.0
- Microsoft.Extensions.* and EntityFrameworkCore.Relational: 8.0.x -> 10.0.10
- Test stack: Test.Sdk 18.8.1, xunit 2.9.3, xunit.runner.visualstudio 3.1.5,
  coverlet.collector 10.0.1
- Nerdbank.GitVersioning: 3.7.115 -> 3.10.91 for SDK 10 compatibility
- CI: actions/setup-dotnet 8.0.x -> 10.0.x

Verified with clean Debug and Release builds (0 warnings), 92/92 tests
passing in both configurations, dotnet pack producing all three packages,
and both example projects running correctly.

Note: this drops support for consumers targeting .NET 8 and 9, since the
published packages no longer target net8.0. Happy to multi-target
net8.0;net10.0 instead if that is preferred.

Fixes feature23#17

Co-Authored-By: Claude
@Hamad-canCode

Copy link
Copy Markdown
Author

Retargets all projects from net8.0 to net10.0 and moves dependencies to the 10.0.x band.

Changes

  • global.json: SDK pin 8.0.010.0.100 (rollForward: latestMinor)
  • All six projects: net8.0net10.0
  • Microsoft.Extensions.* and Microsoft.EntityFrameworkCore.Relational: 8.0.x → 10.0.10
  • Test stack: Microsoft.NET.Test.Sdk 18.8.1, xunit 2.9.3, xunit.runner.visualstudio 3.1.5, coverlet.collector 10.0.1
  • Nerdbank.GitVersioning: 3.7.115 → 3.10.91, for SDK 10 compatibility
  • CI: actions/setup-dotnet 8.0.x10.0.x

No source files changed — this is entirely build configuration.

Verification

  • dotnet build clean in both Debug and Release, 0 warnings
  • 92/92 tests pass in both configurations
  • dotnet pack produces all three packages plus symbols
  • ExampleApiUsage and ExampleInMemoryQueue both run correctly
  • Packed F23.Syncerbell to a local feed and consumed it from a clean net10.0 console app
    outside the repo: restore, DI registration, sync execution, and progress persistence all work
    against the packaged assembly rather than a project reference

One call I'd like your input on: this breaks .NET 8/9 consumers

The published packages no longer target net8.0, so existing consumers will hit:

error NU1202: Package F23.Syncerbell 0.5.14-alpha is not compatible with net8.0
(.NETCoreApp,Version=v8.0). Package F23.Syncerbell 0.5.14-alpha supports:
net10.0 (.NETCoreApp,Version=v10.0)

Verified against a clean net8.0 console app using the packed output.

I went with a straight retarget on the assumption that pre-1.0 (0.5-alpha) is the right time
to make this break. If you'd rather keep .NET 8 consumers working, I'm happy to switch the three
library projects to <TargetFrameworks>net8.0;net10.0</TargetFrameworks> instead — that also
needs conditional PackageReference items in Syncerbell.EntityFrameworkCore, since EF Core 10
requires net10.0. Happy to push that variant if you prefer it.

@Hamad-canCode

Copy link
Copy Markdown
Author

Title: Skipped eligibility checks reset the interval clock, so entities stop syncing after the first run

Summary

When the host polls more frequently than the configured eligibility interval, an entity syncs
exactly once and then never again.

This is triggered by the configuration the README recommends — "the frequency of these checks
should be at least as frequent as your smallest eligibility interval" — so following the
documented guidance is what surfaces it.

Reproduction

ExampleApiUsage reproduces it as shipped (CheckInterval 30s, IntervalEligibilityStrategy
1 minute). It syncs once at startup, then logs Entity ToDoItem is not eligible for sync every
30 seconds indefinitely.

Minimal executable repro — fails on Assert.Equal(2, ...) with Actual: 1:

public class IntervalSkipReproTests
{
    private class ReproEntity;

    private class CountingSync : IEntitySync
    {
        public static int RunCount;

        public Task<SyncResult> Run(EntitySyncContext context, CancellationToken cancellationToken = default)
        {
            Interlocked.Increment(ref RunCount);
            return Task.FromResult(new SyncResult(context.Entity, Success: true));
        }
    }

    [Fact]
    public async Task SkippedChecks_ShouldNotResetTheIntervalClock()
    {
        CountingSync.RunCount = 0;

        var services = new ServiceCollection();
        services.AddLogging();
        services.AddSyncerbell(options =>
        {
            options.AddEntity<ReproEntity, CountingSync>(entity =>
                entity.Eligibility = new IntervalEligibilityStrategy(TimeSpan.FromMilliseconds(500)));
        })
        .AddSyncerbellInMemoryPersistence();

        var syncService = services.BuildServiceProvider().GetRequiredService<ISyncService>();

        // t=0: no prior sync, so this should run.
        await syncService.SyncAllEligible(SyncTriggerType.Timer);
        Assert.Equal(1, CountingSync.RunCount);

        // t=300ms: only 300ms since the last sync, so skipping is correct.
        await Task.Delay(300);
        await syncService.SyncAllEligible(SyncTriggerType.Timer);
        Assert.Equal(1, CountingSync.RunCount);

        // t=600ms: 600ms since the last actual sync exceeds the 500ms interval,
        // so this should run again.
        await Task.Delay(300);
        await syncService.SyncAllEligible(SyncTriggerType.Timer);
        Assert.Equal(2, CountingSync.RunCount);
    }
}

Expected: the entity syncs again once the interval has elapsed since the last actual sync.
Actual: it never syncs again while polling continues.

Root cause

Each ineligible check still creates and leases a log entry, and that entry then feeds the next
check's PriorSyncInfo:

  1. TryAcquireLogEntry creates a new entry and sets LeasedAt = UtcNow before eligibility is
    ever evaluated.
  2. SyncService.ProcessSyncLogEntry finds the entity ineligible and writes that entry as
    SyncStatus.Skipped (SyncService.cs:127).
  3. On the next poll, GetPriorSyncInfo picks the most recent entry ordered by CreatedAt — the
    Skipped one — and reports its LeasedAt as LastSyncLeasedAt.
  4. IntervalEligibilityStrategy measures elapsed time from LastSyncLeasedAt
    (IntervalEligibilityStrategy.cs:33-40), so the clock restarts on every poll.

With poll period < interval, elapsed never reaches the interval and the entity is permanently
ineligible.

Both persistence implementations are affected:

  • InMemorySyncLogPersistence.GetPriorSyncInfo (:139-153) excludes only Pending and
    InProgress, so Skipped entries are included.
  • EntityFrameworkCoreSyncLogPersistence.GetPriorSyncInfo (:105-119) applies no status filter
    at all, so it is affected as well.

Secondary concern

Every skipped poll also writes a row. At a 30-second poll interval that is ~2,880 Skipped rows
per entity per day in SyncLogEntries, none of which represent real work.

Possible fixes

  1. Minimal: exclude SyncStatus.Skipped from the prior-sync queries in both persistence
    implementations, so LastSyncLeasedAt reflects only genuine sync attempts.
  2. More thorough: evaluate eligibility before creating/leasing a log entry, so ineligible
    checks leave no entry at all. This fixes the row growth too, but changes the
    ISyncLogPersistence contract.

Happy to open a PR for either — just let me know which direction you'd prefer.

Notes

Verified against main. Unrelated to the .NET 10 upgrade in #17 — that change touches only
build configuration, and this reproduces independently. The repro above runs against
InMemorySyncLogPersistence; the EF Core path is identified by inspection rather than an
executed test

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.

Upgrade to .NET 10

1 participant