Skip to content

Commit 7a9e601

Browse files
committed
Add reversed order context creation methods and tests for UseExpressives
1 parent ace68aa commit 7a9e601

4 files changed

Lines changed: 169 additions & 15 deletions

File tree

src/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions/RelationalExpressivePlugin.cs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Infrastructure.Internal;
44
using ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Transformers;
55
using Microsoft.EntityFrameworkCore.Query;
6+
using Microsoft.EntityFrameworkCore.Query.Internal;
67
using Microsoft.Extensions.DependencyInjection;
78
using Microsoft.Extensions.DependencyInjection.Extensions;
89

@@ -37,21 +38,37 @@ public void ApplyServices(IServiceCollection services)
3738
public IExpressionTreeTransformer[] GetTransformers() =>
3839
[new RewriteIndexedSelectToRowNumber()];
3940

41+
[SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Required to decorate EF Core services")]
4042
private static void DecorateParameterBasedSqlProcessorFactory(IServiceCollection services)
4143
{
4244
var targetDescriptor = services.FirstOrDefault(
4345
x => x.ServiceType == typeof(IRelationalParameterBasedSqlProcessorFactory));
44-
if (targetDescriptor is null) return;
4546

46-
services.Replace(ServiceDescriptor.Describe(
47-
typeof(IRelationalParameterBasedSqlProcessorFactory),
48-
sp =>
47+
if (targetDescriptor is not null)
48+
{
49+
// Provider registered first — decorate immediately
50+
services.Replace(ServiceDescriptor.Describe(
51+
typeof(IRelationalParameterBasedSqlProcessorFactory),
52+
sp =>
53+
{
54+
var inner = CreateTargetInstance<IRelationalParameterBasedSqlProcessorFactory>(sp, targetDescriptor);
55+
var dependencies = sp.GetRequiredService<RelationalParameterBasedSqlProcessorDependencies>();
56+
return new WindowFunctionParameterBasedSqlProcessorFactory(inner, dependencies);
57+
},
58+
targetDescriptor.Lifetime));
59+
}
60+
else
61+
{
62+
// UseExpressives() called before provider — deferred decoration.
63+
// Pre-register so the provider's TryAdd becomes a no-op.
64+
// At resolution time, create the default factory and wrap it.
65+
services.AddScoped<IRelationalParameterBasedSqlProcessorFactory>(sp =>
4966
{
50-
var inner = CreateTargetInstance<IRelationalParameterBasedSqlProcessorFactory>(sp, targetDescriptor);
67+
var inner = ActivatorUtilities.CreateInstance<RelationalParameterBasedSqlProcessorFactory>(sp);
5168
var dependencies = sp.GetRequiredService<RelationalParameterBasedSqlProcessorDependencies>();
5269
return new WindowFunctionParameterBasedSqlProcessorFactory(inner, dependencies);
53-
},
54-
targetDescriptor.Lifetime));
70+
});
71+
}
5572
}
5673

5774
private static T CreateTargetInstance<T>(IServiceProvider services, ServiceDescriptor descriptor)

src/ExpressiveSharp.EntityFrameworkCore/Infrastructure/Internal/ExpressiveOptionsExtension.cs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,27 @@ public void ApplyServices(IServiceCollection services)
4545
services.AddScoped<IConventionSetPlugin, ExpressiveExpandQueryFiltersConventionPlugin>();
4646

4747
// Decorate IQueryCompiler with ExpressiveQueryCompiler
48-
var targetDescriptor = services.FirstOrDefault(x => x.ServiceType == typeof(IQueryCompiler))
49-
?? throw new InvalidOperationException(
50-
"No QueryCompiler is configured. Ensure a database provider is configured before calling UseExpressives().");
48+
var targetDescriptor = services.FirstOrDefault(x => x.ServiceType == typeof(IQueryCompiler));
5149

5250
var decoratorFactory = ActivatorUtilities.CreateFactory(
53-
typeof(ExpressiveQueryCompiler), [targetDescriptor.ServiceType]);
51+
typeof(ExpressiveQueryCompiler), [typeof(IQueryCompiler)]);
5452

