Skip to content

Commit ec55eca

Browse files
committed
.
1 parent 848665c commit ec55eca

6 files changed

Lines changed: 87 additions & 117 deletions

File tree

readme.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,7 @@ order by c.Name,
717717

718718
## Descriptive Parameter Names
719719

720-
By default EF generates generic parameter names in SQL (eg `@p0`, `@p1`). `UseDescriptiveParameterNames` replaces these with the column name, making recorded and verified SQL easier to read. When the same column name appears across multiple tables in a batch, a counter suffix is appended to subsequent occurrences to keep names unique (eg `@Id` for the first table, `@Id1` for the second).
720+
By default EF generates generic parameter names in SQL (eg `@p0`, `@p1`). `UseDescriptiveParameterNames` replaces these with the column name, making recorded and verified SQL easier to read. When the same column name appears across multiple tables in a batch, subsequent occurrences are prefixed with the entity type name (eg `@Id` for the first table, `@EmployeeId` for the second).
721721

722722

723723
### Enable
@@ -774,7 +774,7 @@ values (@p0, @p1)
774774

775775
### Duplicate column names
776776

777-
When multiple tables in the same batch have columns with the same name, the counter increments to keep parameter names unique:
777+
When multiple tables in the same batch have columns with the same name, subsequent occurrences are prefixed with the entity type name:
778778

