Skip to content

Commit 99510e5

Browse files
authored
Better alias (#945)
1 parent cc46f75 commit 99510e5

11 files changed

Lines changed: 164 additions & 18 deletions

claude.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Tests use `[ModuleInitializer]` to call `VerifyEntityFramework.Initialize(model)
6363
- `IgnoreNavigationProperties()` - Exclude EF navigation properties from serialization
6464
- `EnableRecording()` - Enable SQL command recording on DbContextOptionsBuilder
6565
- `DisableRecording()` - Stop recording for a specific context instance
66+
- `UseDescriptiveTableAliases()` - Replace single-char SQL table aliases with full table names via custom `ISqlAliasManagerFactory`
6667
- `ScrubInlineEfDateTimes()` - Sanitize DateTime values in SQL
6768

6869
## Testing Conventions

readme.md

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ builder.UseSqlServer(connection);
8787
builder.EnableRecording();
8888
var data = new SampleDbContext(builder.Options);
8989
```
90-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L329-L336' title='Snippet source file'>snippet source</a> | <a href='#snippet-EnableRecording' title='Start of snippet'>anchor</a></sup>
90+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L358-L365' title='Snippet source file'>snippet source</a> | <a href='#snippet-EnableRecording' title='Start of snippet'>anchor</a></sup>
9191
<!-- endSnippet -->
9292

9393
`EnableRecording` should only be called in the test context.
@@ -116,7 +116,7 @@ await data
116116

117117
await Verify();
118118
```
119-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L428-L446' title='Snippet source file'>snippet source</a> | <a href='#snippet-Recording' title='Start of snippet'>anchor</a></sup>
119+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L457-L475' title='Snippet source file'>snippet source</a> | <a href='#snippet-Recording' title='Start of snippet'>anchor</a></sup>
120120
<!-- endSnippet -->
121121

122122
Will result in the following verified file:
@@ -168,7 +168,7 @@ await Verify(
168168
entries
169169
});
170170
```
171-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L619-L644' title='Snippet source file'>snippet source</a> | <a href='#snippet-RecordingSpecific' title='Start of snippet'>anchor</a></sup>
171+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L648-L673' title='Snippet source file'>snippet source</a> | <a href='#snippet-RecordingSpecific' title='Start of snippet'>anchor</a></sup>
172172
<!-- endSnippet -->
173173

174174

@@ -200,7 +200,7 @@ await data2
200200

201201
await Verify();
202202
```
203-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L396-L419' title='Snippet source file'>snippet source</a> | <a href='#snippet-MultiDbContexts' title='Start of snippet'>anchor</a></sup>
203+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L425-L448' title='Snippet source file'>snippet source</a> | <a href='#snippet-MultiDbContexts' title='Start of snippet'>anchor</a></sup>
204204
<!-- endSnippet -->
205205

206206
<!-- snippet: CoreTests.MultiDbContexts.verified.txt -->
@@ -265,7 +265,7 @@ await data
265265

266266
await Verify();
267267
```
268-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L490-L513' title='Snippet source file'>snippet source</a> | <a href='#snippet-RecordingDisableForInstance' title='Start of snippet'>anchor</a></sup>
268+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L519-L542' title='Snippet source file'>snippet source</a> | <a href='#snippet-RecordingDisableForInstance' title='Start of snippet'>anchor</a></sup>
269269
<!-- endSnippet -->
270270

271271
<!-- snippet: CoreTests.RecordingDisabledTest.verified.txt -->
@@ -313,7 +313,7 @@ public async Task Added()
313313
await Verify(data.ChangeTracker);
314314
}
315315
```
316-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L32-L48' title='Snippet source file'>snippet source</a> | <a href='#snippet-Added' title='Start of snippet'>anchor</a></sup>
316+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L61-L77' title='Snippet source file'>snippet source</a> | <a href='#snippet-Added' title='Start of snippet'>anchor</a></sup>
317317
<!-- endSnippet -->
318318

319319
Will result in the following verified file:
@@ -358,7 +358,7 @@ public async Task Deleted()
358358
await Verify(data.ChangeTracker);
359359
}
360360
```
361-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L50-L69' title='Snippet source file'>snippet source</a> | <a href='#snippet-Deleted' title='Start of snippet'>anchor</a></sup>
361+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L79-L98' title='Snippet source file'>snippet source</a> | <a href='#snippet-Deleted' title='Start of snippet'>anchor</a></sup>
362362
<!-- endSnippet -->
363363

