Skip to content

feat: complete async LINQ materialiser layer for IBLiteQueryable#30

Merged
mrdevrobot merged 2 commits intomainfrom
copilot/feat-complete-async-linq-materialiser
Apr 1, 2026
Merged

feat: complete async LINQ materialiser layer for IBLiteQueryable#30
mrdevrobot merged 2 commits intomainfrom
copilot/feat-complete-async-linq-materialiser

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 1, 2026

IBLiteQueryable<T> was missing async counterparts for most common LINQ terminators, forcing users to fall back to sync (blocking) .First(), .Last(), .Min(), etc.

New extensions (BLiteQueryableExtensions.cs)

All follow the existing pattern — cast to IAsyncEnumerable<T> at runtime, materialise manually, no System.Linq.Async dependency.

Extension Overloads Empty-sequence behaviour
FirstAsync plain + predicate throws InvalidOperationException
SingleAsync plain + predicate throws on empty or multi-element
LastAsync plain + predicate throws InvalidOperationException
LastOrDefaultAsync plain + predicate returns default
ElementAtAsync index throws ArgumentOutOfRangeException
ElementAtOrDefaultAsync index returns default
MinAsync<T,TResult> selector throws on empty
MaxAsync<T,TResult> selector throws on empty
SumAsync int / long / double / decimal returns zero
AverageAsync intdouble / longdouble / double / decimal throws on empty
ForEachAsync Action<T> + CancellationToken no-op
// Before: silently blocks
var first = collection.AsQueryable().Where(x => x.Active).First();

// After: fully async
var first   = await collection.AsQueryable().Where(x => x.Active).FirstAsync(ct);
var last    = await collection.AsQueryable().OrderBy(x => x.Ts).LastAsync(ct);
var total   = await collection.AsQueryable().SumAsync(x => x.Amount, ct);
var average = await collection.AsQueryable().AverageAsync(x => x.Score, ct);
await collection.AsQueryable().Where(x => x.Dirty).ForEachAsync(x => x.Flush(), ct);

IBLiteQueryable<T> XML docs

Expanded <remarks> to enumerate all async terminators and explicitly warn against the inherited sync LINQ terminators on a BLite queryable.

Tests (tests/BLite.Tests/Query/BLiteQueryableExtensionsTests.cs)

42 new tests; each new method has a happy-path case and an empty-source case validating throw-vs-default semantics.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • av-build-tel-api-v1.avaloniaui.net
    • Triggering command: /usr/share/dotnet/dotnet dotnet exec --runtimeconfig /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/runtimeconfig.json /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/Avalonia.BuildServices.Collector.dll (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title [WIP] Complete async LINQ materialiser layer for IBLiteQueryable<T> feat: complete async LINQ materialiser layer for IBLiteQueryable Apr 1, 2026
Copilot AI requested a review from mrdevrobot April 1, 2026 20:16
@mrdevrobot mrdevrobot marked this pull request as ready for review April 1, 2026 23:05
Copilot AI review requested due to automatic review settings April 1, 2026 23:05
@mrdevrobot mrdevrobot merged commit 2499054 into main Apr 1, 2026
1 check passed
@mrdevrobot mrdevrobot deleted the copilot/feat-complete-async-linq-materialiser branch April 1, 2026 23:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a comprehensive async LINQ “terminator” layer for IBLiteQueryable<T> so consumers can materialize/query results without falling back to blocking sync LINQ methods, plus updated docs and new test coverage.

Changes:

  • Added async terminator extensions (e.g., FirstAsync, SingleAsync, LastAsync, ElementAtAsync, MinAsync/MaxAsync, SumAsync, AverageAsync, ForEachAsync) to BLiteQueryableExtensions.
  • Expanded IBLiteQueryable<T> XML remarks to enumerate supported async terminators and warn against sync LINQ terminators on BLite queryables.
  • Introduced a new dedicated test suite covering the new async terminators and their empty-sequence semantics.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/BLite.Core/Query/BLiteQueryableExtensions.cs Adds async materializer/terminator extensions for common LINQ end-operations over BLite queryables.
src/BLite.Core/Query/IBLiteQueryable.cs Updates XML docs to guide users toward async terminators and away from blocking sync LINQ.
tests/BLite.Tests/Query/BLiteQueryableExtensionsTests.cs Adds tests validating behavior (happy paths + empty-sequence behavior) for the new async extensions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +470 to +474
var compiled = selector.Compile();
int sum = 0;
await foreach (var item in asyncEnum.WithCancellation(ct).ConfigureAwait(false))
sum += compiled(item);
return sum;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

SumAsync for IAsyncEnumerable uses unchecked sum += ... for int (and similarly for the long overload). This differs from System.Linq Sum, which throws OverflowException on overflow; here it can silently wrap depending on compilation settings. Use checked arithmetic (or an explicit overflow-checked helper) to match LINQ semantics.

Copilot uses AI. Check for mistakes.
Comment on lines +545 to +551
long sum = 0L;
long count = 0L;
await foreach (var item in asyncEnum.WithCancellation(ct).ConfigureAwait(false))
{
sum += compiled(item);
count++;
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

AverageAsync for IAsyncEnumerable accumulates sum using unchecked sum += ... (and the long overload does the same). LINQ’s Average over int/long uses overflow-checked accumulation; without checked arithmetic this can silently overflow and produce incorrect averages for large sequences/values. Consider using checked addition (and keep count increment separate).

Copilot uses AI. Check for mistakes.
Comment on lines +204 to +210
[Fact]
public async Task LastOrDefaultAsync_WithPredicate_ReturnsNull_WhenNoMatch()
{
using var db = await CreateAndSeed(3);

var doc = await db.AsyncDocs.AsQueryable()
.LastOrDefaultAsync(d => d.Name == "Ghost");
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Test coverage gap: the LastOrDefaultAsync(predicate) overload is only exercised for the no-match case. Add a positive predicate test that asserts it returns the last matching element to guard against regressions in the Where(...).LastOrDefaultAsync() chaining path.

Copilot uses AI. Check for mistakes.
Comment on lines +368 to +372
[Fact]
public async Task SumAsync_Long_ReturnsTotalSum()
{
using var db = await CreateAndSeed(5);

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Test coverage gap: empty-sequence behavior is only asserted for SumAsync(int) and AverageAsync(int/decimal). Since the long/double/decimal overloads have separate implementations, add empty-source tests for SumAsync(long/double/decimal) returning 0 and AverageAsync(long/double) throwing InvalidOperationException.

Copilot uses AI. Check for mistakes.
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.

feat: complete async LINQ materialiser layer for IBLiteQueryable<T>

3 participants