Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/rules/DAP052.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion notes/parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` 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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
20 changes: 18 additions & 2 deletions src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -727,11 +727,24 @@ 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, saying exactly what is missing
// (never emit code that cannot compile against the referenced Dapper)
flags |= OperationFlags.DoNotGenerate;
reportDiagnostic?.Invoke(Diagnostic.Create(Diagnostics.FeatureNeedsNewerDapper, argLocation,
"DynamicParameters", "DynamicParameters.AddParameters(IDbCommand)"));
}
// else: supported - the generated factory delegates to the bag itself
}
else if (!IsPublicOrAssemblyLocal(paramType, ctx, out var failing))
{
flags |= OperationFlags.DoNotGenerate;
Expand Down Expand Up @@ -1043,7 +1056,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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>); 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))
{
Expand Down
28 changes: 18 additions & 10 deletions src/Dapper.AOT.Analyzers/CodeAnalysis/Model/ParamPlan.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Dapper.Internal;
using Dapper.Internal;
using Microsoft.CodeAnalysis;
using System;
using System.Data;
Expand All @@ -19,13 +19,14 @@ internal sealed class ParamPlan : IEquatable<ParamPlan>
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<ParamMember> 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<ParamMember> members)
{
TypeName = typeName;
Expand All @@ -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;
Expand All @@ -57,30 +59,35 @@ 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;
element = Create(elementType, allowCollection: false);
}

EquatableArray<ParamMember> 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<ParamMember>(arr);
}
members = new EquatableArray<ParamMember>(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)
Expand Down Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions src/Dapper.AOT.Analyzers/Internal/Inspection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,45 @@ internal static bool IsDynamicParameters(ITypeSymbol? type, out bool needsConstr
return false;
}

/// <summary>
/// Does this parameter-bag type expose the identity-free <c>AddParameters(IDbCommand)</c>
/// 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.
/// </summary>
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);

Expand Down
5 changes: 4 additions & 1 deletion test/Dapper.AOT.Test/Verifiers/DAP015.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)")]);

}
36 changes: 36 additions & 0 deletions test/Dapper.AOT.Test/Verifiers/DAP052.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Dapper.CodeAnalysis;
using System.Threading.Tasks;
using Xunit;
using static Dapper.CodeAnalysis.DapperAnalyzer;

namespace Dapper.AOT.Test.Verifiers;

public class DAP052 : Verifier<DapperAnalyzer>
{
// 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;

[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)"),
], pinDapperPackageVersion: DapperWithoutTheApi);
}
29 changes: 22 additions & 7 deletions test/Dapper.AOT.Test/Verifiers/Verifier.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Dapper.CodeAnalysis;
using Dapper.CodeAnalysis;
using Dapper.SqlAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
Expand All @@ -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;
Expand All @@ -32,11 +33,12 @@

internal Task CSVerifyAsync<TAnalyzer>(string source,
Func<Solution, ProjectId, Solution>[] 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<TAnalyzer, DefaultVerifier>();
return ExecuteAsync(test, source, transforms, expected, sqlSyntax, sqlParseInputFlags, refDapperAot);
return ExecuteAsync(test, source, transforms, expected, sqlSyntax, sqlParseInputFlags, refDapperAot, pinDapperPackageVersion);
}
internal Task CSVerifyAsync<TAnalyzer, TCodeFix>(string source,
Func<Solution, ProjectId, Solution>[] transforms,
Expand Down Expand Up @@ -68,7 +70,8 @@

internal Task ExecuteAsync(AnalyzerTest<DefaultVerifier> test, string source,
Func<Solution, ProjectId, Solution>[] transforms,
DiagnosticResult[] expected, SqlSyntax sqlSyntax, SqlParseInputFlags sqlParseInputFlags, bool refDapperAot)
DiagnosticResult[] expected, SqlSyntax sqlSyntax, SqlParseInputFlags sqlParseInputFlags, bool refDapperAot,
string? pinDapperPackageVersion = null)
{
test.TestCode = source;

Expand All @@ -95,7 +98,18 @@
{
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);
Expand Down Expand Up @@ -199,10 +213,11 @@

internal Task CSVerifyAsync(string source,
Func<Solution, ProjectId, Solution>[] transforms,
DiagnosticResult[] expected, SqlSyntax sqlSyntax = SqlSyntax.SqlServer, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None, bool refDapperAot = true)
=> base.CSVerifyAsync<TAnalyzer>(source, transforms, expected, sqlSyntax, sqlParseInputFlags, refDapperAot);
DiagnosticResult[] expected, SqlSyntax sqlSyntax = SqlSyntax.SqlServer, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None, bool refDapperAot = true,
string? pinDapperPackageVersion = null)
=> base.CSVerifyAsync<TAnalyzer>(source, transforms, expected, sqlSyntax, sqlParseInputFlags, refDapperAot, pinDapperPackageVersion);

new internal Task CSVerifyAsync<TCodeFix>(string source,

Check warning on line 220 in test/Dapper.AOT.Test/Verifiers/Verifier.cs

View workflow job for this annotation

GitHub Actions / build

The member 'Verifier<TAnalyzer>.CSVerifyAsync<TCodeFix>(string, Func<Solution, ProjectId, Solution>[], DiagnosticResult[], SqlSyntax, SqlParseInputFlags, bool)' does not hide an accessible member. The new keyword is not required.

Check warning on line 220 in test/Dapper.AOT.Test/Verifiers/Verifier.cs

View workflow job for this annotation

GitHub Actions / build

The member 'Verifier<TAnalyzer>.CSVerifyAsync<TCodeFix>(string, Func<Solution, ProjectId, Solution>[], DiagnosticResult[], SqlSyntax, SqlParseInputFlags, bool)' does not hide an accessible member. The new keyword is not required.
Func<Solution, ProjectId, Solution>[] transforms,
DiagnosticResult[] expected, SqlSyntax sqlSyntax = SqlSyntax.SqlServer, SqlParseInputFlags sqlParseInputFlags = SqlParseInputFlags.None, bool refDapperAot = true)
where TCodeFix : CodeFixProvider, new()
Expand Down
Loading