364364
Will result in the following verified file:
@@ -403,7 +403,7 @@ public async Task Modified()
403403
await Verify(data.ChangeTracker);
404404
}
405405
```
406-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L71-L91' title='Snippet source file'>snippet source</a> | <a href='#snippet-Modified' title='Start of snippet'>anchor</a></sup>
406+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L100-L120' title='Snippet source file'>snippet source</a> | <a href='#snippet-Modified' title='Start of snippet'>anchor</a></sup>
407407
<!-- endSnippet -->
408408

409409
Will result in the following verified file:
@@ -438,7 +438,7 @@ var queryable = data.Companies
438438
.Where(_ => _.Name == "company name");
439439
await Verify(queryable);
440440
```
441-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L286-L292' title='Snippet source file'>snippet source</a> | <a href='#snippet-Queryable' title='Start of snippet'>anchor</a></sup>
441+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L315-L321' title='Snippet source file'>snippet source</a> | <a href='#snippet-Queryable' title='Start of snippet'>anchor</a></sup>
442442
<!-- endSnippet -->
443443

444444
Will result in the following verified files:
@@ -505,7 +505,7 @@ await Verify(data.AllData())
505505
serializer =>
506506
serializer.TypeNameHandling = TypeNameHandling.Objects);
507507
```
508-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L265-L272' title='Snippet source file'>snippet source</a> | <a href='#snippet-AllData' title='Start of snippet'>anchor</a></sup>
508+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L294-L301' title='Snippet source file'>snippet source</a> | <a href='#snippet-AllData' title='Start of snippet'>anchor</a></sup>
509509
<!-- endSnippet -->
510510

511511
Will result in the following verified file with all data in the database:
@@ -588,7 +588,7 @@ public async Task IgnoreNavigationProperties()
588588
.IgnoreNavigationProperties();
589589
}
590590
```
591-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L93-L115' title='Snippet source file'>snippet source</a> | <a href='#snippet-IgnoreNavigationProperties' title='Start of snippet'>anchor</a></sup>
591+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L122-L144' title='Snippet source file'>snippet source</a> | <a href='#snippet-IgnoreNavigationProperties' title='Start of snippet'>anchor</a></sup>
592592
<!-- endSnippet -->
593593

594594

@@ -601,7 +601,7 @@ var options = DbContextOptions();
601601
using var data = new SampleDbContext(options);
602602
VerifyEntityFramework.IgnoreNavigationProperties();
603603
```
604-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L143-L149' title='Snippet source file'>snippet source</a> | <a href='#snippet-IgnoreNavigationPropertiesGlobal' title='Start of snippet'>anchor</a></sup>
604+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L172-L178' title='Snippet source file'>snippet source</a> | <a href='#snippet-IgnoreNavigationPropertiesGlobal' title='Start of snippet'>anchor</a></sup>
605605
<!-- endSnippet -->
606606

607607

@@ -622,7 +622,7 @@ protected override void ConfigureWebHost(IWebHostBuilder webBuilder)
622622
_ => dataBuilder.Options));
623623
}
624624
```
625-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L573-L585' title='Snippet source file'>snippet source</a> | <a href='#snippet-EnableRecordingWithIdentifier' title='Start of snippet'>anchor</a></sup>
625+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L602-L614' title='Snippet source file'>snippet source</a> | <a href='#snippet-EnableRecordingWithIdentifier' title='Start of snippet'>anchor</a></sup>
626626
<!-- endSnippet -->
627627

628628
Then use the same identifier for recording:
@@ -638,7 +638,7 @@ var companies = await httpClient.GetFromJsonAsync<Company[]>("/companies");
638638

