Skip to content

Commit 21b383a

Browse files
authored
feature: support DbString in DapperAOT (#119)
* start * add dbstring example to query * create test * start processing * support DbString! * last checks * regenerate for netfx * fix DAP44 error appeared in refactor * pre-review fixes * change DbString configuration method declaration * address initial comments * DbStringHelpers to file class * include `file` via preprocessor directive * only in dapper AOT * move to Dapper.AOT.Test.Integration.csproj * setup the infra for test * test is executed targeting docker db * remove debug * some other tries * Revert "some other tries" This reverts commit a95b6ff. * Revert "remove debug" This reverts commit 44e37d0. * Revert "test is executed targeting docker db" This reverts commit c5a4fee. * Revert "setup the infra for test" This reverts commit 63fd1a9. * Revert "move to Dapper.AOT.Test.Integration.csproj" This reverts commit e804648. * address PR comments * adjust a test * use global:: for DapperSpecialType
1 parent 42778fa commit 21b383a

22 files changed

+927
-57
lines changed

docs/rules/DAP048.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# DAP048
2+
3+
[DbString](https://github.com/DapperLib/Dapper/blob/main/Dapper/DbString.cs) causes heap allocations, but achieves the same as
4+
[DbValueAttribute](https://github.com/DapperLib/DapperAOT/blob/main/src/Dapper.AOT/DbValueAttribute.cs).
5+
6+
Bad:
7+
8+
``` c#
9+
public void DapperCode(DbConnection conn)
10+
{
11+
var sql = "SELECT COUNT(*) FROM Foo WHERE Name = @Name;";
12+
var cars = conn.Query<int>(sql,
13+
new
14+
{
15+
Name = new DbString
16+
{
17+
Value = "MyFoo",
18+
IsFixedLength = false,
19+
Length = 5,
20+
IsAnsi = true
21+
}
22+
});
23+
}
24+
```
25+
26+
Good:
27+
28+
``` c#
29+
public void DapperCode(DbConnection conn)
30+
{
31+
var sql = "SELECT COUNT(*) FROM Foo WHERE Name = @Name;";
32+
var cars = conn.Query<int>(sql,
33+
new MyPoco
34+
{
35+
Name = "MyFoo"
36+
});
37+
}
38+
39+
class MyPoco
40+
{
41+
[DbValue(Length = 5, DbType = DbType.AnsiStringFixedLength)] // specify properties here
42+
public string Name { get; set; }
43+
}
44+
```

src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public static readonly DiagnosticDescriptor
5555
CancellationDuplicated = LibraryWarning("DAP045", "Duplicate cancellation", "Multiple parameter values cannot define cancellation"),
5656
AmbiguousProperties = LibraryWarning("DAP046", "Ambiguous properties", "Properties have same name '{0}' after normalization and can be conflated"),
5757
AmbiguousFields = LibraryWarning("DAP047", "Ambiguous fields", "Fields have same name '{0}' after normalization and can be conflated"),
58+
MoveFromDbString = LibraryWarning("DAP048", "Move from DbString to DbValue", "DbString achieves the same as [DbValue] does. Use it instead."),
5859

5960
// SQL parse specific
6061
GeneralSqlError = SqlWarning("DAP200", "SQL error", "SQL error: {0}"),

src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ private void ValidateDapperMethod(in OperationAnalysisContext ctx, IOperation sq
191191
}
192192

193193
// check the types
194-
var resultType = invoke.GetResultType(flags);
194+
var resultType = invoke.GetResultType(flags);
195195
if (resultType is not null && IdentifyDbType(resultType, out _) is null) // don't warn if handled as an inbuilt
196196
{
197197
var resultMap = MemberMap.CreateForResults(resultType, location);
@@ -250,34 +250,9 @@ private void ValidateDapperMethod(in OperationAnalysisContext ctx, IOperation sq
250250
{
251251
_ = AdditionalCommandState.Parse(GetSymbol(parseState, invoke), parameters, onDiagnostic);
252252
}
253-
if (parameters is not null)
254-
{
255-
if (flags.HasAny(OperationFlags.DoNotGenerate)) // using vanilla Dapper mode
256-
{
257-
if (parameters.Members.Any(s => s.IsCancellation) || IsCancellationToken(parameters.ElementType))
258-
{
259-
ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.CancellationNotSupported, parameters.Location));
260-
}
261-
}
262-
else
263-
{
264-
bool first = true;
265-
foreach(var member in parameters.Members)
266-
{
267-
if (member.IsCancellation)
268-
{
269-
if (first)
270-
{
271-
first = false;
272-
}
273-
else
274-
{
275-
ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.CancellationDuplicated, member.GetLocation()));
276-
}
277-
}
278-
}
279-
}
280-
}
253+
254+
ValidateParameters(parameters, flags, onDiagnostic);
255+
281256
var args = SharedGetParametersToInclude(parameters, ref flags, sql, onDiagnostic, out var parseFlags);
282257

283258
ValidateSql(ctx, sqlSource, GetModeFlags(flags), SqlParameters.From(args), location);
@@ -850,6 +825,50 @@ enum ParameterMode
850825
? null : new(rowCountHint, rowCountHintMember?.Member.Name, batchSize, cmdProps);
851826
}
852827

