diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7a1712b7..499b69b6 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,4 +1,5 @@  + net9.0 https://github.com/arcenox-co/TickerQ @@ -8,7 +9,7 @@ icon.jpg true 9.1.0 - [9.0.0,10.0.0) + [10.0.0,11.0.0) default @@ -17,5 +18,4 @@ - diff --git a/src/TickerQ.EntityFrameworkCore/Infrastructure/BasePersistenceProvider.cs b/src/TickerQ.EntityFrameworkCore/Infrastructure/BasePersistenceProvider.cs index b4d0fd49..f81ecb53 100644 --- a/src/TickerQ.EntityFrameworkCore/Infrastructure/BasePersistenceProvider.cs +++ b/src/TickerQ.EntityFrameworkCore/Infrastructure/BasePersistenceProvider.cs @@ -103,9 +103,10 @@ public async Task ReleaseAcquiredTimeTickers(Guid[] timeTickerIds, CancellationT await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);; var now = _clock.UtcNow; - var baseQuery = timeTickerIds.Length == 0 + var idList = timeTickerIds.ToList(); + var baseQuery = idList.Count == 0 ? dbContext.Set() - : dbContext.Set().Where(x => timeTickerIds.Contains(x.Id)); + : dbContext.Set().Where(x => idList.Contains(x.Id)); await baseQuery .WhereCanAcquire(_lockHolder) @@ -127,9 +128,15 @@ public async Task UpdateTimeTicker(InternalFunctionContext functionContexts public async Task UpdateTimeTickersWithUnifiedContext(Guid[] timeTickerIds, InternalFunctionContext functionContext, CancellationToken cancellationToken = default) { await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);; + var idList = timeTickerIds.ToList(); await dbContext.Set() +<<<<<<< HEAD .Where(x => timeTickerIds.Contains(x.Id)) .ExecuteUpdateAsync(MappingExtensions.UpdateTimeTicker(functionContext, _clock.UtcNow), cancellationToken).ConfigureAwait(false); +======= + .Where(x => idList.Contains(x.Id)) + .ExecuteUpdateAsync(setter => setter.UpdateTimeTicker(functionContext, _clock.UtcNow), cancellationToken).ConfigureAwait(false); +>>>>>>> 39b9b90 (Fix .NET 9+ EF Core query failures caused by ReadOnlySpan array.Contains() (#574)) } public async Task GetEarliestTimeTickers(CancellationToken cancellationToken) @@ -214,10 +221,11 @@ public async Task AcquireImmediateTimeTickersAsync(Guid[] id await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); var now = _clock.UtcNow; + var idList = ids.ToList(); // Acquire and mark InProgress in a single update var affected = await dbContext.Set() - .Where(x => ids.Contains(x.Id)) + .Where(x => idList.Contains(x.Id)) .WhereCanAcquire(_lockHolder) .ExecuteUpdateAsync(setter => setter .SetProperty(x => x.LockHolder, _lockHolder) @@ -232,7 +240,7 @@ public async Task AcquireImmediateTimeTickersAsync(Guid[] id // Return the acquired tickers for immediate execution, with children return await dbContext.Set() .AsNoTracking() - .Where(x => ids.Contains(x.Id) && x.LockHolder == _lockHolder && x.Status == TickerStatus.InProgress) + .Where(x => idList.Contains(x.Id) && x.LockHolder == _lockHolder && x.Status == TickerStatus.InProgress) .Include(x => x.Children.Where(y => y.ExecutionTime == null)) .Select(MappingExtensions.ForQueueTimeTickers()) .ToArrayAsync(cancellationToken) @@ -245,7 +253,7 @@ public async Task MigrateDefinedCronTickers((string Function, string Expression) await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); var now = _clock.UtcNow; - var functions = cronTickers.Select(x => x.Function).ToArray(); + var functions = cronTickers.Select(x => x.Function).ToList(); var cronSet = dbContext.Set(); // Build the complete set of registered function names to detect orphaned tickers. @@ -262,16 +270,17 @@ public async Task MigrateDefinedCronTickers((string Function, string Expression) .ToArrayAsync(cancellationToken) .ConfigureAwait(false); - if (orphanedCron.Length > 0) + var orphanedCronList = orphanedCron.ToList(); + if (orphanedCronList.Count > 0) { // Delete related occurrences first (if any), then the cron tickers await dbContext.Set>() - .Where(o => orphanedCron.Contains(o.CronTickerId)) + .Where(o => orphanedCronList.Contains(o.CronTickerId)) .ExecuteDeleteAsync(cancellationToken) .ConfigureAwait(false); await cronSet - .Where(c => orphanedCron.Contains(c.Id)) + .Where(c => orphanedCronList.Contains(c.Id)) .ExecuteDeleteAsync(cancellationToken) .ConfigureAwait(false); } @@ -421,9 +430,10 @@ public async Task ReleaseAcquiredCronTickerOccurrences(Guid[] occurrenceIds, Can var now = _clock.UtcNow; await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);; - var baseQuery = occurrenceIds.Length == 0 - ? dbContext.Set>() - : dbContext.Set>().Where(x => occurrenceIds.Contains(x.Id)); + var idList = occurrenceIds.ToList(); + var baseQuery = idList.Count == 0 + ? dbContext.Set>() + : dbContext.Set>().Where(x => idList.Contains(x.Id)); await baseQuery .WhereCanAcquire(_lockHolder) @@ -525,11 +535,12 @@ public async Task> GetEarliestAvailableC { var now = _clock.UtcNow; var mainSchedulerThreshold = now.AddSeconds(-1); + var idList = ids.ToList(); await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);; return await dbContext.Set>() .AsNoTracking() .Include(x => x.CronTicker) - .Where(x => ids.Contains(x.CronTickerId)) + .Where(x => idList.Contains(x.CronTickerId)) .Where(x => x.ExecutionTime >= mainSchedulerThreshold) // Only items within the 1-second main scheduler window .WhereCanAcquire(_lockHolder) .OrderBy(x => x.ExecutionTime) @@ -554,9 +565,15 @@ public async Task UpdateCronTickerOccurrencesWithUnifiedContext(Guid[] cronOccur CancellationToken cancellationToken = default) { await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);; + var idList = cronOccurrenceIds.ToList(); await dbContext.Set>() +<<<<<<< HEAD .Where(x => cronOccurrenceIds.Contains(x.Id)) .ExecuteUpdateAsync(MappingExtensions.UpdateCronTickerOccurrence(functionContext), cancellationToken) +======= + .Where(x => idList.Contains(x.Id)) + .ExecuteUpdateAsync(setter => setter.UpdateCronTickerOccurrence(functionContext), cancellationToken) +>>>>>>> 39b9b90 (Fix .NET 9+ EF Core query failures caused by ReadOnlySpan array.Contains() (#574)) .ConfigureAwait(false); } diff --git a/src/TickerQ.EntityFrameworkCore/Infrastructure/TickerEFCorePersistenceProvider.cs b/src/TickerQ.EntityFrameworkCore/Infrastructure/TickerEFCorePersistenceProvider.cs index c4b0cc11..2e3b1328 100644 --- a/src/TickerQ.EntityFrameworkCore/Infrastructure/TickerEFCorePersistenceProvider.cs +++ b/src/TickerQ.EntityFrameworkCore/Infrastructure/TickerEFCorePersistenceProvider.cs @@ -101,10 +101,11 @@ public async Task RemoveTimeTickers(Guid[] timeTickerIds, CancellationToken await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);; // Load the entities to be deleted (including children for cascade delete) + var idList = timeTickerIds.ToList(); var tickersToDelete = await dbContext.Set() .Include(x => x.Children) .ThenInclude(x => x.Children) // Include grandchildren if needed - .Where(x => timeTickerIds.Contains(x.Id)) + .Where(x => idList.Contains(x.Id)) .ToListAsync(cancellationToken) .ConfigureAwait(false); @@ -189,7 +190,8 @@ public async Task UpdateCronTickers(TCronTicker[] cronTickers, Cancellation public async Task RemoveCronTickers(Guid[] cronTickerIds, CancellationToken cancellationToken) { await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - var result = await dbContext.Set().Where(x => cronTickerIds.Contains(x.Id)) + var idList = cronTickerIds.ToList(); + var result = await dbContext.Set().Where(x => idList.Contains(x.Id)) .ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); if(RedisContext.HasRedisConnection) @@ -246,8 +248,9 @@ public async Task InsertCronTickerOccurrences(CronTickerOccurrenceEntity RemoveCronTickerOccurrences(Guid[] cronTickerOccurrences, CancellationToken cancellationToken = default) { await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + var idList = cronTickerOccurrences.ToList(); return await dbContext.Set>() - .Where(x => cronTickerOccurrences.Contains(x.Id)) + .Where(x => idList.Contains(x.Id)) .ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); } @@ -258,10 +261,11 @@ public async Task[]> AcquireImmediateCro await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); var now = _clock.UtcNow; + var idList = occurrenceIds.ToList(); // Only acquire occurrences that are acquirable (Idle/Queued and not locked by another node) var query = dbContext.Set>() - .Where(x => occurrenceIds.Contains(x.Id)) + .Where(x => idList.Contains(x.Id)) .WhereCanAcquire(_lockHolder); // Lock and mark InProgress @@ -279,7 +283,7 @@ public async Task[]> AcquireImmediateCro // Return acquired occurrences with CronTicker populated return await dbContext.Set>() .AsNoTracking() - .Where(x => occurrenceIds.Contains(x.Id) && x.LockHolder == _lockHolder && x.Status == TickerStatus.InProgress) + .Where(x => idList.Contains(x.Id) && x.LockHolder == _lockHolder && x.Status == TickerStatus.InProgress) .Include(x => x.CronTicker) .ToArrayAsync(cancellationToken) .ConfigureAwait(false);