639639
var entries = Recording.Stop(testName);
640640
```
641-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L546-L556' title='Snippet source file'>snippet source</a> | <a href='#snippet-RecordWithIdentifier' title='Start of snippet'>anchor</a></sup>
641+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L575-L585' title='Snippet source file'>snippet source</a> | <a href='#snippet-RecordWithIdentifier' title='Start of snippet'>anchor</a></sup>
642642
<!-- endSnippet -->
643643

644644
The results will not be automatically included in verified file so it will have to be verified manually:
@@ -653,10 +653,47 @@ await Verify(
653653
sql = entries
654654
});
655655
```
656-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L558-L567' title='Snippet source file'>snippet source</a> | <a href='#snippet-VerifyRecordedCommandsWithIdentifier' title='Start of snippet'>anchor</a></sup>
656+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L587-L596' title='Snippet source file'>snippet source</a> | <a href='#snippet-VerifyRecordedCommandsWithIdentifier' title='Start of snippet'>anchor</a></sup>
657657
<!-- endSnippet -->
658658

659659

660+
## Descriptive Table Aliases
661+
662+
By default EF generates single character table aliases in SQL (eg `c` for Companies, `e` for Employees). `UseDescriptiveTableAliases` replaces these with the full table name, making recorded and verified SQL easier to read.
663+
664+
665+
### Enable
666+
667+
Call `UseDescriptiveTableAliases()` on `DbContextOptionsBuilder`.
668+
669+
```cs
670+
var builder = new DbContextOptionsBuilder<SampleDbContext>();
671+
builder.UseSqlServer(connection);
672+
builder.UseDescriptiveTableAliases();
673+
```
674+
675+
676+
### Result
677+
678+
With descriptive aliases enabled, the generated SQL:
679+
680+
```sql
681+
SELECT [companies].[Id], [companies].[Name], [employees].[Id], [employees].[Age], [employees].[CompanyId], [employees].[Name]
682+
FROM [Companies] AS [companies]
683+
LEFT JOIN [Employees] AS [employees] ON [companies].[Id] = [employees].[CompanyId]
684+
ORDER BY [companies].[Name], [companies].[Id]
685+
```
686+
687+
Instead of the default:
688+
689+
```sql
690+
SELECT [c].[Id], [c].[Name], [e].[Id], [e].[Age], [e].[CompanyId], [e].[Name]
691+
FROM [Companies] AS [c]
692+
LEFT JOIN [Employees] AS [e] ON [c].[Id] = [e].[CompanyId]
693+
ORDER BY [c].[Name], [c].[Id]
694+
```
695+
696+
660697
## Missing OrderBy
661698