828+
static void ValidateParameters(MemberMap? parameters, OperationFlags flags, Action<Diagnostic> onDiagnostic)
829+
{
830+
if (parameters is null) return;
831+
832+
var usingVanillaDapperMode = flags.HasAny(OperationFlags.DoNotGenerate); // using vanilla Dapper mode
833+
if (usingVanillaDapperMode)
834+
{
835+
if (parameters.Members.Any(s => s.IsCancellation) || IsCancellationToken(parameters.ElementType))
836+
{
837+
onDiagnostic(Diagnostic.Create(Diagnostics.CancellationNotSupported, parameters.Location));
838+
}
839+
}
840+
841+
var isFirstCancellation = true;
842+
foreach (var member in parameters.Members)
843+
{
844+
ValidateCancellationTokenParameter(member);
845+
ValidateDbStringParameter(member);
846+
}
847+
848+
void ValidateDbStringParameter(ElementMember member)
849+
{
850+
if (usingVanillaDapperMode)
851+
{
852+
// reporting ONLY in Dapper AOT
853+
return;
854+
}
855+
856+
if (member.DapperSpecialType == DapperSpecialType.DbString)
857+
{
858+
onDiagnostic(Diagnostic.Create(Diagnostics.MoveFromDbString, member.GetLocation()));
859+
}
860+
}
861+
862+
void ValidateCancellationTokenParameter(ElementMember member)
863+
{
864+
if (!usingVanillaDapperMode && member.IsCancellation)
865+
{
866+
if (isFirstCancellation) isFirstCancellation = false;
867+
else onDiagnostic(Diagnostic.Create(Diagnostics.CancellationDuplicated, member.GetLocation()));
868+
}
869+
}
870+
}
871+
853872
static void ValidateMembers(MemberMap memberMap, Action<Diagnostic> onDiagnostic)
854873
{
855874
if (memberMap.Members.Length == 0)

src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
using System.Diagnostics;
1717
using System.Globalization;
1818
using System.Linq;
19+
using System.Runtime.InteropServices.ComTypes;
1920
using System.Text;
2021
using System.Threading;
22+
using static Dapper.Internal.Inspection;
2123

2224
namespace Dapper.CodeAnalysis;
2325

@@ -423,16 +425,15 @@ internal void Generate(in GenerateState ctx)
423425
WriteRowFactory(ctx, sb, pair.Type, pair.Index);
424426
}
425427

426-
427428
foreach (var tuple in factories)
428429
{
429430
WriteCommandFactory(ctx, baseCommandFactory, sb, tuple.Type, tuple.Index, tuple.Map, tuple.CacheCount, tuple.AdditionalCommandState);
430431
}
431432

432433
sb.Outdent().Outdent(); // ends our generated file-scoped class and the namespace
433-
434-
var interceptsLocationWriter = new InterceptorsLocationAttributeWriter(sb);
435-
interceptsLocationWriter.Write(ctx.Compilation);
434+
435+
var preGeneratedCodeWriter = new PreGeneratedCodeWriter(sb, ctx.Compilation);
436+
preGeneratedCodeWriter.Write(ctx.GeneratorContext.IncludedGenerationTypes);
436437