55-
services.Replace(ServiceDescriptor.Describe(
56-
targetDescriptor.ServiceType,
57-
serviceProvider => decoratorFactory(serviceProvider, [CreateTargetInstance(serviceProvider, targetDescriptor)]),
58-
targetDescriptor.Lifetime));
53+
if (targetDescriptor is not null)
54+
{
55+
// Provider registered first (e.g. UseSqlite().UseExpressives()) — decorate immediately
56+
services.Replace(ServiceDescriptor.Describe(
57+
targetDescriptor.ServiceType,
58+
serviceProvider => decoratorFactory(serviceProvider, [CreateTargetInstance(serviceProvider, targetDescriptor)]),
59+
targetDescriptor.Lifetime));
60+
}
61+
else
62+
{
63+
// UseExpressives() called before provider (e.g. AddSqlite optionsAction) — deferred decoration.
64+
// Pre-register IQueryCompiler so the provider's TryAdd becomes a no-op.
65+
// At resolution time, all provider services (IDatabase, IModel, etc.) are available.
66+
services.AddScoped<IQueryCompiler>(sp =>
67+
(IQueryCompiler)decoratorFactory(sp, [ActivatorUtilities.CreateInstance<QueryCompiler>(sp)]));
68+
}
5969

6070
// Apply plugin services
6171
foreach (var plugin in _plugins)

tests/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests/WindowFunctionIntegrationTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ private WindowTestDbContext CreateContext()
3838
return ctx;
3939
}
4040

41+
private WindowTestDbContext CreateContextReversedOrder()
42+
{
43+
// UseExpressives() before UseSqlite() — simulates AddSqlite optionsAction ordering
44+
var options = new DbContextOptionsBuilder<WindowTestDbContext>()
45+
.UseExpressives(o => o.UseRelationalExtensions())
46+
.UseSqlite(_connection)
47+
.Options;
48+
var ctx = new WindowTestDbContext(options);
49+
ctx.Database.EnsureCreated();
50+
return ctx;
51+
}
52+
4153
private static void SeedTestData(WindowTestDbContext ctx)
4254
{
4355
ctx.Customers.AddRange(
@@ -229,4 +241,30 @@ public async Task IndexedSelect_ReturnsZeroBasedIndices()
229241
var positions = results.Select(r => r.Position).OrderBy(p => p).ToList();
230242
CollectionAssert.AreEqual(new[] { 0, 1, 2 }, positions);
231243
}
244+
245+
// ── Reversed ordering tests (UseExpressives before UseSqlite) ─────
246+
247+
[TestMethod]
248+
public async Task RowNumber_BeforeProvider_ReturnsCorrectSequentialNumbers()
249+
{
250+
using var ctx = CreateContextReversedOrder();
251+
SeedTestData(ctx);
252+
253+
var results = await ctx.Orders
254+
.Select(o => new
255+
{
256+
o.Id,
257+
o.Price,
258+
RowNum = WindowFunction.RowNumber(Window.OrderBy(o.Price))
259+
})
260+
.OrderBy(x => x.RowNum)
261+
.ToListAsync();
262+
263+
Assert.AreEqual(10, results.Count);
264+
for (var i = 0; i < results.Count; i++)
265+
{
266+
Assert.AreEqual(i + 1, results[i].RowNum,
267+
$"Expected RowNum {i + 1} at index {i}, got {results[i].RowNum}");
268+
}
269+
}
232270
}

tests/ExpressiveSharp.EntityFrameworkCore.Tests/UseExpressivesTests.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ private TestDbContext CreateContext()
3434
return ctx;
3535
}
3636

37+
private TestDbContext CreateContextReversedOrder()
38+
{
39+
// UseExpressives() before UseSqlite() — simulates AddSqlite optionsAction ordering
40+
var options = new DbContextOptionsBuilder<TestDbContext>()
41+
.UseExpressives()
42+
.UseSqlite(_connection)
43+
.Options;
44+
var ctx = new TestDbContext(options);
45+
ctx.Database.EnsureCreated();
46+
return ctx;
47+
}
48+
3749
private TestDbContextWithQueryFilter CreateContextWithQueryFilter()
3850
{
3951
var options = new DbContextOptionsBuilder<TestDbContextWithQueryFilter>()
@@ -45,6 +57,17 @@ private TestDbContextWithQueryFilter CreateContextWithQueryFilter()
4557
return ctx;
4658
}
4759