662699
To detect and correct missing `OrderBy` clauses in EF queries, use [EntityFramework.OrderBy](https://github.com/SimonCropp/EntityFramework.OrderBy).
@@ -685,7 +722,7 @@ var settings = new VerifySettings();
685722
settings.ScrubInlineEfDateTimes();
686723
await Verify(target, settings);
687724
```
688-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L10-L16' title='Snippet source file'>snippet source</a> | <a href='#snippet-ScrubInlineEfDateTimesInstance' title='Start of snippet'>anchor</a></sup>
725+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L39-L45' title='Snippet source file'>snippet source</a> | <a href='#snippet-ScrubInlineEfDateTimesInstance' title='Start of snippet'>anchor</a></sup>
689726
<!-- endSnippet -->
690727

691728

@@ -697,7 +734,7 @@ await Verify(target, settings);
697734
await Verify(target)
698735
.ScrubInlineEfDateTimes();
699736
```
700-
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L24-L29' title='Snippet source file'>snippet source</a> | <a href='#snippet-ScrubInlineEfDateTimesFluent' title='Start of snippet'>anchor</a></sup>
737+
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.cs#L53-L58' title='Snippet source file'>snippet source</a> | <a href='#snippet-ScrubInlineEfDateTimesFluent' title='Start of snippet'>anchor</a></sup>
701738
<!-- endSnippet -->
702739

703740

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
ef: {
3+
Type: ReaderExecutedAsync,
4+
HasTransaction: false,
5+
Text:
6+
select companies.Id,
7+
companies.Name,
8+
employees.Id,
9+
employees.Age,
10+
employees.CompanyId,
11+
employees.Name
12+
from Companies as companies
13+
left outer join
14+
Employees as employees
15+
on companies.Id = employees.CompanyId
16+
order by companies.Name,
17+
companies.Id
18+
}
19+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SELECT [companies].[Id], [companies].[Name]
2+
FROM [Companies] AS [companies]
3+
WHERE [companies].[Name] = N'company name'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[
2+
{
3+
Name: company name
4+
}
5+
]

src/Verify.EntityFramework.Tests/CoreTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,35 @@
22
[Parallelizable(ParallelScope.All)]
33
public class CoreTests
44
{
5+
[Test]
6+
public async Task DescriptiveTableAliasesQueryable()
7+
{
8+
var database = await DbContextBuilder.GetDescriptiveAliasDatabase();
9+
await database.AddData(
10+
new Company
11+
{
12+
Name = "company name"
13+
});
14+
var data = database.Context;
15+
16+
var queryable = data.Companies
17+
.Where(_ => _.Name == "company name");
18+
await Verify(queryable);
19+
}
20+
21+
[Test]
22+
public async Task DescriptiveTableAliases()
23+
{
24+
await using var database = await DbContextBuilder.GetDescriptiveAliasDatabase();
25+
var data = database.Context;
26+
Recording.Start();
27+
await data.Companies
28+
.Include(_ => _.Employees)
29+
.OrderBy(_ => _.Name)
30+
.ToListAsync();
31+
await Verify();
32+
}
33+
534
[Test]
635
public async Task ScrubInlineEfDateTimes()
736
{

src/Verify.EntityFramework.Tests/Snippets/DbContextBuilder.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,28 @@
33

44
public static class DbContextBuilder
55
{
6-
static DbContextBuilder() =>
6+
static DbContextBuilder()
7+
{
78
sqlInstance = new(
89
buildTemplate: CreateDb,
910
constructInstance: builder =>
1011
{
1112
builder.EnableRecording();
1213
return new(builder.Options);
1314
});
15+
descriptiveAliasSqlInstance = new(
16+
buildTemplate: CreateDb,
17+
storage: Storage.FromSuffix<SampleDbContext>("DescriptiveTableAliases"),
18+
constructInstance: builder =>
19+
{
20+
builder.EnableRecording();
21+
builder.UseDescriptiveTableAliases();
22+
return new(builder.Options);
23+
});
24+
}
1425

1526
static SqlInstance<SampleDbContext> sqlInstance;
27+
static SqlInstance<SampleDbContext> descriptiveAliasSqlInstance;
1628

1729
static async Task CreateDb(SampleDbContext data)
1830
{
@@ -65,4 +77,7 @@ static async Task CreateDb(SampleDbContext data)
6577

6678
public static Task<SqlDatabase<SampleDbContext>> GetDatabase([CallerMemberName] string suffix = "")
6779
=> sqlInstance.Build(suffix);
80+
81+
public static Task<SqlDatabase<SampleDbContext>> GetDescriptiveAliasDatabase([CallerMemberName] string suffix = "")
82+
=> descriptiveAliasSqlInstance.Build(suffix);
6883
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#pragma warning disable EF9002
2+
3+
class DescriptiveSqlAliasManager : SqlAliasManager
4+
{
5+
Dictionary<string, int> aliases = new(StringComparer.OrdinalIgnoreCase);
6+
7+
public override string GenerateTableAlias(string name)
8+
{
9+
var lowerName = name.ToLowerInvariant();
10+
11+
if (aliases.TryGetValue(lowerName, out var counter))
12+
{
13+
aliases[lowerName] = counter + 1;
14+
return lowerName + counter;
15+
}
16+
17+
aliases[lowerName] = 0;
18+
return lowerName;
19+
}
20+
21+
protected override Dictionary<string, string>? RemapTableAliases(IReadOnlySet<string> usedAliases) =>
22+
// Skip gap-closing since the base implementation assumes single-char alias prefixes
23+
null;
24+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#pragma warning disable EF9002
2+
3+
class DescriptiveSqlAliasManagerFactory : ISqlAliasManagerFactory
4+
{
5+
public SqlAliasManager Create() => new DescriptiveSqlAliasManager();
6+
}

src/Verify.EntityFramework/GlobalUsings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
global using Microsoft.EntityFrameworkCore.ChangeTracking;
99
global using Microsoft.EntityFrameworkCore.Diagnostics;
1010
global using Microsoft.EntityFrameworkCore.Metadata;
11+
global using Microsoft.EntityFrameworkCore.Query;
1112
global using Microsoft.EntityFrameworkCore.Query.Internal;
1213
global using Microsoft.SqlServer.TransactSql.ScriptDom;
1314
global using VerifyTests.EntityFramework;

0 commit comments

Comments
 (0)