437438
ctx.AddSource((ctx.Compilation.AssemblyName ?? "package") + ".generated.cs", sb.ToString());
438439
ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.InterceptorsGenerated, null, callSiteCount, ctx.Nodes.Length, methodIndex, factories.Count(), readers.Count()));
@@ -490,11 +491,11 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory
490491
else
491492
{
492493
sb.Append("public override void AddParameters(in global::Dapper.UnifiedCommand cmd, ").Append(declaredType).Append(" args)").Indent().NewLine();
493-
WriteArgs(type, sb, WriteArgsMode.Add, map, ref flags);
494+
WriteArgs(in ctx, type, sb, WriteArgsMode.Add, map, ref flags);
494495
sb.Outdent().NewLine();
495496

496497
sb.Append("public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, ").Append(declaredType).Append(" args)").Indent().NewLine();
497-
WriteArgs(type, sb, WriteArgsMode.Update, map, ref flags);
498+
WriteArgs(in ctx, type, sb, WriteArgsMode.Update, map, ref flags);
498499
sb.Outdent().NewLine();
499500

500501
if ((flags & (WriteArgsFlags.NeedsRowCount | WriteArgsFlags.NeedsPostProcess)) != 0)
@@ -507,11 +508,11 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory
507508
sb.Append("public override void PostProcess(in global::Dapper.UnifiedCommand cmd, ").Append(declaredType).Append(" args, int rowCount)").Indent().NewLine();
508509
if ((flags & WriteArgsFlags.NeedsPostProcess) != 0)
509510
{
510-
WriteArgs(type, sb, WriteArgsMode.PostProcess, map, ref flags);
511+
WriteArgs(in ctx, type, sb, WriteArgsMode.PostProcess, map, ref flags);
511512
}
512513
if ((flags & WriteArgsFlags.NeedsRowCount) != 0)
513514
{
514-
WriteArgs(type, sb, WriteArgsMode.SetRowCount, map, ref flags);
515+
WriteArgs(in ctx, type, sb, WriteArgsMode.SetRowCount, map, ref flags);
515516
}
516517
if (baseFactory != DapperBaseCommandFactory)
517518
{
@@ -524,7 +525,7 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory
524525
{
525526
sb.Append("public override global::System.Threading.CancellationToken GetCancellationToken(").Append(declaredType).Append(" args)")
526527
.Indent().NewLine();
527-
WriteArgs(type, sb, WriteArgsMode.GetCancellationToken, map, ref flags);
528+
WriteArgs(in ctx, type, sb, WriteArgsMode.GetCancellationToken, map, ref flags);
528529
sb.Outdent().NewLine();
529530
}
530531
}
@@ -966,7 +967,7 @@ enum WriteArgsMode
966967
GetCancellationToken
967968
}
968969

