diff --git a/src/Docs/PlaygroundModel/Scenarios/Webshop/Customer.cs b/src/Docs/PlaygroundModel/Scenarios/Webshop/Customer.cs index 0db60b0..6920331 100644 --- a/src/Docs/PlaygroundModel/Scenarios/Webshop/Customer.cs +++ b/src/Docs/PlaygroundModel/Scenarios/Webshop/Customer.cs @@ -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; } diff --git a/src/Docs/PlaygroundModel/Scenarios/Webshop/LineItem.cs b/src/Docs/PlaygroundModel/Scenarios/Webshop/LineItem.cs index a7525f5..fb98af2 100644 --- a/src/Docs/PlaygroundModel/Scenarios/Webshop/LineItem.cs +++ b/src/Docs/PlaygroundModel/Scenarios/Webshop/LineItem.cs @@ -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; } diff --git a/src/Docs/PlaygroundModel/Scenarios/Webshop/Order.cs b/src/Docs/PlaygroundModel/Scenarios/Webshop/Order.cs index 2df5f82..91673ab 100644 --- a/src/Docs/PlaygroundModel/Scenarios/Webshop/Order.cs +++ b/src/Docs/PlaygroundModel/Scenarios/Webshop/Order.cs @@ -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; } diff --git a/src/Docs/PlaygroundModel/Scenarios/Webshop/Product.cs b/src/Docs/PlaygroundModel/Scenarios/Webshop/Product.cs index ca44c34..9d7a52c 100644 --- a/src/Docs/PlaygroundModel/Scenarios/Webshop/Product.cs +++ b/src/Docs/PlaygroundModel/Scenarios/Webshop/Product.cs @@ -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; } diff --git a/src/Docs/PlaygroundModel/Scenarios/Webshop/WebshopEntity.cs b/src/Docs/PlaygroundModel/Scenarios/Webshop/WebshopEntity.cs new file mode 100644 index 0000000..7538502 --- /dev/null +++ b/src/Docs/PlaygroundModel/Scenarios/Webshop/WebshopEntity.cs @@ -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.OfType()`. `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; } +} diff --git a/src/Docs/Prerenderer/SampleRenderer.cs b/src/Docs/Prerenderer/SampleRenderer.cs index 16c349f..0b4b65d 100644 --- a/src/Docs/Prerenderer/SampleRenderer.cs +++ b/src/Docs/Prerenderer/SampleRenderer.cs @@ -37,7 +37,7 @@ private static WebshopDbContext BuildSqlServerContext() => .EnableServiceProviderCaching(false) .Options); - private static WebshopDbContext BuildCosmosContext() => + private static WebshopCosmosDbContext BuildCosmosContext() => new(new DbContextOptionsBuilder() .UseCosmos("AccountEndpoint=https://localhost:8081/;AccountKey=dW5pdHRlc3Q=", "playground") .UseExpressives() @@ -64,6 +64,19 @@ private sealed class DbContextRoots : IWebshopQueryRoots public IExpressiveQueryable LineItems => _ctx.LineItems; } + // Cosmos roots: every entity lives in the same container and is filtered + // via OfType() on the single DbSet, 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 Customers => _ctx.Entities.AsExpressive().OfType(); + public IExpressiveQueryable Orders => _ctx.Entities.AsExpressive().OfType(); + public IExpressiveQueryable Products => _ctx.Entities.AsExpressive().OfType(); + public IExpressiveQueryable LineItems => _ctx.Entities.AsExpressive().OfType(); + } + private sealed class MongoRootsImpl : IWebshopQueryRoots { public MongoRootsImpl( @@ -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()!)); } diff --git a/src/Docs/Prerenderer/WebshopCosmosDbContext.cs b/src/Docs/Prerenderer/WebshopCosmosDbContext.cs new file mode 100644 index 0000000..e682c05 --- /dev/null +++ b/src/Docs/Prerenderer/WebshopCosmosDbContext.cs @@ -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.OfType()`, 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 options) : base(options) { } + + public DbSet Entities => Set(); + + protected override void OnModelCreating(ModelBuilder b) + { + b.Entity(e => + { + e.ToContainer("webshop"); + e.HasKey(x => x.Id); + e.HasPartitionKey(x => x.Id); + e.HasDiscriminator("$type") + .HasValue("Customer") + .HasValue("Order") + .HasValue("Product") + .HasValue("LineItem"); + }); + + b.Entity(e => e.Ignore(o => o.Customer)); + + b.Entity(e => + { + e.Ignore(i => i.Order); + e.Ignore(i => i.Product); + e.Property(i => i.UnitPrice).HasPrecision(18, 2); + }); + + b.Entity(e => e.Property(p => p.ListPrice).HasPrecision(18, 2)); + } +} diff --git a/src/ExpressiveSharp/Extensions/ExpressiveQueryableLinqExtensions.cs b/src/ExpressiveSharp/Extensions/ExpressiveQueryableLinqExtensions.cs index be46a54..8a838d2 100644 --- a/src/ExpressiveSharp/Extensions/ExpressiveQueryableLinqExtensions.cs +++ b/src/ExpressiveSharp/Extensions/ExpressiveQueryableLinqExtensions.cs @@ -399,6 +399,11 @@ public static IExpressiveQueryable DefaultIfEmpty( => Queryable.Index(source).AsExpressive(); #endif + [EditorBrowsable(EditorBrowsableState.Never)] + public static IExpressiveQueryable OfType( + this IExpressiveQueryable source) + => Queryable.OfType(source).AsExpressive(); + // ── Non-lambda-first intercepted methods ───────────────────────────── [EditorBrowsable(EditorBrowsableState.Never)]