779779
<!-- snippet: CoreTests.DescriptiveParameterNamesDuplicate.verified.txt -->
780780
<a id='snippet-CoreTests.DescriptiveParameterNamesDuplicate.verified.txt'></a>
@@ -786,25 +786,27 @@ When multiple tables in the same batch have columns with the same name, the coun
786786
Parameters: {
787787
@Age (Int32): 25,
788788
@CompanyId (Int32): 100,
789-
@Id0 (Int32): 100,
790-
@Id1 (Int32): 200,
791-
@Name0 (String): CompanyName,
792-
@Name1 (String): EmployeeName
789+
@EmployeeId (Int32): 200,
790+
@EmployeeName (String): EmployeeName,
791+
@Id (Int32): 100,
792+
@Name (String): CompanyName
793793
},
794794
Text:
795795
set nocount on;
796796
797797
insert into Companies (Id, Name)
798-
values (@Id0, @Name0);
798+
values (@Id, @Name);
799799
800800
insert into Employees (Id, Age, CompanyId, Name)
801-
values (@Id1, @Age, @CompanyId, @Name1)
801+
values (@EmployeeId, @Age, @CompanyId, @EmployeeName)
802802
}
803803
}
804804
```
805805
<sup><a href='/src/Verify.EntityFramework.Tests/CoreTests.DescriptiveParameterNamesDuplicate.verified.txt#L1-L22' title='Snippet source file'>snippet source</a> | <a href='#snippet-CoreTests.DescriptiveParameterNamesDuplicate.verified.txt' title='Start of snippet'>anchor</a></sup>
806806
<!-- endSnippet -->
807807

808+
If the entity-prefixed name itself collides with an existing column name (eg `Company` + `Id` = `CompanyId` which is already a column on `Employee`), a counter suffix is used as a fallback.
809+
808810

809811
## Missing OrderBy
810812

src/Verify.EntityFramework.Tests/CoreTests.DescriptiveParameterNamesDuplicate.verified.txt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@
55
Parameters: {
66
@Age (Int32): 25,
77
@CompanyId (Int32): 100,
8-
@Id0 (Int32): 100,
9-
@Id1 (Int32): 200,
10-
@Name0 (String): CompanyName,
11-
@Name1 (String): EmployeeName
8+
@EmployeeId (Int32): 200,
9+
@EmployeeName (String): EmployeeName,
10+
@Id (Int32): 100,
11+
@Name (String): CompanyName
1212
},
1313
Text:
1414
set nocount on;
1515

1616
insert into Companies (Id, Name)
17-
values (@Id0, @Name0);
17+
values (@Id, @Name);
1818

1919
insert into Employees (Id, Age, CompanyId, Name)
20-
values (@Id1, @Age, @CompanyId, @Name1)
20+
values (@EmployeeId, @Age, @CompanyId, @EmployeeName)
2121
}
2222
}

src/Verify.EntityFramework/DescriptiveModificationCommand.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ protected override IColumnModification CreateColumnModification(in ColumnModific
99
}
1010

1111
var columnName = parameters.ColumnName;
12+
var entityName = parameters.Entry?.EntityType.ClrType.Name ?? "";
1213
var original = parameters.GenerateParameterName;
1314

1415
var modified = parameters with
1516
{
1617
GenerateParameterName = () =>
1718
{
18-
generator.SetColumnHint(columnName);
19+
generator.SetColumnHint(entityName, columnName);
1920
return original();
2021
}
2122
};
Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,85 @@
11
class DescriptiveParameterNameGenerator :
22
ParameterNameGenerator
33
{
4-
Dictionary<string, int> names = new(StringComparer.OrdinalIgnoreCase);
4+
// columnName → (count, firstEntityName)
5+
Dictionary<string, (int count, string entityName)> names = new(StringComparer.OrdinalIgnoreCase);
6+
7+
// All generated param names, for collision detection
8+
HashSet<string> allGenerated = new(StringComparer.OrdinalIgnoreCase);
9+
510
string? pendingColumnName;
11+
string? pendingEntityName;
612

7-
public void SetColumnHint(string columnName) => pendingColumnName = columnName;
13+
public void SetColumnHint(string entityName, string columnName)
14+
{
15+
pendingEntityName = entityName;
16+
pendingColumnName = columnName;
17+
}
818

919
public override string GenerateNext()
1020
{
11-
var hint = pendingColumnName;
21+
var col = pendingColumnName;
22+
var entity = pendingEntityName;
1223
pendingColumnName = null;
24+
pendingEntityName = null;
1325

14-
if (hint == null)
26+
if (col == null)
1527
{
1628
return base.GenerateNext();
1729
}
1830

1931
// Keep the base counter in sync
2032
base.GenerateNext();
2133

22-
if (names.TryGetValue(hint, out var counter))
34+
if (names.TryGetValue(col, out var info))
35+
{
36+
// Collision on column name - try entity-prefixed name
37+
var prefixed = entity + col;
38+
39+
if (!allGenerated.Contains(prefixed))
40+
{
41+
names[col] = (info.count + 1, info.entityName);
42+
allGenerated.Add(prefixed);
43+
return prefixed;
44+
}
45+
46+
// Entity-prefixed name also collides, fall back to counter
47+
names[col] = (info.count + 1, info.entityName);
48+
var fallback = col + info.count;
49+
allGenerated.Add(fallback);
50+
return fallback;
51+
}
52+
53+
// First occurrence of this column name
54+
if (!allGenerated.Contains(col))
55+
{
56+
names[col] = (1, entity!);
57+
allGenerated.Add(col);
58+
return col;
59+
}
60+
61+
// Column name collides with a previously generated name (e.g. from an entity-prefix)
62+
var entityPrefixed = entity + col;
63+
if (!allGenerated.Contains(entityPrefixed))
2364
{
24-
names[hint] = counter + 1;
25-
return hint + counter;
65+
names[col] = (1, entity!);
66+
allGenerated.Add(entityPrefixed);
67+
return entityPrefixed;
2668
}
2769

28-
names[hint] = 1;
29-
return hint;
70+
// Both collide, use counter
71+
names[col] = (1, entity!);
72+
var numbered = col + "1";
73+
allGenerated.Add(numbered);
74+
return numbered;
3075
}
3176

3277
public override void Reset()
3378
{
3479
base.Reset();
3580
names.Clear();
81+
allGenerated.Clear();
3682
pendingColumnName = null;
83+
pendingEntityName = null;
3784
}
3885
}

src/Verify.EntityFramework/GlobalUsings.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
global using System.Diagnostics.CodeAnalysis;
44
global using System.Globalization;
55
global using System.Linq.Expressions;
6-
global using System.Text.RegularExpressions;
76
global using Argon;
87
global using Microsoft.EntityFrameworkCore;
98
global using Microsoft.EntityFrameworkCore.ChangeTracking;
Lines changed: 13 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,17 @@
11
namespace VerifyTests.EntityFramework;
22

3-
public class LogEntry
3+
public class LogEntry(
4+
string type,
5+
DbCommand command,
6+
CommandEndEventData data,
7+
Exception? exception = null)
48
{
5-
public string Type { get; }
6-
public DateTimeOffset StartTime { get; }
7-
internal bool IsSqlServer { get; }
8-
public TimeSpan Duration { get; }
9-
public bool HasTransaction { get; }
10-
public Exception? Exception { get; }
11-
public IDictionary<string, object> Parameters { get; }
12-
public string Text { get; }
13-
14-
public LogEntry(
15-
string type,
16-
DbCommand command,
17-
CommandEndEventData data,
18-
Exception? exception = null)
19-
{
20-
Type = type;
21-
StartTime = data.StartTime;
22-
IsSqlServer = command.GetType().Name == "SqlCommand";
23-
Duration = data.Duration;
24-
HasTransaction = command.Transaction != null;
25-
Exception = exception;
26-
27-
var parameters = command.Parameters.ToDictionary();
28-
var text = command.CommandText.Trim();
29-
NormalizeDescriptiveParameterNames(ref parameters, ref text);
30-
Parameters = parameters;
31-
Text = text;
32-
}
33-
34-
// When using descriptive parameter names, the generator produces:
35-
// first occurrence: @Id (no suffix)
36-
// subsequent: @Id1, @Id2, etc.
37-
// This normalizes to 0-based numbering for duplicates:
38-
// @Id → @Id0 (when @Id1 also exists)
39-
// while leaving singletons without a suffix.
40-
static void NormalizeDescriptiveParameterNames(
41-
ref Dictionary<string, object> parameters,
42-
ref string text)
43-
{
44-
// Dictionary keys are formatted as "@Name (Type)"
45-
// Find parameter names (the @Name part) that need renaming
46-
List<(string oldKey, string newKey, string oldParamName, string newParamName)>? renames = null;
47-
48-
foreach (var key in parameters.Keys)
49-
{
50-
var spaceIndex = key.IndexOf(' ');
51-
if (spaceIndex < 0)
52-
{
53-
continue;
54-
}
55-
56-
var paramName = key[..spaceIndex];
57-
var suffix = key[spaceIndex..];
58-
var paramName1 = paramName + "1";
59-
60-
// Check if paramName + "1" exists as a parameter name
61-
var hasDuplicate = false;
62-
foreach (var otherKey in parameters.Keys)
63-
{
64-
if (otherKey.StartsWith(paramName1) &&
65-
otherKey.Length > paramName1.Length &&
66-
otherKey[paramName1.Length] == ' ')
67-
{
68-
hasDuplicate = true;
69-
break;
70-
}
71-
}
72-
73-
if (!hasDuplicate)
74-
{
75-
continue;
76-
}
77-
78-
renames ??= [];
79-
var newParamName = paramName + "0";
80-
renames.Add((key, newParamName + suffix, paramName, newParamName));
81-
}
82-
83-
if (renames is null)
84-
{
85-
return;
86-
}
87-
88-
foreach (var (oldKey, newKey, oldParamName, newParamName) in renames)
89-
{
90-
var value = parameters[oldKey];
91-
parameters.Remove(oldKey);
92-
parameters[newKey] = value;
93-
text = Regex.Replace(text, Regex.Escape(oldParamName) + @"(?!\w)", newParamName);
94-
}
95-
}
9+
public string Type { get; } = type;
10+
public DateTimeOffset StartTime { get; } = data.StartTime;
11+
internal bool IsSqlServer { get; } = command.GetType().Name == "SqlCommand";
12+
public TimeSpan Duration { get; } = data.Duration;
13+
public bool HasTransaction { get; } = command.Transaction != null;
14+
public Exception? Exception { get; } = exception;
15+
public IDictionary<string, object> Parameters { get; } = command.Parameters.ToDictionary();
16+
public string Text { get; } = command.CommandText.Trim();
9617
}

0 commit comments

Comments
 (0)