969-
private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteArgsMode mode, string map, ref WriteArgsFlags flags)
970+
private static void WriteArgs(in GenerateState ctx, ITypeSymbol? parameterType, CodeWriter sb, WriteArgsMode mode, string map, ref WriteArgsFlags flags)
970971
{
971972
if (parameterType is null)
972973
{
@@ -1073,8 +1074,19 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr
10731074
switch (mode)
10741075
{
10751076
case WriteArgsMode.Add:
1076-
sb.Append("p = cmd.CreateParameter();").NewLine()
1077-
.Append("p.ParameterName = ").AppendVerbatimLiteral(member.DbName).Append(";").NewLine();
1077+
sb.Append("p = cmd.CreateParameter();").NewLine();
1078+
sb.Append("p.ParameterName = ").AppendVerbatimLiteral(member.DbName).Append(";").NewLine();
1079+
1080+
if (member.DapperSpecialType is DapperSpecialType.DbString)
1081+
{
1082+
ctx.GeneratorContext.IncludeGenerationType(IncludedGeneration.DbStringHelpers);
1083+
1084+
sb.Append("global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(p, ")
1085+
.Append(source).Append(".").Append(member.DbName).Append(");").NewLine();
1086+
1087+
sb.Append("ps.Add(p);").NewLine(); // dont forget to add parameter to command parameters collection
1088+
break;
1089+
}
10781090

10791091
var dbType = member.GetDbType(out _);
10801092
var size = member.TryGetValue<int>("Size");
@@ -1149,6 +1161,18 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr
11491161
}
11501162
break;
11511163
case WriteArgsMode.Update:
1164+
if (member.DapperSpecialType is DapperSpecialType.DbString)
1165+
{
1166+
ctx.GeneratorContext.IncludeGenerationType(IncludedGeneration.DbStringHelpers);
1167+
1168+
sb.Append("global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter")
1169+
.Append("(ps[").Append(parameterIndex).Append("], ")
1170+
.Append(source).Append(".").Append(member.CodeName)
1171+
.Append(");").NewLine();
1172+
1173+
break;
1174+
}
1175+
11521176
sb.Append("ps[");
11531177
if ((flags & WriteArgsFlags.NeedsTest) != 0) sb.AppendVerbatimLiteral(member.DbName);
11541178
else sb.Append(parameterIndex);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Dapper.CodeAnalysis.Extensions
2+
{
3+
internal static class IncludedGenerationExtensions
4+
{
5+
public static bool HasAny(this IncludedGeneration value, IncludedGeneration flag) => (value & flag) != 0;
6+
}
7+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace Dapper.CodeAnalysis
2+
{
3+
/// <summary>
4+
/// Contains data about current generation run.
5+
/// </summary>
6+
internal class GeneratorContext
7+
{
8+
/// <summary>
9+
/// Specifies which generation types should be included in the output.
10+
/// </summary>
11+
public IncludedGeneration IncludedGenerationTypes { get; private set; }
12+
13+
public GeneratorContext()
14+
{
15+
// set default included generation types here
16+
IncludedGenerationTypes = IncludedGeneration.InterceptsLocationAttribute;
17+
}
18+
19+
/// <summary>
20+
/// Adds another generation type to the list of already included types.
21+
/// </summary>
22+
/// <param name="anotherType">another generation type to include in the output</param>
23+
public void IncludeGenerationType(IncludedGeneration anotherType)
24+
{
25+
IncludedGenerationTypes |= anotherType;
26+
}
27+
}
28+
}

src/Dapper.AOT.Analyzers/CodeAnalysis/ParseState.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public GenerateState(SourceProductionContext ctx, in (Compilation Compilation, I
8282
private readonly GenerateContextProxy? proxy;
8383
public readonly ImmutableArray<SourceState> Nodes;
8484
public readonly Compilation Compilation;
85+
public readonly GeneratorContext GeneratorContext = new();
8586

8687
internal void ReportDiagnostic(Diagnostic diagnostic)
8788
{

src/Dapper.AOT.Analyzers/CodeAnalysis/TypeAccessorInterceptorGenerator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,8 @@ void ReportDiagnosticInUsages(DiagnosticDescriptor diagnosticDescriptor)
164164
}
165165
});
166166

167-
var interceptsLocWriter = new InterceptorsLocationAttributeWriter(codeWriter);
168-
interceptsLocWriter.Write(state.Compilation);
167+
var preGenerator = new PreGeneratedCodeWriter(codeWriter, state.Compilation);
168+
preGenerator.Write(IncludedGeneration.InterceptsLocationAttribute);
169169

170170
context.AddSource((state.Compilation.AssemblyName ?? "package") + ".generated.cs", sb.GetSourceText());
171171
}

src/Dapper.AOT.Analyzers/CodeAnalysis/Writers/InterceptorsLocationAttributeWriter.cs renamed to src/Dapper.AOT.Analyzers/CodeAnalysis/Writers/PreGeneratedCodeWriter.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,41 @@
1-
using Dapper.Internal;
1+
using Dapper.CodeAnalysis.Extensions;
2+
using Dapper.Internal;
23
using Microsoft.CodeAnalysis;
34

45
namespace Dapper.CodeAnalysis.Writers
56
{
6-
internal struct InterceptorsLocationAttributeWriter
7+
internal struct PreGeneratedCodeWriter
78
{
9+
readonly Compilation _compilation;
810
readonly CodeWriter _codeWriter;
911

10-
public InterceptorsLocationAttributeWriter(CodeWriter codeWriter)
12+
public PreGeneratedCodeWriter(
13+
CodeWriter codeWriter,
14+
Compilation compilation)
1115
{
1216
_codeWriter = codeWriter;
17+
_compilation = compilation;
1318
}
1419

15-
/// <summary>
16-
/// Writes the "InterceptsLocationAttribute" to inner <see cref="CodeWriter"/>.
17-
/// </summary>
18-
/// <remarks>Does so only when "InterceptsLocationAttribute" is NOT visible by <see cref="Compilation"/>.</remarks>
19-
public void Write(Compilation compilation)
20+
public void Write(IncludedGeneration includedGenerations)
2021
{
21-
var attrib = compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.InterceptsLocationAttribute");
22-
if (!IsAvailable(attrib, compilation))
22+
if (includedGenerations.HasAny(IncludedGeneration.InterceptsLocationAttribute))
2323
{
24-
_codeWriter.NewLine().Append(Resources.ReadString("Dapper.InterceptsLocationAttribute.cs"));
24+
WriteInterceptsLocationAttribute();
25+
}
26+
27+
if (includedGenerations.HasAny(IncludedGeneration.DbStringHelpers))
28+
{
29+
_codeWriter.NewLine().Append(Resources.ReadString("Dapper.InGeneration.DapperHelpers.cs"));
30+
}
31+
}
32+
33+
void WriteInterceptsLocationAttribute()
34+
{
35+
var attrib = _compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.InterceptsLocationAttribute");
36+
if (!IsAvailable(attrib, _compilation))
37+
{
38+
_codeWriter.NewLine().Append(Resources.ReadString("Dapper.InGeneration.InterceptsLocationAttribute.cs"));
2539
}
2640

2741
static bool IsAvailable(INamedTypeSymbol? type, Compilation compilation)

src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616

1717
<ItemGroup>
1818
<Compile Remove="AotGridReader.cs" />
19-
<Compile Remove="InterceptsLocationAttribute.cs" />
2019
<EmbeddedResource Include="AotGridReader.cs" />
21-
<EmbeddedResource Include="InterceptsLocationAttribute.cs" />
20+
21+
<Compile Remove="InGeneration\*.cs" />
22+
<EmbeddedResource Include="InGeneration\*.cs" />
2223

2324
<Compile Update="CodeAnalysis/DapperInterceptorGenerator.*.cs">
2425
<DependentUpon>DapperInterceptorGenerator.cs</DependentUpon>

0 commit comments

Comments
 (0)