Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/ExpressiveSharp.EntityFrameworkCore/ExpressiveDbSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ public class ExpressiveDbSet<TEntity> : DbSet<TEntity>, IExpressiveQueryable<TEn
private readonly DbSet<TEntity> _inner;
private readonly IQueryable<TEntity> _queryable;

public ExpressiveDbSet(DbSet<TEntity> inner)
// Constructed only via AsExpressiveDbSet() (through InternalExpressiveDbSet) so every
// instance is the async-capable runtime subclass. See InternalExpressiveDbSet.
private protected ExpressiveDbSet(DbSet<TEntity> inner)
{
_inner = inner;
_queryable = inner;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ExpressiveSharp.EntityFrameworkCore;
using ExpressiveSharp.EntityFrameworkCore.Infrastructure.Internal;

// ReSharper disable once CheckNamespace — intentionally in Microsoft.EntityFrameworkCore for discoverability
namespace Microsoft.EntityFrameworkCore;
Expand All @@ -7,5 +8,5 @@ public static class DbSetExtensions
{
public static ExpressiveDbSet<TEntity> AsExpressiveDbSet<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class
=> new(dbSet);
=> new InternalExpressiveDbSet<TEntity>(dbSet);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace ExpressiveSharp.EntityFrameworkCore.Infrastructure;

internal sealed class IncludableExpressiveQueryableWrapper<TEntity, TProperty>
: IIncludableExpressiveQueryable<TEntity, TProperty>
: IIncludableExpressiveQueryable<TEntity, TProperty>, IAsyncEnumerable<TEntity>
where TEntity : class
{
private readonly IIncludableQueryable<TEntity, TProperty> _inner;
Expand All @@ -19,4 +19,14 @@ public IncludableExpressiveQueryableWrapper(IIncludableQueryable<TEntity, TPrope

IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator() => _inner.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_inner).GetEnumerator();

IAsyncEnumerator<TEntity> IAsyncEnumerable<TEntity>.GetAsyncEnumerator(CancellationToken cancellationToken)
{
if (_inner is IAsyncEnumerable<TEntity> asyncEnumerable)
return asyncEnumerable.GetAsyncEnumerator(cancellationToken);

throw new InvalidOperationException(
$"The source IQueryable<{typeof(TEntity).Name}> does not implement IAsyncEnumerable<{typeof(TEntity).Name}>. " +
"Async operations require an async-capable provider such as Entity Framework Core.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;

namespace ExpressiveSharp.EntityFrameworkCore.Infrastructure.Internal;

/// <summary>
/// The concrete runtime instance produced by <c>AsExpressiveDbSet()</c>. It implements
/// <see cref="IAsyncEnumerable{T}"/> — satisfied by the inherited
/// <see cref="ExpressiveDbSet{TEntity}.GetAsyncEnumerator"/> — so EF Core's streaming async
/// terminals (<c>ToListAsync</c>/<c>ToArrayAsync</c>/...), which runtime-cast the source to
/// <see cref="IAsyncEnumerable{T}"/>, work directly on the set.
/// <para>
/// The interface lives here rather than on the public <see cref="ExpressiveDbSet{TEntity}"/> so
/// callers never hold a static type that is both <see cref="IQueryable{T}"/> and
/// <see cref="IAsyncEnumerable{T}"/> — which on .NET 10 makes those terminals ambiguous between
/// <c>System.Linq.AsyncEnumerable</c> and EF Core's extensions. This mirrors EF Core's own
/// public <c>DbSet&lt;T&gt;</c> / internal <c>InternalDbSet&lt;T&gt;</c> split.
/// </para>
/// </summary>
internal sealed class InternalExpressiveDbSet<TEntity> : ExpressiveDbSet<TEntity>, IAsyncEnumerable<TEntity>
where TEntity : class
{
public InternalExpressiveDbSet(DbSet<TEntity> inner) : base(inner)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ namespace ExpressiveSharp.EntityFrameworkCore.IntegrationTests.Infrastructure;
public abstract class AsyncQueryableTestBase : EFCoreRelationalTestBase
{
[TestInitialize]
public Task SeedStoreData() => Context.SeedStoreAsync();
public async Task SeedStoreData()
{
await Context.SeedStoreAsync();
Context.ChangeTracker.Clear();
}

[TestMethod]
public async Task AnyAsync_WithPredicate_Executes()
Expand Down Expand Up @@ -117,6 +121,61 @@ public async Task Include_Where_ToListAsync_LoadsNavigation()
Assert.IsTrue(results.All(o => o.Customer != null));
}

[TestMethod]
public async Task ExpressiveDbSet_ToListAsync_DirectTerminal_Executes()
{
// Streaming terminal directly on the ExpressiveDbSet (no intervening operator to
// re-wrap it). ToListAsync casts the source to IAsyncEnumerable<T>.
var results = await Context.ExpressiveOrders.ToListAsync();

Assert.AreEqual(4, results.Count);
}

[TestMethod]
public async Task ExpressiveDbSet_ToArrayAsync_DirectTerminal_Executes()
{
var results = await Context.ExpressiveOrders.ToArrayAsync();

Assert.AreEqual(4, results.Length);
}

[TestMethod]
public async Task Include_ToListAsync_DirectTerminal_LoadsNavigation()
{
var results = await Context.ExpressiveOrders
Comment on lines +143 to +145
.Include(o => o.Customer)
.ToListAsync();

Assert.AreEqual(4, results.Count);
Assert.AreEqual(3, results.Count(o => o.Customer != null));
}

[TestMethod]
public async Task Include_ToArrayAsync_DirectTerminal_LoadsNavigation()
{
var results = await Context.ExpressiveOrders
Comment on lines +154 to +156
.Include(o => o.Customer)
.ToArrayAsync();

Assert.AreEqual(4, results.Length);
Assert.AreEqual(3, results.Count(o => o.Customer != null));
}

[TestMethod]
public async Task Include_ThenInclude_ToListAsync_DirectTerminal_LoadsTwoLevelNavigation()
{
var results = await Context.ExpressiveOrders
Comment thread
koenbeuk marked this conversation as resolved.
.Include(o => o.Customer)
.ThenInclude(c => c!.Address)
.ToListAsync();

Assert.AreEqual(4, results.Count);
var aliceOrder = results.Single(o => o.CustomerId == 1);
Assert.IsNotNull(aliceOrder.Customer);
Assert.IsNotNull(aliceOrder.Customer!.Address);
Assert.AreEqual("New York", aliceOrder.Customer.Address!.City);
}

[TestMethod]
public async Task TagWith_Where_FirstAsync_ExecutesCorrectly()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ namespace ExpressiveSharp.EntityFrameworkCore.IntegrationTests.Infrastructure;
public abstract class IncludeTestBase : EFCoreRelationalTestBase
{
[TestInitialize]
public Task SeedStoreData() => Context.SeedStoreAsync();
public async Task SeedStoreData()
{
await Context.SeedStoreAsync();
Context.ChangeTracker.Clear();
}

[TestMethod]
public async Task Include_LoadsRelatedCustomer()
Expand Down
Loading