From 26c24d63c30ad9ea07596e85f382ed49f33e82f9 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 18 Aug 2026 17:16:21 +0100 Subject: [PATCH 1/5] Support DynamicParameters by delegating to the bag's own protocol A DynamicParameters argument is runtime state - no generator can know its members. But the bag already implements the entire vanilla protocol (per-parameter DbType/direction/size/precision/scale, templates, literal replacement, RemoveUnused, storing attached parameters so Get reads outputs), so the generated command factory simply delegates: AddParameters(cmd) via the new identity-free Dapper overload, plus a PostProcess that fires IParameterCallbacks.OnCompleted. That is vanilla's own code path, so behavior is exact - including templates on JIT; under native AOT templates fail inside Dapper's ref-emit exactly as vanilla does there. The generator probes for the AddParameters(IDbCommand) symbol and only takes this path when the referenced Dapper has it - older Dapper keeps the DAP015 refusal, so nothing changes for existing consumers (the full golden suite is untouched against the current package). Dynamic bags already route to the deferred parameter map, which disables command caching; collection/multi-exec detection is skipped for them. On the Dapper test suite (with the Dapper-side overload): 494 -> 533 of 725 call-sites handled; DAP015 drops 114 -> 30 (the remainder are object-typed arguments, which stay refused - announced-types territory). --- .../CodeAnalysis/DapperAnalyzer.cs | 13 ++++++- .../DapperInterceptorGenerator.cs | 12 ++++++ .../CodeAnalysis/Model/ParamPlan.cs | 28 ++++++++----- .../Internal/Inspection.cs | 39 +++++++++++++++++++ 4 files changed, 81 insertions(+), 11 deletions(-) diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs index c8f390ad..33fdbd7b 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs @@ -727,11 +727,22 @@ internal static Location SharedParseArgsAndFlags(in ParseState ctx, IInvocationO flags |= OperationFlags.DoNotGenerate; ReportGenericTypeParameter(reportDiagnostic, paramType!, argLocation); } - else if (IsMissingOrObjectOrDynamic(paramType) || IsDynamicParameters(paramType, out _)) + else if (IsMissingOrObjectOrDynamic(paramType)) { flags |= OperationFlags.DoNotGenerate; reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.UntypedParameter, argLocation)); } + else if (IsDynamicParameters(paramType, out _)) + { + if (!HasIdentityFreeAddParameters(paramType)) + { + // no way to invoke the bag protocol externally on this Dapper version; + // leave the call-site on vanilla Dapper + flags |= OperationFlags.DoNotGenerate; + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.UntypedParameter, argLocation)); + } + // else: supported - the generated factory delegates to the bag itself + } else if (!IsPublicOrAssemblyLocal(paramType, ctx, out var failing)) { flags |= OperationFlags.DoNotGenerate; diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs index 4a527d60..f030779a 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs @@ -595,6 +595,18 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory { sb.Append("public override global::System.Threading.CancellationToken GetCancellationToken(").Append(declaredType).Append(" args) => args;").NewLine(); } + if (type.IsDynamicBag) + { + // the bag already implements the entire vanilla protocol (per-parameter settings, + // templates, literals, RemoveUnused, output storage for Get); delegate to it + sb.Append("public override void AddParameters(in global::Dapper.UnifiedCommand cmd, ").Append(declaredType).Append(" args)") + .Indent(false).NewLine().Append("=> args.AddParameters(cmd.Command!);").Outdent(false).NewLine().NewLine(); + sb.Append("public override bool RequirePostProcess => true;").NewLine().NewLine(); + sb.Append("public override void PostProcess(in global::Dapper.UnifiedCommand cmd, ").Append(declaredType).Append(" args, int rowCount)").Indent().NewLine() + .Append("if (args is global::Dapper.SqlMapper.IParameterCallbacks callbacks) callbacks.OnCompleted();").Outdent().NewLine(); + sb.Outdent().NewLine().NewLine(); + return; + } var flags = WriteArgsFlags.None; if (string.IsNullOrWhiteSpace(map)) { diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/Model/ParamPlan.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/Model/ParamPlan.cs index 739bf621..c0023e86 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/Model/ParamPlan.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/Model/ParamPlan.cs @@ -1,4 +1,4 @@ -using Dapper.Internal; +using Dapper.Internal; using Microsoft.CodeAnalysis; using System; using System.Data; @@ -19,13 +19,14 @@ internal sealed class ParamPlan : IEquatable public bool IsReferenceType { get; } public bool IsCancellationTokenType { get; } public string? ShapeLambda { get; } // the Cast(args, ...) witness, anonymous types only + public bool IsDynamicBag { get; } // DynamicParameters-style: the factory delegates to the bag itself public bool IsCollection { get; } // multi-exec candidate public string? CastType { get; } public ParamPlan? Element { get; } // multi-exec element (one level only) public EquatableArray Members { get; } private ParamPlan(string typeName, string declaredType, bool isAnonymous, bool isReferenceType, - bool isCancellationTokenType, string? shapeLambda, bool isCollection, string? castType, + bool isCancellationTokenType, string? shapeLambda, bool isDynamicBag, bool isCollection, string? castType, ParamPlan? element, in EquatableArray members) { TypeName = typeName; @@ -34,6 +35,7 @@ private ParamPlan(string typeName, string declaredType, bool isAnonymous, bool i IsReferenceType = isReferenceType; IsCancellationTokenType = isCancellationTokenType; ShapeLambda = shapeLambda; + IsDynamicBag = isDynamicBag; IsCollection = isCollection; CastType = castType; Element = element; @@ -57,10 +59,12 @@ private ParamPlan(string typeName, string declaredType, bool isAnonymous, bool i shapeLambda = sb.ToString(); } + bool isDynamicBag = Inspection.IsDynamicParameters(type, out _) && Inspection.HasIdentityFreeAddParameters(type); + bool isCollection = false; string? castType = null; ParamPlan? element = null; - if (allowCollection && Inspection.IsCollectionType(type, out var elementType, out var castTypeValue)) + if (!isDynamicBag && allowCollection && Inspection.IsCollectionType(type, out var elementType, out var castTypeValue)) { isCollection = true; castType = castTypeValue; @@ -68,19 +72,22 @@ private ParamPlan(string typeName, string declaredType, bool isAnonymous, bool i } EquatableArray members = default; - var memberMap = MemberMap.CreateForParameters(type); - if (memberMap is not null && !memberMap.Members.IsDefaultOrEmpty) + if (!isDynamicBag) { - var arr = new ParamMember[memberMap.Members.Length]; - for (int i = 0; i < arr.Length; i++) + var memberMap = MemberMap.CreateForParameters(type); + if (memberMap is not null && !memberMap.Members.IsDefaultOrEmpty) { - arr[i] = ParamMember.Create(memberMap.Members[i]); + var arr = new ParamMember[memberMap.Members.Length]; + for (int i = 0; i < arr.Length; i++) + { + arr[i] = ParamMember.Create(memberMap.Members[i]); + } + members = new EquatableArray(arr); } - members = new EquatableArray(arr); } return new(typeName, declaredType, type.IsAnonymousType, type.IsReferenceType, - Inspection.IsCancellationToken(type), shapeLambda, isCollection, castType, element, members); + Inspection.IsCancellationToken(type), shapeLambda, isDynamicBag, isCollection, castType, element, members); } private static void AppendShapeLambda(CodeWriter sb, ITypeSymbol parameterType) @@ -119,6 +126,7 @@ public bool Equals(ParamPlan? other) => other is not null && IsReferenceType == other.IsReferenceType && IsCancellationTokenType == other.IsCancellationTokenType && string.Equals(ShapeLambda, other.ShapeLambda, StringComparison.Ordinal) + && IsDynamicBag == other.IsDynamicBag && IsCollection == other.IsCollection && string.Equals(CastType, other.CastType, StringComparison.Ordinal) && Equals(Element, other.Element) diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index b4cb2679..a43c1d22 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -293,6 +293,45 @@ internal static bool IsDynamicParameters(ITypeSymbol? type, out bool needsConstr return false; } + /// + /// Does this parameter-bag type expose the identity-free AddParameters(IDbCommand) + /// self-apply API (Dapper vNext)? When it does, the generated command factory can simply + /// delegate to the bag's own (vanilla) protocol; when it does not, the call-site must be + /// left on vanilla Dapper, since Identity cannot be constructed externally. + /// + public static bool HasIdentityFreeAddParameters(ITypeSymbol? type) + { + while (type is not null) + { + foreach (var member in type.GetMembers("AddParameters")) + { + if (member is IMethodSymbol + { + IsStatic: false, DeclaredAccessibility: Accessibility.Public, + Parameters.Length: 1 + } method + && method.Parameters[0].Type is INamedTypeSymbol + { + Name: "IDbCommand", TypeKind: TypeKind.Interface, ContainingType: null, + ContainingNamespace: + { + Name: "Data", + ContainingNamespace: + { + Name: "System", + ContainingNamespace.IsGlobalNamespace: true + } + } + }) + { + return true; + } + } + type = type.BaseType; + } + return false; + } + public static bool IsPublicOrAssemblyLocal(ISymbol? symbol, in ParseState ctx, out ISymbol? failingSymbol) => IsPublicOrAssemblyLocal(symbol, ctx.SemanticModel.Compilation.Assembly, out failingSymbol); From 2311931f46f137b6228fddaba752c62bc4190a86 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 18 Aug 2026 17:35:24 +0100 Subject: [PATCH 2/5] Defer dynamic-bag parameter maps for stored procedures too The Defer decision for DynamicParameters-style bags only applied on the command-text branch; stored procedures took ParameterMode.All first, so a proc+bag call-site got an empty map and selected the parameterless fallback factory - parameters never attached ('expects parameter @ID, which was not supplied'). Caught by the Dapper test suite's proc tests on the first behavioral run; they go 16 failures to 0 with this (the two remaining in that file are the known list-expansion gap, unrelated). --- src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs index 33fdbd7b..1388d20f 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs @@ -1054,7 +1054,10 @@ void ValidateColumnAttribute() if (flags.HasAny(OperationFlags.StoredProcedure | OperationFlags.TableDirect)) { parseFlags = flags.HasAny(OperationFlags.StoredProcedure) ? SqlParseOutputFlags.MaybeQuery : SqlParseOutputFlags.Query; - mode = ParameterMode.All; + // a DynamicParameters-style bag supplies its own parameters at execution: defer, + // exactly as for command-text (otherwise the map ends up empty and the call-site + // gets the parameterless fallback factory) + mode = IsDynamicParameters(map?.DeclaredType, out _) ? ParameterMode.Defer : ParameterMode.All; } else { From 5685b25c6cf31988c88a3e6201b0cca91c549e09 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 18 Aug 2026 18:57:36 +0100 Subject: [PATCH 3/5] DAP052: name the missing Dapper API when feature-detection fails Marc's rule for Dapper-side dependencies, now established as the pattern: probe for the symbol, never emit code that cannot compile against the referenced Dapper, and refuse with a diagnostic that names exactly which API is missing and what it enables - not a generic message, and never a baffling compiler error. The DynamicParameters path previously fell back to DAP015 ('parameter type could not be resolved'), which described the symptom rather than the fix; it now reports DAP052 naming DynamicParameters.AddParameters(IDbCommand). Docs page included; the verifier runs against the packaged (older) Dapper, which is exactly the scenario the diagnostic exists for. --- docs/rules/DAP052.md | 14 +++++++++ .../DapperAnalyzer.Diagnostics.cs | 1 + .../CodeAnalysis/DapperAnalyzer.cs | 6 ++-- test/Dapper.AOT.Test/Verifiers/DAP015.cs | 5 +++- test/Dapper.AOT.Test/Verifiers/DAP052.cs | 30 +++++++++++++++++++ 5 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 docs/rules/DAP052.md create mode 100644 test/Dapper.AOT.Test/Verifiers/DAP052.cs diff --git a/docs/rules/DAP052.md b/docs/rules/DAP052.md new file mode 100644 index 00000000..19ca5e54 --- /dev/null +++ b/docs/rules/DAP052.md @@ -0,0 +1,14 @@ +# DAP052 + +Feature requires a newer Dapper + +Some Dapper.AOT features work by calling into APIs that were added to Dapper itself for the +purpose - for example, `DynamicParameters` support delegates to the bag's own +`AddParameters(IDbCommand)` overload. Dapper.AOT *feature-detects* these APIs on the Dapper +version your project actually references: when the API is present the feature lights up, and +when it is not, the call-site is left on vanilla Dapper and this message tells you exactly +which API was missing - rather than generating code that cannot compile. + +Note that "left on vanilla Dapper" means reflection at runtime: fine under JIT, but it will +not work under native AOT. To enable the feature, update the Dapper package to a version +that includes the named API. diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs index 61f5a7c4..342d636f 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs @@ -25,6 +25,7 @@ public static readonly DiagnosticDescriptor DapperAotTupleParameter = LibraryInfo("DAP014", "Tuple-type parameter", "Tuple-type parameters are not currently supported"), UntypedParameter = LibraryInfo("DAP015", "Untyped parameter", "The parameter type could not be resolved"), GenericTypeParameter = LibraryInfo("DAP016", "Generic type parameter", "Generic type parameters ({0}) are not currently supported"), + FeatureNeedsNewerDapper = LibraryInfo("DAP052", "Feature requires a newer Dapper", "Dapper.AOT support for {0} needs '{1}', which the referenced Dapper version does not expose; the call-site is left on vanilla Dapper (which will not work under native AOT) - update the Dapper package to enable this"), NestedInGenericType = LibraryWarning("DAP051", "Type is only generic by containment", "Type '{0}' is generic only because it is declared inside generic type '{1}'; if it does not need the enclosing type parameters, move it to non-generic scope"), NonPublicType = LibraryInfo("DAP017", "Non-accessible type", "Type '{0}' is not accessible; {1} types are not currently supported"), SqlParametersNotDetected = SqlWarning("DAP018", "SQL parameters not detected", "Parameters are being supplied, but no parameters were detected in the command"), diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs index 1388d20f..9c2110dc 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs @@ -737,9 +737,11 @@ internal static Location SharedParseArgsAndFlags(in ParseState ctx, IInvocationO if (!HasIdentityFreeAddParameters(paramType)) { // no way to invoke the bag protocol externally on this Dapper version; - // leave the call-site on vanilla Dapper + // leave the call-site on vanilla Dapper, saying exactly what is missing + // (never emit code that cannot compile against the referenced Dapper) flags |= OperationFlags.DoNotGenerate; - reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.UntypedParameter, argLocation)); + reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.FeatureNeedsNewerDapper, argLocation, + "DynamicParameters", "DynamicParameters.AddParameters(IDbCommand)")); } // else: supported - the generated factory delegates to the bag itself } diff --git a/test/Dapper.AOT.Test/Verifiers/DAP015.cs b/test/Dapper.AOT.Test/Verifiers/DAP015.cs index 1fd28f19..80a0fab9 100644 --- a/test/Dapper.AOT.Test/Verifiers/DAP015.cs +++ b/test/Dapper.AOT.Test/Verifiers/DAP015.cs @@ -28,6 +28,9 @@ public class Customer {} } """, DefaultConfig, [ Diagnostic(Diagnostics.UntypedParameter).WithLocation(0), - Diagnostic(Diagnostics.UntypedParameter).WithLocation(1)]); + // DynamicParameters is no longer 'untyped': it gets the specific needs-newer-Dapper + // guidance (or works outright, once the referenced Dapper has the self-apply API) + Diagnostic(Diagnostics.FeatureNeedsNewerDapper).WithLocation(1) + .WithArguments("DynamicParameters", "DynamicParameters.AddParameters(IDbCommand)")]); } \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP052.cs b/test/Dapper.AOT.Test/Verifiers/DAP052.cs new file mode 100644 index 00000000..09edbc4e --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP052.cs @@ -0,0 +1,30 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP052 : Verifier +{ + [Fact] // the referenced (packaged) Dapper does not expose AddParameters(IDbCommand), + // so the bag call-site is refused with a message naming exactly what is missing + public Task DynamicParametersNeedsNewerDapper() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot] + class SomeCode + { + public void Foo(DbConnection conn) + { + var bag = new DynamicParameters(); + bag.Add("id", 42); + conn.Execute("somesql", {|#0:bag|}); + } + } + """, DefaultConfig, [ + Diagnostic(Diagnostics.FeatureNeedsNewerDapper).WithLocation(0) + .WithArguments("DynamicParameters", "DynamicParameters.AddParameters(IDbCommand)"), + ]); +} From fc51f77454a943cf42c8301cc78e4edf1ff5530f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 19 Aug 2026 10:19:32 +0100 Subject: [PATCH 4/5] Tick the parity cells this lands --- notes/parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notes/parity.md b/notes/parity.md index 2f7a31c4..8aaff32c 100644 --- a/notes/parity.md +++ b/notes/parity.md @@ -59,7 +59,7 @@ Two levers change several complexity scores and are worth naming up front: | --- | --- | --- | --- | --- | | anonymous types / concrete POCOs | ✅ | — | — | | | fields as members | ❓ | low | low | verify | -| `DynamicParameters` | ❌ | **high** | high | **PR #195 open**: delegate to the bag's own protocol (pairs with Dapper #2225); covers subclasses via interface dispatch. Templates ride on the same path | +| `DynamicParameters` | ✅ | — | — | delegates to the bag's own protocol, so templates, per-param options, `Get` and output callbacks all ride along; subclasses covered via interface dispatch. Needs a Dapper with the identity-free overload (Dapper #2225) — probe-gated, DAP052 otherwise | | `SqlMapper.IDynamicParameters` (custom impls) | ❌ | low-med | med | interface receives the `IDbCommand`, so callable directly — blocked on `Identity` (Dapper-internal) in the signature; owning Dapper permits an AOT-friendly overload | | `SqlMapper.ICustomQueryParameter` | ❌ ❓ | med | low | **PR #198 open**: generated code calls it, with vanilla's null semantics; uncovered a teardown bug (**PR #199**: parameters must be cleared on dispose, as vanilla does) | | `IParameterLookup` / `IParameterCallbacks` | ❌ ❓ | low | low-med | obscure but public | From fa39f52f3b81d4346071ed827217106fa292ce18 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 20 Aug 2026 05:03:05 +0100 Subject: [PATCH 5/5] Pin the DAP052 verifier to a Dapper without the probed API The verifier previously relied on the project-wide Dapper reference not yet exposing AddParameters(IDbCommand) - which means a future package bump silently flips the probe and fails this test. It now references the shipped 2.1.72 package explicitly (a new pinDapperPackageVersion knob on the verifier, which swaps the live assembly for a resolved package), so it guards the probe-and-refuse path permanently, against the genuine old artifact. When the bump happens, the remaining work is the positive twin (same code, live reference, no diagnostic) and a golden fixture for the defer emit - both impossible to write until a released Dapper has the API. --- test/Dapper.AOT.Test/Verifiers/DAP052.cs | 14 ++++++++--- test/Dapper.AOT.Test/Verifiers/Verifier.cs | 29 ++++++++++++++++------ 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/test/Dapper.AOT.Test/Verifiers/DAP052.cs b/test/Dapper.AOT.Test/Verifiers/DAP052.cs index 09edbc4e..30f515ec 100644 --- a/test/Dapper.AOT.Test/Verifiers/DAP052.cs +++ b/test/Dapper.AOT.Test/Verifiers/DAP052.cs @@ -1,4 +1,4 @@ -using Dapper.CodeAnalysis; +using Dapper.CodeAnalysis; using System.Threading.Tasks; using Xunit; using static Dapper.CodeAnalysis.DapperAnalyzer; @@ -7,8 +7,14 @@ namespace Dapper.AOT.Test.Verifiers; public class DAP052 : Verifier { - [Fact] // the referenced (packaged) Dapper does not expose AddParameters(IDbCommand), - // so the bag call-site is refused with a message naming exactly what is missing + // the last Dapper release WITHOUT AddParameters(IDbCommand); pinned so this test keeps + // guarding the probe-and-refuse path after the project-wide Dapper reference gains the + // API (at which point a positive twin - same code, live reference, no diagnostic - + // becomes writable for the first time) + private const string DapperWithoutTheApi = "2.1.72"; + + [Fact] // a Dapper without AddParameters(IDbCommand): the bag call-site is refused + // with a message naming exactly what is missing public Task DynamicParametersNeedsNewerDapper() => CSVerifyAsync(""" using Dapper; using System.Data.Common; @@ -26,5 +32,5 @@ public void Foo(DbConnection conn) """, DefaultConfig, [ Diagnostic(Diagnostics.FeatureNeedsNewerDapper).WithLocation(0) .WithArguments("DynamicParameters", "DynamicParameters.AddParameters(IDbCommand)"), - ]); + ], pinDapperPackageVersion: DapperWithoutTheApi); } diff --git a/test/Dapper.AOT.Test/Verifiers/Verifier.cs b/test/Dapper.AOT.Test/Verifiers/Verifier.cs index aff134c4..abc53ebf 100644 --- a/test/Dapper.AOT.Test/Verifiers/Verifier.cs +++ b/test/Dapper.AOT.Test/Verifiers/Verifier.cs @@ -1,4 +1,4 @@ -using Dapper.CodeAnalysis; +using Dapper.CodeAnalysis; using Dapper.SqlAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.VisualBasic.Testing; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Text; using System.Threading; @@ -32,11 +33,12 @@ protected static DiagnosticResult InterceptorsGenerated(int handled, int total, internal Task CSVerifyAsync(string source, Func[] transforms, - DiagnosticResult[] expected, SqlSyntax sqlSyntax, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None, bool refDapperAot = true) + DiagnosticResult[] expected, SqlSyntax sqlSyntax, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None, bool refDapperAot = true, + string? pinDapperPackageVersion = null) where TAnalyzer : DiagnosticAnalyzer, new() { var test = new CSharpAnalyzerTest(); - return ExecuteAsync(test, source, transforms, expected, sqlSyntax, sqlParseInputFlags, refDapperAot); + return ExecuteAsync(test, source, transforms, expected, sqlSyntax, sqlParseInputFlags, refDapperAot, pinDapperPackageVersion); } internal Task CSVerifyAsync(string source, Func[] transforms, @@ -68,7 +70,8 @@ internal Task VBVerifyAsync(string source, internal Task ExecuteAsync(AnalyzerTest test, string source, Func[] transforms, - DiagnosticResult[] expected, SqlSyntax sqlSyntax, SqlParseInputFlags sqlParseInputFlags, bool refDapperAot) + DiagnosticResult[] expected, SqlSyntax sqlSyntax, SqlParseInputFlags sqlParseInputFlags, bool refDapperAot, + string? pinDapperPackageVersion = null) { test.TestCode = source; @@ -95,7 +98,18 @@ internal Task ExecuteAsync(AnalyzerTest test, string source, { test.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", CreateEditorConfig(sqlSyntax, sqlParseInputFlags))); } - test.TestState.AdditionalReferences.Add(typeof(SqlMapper).Assembly); + if (pinDapperPackageVersion is null) + { + test.TestState.AdditionalReferences.Add(typeof(SqlMapper).Assembly); + } + else + { + // reference a specific shipped Dapper *package* instead of the live assembly; + // this is how feature-detection (DAP052) stays testable after the project-wide + // Dapper reference gains the API being probed for + test.ReferenceAssemblies = test.ReferenceAssemblies.AddPackages( + [new PackageIdentity("Dapper", pinDapperPackageVersion)]); + } if (refDapperAot) { test.TestState.AdditionalReferences.Add(typeof(DapperAotAttribute).Assembly); @@ -199,8 +213,9 @@ class SomeCode internal Task CSVerifyAsync(string source, Func[] transforms, - DiagnosticResult[] expected, SqlSyntax sqlSyntax = SqlSyntax.SqlServer, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None, bool refDapperAot = true) - => base.CSVerifyAsync(source, transforms, expected, sqlSyntax, sqlParseInputFlags, refDapperAot); + DiagnosticResult[] expected, SqlSyntax sqlSyntax = SqlSyntax.SqlServer, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None, bool refDapperAot = true, + string? pinDapperPackageVersion = null) + => base.CSVerifyAsync(source, transforms, expected, sqlSyntax, sqlParseInputFlags, refDapperAot, pinDapperPackageVersion); new internal Task CSVerifyAsync(string source, Func[] transforms,