Skip to content
Closed
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
3 changes: 1 addition & 2 deletions src/Docs/PlaygroundModel/Scenarios/Webshop/Customer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@

namespace ExpressiveSharp.Docs.PlaygroundModel.Webshop;

public class Customer
public class Customer : WebshopEntity
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string? Email { get; set; }
public string? Country { get; set; }
Expand Down
3 changes: 1 addition & 2 deletions src/Docs/PlaygroundModel/Scenarios/Webshop/LineItem.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
namespace ExpressiveSharp.Docs.PlaygroundModel.Webshop;

public class LineItem
public class LineItem : WebshopEntity
{
public int Id { get; set; }
public int OrderId { get; set; }
public Order Order { get; set; } = null!;
public int ProductId { get; set; }
Expand Down
3 changes: 1 addition & 2 deletions src/Docs/PlaygroundModel/Scenarios/Webshop/Order.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
namespace ExpressiveSharp.Docs.PlaygroundModel.Webshop;

public class Order
public class Order : WebshopEntity
{
public int Id { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; } = null!;
public DateTime PlacedAt { get; set; }
Expand Down
3 changes: 1 addition & 2 deletions src/Docs/PlaygroundModel/Scenarios/Webshop/Product.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
namespace ExpressiveSharp.Docs.PlaygroundModel.Webshop;

public class Product
public class Product : WebshopEntity
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Category { get; set; } = "";
public decimal ListPrice { get; set; }
Expand Down
9 changes: 9 additions & 0 deletions src/Docs/PlaygroundModel/Scenarios/Webshop/WebshopEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ExpressiveSharp.Docs.PlaygroundModel.Webshop;

// Shared base so Cosmos can host all 4 types in a single container, accessed
// as `DbSet<WebshopEntity>.OfType<T>()`. `Id` lives here because EF Core's TPH
// discriminator model requires the key on the root type.
public abstract class WebshopEntity
{
public int Id { get; set; }
}
17 changes: 15 additions & 2 deletions src/Docs/Prerenderer/SampleRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ private static WebshopDbContext BuildSqlServerContext() =>
.EnableServiceProviderCaching(false)
.Options);

private static WebshopDbContext BuildCosmosContext() =>
private static WebshopCosmosDbContext BuildCosmosContext() =>
new(new DbContextOptionsBuilder<WebshopDbContext>()
.UseCosmos("AccountEndpoint=https://localhost:8081/;AccountKey=dW5pdHRlc3Q=", "playground")
.UseExpressives()
Expand All @@ -64,6 +64,19 @@ private sealed class DbContextRoots : IWebshopQueryRoots
public IExpressiveQueryable<LineItem> LineItems => _ctx.LineItems;
}

// Cosmos roots: every entity lives in the same container and is filtered
// via OfType<T>() on the single DbSet<WebshopEntity>, keeping the query
// tree rooted at one entity type.
private sealed class CosmosDbContextRoots : IWebshopQueryRoots
{
private readonly WebshopCosmosDbContext _ctx;
public CosmosDbContextRoots(WebshopCosmosDbContext ctx) { _ctx = ctx; }
public IExpressiveQueryable<Customer> Customers => _ctx.Entities.AsExpressive().OfType<WebshopEntity, Customer>();
Comment on lines +67 to +74
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

Comment mentions filtering via OfType<T>(), but the actual call site uses the new OfType<T, TResult>() overload (requiring two generic arguments). Tweaking the comment to match the actual API shape would make this easier to follow for future maintainers.

Copilot uses AI. Check for mistakes.
public IExpressiveQueryable<Order> Orders => _ctx.Entities.AsExpressive().OfType<WebshopEntity, Order>();
public IExpressiveQueryable<Product> Products => _ctx.Entities.AsExpressive().OfType<WebshopEntity, Product>();
public IExpressiveQueryable<LineItem> LineItems => _ctx.Entities.AsExpressive().OfType<WebshopEntity, LineItem>();
}

private sealed class MongoRootsImpl : IWebshopQueryRoots
{
public MongoRootsImpl(
Expand Down Expand Up @@ -156,7 +169,7 @@ public RenderedSample Render(DocSample sample)
RenderPrerendererTarget(targets, invoke, "sqlserver", "EF Core + SQL Server", "sql",
new DbContextRoots(sqlServer), static (q, _) => q.ToQueryString());
RenderPrerendererTarget(targets, invoke, "cosmos", "EF Core + Cosmos DB", "sql",
new DbContextRoots(cosmos), static (q, _) => q.ToQueryString());
new CosmosDbContextRoots(cosmos), static (q, _) => q.ToQueryString());
RenderPrerendererTarget(targets, invoke, "mongodb", "MongoDB", "javascript",
BuildMongoRoots(), static (q, _) => FormatMongoOutput(q.ToString()!));
}
Expand Down
42 changes: 42 additions & 0 deletions src/Docs/Prerenderer/WebshopCosmosDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using ExpressiveSharp.Docs.PlaygroundModel.Webshop;
using Microsoft.EntityFrameworkCore;

namespace ExpressiveSharp.Docs.Prerenderer;

// Single shared Cosmos container; every entity lives in it and is
// discriminated by $type. Samples access each concrete type via
// `DbSet<WebshopEntity>.OfType<T>()`, so every query stays rooted at
// one entity type from Cosmos's perspective — sidestepping the
// "Root entity X already being referenced" failure.
internal sealed class WebshopCosmosDbContext : WebshopDbContext
{
public WebshopCosmosDbContext(DbContextOptions<WebshopDbContext> options) : base(options) { }

public DbSet<WebshopEntity> Entities => Set<WebshopEntity>();

protected override void OnModelCreating(ModelBuilder b)
{
b.Entity<WebshopEntity>(e =>
{
e.ToContainer("webshop");
e.HasKey(x => x.Id);
e.HasPartitionKey(x => x.Id);
e.HasDiscriminator<string>("$type")
.HasValue<Customer>("Customer")
.HasValue<Order>("Order")
.HasValue<Product>("Product")
.HasValue<LineItem>("LineItem");
});

b.Entity<Order>(e => e.Ignore(o => o.Customer));

b.Entity<LineItem>(e =>
{
e.Ignore(i => i.Order);
e.Ignore(i => i.Product);
Comment on lines +31 to +36
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

WebshopCosmosDbContext only ignores the reference navigations (Order.Customer, LineItem.Order, LineItem.Product), but the collection navigations (Customer.Orders, Order.Items) are still present. EF Core conventions can still create relationships from those collections (and the FK properties), which undermines the intent to remove cross-document navigations for Cosmos. Consider also ignoring Customer.Orders and Order.Items (and any other back-navs) in this Cosmos-specific model to ensure no relationships are configured.

Copilot uses AI. Check for mistakes.
e.Property(i => i.UnitPrice).HasPrecision(18, 2);
});

b.Entity<Product>(e => e.Property(p => p.ListPrice).HasPrecision(18, 2));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,11 @@ public static IExpressiveQueryable<T> DefaultIfEmpty<T>(
=> Queryable.Index(source).AsExpressive();
#endif

[EditorBrowsable(EditorBrowsableState.Never)]
public static IExpressiveQueryable<TResult> OfType<T, TResult>(
this IExpressiveQueryable<T> source)
=> Queryable.OfType<TResult>(source).AsExpressive();
Comment on lines +402 to +405
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

A new passthrough OfType stub was added to preserve IExpressiveQueryable<> chaining, but there’s no corresponding unit test alongside the existing chain-continuity tests in ExpressiveQueryableLinqExtensionsTests. Adding a test that source.OfType<...>() returns an IExpressiveQueryable<TResult> would help prevent regressions.

Copilot uses AI. Check for mistakes.

// ── Non-lambda-first intercepted methods ─────────────────────────────

[EditorBrowsable(EditorBrowsableState.Never)]
Expand Down
Loading