Skip to content

Commit c7e9cba

Browse files
authored
Merge pull request #72 from EFNext/fix/include-then-async
added IAsyncEnumerable support to IncludableExpressiveQueryableWrapper
2 parents bb25846 + d339116 commit c7e9cba

6 files changed

Lines changed: 106 additions & 5 deletions

File tree

src/ExpressiveSharp.EntityFrameworkCore/ExpressiveDbSet.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ public class ExpressiveDbSet<TEntity> : DbSet<TEntity>, IExpressiveQueryable<TEn
1717
private readonly DbSet<TEntity> _inner;
1818
private readonly IQueryable<TEntity> _queryable;
1919

20-
public ExpressiveDbSet(DbSet<TEntity> inner)
20+
// Constructed only via AsExpressiveDbSet() (through InternalExpressiveDbSet) so every
21+
// instance is the async-capable runtime subclass. See InternalExpressiveDbSet.
22+
private protected ExpressiveDbSet(DbSet<TEntity> inner)
2123
{
2224
_inner = inner;
2325
_queryable = inner;
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using ExpressiveSharp.EntityFrameworkCore;
2+
using ExpressiveSharp.EntityFrameworkCore.Infrastructure.Internal;
23

34
// ReSharper disable once CheckNamespace — intentionally in Microsoft.EntityFrameworkCore for discoverability
45
namespace Microsoft.EntityFrameworkCore;
@@ -7,5 +8,5 @@ public static class DbSetExtensions
78
{
89
public static ExpressiveDbSet<TEntity> AsExpressiveDbSet<TEntity>(this DbSet<TEntity> dbSet)
910
where TEntity : class
10-
=> new(dbSet);
11+
=> new InternalExpressiveDbSet<TEntity>(dbSet);
1112
}

src/ExpressiveSharp.EntityFrameworkCore/Infrastructure/IncludableExpressiveQueryableWrapper.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace ExpressiveSharp.EntityFrameworkCore.Infrastructure;
66

77
internal sealed class IncludableExpressiveQueryableWrapper<TEntity, TProperty>
8-
: IIncludableExpressiveQueryable<TEntity, TProperty>
8+
: IIncludableExpressiveQueryable<TEntity, TProperty>, IAsyncEnumerable<TEntity>
99
where TEntity : class
1010
{
1111
private readonly IIncludableQueryable<TEntity, TProperty> _inner;
@@ -19,4 +19,14 @@ public IncludableExpressiveQueryableWrapper(IIncludableQueryable<TEntity, TPrope
1919

2020
IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator() => _inner.GetEnumerator();
2121
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_inner).GetEnumerator();
22+
23+
IAsyncEnumerator<TEntity> IAsyncEnumerable<TEntity>.GetAsyncEnumerator(CancellationToken cancellationToken)
24+
{
25+
if (_inner is IAsyncEnumerable<TEntity> asyncEnumerable)
26+
return asyncEnumerable.GetAsyncEnumerator(cancellationToken);
27+
28+
throw new InvalidOperationException(
29+
$"The source IQueryable<{typeof(TEntity).Name}> does not implement IAsyncEnumerable<{typeof(TEntity).Name}>. " +
30+
"Async operations require an async-capable provider such as Entity Framework Core.");
31+
}
2232
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Microsoft.EntityFrameworkCore;
2+
3+
namespace ExpressiveSharp.EntityFrameworkCore.Infrastructure.Internal;
4+
5+
/// <summary>
6+
/// The concrete runtime instance produced by <c>AsExpressiveDbSet()</c>. It implements
7+
/// <see cref="IAsyncEnumerable{T}"/> — satisfied by the inherited
8+
/// <see cref="ExpressiveDbSet{TEntity}.GetAsyncEnumerator"/> — so EF Core's streaming async
9+
/// terminals (<c>ToListAsync</c>/<c>ToArrayAsync</c>/...), which runtime-cast the source to
10+
/// <see cref="IAsyncEnumerable{T}"/>, work directly on the set.
11+
/// <para>
12+
/// The interface lives here rather than on the public <see cref="ExpressiveDbSet{TEntity}"/> so
13+
/// callers never hold a static type that is both <see cref="IQueryable{T}"/> and
14+
/// <see cref="IAsyncEnumerable{T}"/> — which on .NET 10 makes those terminals ambiguous between
15+
/// <c>System.Linq.AsyncEnumerable</c> and EF Core's extensions. This mirrors EF Core's own
16+
/// public <c>DbSet&lt;T&gt;</c> / internal <c>InternalDbSet&lt;T&gt;</c> split.
17+
/// </para>
18+
/// </summary>
19+
internal sealed class InternalExpressiveDbSet<TEntity> : ExpressiveDbSet<TEntity>, IAsyncEnumerable<TEntity>
20+
where TEntity : class
21+
{
22+
public InternalExpressiveDbSet(DbSet<TEntity> inner) : base(inner)
23+
{
24+
}
25+
}

tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Infrastructure/AsyncQueryableTestBase.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ namespace ExpressiveSharp.EntityFrameworkCore.IntegrationTests.Infrastructure;
77
public abstract class AsyncQueryableTestBase : EFCoreRelationalTestBase
88
{
99
[TestInitialize]
10-
public Task SeedStoreData() => Context.SeedStoreAsync();
10+
public async Task SeedStoreData()
11+
{
12+
await Context.SeedStoreAsync();
13+
Context.ChangeTracker.Clear();
14+
}
1115

1216
[TestMethod]
1317
public async Task AnyAsync_WithPredicate_Executes()
@@ -117,6 +121,61 @@ public async Task Include_Where_ToListAsync_LoadsNavigation()
117121
Assert.IsTrue(results.All(o => o.Customer != null));
118122
}
119123

124+
[TestMethod]
125+
public async Task ExpressiveDbSet_ToListAsync_DirectTerminal_Executes()
126+
{
127+
// Streaming terminal directly on the ExpressiveDbSet (no intervening operator to
128+
// re-wrap it). ToListAsync casts the source to IAsyncEnumerable<T>.
129+
var results = await Context.ExpressiveOrders.ToListAsync();
130+
131+
Assert.AreEqual(4, results.Count);
132+
}
133+
134+
[TestMethod]
135+
public async Task ExpressiveDbSet_ToArrayAsync_DirectTerminal_Executes()
136+
{
137+
var results = await Context.ExpressiveOrders.ToArrayAsync();
138+
139+
Assert.AreEqual(4, results.Length);
140+
}
141+
142+
[TestMethod]
143+
public async Task Include_ToListAsync_DirectTerminal_LoadsNavigation()
144+
{
145+
var results = await Context.ExpressiveOrders
146+
.Include(o => o.Customer)
147+
.ToListAsync();
148+
149+
Assert.AreEqual(4, results.Count);
150+
Assert.AreEqual(3, results.Count(o => o.Customer != null));
151+
}
152+
153+
[TestMethod]
154+
public async Task Include_ToArrayAsync_DirectTerminal_LoadsNavigation()
155+
{
156+
var results = await Context.ExpressiveOrders
157+
.Include(o => o.Customer)
158+
.ToArrayAsync();
159+
160+
Assert.AreEqual(4, results.Length);
161+
Assert.AreEqual(3, results.Count(o => o.Customer != null));
162+
}
163+
164+
[TestMethod]
165+
public async Task Include_ThenInclude_ToListAsync_DirectTerminal_LoadsTwoLevelNavigation()
166+
{
167+
var results = await Context.ExpressiveOrders
168+
.Include(o => o.Customer)
169+
.ThenInclude(c => c!.Address)
170+
.ToListAsync();
171+
172+
Assert.AreEqual(4, results.Count);
173+
var aliceOrder = results.Single(o => o.CustomerId == 1);
174+
Assert.IsNotNull(aliceOrder.Customer);
175+
Assert.IsNotNull(aliceOrder.Customer!.Address);
176+
Assert.AreEqual("New York", aliceOrder.Customer.Address!.City);
177+
}
178+
120179
[TestMethod]
121180
public async Task TagWith_Where_FirstAsync_ExecutesCorrectly()
122181
{

tests/ExpressiveSharp.EntityFrameworkCore.IntegrationTests/Infrastructure/IncludeTestBase.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ namespace ExpressiveSharp.EntityFrameworkCore.IntegrationTests.Infrastructure;
77
public abstract class IncludeTestBase : EFCoreRelationalTestBase
88
{
99
[TestInitialize]
10-
public Task SeedStoreData() => Context.SeedStoreAsync();
10+
public async Task SeedStoreData()
11+
{
12+
await Context.SeedStoreAsync();
13+
Context.ChangeTracker.Clear();
14+
}
1115

1216
[TestMethod]
1317
public async Task Include_LoadsRelatedCustomer()

0 commit comments

Comments
 (0)