diff --git a/readme.md b/readme.md
index 4e616db..c3836b4 100644
--- a/readme.md
+++ b/readme.md
@@ -77,7 +77,7 @@ builder.UseSqlServer(connection);
builder.EnableRecording();
var data = new SampleDbContext(builder.Options);
```
-snippet source | anchor
+snippet source | anchor
`EnableRecording` should only be called in the test context.
@@ -106,7 +106,7 @@ await data
await Verify();
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file:
@@ -157,7 +157,7 @@ await Verify(
entries
});
```
-snippet source | anchor
+snippet source | anchor
@@ -189,7 +189,7 @@ await data2
await Verify();
```
-snippet source | anchor
+snippet source | anchor
@@ -251,7 +251,7 @@ await data
await Verify();
```
-snippet source | anchor
+snippet source | anchor
@@ -298,7 +298,7 @@ public async Task Added()
await Verify(data.ChangeTracker);
}
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file:
@@ -343,7 +343,7 @@ public async Task Deleted()
await Verify(data.ChangeTracker);
}
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file:
@@ -388,7 +388,7 @@ public async Task Modified()
await Verify(data.ChangeTracker);
}
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file:
@@ -423,7 +423,7 @@ var queryable = data.Companies
.Where(_ => _.Content == "value");
await Verify(queryable);
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file:
@@ -481,7 +481,7 @@ await Verify(data.AllData())
serializer =>
serializer.TypeNameHandling = TypeNameHandling.Objects);
```
-snippet source | anchor
+snippet source | anchor
Will result in the following verified file with all data in the database:
@@ -564,7 +564,7 @@ public async Task IgnoreNavigationProperties()
.IgnoreNavigationProperties();
}
```
-snippet source | anchor
+snippet source | anchor
@@ -577,7 +577,7 @@ var options = DbContextOptions();
using var data = new SampleDbContext(options);
VerifyEntityFramework.IgnoreNavigationProperties();
```
-snippet source | anchor
+snippet source | anchor
@@ -598,7 +598,7 @@ protected override void ConfigureWebHost(IWebHostBuilder webBuilder)
_ => dataBuilder.Options));
}
```
-snippet source | anchor
+snippet source | anchor
Then use the same identifier for recording:
@@ -614,7 +614,7 @@ var companies = await httpClient.GetFromJsonAsync("/companies");
var entries = Recording.Stop(testName);
```
-snippet source | anchor
+snippet source | anchor
The results will not be automatically included in verified file so it will have to be verified manually:
@@ -629,7 +629,7 @@ await Verify(
sql = entries
});
```
-snippet source | anchor
+snippet source | anchor
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 02cc709..b06167a 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -10,7 +10,7 @@
true
all
low
- true
+ false
true
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index b4c7902..bc41d0e 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -13,6 +13,7 @@
+
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.MissingOrderBy.verified.txt b/src/Verify.EntityFramework.Tests/CoreTests.MissingOrderBy.verified.txt
new file mode 100644
index 0000000..a1b3dfc
--- /dev/null
+++ b/src/Verify.EntityFramework.Tests/CoreTests.MissingOrderBy.verified.txt
@@ -0,0 +1,8 @@
+{
+ Type: Exception,
+ Message:
+SelectExpression must have at least one ordering.
+Expression:
+SELECT c.Id, c.Content
+FROM Companies AS c
+}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.NestedMissingOrderBy.verified.txt b/src/Verify.EntityFramework.Tests/CoreTests.NestedMissingOrderBy.verified.txt
new file mode 100644
index 0000000..ac169be
--- /dev/null
+++ b/src/Verify.EntityFramework.Tests/CoreTests.NestedMissingOrderBy.verified.txt
@@ -0,0 +1,7 @@
+{
+ Type: Exception,
+ Message:
+TableExpression must have at least one ordering.
+Expression:
+Employees AS e
+}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.WithNestedOrderBy.verified.txt b/src/Verify.EntityFramework.Tests/CoreTests.WithNestedOrderBy.verified.txt
new file mode 100644
index 0000000..53a55ea
--- /dev/null
+++ b/src/Verify.EntityFramework.Tests/CoreTests.WithNestedOrderBy.verified.txt
@@ -0,0 +1,40 @@
+[
+ {
+ Id: 1,
+ Content: Company1,
+ Employees: [
+ {
+ Id: 2,
+ CompanyId: 1,
+ Content: Employee1,
+ Age: 25
+ },
+ {
+ Id: 3,
+ CompanyId: 1,
+ Content: Employee2,
+ Age: 31
+ }
+ ]
+ },
+ {
+ Id: 4,
+ Content: Company2,
+ Employees: [
+ {
+ Id: 5,
+ CompanyId: 4,
+ Content: Employee4,
+ Age: 34
+ }
+ ]
+ },
+ {
+ Id: 6,
+ Content: Company3
+ },
+ {
+ Id: 7,
+ Content: Company4
+ }
+]
\ No newline at end of file
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.WithOrderBy.verified.txt b/src/Verify.EntityFramework.Tests/CoreTests.WithOrderBy.verified.txt
new file mode 100644
index 0000000..f0dca98
--- /dev/null
+++ b/src/Verify.EntityFramework.Tests/CoreTests.WithOrderBy.verified.txt
@@ -0,0 +1,18 @@
+[
+ {
+ Id: 1,
+ Content: Company1
+ },
+ {
+ Id: 4,
+ Content: Company2
+ },
+ {
+ Id: 6,
+ Content: Company3
+ },
+ {
+ Id: 7,
+ Content: Company4
+ }
+]
\ No newline at end of file
diff --git a/src/Verify.EntityFramework.Tests/CoreTests.cs b/src/Verify.EntityFramework.Tests/CoreTests.cs
index d6af7eb..85ad605 100644
--- a/src/Verify.EntityFramework.Tests/CoreTests.cs
+++ b/src/Verify.EntityFramework.Tests/CoreTests.cs
@@ -2,6 +2,53 @@
[Parallelizable(ParallelScope.All)]
public class CoreTests
{
+ [Test]
+ public async Task MissingOrderBy()
+ {
+ await using var database = await DbContextBuilder.GetOrderRequiredDatabase();
+ var data = database.Context;
+ await ThrowsTask(
+ () => data.Companies
+ .ToListAsync())
+ .IgnoreStackTrace();
+ }
+
+ [Test]
+ public async Task NestedMissingOrderBy()
+ {
+ await using var database = await DbContextBuilder.GetOrderRequiredDatabase();
+ var data = database.Context;
+ await ThrowsTask(
+ () => data.Companies
+ .Include(_ => _.Employees)
+ .OrderBy(_ => _.Content)
+ .ToListAsync())
+ .IgnoreStackTrace();
+ }
+
+ [Test]
+ public async Task WithOrderBy()
+ {
+ await using var database = await DbContextBuilder.GetOrderRequiredDatabase();
+ var data = database.Context;
+ await Verify(
+ data.Companies
+ .OrderBy(_ => _.Content)
+ .ToListAsync());
+ }
+
+ [Test]
+ public async Task WithNestedOrderBy()
+ {
+ await using var database = await DbContextBuilder.GetOrderRequiredDatabase();
+ var data = database.Context;
+ await Verify(
+ data.Companies
+ .Include(_ => _.Employees.OrderBy(_ => _.Age))
+ .OrderBy(_ => _.Content)
+ .ToListAsync());
+ }
+
#region Added
[Test]
@@ -232,7 +279,7 @@ public class CoreTests
[Test]
public async Task AllData()
{
- var database = await DbContextBuilder.GetDatabase("AllData");
+ var database = await DbContextBuilder.GetDatabase();
var data = database.Context;
#region AllData
@@ -248,7 +295,7 @@ public class CoreTests
[Test]
public async Task Queryable()
{
- var database = await DbContextBuilder.GetDatabase("Queryable");
+ var database = await DbContextBuilder.GetDatabase();
await database.AddData(
new Company
{
@@ -268,7 +315,7 @@ public class CoreTests
[Test]
public async Task SetSelect()
{
- var database = await DbContextBuilder.GetDatabase("SetSelect");
+ var database = await DbContextBuilder.GetDatabase();
var data = database.Context;
var query = data
@@ -280,7 +327,7 @@ public class CoreTests
[Test]
public async Task NestedQueryable()
{
- var database = await DbContextBuilder.GetDatabase("NestedQueryable");
+ var database = await DbContextBuilder.GetDatabase();
await database.AddData(
new Company
{
@@ -312,7 +359,7 @@ public class CoreTests
[Test]
public async Task Parameters()
{
- var database = await DbContextBuilder.GetDatabase("Parameters");
+ var database = await DbContextBuilder.GetDatabase();
var data = database.Context;
data.Add(
new Company
@@ -330,7 +377,7 @@ public class CoreTests
[Test]
public async Task MultiRecording()
{
- var database = await DbContextBuilder.GetDatabase("MultiRecording");
+ var database = await DbContextBuilder.GetDatabase();
var data = database.Context;
Recording.Start();
var company = new Company
@@ -363,7 +410,7 @@ public class CoreTests
[Test]
public async Task MultiDbContexts()
{
- var database = await DbContextBuilder.GetDatabase("MultiDbContexts");
+ var database = await DbContextBuilder.GetDatabase();
var connectionString = database.ConnectionString;
#region MultiDbContexts
@@ -395,7 +442,7 @@ public class CoreTests
[Test]
public async Task RecordingTest()
{
- var database = await DbContextBuilder.GetDatabase("Recording");
+ var database = await DbContextBuilder.GetDatabase();
var data = database.Context;
#region Recording
@@ -422,7 +469,7 @@ public class CoreTests
[Test]
public async Task RecordingDisabledTest()
{
- var database = await DbContextBuilder.GetDatabase("RecordingDisabledTest");
+ var database = await DbContextBuilder.GetDatabase();
var data = database.Context;
#region RecordingDisableForInstance
@@ -551,7 +598,7 @@ public class CoreTests
[Test]
public async Task RecordingSpecific()
{
- var database = await DbContextBuilder.GetDatabase("RecordingSpecific");
+ var database = await DbContextBuilder.GetDatabase();
var data = database.Context;
#region RecordingSpecific
diff --git a/src/Verify.EntityFramework.Tests/Snippets/DbContextBuilder.cs b/src/Verify.EntityFramework.Tests/Snippets/DbContextBuilder.cs
index 9c00050..caea6be 100644
--- a/src/Verify.EntityFramework.Tests/Snippets/DbContextBuilder.cs
+++ b/src/Verify.EntityFramework.Tests/Snippets/DbContextBuilder.cs
@@ -3,7 +3,8 @@
public static class DbContextBuilder
{
- static DbContextBuilder() =>
+ static DbContextBuilder()
+ {
sqlInstance = new(
buildTemplate: CreateDb,
constructInstance: builder =>
@@ -11,8 +12,19 @@ public static class DbContextBuilder
builder.EnableRecording();
return new(builder.Options);
});
+ orderRequiredSqlInstance = new(
+ buildTemplate: CreateDb,
+ storage: Storage.FromSuffix("ThrowForMissingOrderBy"),
+ constructInstance: builder =>
+ {
+ builder.EnableRecording();
+ builder.ThrowForMissingOrderBy();
+ return new(builder.Options);
+ });
+ }
static SqlInstance sqlInstance;
+ static SqlInstance orderRequiredSqlInstance;
static async Task CreateDb(SampleDbContext data)
{
@@ -63,6 +75,9 @@ public static class DbContextBuilder
await data.SaveChangesAsync();
}
- public static Task> GetDatabase(string suffix)
+ public static Task> GetDatabase([CallerMemberName] string suffix = "")
=> sqlInstance.Build(suffix);
+
+ public static Task> GetOrderRequiredDatabase([CallerMemberName] string suffix = "")
+ => orderRequiredSqlInstance.Build(suffix);
}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework/GlobalUsings.cs b/src/Verify.EntityFramework/GlobalUsings.cs
index eebd66a..8ef0c1a 100644
--- a/src/Verify.EntityFramework/GlobalUsings.cs
+++ b/src/Verify.EntityFramework/GlobalUsings.cs
@@ -1,10 +1,13 @@
global using System.Data;
global using System.Data.Common;
global using System.Diagnostics.CodeAnalysis;
+global using System.Linq.Expressions;
global using Argon;
global using Microsoft.EntityFrameworkCore;
global using Microsoft.EntityFrameworkCore.ChangeTracking;
global using Microsoft.EntityFrameworkCore.Diagnostics;
global using Microsoft.EntityFrameworkCore.Metadata;
+global using Microsoft.EntityFrameworkCore.Query;
global using Microsoft.EntityFrameworkCore.Query.Internal;
+global using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
global using VerifyTests.EntityFramework;
\ No newline at end of file
diff --git a/src/Verify.EntityFramework/MissingOrder/MissingOrderByVisitor.cs b/src/Verify.EntityFramework/MissingOrder/MissingOrderByVisitor.cs
new file mode 100644
index 0000000..e76f9c0
--- /dev/null
+++ b/src/Verify.EntityFramework/MissingOrder/MissingOrderByVisitor.cs
@@ -0,0 +1,88 @@
+sealed class MissingOrderByVisitor : ExpressionVisitor
+{
+ List orderedExpressions = [];
+
+ [return: NotNullIfNotNull(nameof(expression))]
+ public override Expression? Visit(Expression? expression)
+ {
+ if (expression is null)
+ {
+ return null;
+ }
+
+ switch (expression)
+ {
+ case ShapedQueryExpression shapedQueryExpression:
+ Visit(shapedQueryExpression.QueryExpression);
+ return shapedQueryExpression;
+
+ case RelationalSplitCollectionShaperExpression splitExpression:
+ foreach (var table in splitExpression.SelectExpression.Tables)
+ {
+ Visit(table);
+ }
+
+ Visit(splitExpression.InnerShaper);
+
+ return splitExpression;
+
+ case TableExpression tableExpression:
+ {
+ foreach (var orderedExpression in orderedExpressions)
+ {
+ if (orderedExpression.Expression is ColumnExpression columnExpression)
+ {
+ if (columnExpression.Table == tableExpression)
+ {
+ return base.Visit(expression);
+ }
+
+ if (columnExpression.Table is PredicateJoinExpressionBase joinExpression)
+ {
+ if (joinExpression.Table == tableExpression)
+ {
+ return base.Visit(expression);
+ }
+ }
+ }
+ }
+
+ throw new(
+ $"""
+ TableExpression must have at least one ordering.
+ Expression:
+ {ExpressionPrinter.Print(tableExpression)}
+ """);
+ }
+ case SelectExpression selectExpression:
+ {
+ var orderings = selectExpression.Orderings;
+ if (orderings.Count == 0)
+ {
+ throw new(
+ $"""
+ SelectExpression must have at least one ordering.
+ Expression:
+ {PrintShortSql(selectExpression)}
+ """);
+ }
+
+ foreach (var ordering in orderings)
+ {
+ orderedExpressions.Add(ordering);
+ }
+
+ return base.Visit(expression);
+ }
+
+ case NonQueryExpression nonQueryExpression:
+ return nonQueryExpression;
+
+ default:
+ return base.Visit(expression);
+ }
+ }
+
+ [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "PrintShortSql")]
+ static extern string PrintShortSql(SelectExpression expression);
+}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework/MissingOrder/RelationalFactory.cs b/src/Verify.EntityFramework/MissingOrder/RelationalFactory.cs
new file mode 100644
index 0000000..e383665
--- /dev/null
+++ b/src/Verify.EntityFramework/MissingOrder/RelationalFactory.cs
@@ -0,0 +1,13 @@
+class RelationalFactory : RelationalShapedQueryCompilingExpressionVisitorFactory
+{
+ public RelationalFactory(ShapedQueryCompilingExpressionVisitorDependencies dependencies, RelationalShapedQueryCompilingExpressionVisitorDependencies relationalDependencies) :
+ base(dependencies, relationalDependencies)
+ {
+ }
+
+ public override ShapedQueryCompilingExpressionVisitor Create(QueryCompilationContext context)
+ => new RelationalVisitor(
+ Dependencies,
+ RelationalDependencies,
+ context);
+}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework/MissingOrder/RelationalVisitor.cs b/src/Verify.EntityFramework/MissingOrder/RelationalVisitor.cs
new file mode 100644
index 0000000..c8f6304
--- /dev/null
+++ b/src/Verify.EntityFramework/MissingOrder/RelationalVisitor.cs
@@ -0,0 +1,15 @@
+class RelationalVisitor :
+ RelationalShapedQueryCompilingExpressionVisitor
+{
+ public RelationalVisitor(ShapedQueryCompilingExpressionVisitorDependencies dependencies, RelationalShapedQueryCompilingExpressionVisitorDependencies relationalDependencies, QueryCompilationContext context) :
+ base(dependencies, relationalDependencies, context)
+ {
+ }
+
+ [return: NotNullIfNotNull("node")]
+ public override Expression? Visit(Expression? node)
+ {
+ new MissingOrderByVisitor().Visit(node);
+ return base.Visit(node);
+ }
+}
\ No newline at end of file
diff --git a/src/Verify.EntityFramework/Verify.EntityFramework.csproj b/src/Verify.EntityFramework/Verify.EntityFramework.csproj
index d37d08d..a245f41 100644
--- a/src/Verify.EntityFramework/Verify.EntityFramework.csproj
+++ b/src/Verify.EntityFramework/Verify.EntityFramework.csproj
@@ -4,6 +4,9 @@
+
+
+
diff --git a/src/Verify.EntityFramework/VerifyEntityFramework.cs b/src/Verify.EntityFramework/VerifyEntityFramework.cs
index 0b7b56a..ab9e771 100644
--- a/src/Verify.EntityFramework/VerifyEntityFramework.cs
+++ b/src/Verify.EntityFramework/VerifyEntityFramework.cs
@@ -131,10 +131,18 @@ public static class VerifyEntityFramework
return new(result, [new("sql", sql)]);
}
+ public static DbContextOptionsBuilder ThrowForMissingOrderBy(this DbContextOptionsBuilder builder)
+ where TContext : DbContext =>
+ builder.ReplaceService();
+
public static DbContextOptionsBuilder EnableRecording(this DbContextOptionsBuilder builder)
where TContext : DbContext
=> builder.EnableRecording(null);
+ public static DbContextOptionsBuilder EnableRecording(this DbContextOptionsBuilder builder, string? identifier)
+ where TContext : DbContext =>
+ builder.AddInterceptors(new LogCommandInterceptor(identifier));
+
static ConcurrentBag recordingDisabledContextIds = [];
public static void DisableRecording(this TContext context)
@@ -144,8 +152,4 @@ public static class VerifyEntityFramework
internal static bool IsRecordingDisabled(this TContext context)
where TContext : DbContext =>
recordingDisabledContextIds.Contains(context.ContextId.InstanceId);
-
- public static DbContextOptionsBuilder EnableRecording(this DbContextOptionsBuilder builder, string? identifier)
- where TContext : DbContext =>
- builder.AddInterceptors(new LogCommandInterceptor(identifier));
}
\ No newline at end of file