60+
private TestDbContextWithQueryFilter CreateContextWithQueryFilterReversedOrder()
61+
{
62+
var options = new DbContextOptionsBuilder<TestDbContextWithQueryFilter>()
63+
.UseExpressives()
64+
.UseSqlite(_connection)
65+
.Options;
66+
var ctx = new TestDbContextWithQueryFilter(options);
67+
ctx.Database.EnsureCreated();
68+
return ctx;
69+
}
70+
4871
[TestMethod]
4972
public void UseExpressives_MarksExpressivePropertiesAsUnmapped()
5073
{
@@ -161,4 +184,70 @@ public void ExpressiveDbSet_GroupByWithNullConditional()
161184
Assert.IsTrue(sql.Contains("Name", StringComparison.OrdinalIgnoreCase),
162185
$"Expected 'Name' in SQL, got: {sql}");
163186
}
187+
188+
// ── Reversed ordering tests (UseExpressives before UseSqlite) ─────
189+
190+
[TestMethod]
191+
public void UseExpressives_BeforeProvider_MarksExpressivePropertiesAsUnmapped()
192+
{
193+
using var ctx = CreateContextReversedOrder();
194+
var model = ctx.Model;
195+
var orderEntity = model.FindEntityType(typeof(Order))!;
196+
197+
Assert.IsNull(orderEntity.FindProperty(nameof(Order.Total)));
198+
Assert.IsNull(orderEntity.FindProperty(nameof(Order.CustomerName)));
199+
Assert.IsNotNull(orderEntity.FindProperty(nameof(Order.Price)));
200+
Assert.IsNotNull(orderEntity.FindProperty(nameof(Order.Quantity)));
201+
}
202+
203+
[TestMethod]
204+
public void UseExpressives_BeforeProvider_ExpandsExpressiveProperties()
205+
{
206+
using var ctx = CreateContextReversedOrder();
207+
208+
var query = ctx.Set<Order>()
209+
.Where(o => o.Customer != null)
210+
.Select(o => o.Total);
211+
var sql = query.ToQueryString();
212+
213+
Assert.IsTrue(sql.Contains("*"),
214+
$"Expected multiplication in SQL, got: {sql}");
215+
}
216+
217+
[TestMethod]
218+
public void UseExpressives_BeforeProvider_AppliesNullConditionalTransformer()
219+
{
220+
using var ctx = CreateContextReversedOrder();
221+
222+
var query = ctx.Set<Order>().Select(o => o.CustomerName);
223+
var sql = query.ToQueryString();
224+
225+
Assert.IsNotNull(sql);
226+
}
227+
228+
[TestMethod]
229+
public void UseExpressives_BeforeProvider_AppliesFlattenBlockTransformer()
230+
{
231+
using var ctx = CreateContextReversedOrder();
232+
233+
var query = ctx.Set<Order>().Select(o => o.GetCategory());
234+
var sql = query.ToQueryString();
235+
236+
Assert.IsTrue(
237+
sql.Contains("WHEN", StringComparison.OrdinalIgnoreCase) ||
238+
sql.Contains("IIF", StringComparison.OrdinalIgnoreCase) ||
239+
sql.Contains("CASE", StringComparison.OrdinalIgnoreCase),
240+
$"Expected conditional in SQL, got: {sql}");
241+
}
242+
243+
[TestMethod]
244+
public void UseExpressives_BeforeProvider_ExpandsQueryFilters()
245+
{
246+
using var ctx = CreateContextWithQueryFilterReversedOrder();
247+
248+
var query = ctx.Orders.ToQueryString();
249+
250+
Assert.IsTrue(query.Contains("*"),
251+
$"Expected multiplication in query filter SQL, got: {query}");
252+
}
164253
}

0 commit comments

Comments
 (0)