diff --git a/notes/parity.md b/notes/parity.md index 75d5605d..144f70cf 100644 --- a/notes/parity.md +++ b/notes/parity.md @@ -44,7 +44,7 @@ Two levers change several complexity scores and are worth naming up front: | `GetRowParser(reader)` | ✅ | — | — | | | `GetRowParser(reader, Type concreteType, ...)` | ❌ | med | low* | discriminator/polymorphism pattern; dictionary lookup once types are announced | | `Parse` / `Parse(Type)` / `Parse` (dynamic) | ❌ ❓ | low | low | same reader machinery, different entry point | -| `AsTableValuedParameter` (`DataTable` / `SqlDataRecord`) | ❓ | med | med | **PR #198 open** covers it (the result *is* an `ICustomQueryParameter`); a bare `DataTable` member rides the type-handler story instead (vanilla registers `DataTableHandler` by default) | +| `AsTableValuedParameter` (`DataTable` / `SqlDataRecord`) | ⚠️ | low | — | the result *is* an `ICustomQueryParameter`, so covered above; a bare `DataTable` member rides the type-handler story instead (vanilla registers `DataTableHandler` by default) | | `AsList` | n/a | — | — | trivial helper; confirm it doesn't count as a candidate site | | `GetTypeDeserializer(Type, reader, startBound, length, ...)` | ❌ | low-med | low* | a valid raw-materializer API, not mere plumbing: with announced types it's the same dispatch map, returning a boxed `Func`. Its generic strengthening **already exists**: `GetRowParser` (same slicing knobs), which AOT supports | | `CreateParamInfoGenerator(Identity, ...)` | ❌ | low | med | the raw parameter-binder factory; **no generic counterpart exists in Dapper** — see "Strengthened APIs" in [type-vs-generic.md](type-vs-generic.md) for the proposed `` form | @@ -61,11 +61,11 @@ Two levers change several complexity scores and are worth naming up front: | fields as members | ❓ | low | low | verify | | `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) | +| `SqlMapper.ICustomQueryParameter` | ✅ | — | — | generated code calls `AddParameter(command, name)` with vanilla's null semantics; self-binding guards as for list expansion. Uncovered a teardown bug (PR #199: parameters must be cleared on dispose, as vanilla does) | | `IParameterLookup` / `IParameterCallbacks` | ❌ ❓ | low | low-med | obscure but public | | `DbString` | ✅ | — | — | DAP048 nudges to `[DbValue]`; keep the Dapper spelling, the corpus uses it | | output / return params via `[DbValue(Direction=...)]` | ⚠️ | — | — | AOT spelling works; Dapper spelling rides on `DynamicParameters` above | -| list expansion (`in @ids`) | ❌ ❓ | **high** | med | **PR #197 open**: delegates to vanilla's `PackListParameters`, which owns the whole contract — no runtime rewrite helper needed after all. [tokens.md](tokens.md) §2 | +| list expansion (`in @ids`) | ✅ | — | — | delegates to vanilla's `PackListParameters`, which owns the whole contract (rewrite, empty form, padding, split, DbString items). Self-binding guards: no command caching, no multi-exec, not alongside output params. [tokens.md](tokens.md) §2 | | literal injection (`{=name}`) | ❌ ❓ | med | low-med | formatting rules compile-time decidable. [tokens.md](tokens.md) §3 | | pseudo-positional (`?foo?`) | ❌ ❓ | low | med | OleDb/Access corner. [tokens.md](tokens.md) §4 | | enum / nullable / `char` / `Guid` params | ⚠️❓ | med | low | verify edge conversions vs Dapper | @@ -102,12 +102,12 @@ Two levers change several complexity scores and are worth naming up front: | feature | AOT status | impact | complexity | notes | | --- | --- | --- | --- | --- | | `Settings.CommandTimeout` (global default) | ❓ | med | low | AOT has per-site args + `[CommandProperty]`; needs a global knob | -| `Settings.InListStringSplitCount` | ❌ | med | low* | *after* list expansion; SQL Server plan-stability win | -| `Settings.PadListExpansions` | ❌ | low-med | low* | same | +| `Settings.InListStringSplitCount` | ✅ | — | — | honored for free: it lives inside `PackListParameters`, which list expansion delegates to | +| `Settings.PadListExpansions` | ✅ | — | — | same — honored inside `PackListParameters` | | `Settings.UseSingleResult/UseSingleRowOptimization` | ✅ | — | — | verification found a real divergence (AOT hardcoded the opt-in flags; swallowed trailing errors, 10x slower async); now matches vanilla's default — no flags. The runtime knobs stay unread, with the opt-in location noted in code | | `Settings.FetchSize` (Oracle) | ⚠️ | low | low | `GlobalFetchSize` exists; verify | | `SqlMapper.ConnectionStringComparer` | 🚫? | **zero-ish** | — | exists to partition the runtime identity/cache — a concept AOT doesn't have | -| `FeatureSupport` (per-provider null-array quirks) | ❓ | low | low | folds into list-expansion helper | +| `FeatureSupport` (per-provider null-array quirks) | ✅ | — | — | honored inside `PackListParameters` (the arrays branch is its first test) | ## 5. Sibling packages in the Dapper repo diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs index bbdbf04c..1139de5c 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs @@ -102,6 +102,33 @@ private static string InterceptorFilePath(in ParseState ctx, Location location) return ctx.SemanticModel.Compilation.Options.SourceReferenceResolver?.NormalizePath(tree.FilePath, baseFilePath: null) ?? tree.FilePath; } + // expandable (in @ids) and custom (ICustomQueryParameter) members bind themselves, + // contributing an unknowable number of parameters; the guards below treat them alike + private static bool HasSelfBindingMember(ParamPlan? plan) + { + if (plan is not null) + { + foreach (var member in plan.Members) + { + if (member.IsMapped && (member.IsExpandable || member.IsCustom)) return true; + } + } + return false; + } + + private static bool HasNonInputMember(ParamPlan? plan) + { + if (plan is not null) + { + foreach (var member in plan.Members) + { + if (member.IsMapped && !member.IsCancellation && !member.IsRowCount + && member.Direction != System.Data.ParameterDirection.Input) return true; + } + } + return false; + } + private static InterceptedMethod ProjectMethod(IMethodSymbol method) { var args = method.Parameters; @@ -152,6 +179,20 @@ private static InterceptedMethod ProjectMethod(IMethodSymbol method) var map = MemberMap.CreateForParameters(argExpression); var parameterMap = BuildParameterMap(ctx, op, sql, ref flags, map, location, out var parseFlags); + var parameterPlan = ParamPlan.Create(argExpression?.Type); + if (parameterPlan is { IsCollection: true, Element: { } element } && HasSelfBindingMember(element)) + { + // multi-exec batch reuse updates parameters in-place, which cannot re-bind a + // self-binding member; leave such call-sites on vanilla Dapper + return new SkippedSourceState(new LocationSnapshot(location), flags); + } + if (HasSelfBindingMember(parameterPlan) && HasNonInputMember(parameterPlan)) + { + // PostProcess addresses output/return parameters by *index*, and an expanded + // list contributes a runtime-variable number of parameters before them; leave + // such call-sites on vanilla Dapper rather than read back the wrong slot + return new SkippedSourceState(new LocationSnapshot(location), flags); + } if (flags.HasAny(OperationFlags.CacheCommand)) { bool canBeCached = true; @@ -160,6 +201,10 @@ private static InterceptedMethod ProjectMethod(IMethodSymbol method) { canBeCached = false; } + else if (HasSelfBindingMember(parameterPlan)) + { + canBeCached = false; // self-binding members change the parameter shape per call + } if (!canBeCached) flags &= ~OperationFlags.CacheCommand; } @@ -171,7 +216,7 @@ private static InterceptedMethod ProjectMethod(IMethodSymbol method) return new SuccessSourceState(new LocationSnapshot(location), InterceptorFilePath(ctx, location), languageVersion, ProjectMethod(op.TargetMethod), flags, sql, RowPlan.Create(resultType, additionalState?.QueryColumns ?? default), - ParamPlan.Create(argExpression?.Type), parameterMap, additionalState); + parameterPlan, parameterMap, additionalState); } catch (Exception ex) { @@ -1162,6 +1207,24 @@ private static void WriteArgs(in GenerateState ctx, ParamPlan? parameterType, Co var planMembers = parameterType.Members; if (planMembers.IsEmpty) return; + // Add mode uses a shared "p" local, declared at method scope because the per-member + // Include(...) guards each open their own block; a factory whose members all expand + // (PackListParameters) never touches it, and declaring it then is CS0168 + bool needsParameterLocal = false; + if (mode == WriteArgsMode.Add) + { + foreach (var member in planMembers) + { + if (member.IsMapped && !member.IsCancellation && !member.IsRowCount + && !member.IsExpandable && !member.IsCustom + && SqlTools.IncludeParameter(map, member.CodeName, out _)) + { + needsParameterLocal = true; + break; + } + } + } + foreach (var member in planMembers) { if (!member.IsMapped) continue; @@ -1216,11 +1279,9 @@ private static void WriteArgs(in GenerateState ctx, ParamPlan? parameterType, Co if (first && mode != WriteArgsMode.GetCancellationToken) { sb.Append("var ps = cmd.Parameters;").NewLine(); - switch (mode) + if (needsParameterLocal) { - case WriteArgsMode.Add: - sb.Append("global::System.Data.Common.DbParameter p;").NewLine(); - break; + sb.Append("global::System.Data.Common.DbParameter p;").NewLine(); } first = false; } @@ -1244,6 +1305,35 @@ private static void WriteArgs(in GenerateState ctx, ParamPlan? parameterType, Co switch (mode) { case WriteArgsMode.Add: + if (member.IsCustom) + { + // ICustomQueryParameter (TVPs etc): the value adds itself; it declares + // no DbType, so the command cannot be prepared. The null throw matches + // vanilla Dapper's, which has no way to name a parameter it never got. + flags &= ~WriteArgsFlags.CanPrepare; + if (!member.IsValueType) + { + sb.Append("if (").Append(source).Append(".").Append(member.CodeName) + .Append(" is null) throw new global::System.InvalidOperationException(\"Member '") + .Append(member.CodeName).Append("' is an ICustomQueryParameter and cannot be null\");").NewLine(); + } + sb.Append(source).Append(".").Append(member.CodeName).Append(".AddParameter(cmd.Command!, ") + .AppendVerbatimLiteral(member.DbName).Append(");").NewLine(); + break; + } + if (member.IsExpandable) + { + // list-expansion (where X in @ids): delegate to Dapper's own implementation, + // which owns the whole contract - SQL rewrite (including the empty-list and + // optimize-hint forms), per-item parameters, DbString items, padding and + // string_split settings, and provider array support + flags &= ~WriteArgsFlags.CanPrepare; // parameter shape varies by list size + sb.Append("#pragma warning disable CS0618 // list-expansion: this *is* the library usage").NewLine() + .Append("global::Dapper.SqlMapper.PackListParameters(cmd.Command!, ").AppendVerbatimLiteral(member.DbName) + .Append(", ").Append(source).Append(".").Append(member.CodeName).Append(");").NewLine() + .Append("#pragma warning restore CS0618").NewLine(); + break; + } sb.Append("p = cmd.CreateParameter();").NewLine(); sb.Append("p.ParameterName = ").AppendVerbatimLiteral(member.DbName).Append(";").NewLine(); @@ -1311,6 +1401,12 @@ private static void WriteArgs(in GenerateState ctx, ParamPlan? parameterType, Co } break; case WriteArgsMode.Update: + if (member.IsExpandable || member.IsCustom) + { + // update is only reachable via command-cache reuse and batch, both of + // which are refused for self-binding members at parse + break; + } if (member.IsDbString) { ctx.GeneratorContext.IncludeGenerationType(IncludedGeneration.DbStringHelpers); diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/Model/ParamPlan.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/Model/ParamPlan.cs index c0023e86..55eaf640 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/Model/ParamPlan.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/Model/ParamPlan.cs @@ -152,6 +152,9 @@ public override int GetHashCode() public string DbName { get; } public ParameterDirection Direction { get; } public bool IsDbString { get; } + public bool IsExpandable { get; } // enumerable member: list-expansion (in @ids) applies + public bool IsCustom { get; } // SqlMapper.ICustomQueryParameter: the value binds itself + public bool IsValueType { get; } // of the member's own type; decides the null test public bool HasDbType { get; } // no DbType => cannot Prepare public string? DbTypeName { get; } // for "p.DbType = global::System.Data.DbType.X;" public int? EffectiveSize { get; } // after the [n]varchar(max) adjustment @@ -161,7 +164,8 @@ public override int GetHashCode() public string TypeName { get; } // emitted (Append) form, for Parse in post-process private ParamMember(bool isMapped, bool isCancellation, bool isRowCount, string codeName, string dbName, - ParameterDirection direction, bool isDbString, bool hasDbType, string? dbTypeName, int? effectiveSize, + ParameterDirection direction, bool isDbString, bool isExpandable, bool isCustom, bool isValueType, + bool hasDbType, string? dbTypeName, int? effectiveSize, bool useSetValueWithDefaultSize, byte? precision, byte? scale, string typeName) { IsMapped = isMapped; @@ -171,6 +175,9 @@ private ParamMember(bool isMapped, bool isCancellation, bool isRowCount, string DbName = dbName; Direction = direction; IsDbString = isDbString; + IsExpandable = isExpandable; + IsCustom = isCustom; + IsValueType = isValueType; HasDbType = hasDbType; DbTypeName = dbTypeName; EffectiveSize = effectiveSize; @@ -184,7 +191,7 @@ public static ParamMember Create(in ElementMember member) { if (!member.IsMapped) { - return new(false, false, false, "", "", default, false, false, null, null, false, null, null, ""); + return new(false, false, false, "", "", default, false, false, false, false, false, null, null, false, null, null, ""); } var dbType = member.GetDbType(out _); var size = member.TryGetValue("Size"); @@ -208,7 +215,8 @@ public static ParamMember Create(in ElementMember member) } } return new(true, member.IsCancellation, member.IsRowCount, member.CodeName, member.DbName, - member.Direction, member.DapperSpecialType is DapperSpecialType.DbString, + member.Direction, member.DapperSpecialType is DapperSpecialType.DbString, member.IsExpandable, + member.DapperSpecialType is DapperSpecialType.CustomQueryParameter, member.CodeType!.IsValueType, dbType is not null, dbType?.ToString(), size, useSetValueWithDefaultSize, member.TryGetValue("Precision"), member.TryGetValue("Scale"), CodeWriter.GetAppendTypeName(member.CodeType!)); @@ -221,6 +229,9 @@ public bool Equals(ParamMember other) => IsMapped == other.IsMapped && string.Equals(DbName, other.DbName, StringComparison.Ordinal) && Direction == other.Direction && IsDbString == other.IsDbString + && IsExpandable == other.IsExpandable + && IsCustom == other.IsCustom + && IsValueType == other.IsValueType && HasDbType == other.HasDbType && string.Equals(DbTypeName, other.DbTypeName, StringComparison.Ordinal) && EffectiveSize == other.EffectiveSize diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index a43c1d22..ab411079 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -574,6 +574,33 @@ internal enum DapperSpecialType { None = 0, DbString = 1 << 0, + CustomQueryParameter = 1 << 1, // SqlMapper.ICustomQueryParameter: the value binds itself + } + + internal static bool IsCustomQueryParameter(ITypeSymbol? type) + { + static bool IsTheInterface(ITypeSymbol symbol) => symbol is INamedTypeSymbol + { + Name: "ICustomQueryParameter", + Arity: 0, + TypeKind: TypeKind.Interface, + ContainingType: + { + Name: "SqlMapper", + ContainingNamespace: + { + Name: "Dapper", + ContainingNamespace.IsGlobalNamespace: true + } + } + }; + if (type is null) return false; + if (IsTheInterface(type)) return true; + foreach (var iface in type.AllInterfaces) + { + if (IsTheInterface(iface)) return true; + } + return false; } internal static bool IsCancellationToken(ITypeSymbol? type) @@ -662,6 +689,8 @@ public DapperSpecialType DapperSpecialType } }) return DapperSpecialType.DbString; + if (IsCustomQueryParameter(CodeType)) return DapperSpecialType.CustomQueryParameter; + return DapperSpecialType.None; } } diff --git a/test/Dapper.AOT.Test/Interceptors/CustomParameters.input.cs b/test/Dapper.AOT.Test/Interceptors/CustomParameters.input.cs new file mode 100644 index 00000000..eaa52dfe --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/CustomParameters.input.cs @@ -0,0 +1,43 @@ +using Dapper; +using System.Data; +using System.Data.Common; + +[module: DapperAot] + +public static class Foo +{ + static void SomeCode(DbConnection connection, SqlMapper.ICustomQueryParameter tvp) + { + // ICustomQueryParameter members add themselves (TVPs are the common case); a member + // typed as the interface, a class, and a struct - the struct needs no null test + _ = connection.Query("select count(1) from @ids", new { ids = tvp }); + _ = connection.Query("select count(1) from @ids where Id = @id", new { ids = new CustomClass(), id = 42 }); + _ = connection.Query("select count(1) from @ids", new { ids = new CustomStruct() }); + + // skipped: PostProcess reads output parameters back by index, and a self-binding + // member contributes an unknowable number of parameters before them + _ = connection.Execute("exec SomeProc @ids, @total out", new WithOutput { ids = new CustomClass() }); + + // skipped: multi-exec batch reuse updates parameters in-place + _ = connection.Execute("exec SomeProc @ids", new[] { new WithCustom(), new WithCustom() }); + } + + public class CustomClass : SqlMapper.ICustomQueryParameter + { + public void AddParameter(IDbCommand command, string name) { } + } + public struct CustomStruct : SqlMapper.ICustomQueryParameter + { + public void AddParameter(IDbCommand command, string name) { } + } + public class WithOutput + { + public CustomClass ids { get; set; } + [DbValue(Direction = ParameterDirection.Output)] + public int total { get; set; } + } + public class WithCustom + { + public CustomClass ids { get; set; } + } +} diff --git a/test/Dapper.AOT.Test/Interceptors/CustomParameters.output.cs b/test/Dapper.AOT.Test/Interceptors/CustomParameters.output.cs new file mode 100644 index 00000000..dd032334 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/CustomParameters.output.cs @@ -0,0 +1,162 @@ +#nullable enable +#pragma warning disable IDE0078 // unnecessary suppression is necessary +#pragma warning disable CS9270 // SDK-dependent change to interceptors usage +namespace Dapper.AOT // interceptors must be in a known namespace +{ + file static class DapperGeneratedInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\CustomParameters.input.cs", 13, 24)] + internal static global::System.Collections.Generic.IEnumerable Query0(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, TypedResult, HasParameters, Buffered, Text, KnownParameters + // takes parameter: + // parameter map: ids + // returns data: int + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory0.Instance).QueryBuffered(param, global::Dapper.RowFactory.Inbuilt.Value()); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\CustomParameters.input.cs", 14, 24)] + internal static global::System.Collections.Generic.IEnumerable Query1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, TypedResult, HasParameters, Buffered, Text, KnownParameters + // takes parameter: + // parameter map: id ids + // returns data: int + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).QueryBuffered(param, global::Dapper.RowFactory.Inbuilt.Value()); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\CustomParameters.input.cs", 15, 24)] + internal static global::System.Collections.Generic.IEnumerable Query2(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, TypedResult, HasParameters, Buffered, Text, KnownParameters + // takes parameter: + // parameter map: ids + // returns data: int + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory2.Instance).QueryBuffered(param, global::Dapper.RowFactory.Inbuilt.Value()); + + } + + private class CommonCommandFactory : global::Dapper.CommandFactory + { + public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, T args) + { + var cmd = base.GetCommand(connection, sql, commandType, args); + // apply special per-provider command initialization logic for OracleCommand + if (cmd is global::Oracle.ManagedDataAccess.Client.OracleCommand cmd0) + { + cmd0.BindByName = true; + cmd0.InitialLONGFetchSize = -1; + + } + return cmd; + } + + } + + private static readonly CommonCommandFactory DefaultCommandFactory = new(); + + private sealed class CommandFactory0 : CommonCommandFactory // + { + internal static readonly CommandFactory0 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::Dapper.SqlMapper.ICustomQueryParameter)! }); // expected shape + var ps = cmd.Parameters; + if (typed.ids is null) throw new global::System.InvalidOperationException("Member 'ids' is an ICustomQueryParameter and cannot be null"); + typed.ids.AddParameter(cmd.Command!, "ids"); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::Dapper.SqlMapper.ICustomQueryParameter)! }); // expected shape + var ps = cmd.Parameters; + + } + + } + + private sealed class CommandFactory1 : CommonCommandFactory // + { + internal static readonly CommandFactory1 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::Foo.CustomClass)!, id = default(int) }); // expected shape + var ps = cmd.Parameters; + global::System.Data.Common.DbParameter p; + if (typed.ids is null) throw new global::System.InvalidOperationException("Member 'ids' is an ICustomQueryParameter and cannot be null"); + typed.ids.AddParameter(cmd.Command!, "ids"); + + p = cmd.CreateParameter(); + p.ParameterName = "id"; + p.DbType = global::System.Data.DbType.Int32; + p.Direction = global::System.Data.ParameterDirection.Input; + p.Value = AsValue(typed.id); + ps.Add(p); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::Foo.CustomClass)!, id = default(int) }); // expected shape + var ps = cmd.Parameters; + ps[1].Value = AsValue(typed.id); + + } + + } + + private sealed class CommandFactory2 : CommonCommandFactory // + { + internal static readonly CommandFactory2 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::Foo.CustomStruct) }); // expected shape + var ps = cmd.Parameters; + typed.ids.AddParameter(cmd.Command!, "ids"); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::Foo.CustomStruct) }); // expected shape + var ps = cmd.Parameters; + + } + + } + + + } +} +namespace System.Runtime.CompilerServices +{ + // this type is needed by the compiler to implement interceptors - it doesn't need to + // come from the runtime itself, though + + [global::System.Diagnostics.Conditional("DEBUG")] // not needed post-build, so: evaporate + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber) + { + _ = path; + _ = lineNumber; + _ = columnNumber; + } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/CustomParameters.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/CustomParameters.output.netfx.cs new file mode 100644 index 00000000..dd032334 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/CustomParameters.output.netfx.cs @@ -0,0 +1,162 @@ +#nullable enable +#pragma warning disable IDE0078 // unnecessary suppression is necessary +#pragma warning disable CS9270 // SDK-dependent change to interceptors usage +namespace Dapper.AOT // interceptors must be in a known namespace +{ + file static class DapperGeneratedInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\CustomParameters.input.cs", 13, 24)] + internal static global::System.Collections.Generic.IEnumerable Query0(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, TypedResult, HasParameters, Buffered, Text, KnownParameters + // takes parameter: + // parameter map: ids + // returns data: int + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory0.Instance).QueryBuffered(param, global::Dapper.RowFactory.Inbuilt.Value()); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\CustomParameters.input.cs", 14, 24)] + internal static global::System.Collections.Generic.IEnumerable Query1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, TypedResult, HasParameters, Buffered, Text, KnownParameters + // takes parameter: + // parameter map: id ids + // returns data: int + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).QueryBuffered(param, global::Dapper.RowFactory.Inbuilt.Value()); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\CustomParameters.input.cs", 15, 24)] + internal static global::System.Collections.Generic.IEnumerable Query2(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, TypedResult, HasParameters, Buffered, Text, KnownParameters + // takes parameter: + // parameter map: ids + // returns data: int + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory2.Instance).QueryBuffered(param, global::Dapper.RowFactory.Inbuilt.Value()); + + } + + private class CommonCommandFactory : global::Dapper.CommandFactory + { + public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, T args) + { + var cmd = base.GetCommand(connection, sql, commandType, args); + // apply special per-provider command initialization logic for OracleCommand + if (cmd is global::Oracle.ManagedDataAccess.Client.OracleCommand cmd0) + { + cmd0.BindByName = true; + cmd0.InitialLONGFetchSize = -1; + + } + return cmd; + } + + } + + private static readonly CommonCommandFactory DefaultCommandFactory = new(); + + private sealed class CommandFactory0 : CommonCommandFactory // + { + internal static readonly CommandFactory0 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::Dapper.SqlMapper.ICustomQueryParameter)! }); // expected shape + var ps = cmd.Parameters; + if (typed.ids is null) throw new global::System.InvalidOperationException("Member 'ids' is an ICustomQueryParameter and cannot be null"); + typed.ids.AddParameter(cmd.Command!, "ids"); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::Dapper.SqlMapper.ICustomQueryParameter)! }); // expected shape + var ps = cmd.Parameters; + + } + + } + + private sealed class CommandFactory1 : CommonCommandFactory // + { + internal static readonly CommandFactory1 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::Foo.CustomClass)!, id = default(int) }); // expected shape + var ps = cmd.Parameters; + global::System.Data.Common.DbParameter p; + if (typed.ids is null) throw new global::System.InvalidOperationException("Member 'ids' is an ICustomQueryParameter and cannot be null"); + typed.ids.AddParameter(cmd.Command!, "ids"); + + p = cmd.CreateParameter(); + p.ParameterName = "id"; + p.DbType = global::System.Data.DbType.Int32; + p.Direction = global::System.Data.ParameterDirection.Input; + p.Value = AsValue(typed.id); + ps.Add(p); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::Foo.CustomClass)!, id = default(int) }); // expected shape + var ps = cmd.Parameters; + ps[1].Value = AsValue(typed.id); + + } + + } + + private sealed class CommandFactory2 : CommonCommandFactory // + { + internal static readonly CommandFactory2 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::Foo.CustomStruct) }); // expected shape + var ps = cmd.Parameters; + typed.ids.AddParameter(cmd.Command!, "ids"); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::Foo.CustomStruct) }); // expected shape + var ps = cmd.Parameters; + + } + + } + + + } +} +namespace System.Runtime.CompilerServices +{ + // this type is needed by the compiler to implement interceptors - it doesn't need to + // come from the runtime itself, though + + [global::System.Diagnostics.Conditional("DEBUG")] // not needed post-build, so: evaporate + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber) + { + _ = path; + _ = lineNumber; + _ = columnNumber; + } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/CustomParameters.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/CustomParameters.output.netfx.txt new file mode 100644 index 00000000..a65e2434 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/CustomParameters.output.netfx.txt @@ -0,0 +1,4 @@ +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 3 of 5 enabled call-sites (0 unsupported API, 2 skipped due to diagnostics) using 3 interceptors, 3 commands and 0 readers diff --git a/test/Dapper.AOT.Test/Interceptors/CustomParameters.output.txt b/test/Dapper.AOT.Test/Interceptors/CustomParameters.output.txt new file mode 100644 index 00000000..a65e2434 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/CustomParameters.output.txt @@ -0,0 +1,4 @@ +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 3 of 5 enabled call-sites (0 unsupported API, 2 skipped due to diagnostics) using 3 interceptors, 3 commands and 0 readers diff --git a/test/Dapper.AOT.Test/Interceptors/ListExpansion.input.cs b/test/Dapper.AOT.Test/Interceptors/ListExpansion.input.cs new file mode 100644 index 00000000..e0ed4f5c --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/ListExpansion.input.cs @@ -0,0 +1,37 @@ +using Dapper; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; + +[module: DapperAot] + +public static class Foo +{ + static void SomeCode(DbConnection connection) + { + // list expansion: the member binds via Dapper's own PackListParameters, which owns + // the whole in-list contract (rewrite, empty form, padding, split, DbString items) + _ = connection.Query("select Id from Customers where Id in @ids", new { ids = new[] { 1, 2, 3 } }); + _ = connection.Query("select Id from Customers where Id in @ids and Region = @region", new { ids = new List { 1 }, region = "north" }); + _ = connection.Query("select Name from Customers where Name in @names", new { names = new[] { "a", "b" } }); + + // skipped: expansion adds a runtime-variable number of parameters, so the by-index + // read-back of the output parameter cannot be trusted; stays on vanilla Dapper + _ = connection.Execute("declare @dummy int; select @total = count(1) from Customers where Id in @ids", new WithOutput { ids = new[] { 1, 2 } }); + + // skipped: multi-exec batch reuse updates parameters in-place, which cannot re-expand + // a list whose size changed between items; stays on vanilla Dapper + _ = connection.Execute("insert Audit (Id) select v from @ids", new[] { new WithList(), new WithList() }); + } + + public class WithOutput + { + public int[] ids { get; set; } = System.Array.Empty(); + [DbValue(Direction = ParameterDirection.Output)] + public int total { get; set; } + } + public class WithList + { + public int[] ids { get; set; } = System.Array.Empty(); + } +} diff --git a/test/Dapper.AOT.Test/Interceptors/ListExpansion.output.cs b/test/Dapper.AOT.Test/Interceptors/ListExpansion.output.cs new file mode 100644 index 00000000..41e198b8 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/ListExpansion.output.cs @@ -0,0 +1,166 @@ +#nullable enable +#pragma warning disable IDE0078 // unnecessary suppression is necessary +#pragma warning disable CS9270 // SDK-dependent change to interceptors usage +namespace Dapper.AOT // interceptors must be in a known namespace +{ + file static class DapperGeneratedInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\ListExpansion.input.cs", 14, 24)] + internal static global::System.Collections.Generic.IEnumerable Query0(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, TypedResult, HasParameters, Buffered, Text, KnownParameters + // takes parameter: + // parameter map: ids + // returns data: int + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory0.Instance).QueryBuffered(param, global::Dapper.RowFactory.Inbuilt.Value()); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\ListExpansion.input.cs", 15, 24)] + internal static global::System.Collections.Generic.IEnumerable Query1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, TypedResult, HasParameters, Buffered, Text, KnownParameters + // takes parameter: ids, string region> + // parameter map: ids region + // returns data: int + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).QueryBuffered(param, global::Dapper.RowFactory.Inbuilt.Value()); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\ListExpansion.input.cs", 16, 24)] + internal static global::System.Collections.Generic.IEnumerable Query2(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, TypedResult, HasParameters, Buffered, Text, KnownParameters + // takes parameter: + // parameter map: names + // returns data: int + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory2.Instance).QueryBuffered(param, global::Dapper.RowFactory.Inbuilt.Value()); + + } + + private class CommonCommandFactory : global::Dapper.CommandFactory + { + public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, T args) + { + var cmd = base.GetCommand(connection, sql, commandType, args); + // apply special per-provider command initialization logic for OracleCommand + if (cmd is global::Oracle.ManagedDataAccess.Client.OracleCommand cmd0) + { + cmd0.BindByName = true; + cmd0.InitialLONGFetchSize = -1; + + } + return cmd; + } + + } + + private static readonly CommonCommandFactory DefaultCommandFactory = new(); + + private sealed class CommandFactory0 : CommonCommandFactory // + { + internal static readonly CommandFactory0 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(int[])! }); // expected shape + var ps = cmd.Parameters; + #pragma warning disable CS0618 // list-expansion: this *is* the library usage + global::Dapper.SqlMapper.PackListParameters(cmd.Command!, "ids", typed.ids); + #pragma warning restore CS0618 + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(int[])! }); // expected shape + var ps = cmd.Parameters; + + } + + } + + private sealed class CommandFactory1 : CommonCommandFactory // ids, string region> + { + internal static readonly CommandFactory1 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::System.Collections.Generic.List)!, region = default(string)! }); // expected shape + var ps = cmd.Parameters; + global::System.Data.Common.DbParameter p; + #pragma warning disable CS0618 // list-expansion: this *is* the library usage + global::Dapper.SqlMapper.PackListParameters(cmd.Command!, "ids", typed.ids); + #pragma warning restore CS0618 + + p = cmd.CreateParameter(); + p.ParameterName = "region"; + p.DbType = global::System.Data.DbType.String; + p.Direction = global::System.Data.ParameterDirection.Input; + SetValueWithDefaultSize(p, typed.region); + ps.Add(p); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::System.Collections.Generic.List)!, region = default(string)! }); // expected shape + var ps = cmd.Parameters; + ps[1].Value = AsValue(typed.region); + + } + + } + + private sealed class CommandFactory2 : CommonCommandFactory // + { + internal static readonly CommandFactory2 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { names = default(string[])! }); // expected shape + var ps = cmd.Parameters; + #pragma warning disable CS0618 // list-expansion: this *is* the library usage + global::Dapper.SqlMapper.PackListParameters(cmd.Command!, "names", typed.names); + #pragma warning restore CS0618 + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { names = default(string[])! }); // expected shape + var ps = cmd.Parameters; + + } + + } + + + } +} +namespace System.Runtime.CompilerServices +{ + // this type is needed by the compiler to implement interceptors - it doesn't need to + // come from the runtime itself, though + + [global::System.Diagnostics.Conditional("DEBUG")] // not needed post-build, so: evaporate + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber) + { + _ = path; + _ = lineNumber; + _ = columnNumber; + } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/ListExpansion.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/ListExpansion.output.netfx.cs new file mode 100644 index 00000000..41e198b8 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/ListExpansion.output.netfx.cs @@ -0,0 +1,166 @@ +#nullable enable +#pragma warning disable IDE0078 // unnecessary suppression is necessary +#pragma warning disable CS9270 // SDK-dependent change to interceptors usage +namespace Dapper.AOT // interceptors must be in a known namespace +{ + file static class DapperGeneratedInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\ListExpansion.input.cs", 14, 24)] + internal static global::System.Collections.Generic.IEnumerable Query0(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, TypedResult, HasParameters, Buffered, Text, KnownParameters + // takes parameter: + // parameter map: ids + // returns data: int + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory0.Instance).QueryBuffered(param, global::Dapper.RowFactory.Inbuilt.Value()); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\ListExpansion.input.cs", 15, 24)] + internal static global::System.Collections.Generic.IEnumerable Query1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, TypedResult, HasParameters, Buffered, Text, KnownParameters + // takes parameter: ids, string region> + // parameter map: ids region + // returns data: int + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).QueryBuffered(param, global::Dapper.RowFactory.Inbuilt.Value()); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\ListExpansion.input.cs", 16, 24)] + internal static global::System.Collections.Generic.IEnumerable Query2(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, TypedResult, HasParameters, Buffered, Text, KnownParameters + // takes parameter: + // parameter map: names + // returns data: int + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory2.Instance).QueryBuffered(param, global::Dapper.RowFactory.Inbuilt.Value()); + + } + + private class CommonCommandFactory : global::Dapper.CommandFactory + { + public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, T args) + { + var cmd = base.GetCommand(connection, sql, commandType, args); + // apply special per-provider command initialization logic for OracleCommand + if (cmd is global::Oracle.ManagedDataAccess.Client.OracleCommand cmd0) + { + cmd0.BindByName = true; + cmd0.InitialLONGFetchSize = -1; + + } + return cmd; + } + + } + + private static readonly CommonCommandFactory DefaultCommandFactory = new(); + + private sealed class CommandFactory0 : CommonCommandFactory // + { + internal static readonly CommandFactory0 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(int[])! }); // expected shape + var ps = cmd.Parameters; + #pragma warning disable CS0618 // list-expansion: this *is* the library usage + global::Dapper.SqlMapper.PackListParameters(cmd.Command!, "ids", typed.ids); + #pragma warning restore CS0618 + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(int[])! }); // expected shape + var ps = cmd.Parameters; + + } + + } + + private sealed class CommandFactory1 : CommonCommandFactory // ids, string region> + { + internal static readonly CommandFactory1 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::System.Collections.Generic.List)!, region = default(string)! }); // expected shape + var ps = cmd.Parameters; + global::System.Data.Common.DbParameter p; + #pragma warning disable CS0618 // list-expansion: this *is* the library usage + global::Dapper.SqlMapper.PackListParameters(cmd.Command!, "ids", typed.ids); + #pragma warning restore CS0618 + + p = cmd.CreateParameter(); + p.ParameterName = "region"; + p.DbType = global::System.Data.DbType.String; + p.Direction = global::System.Data.ParameterDirection.Input; + SetValueWithDefaultSize(p, typed.region); + ps.Add(p); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { ids = default(global::System.Collections.Generic.List)!, region = default(string)! }); // expected shape + var ps = cmd.Parameters; + ps[1].Value = AsValue(typed.region); + + } + + } + + private sealed class CommandFactory2 : CommonCommandFactory // + { + internal static readonly CommandFactory2 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { names = default(string[])! }); // expected shape + var ps = cmd.Parameters; + #pragma warning disable CS0618 // list-expansion: this *is* the library usage + global::Dapper.SqlMapper.PackListParameters(cmd.Command!, "names", typed.names); + #pragma warning restore CS0618 + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { names = default(string[])! }); // expected shape + var ps = cmd.Parameters; + + } + + } + + + } +} +namespace System.Runtime.CompilerServices +{ + // this type is needed by the compiler to implement interceptors - it doesn't need to + // come from the runtime itself, though + + [global::System.Diagnostics.Conditional("DEBUG")] // not needed post-build, so: evaporate + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber) + { + _ = path; + _ = lineNumber; + _ = columnNumber; + } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/ListExpansion.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/ListExpansion.output.netfx.txt new file mode 100644 index 00000000..a65e2434 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/ListExpansion.output.netfx.txt @@ -0,0 +1,4 @@ +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 3 of 5 enabled call-sites (0 unsupported API, 2 skipped due to diagnostics) using 3 interceptors, 3 commands and 0 readers diff --git a/test/Dapper.AOT.Test/Interceptors/ListExpansion.output.txt b/test/Dapper.AOT.Test/Interceptors/ListExpansion.output.txt new file mode 100644 index 00000000..a65e2434 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/ListExpansion.output.txt @@ -0,0 +1,4 @@ +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 3 of 5 enabled call-sites (0 unsupported API, 2 skipped due to diagnostics) using 3 interceptors, 3 commands and 0 readers diff --git a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.cs b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.cs index 1be948cd..0bdef036 100644 --- a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.cs @@ -287,19 +287,15 @@ public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? { var typed = Cast(args, static () => new { ids = default(int[])! }); // expected shape var ps = cmd.Parameters; - global::System.Data.Common.DbParameter p; - p = cmd.CreateParameter(); - p.ParameterName = "ids"; - p.Direction = global::System.Data.ParameterDirection.Input; - p.Value = AsValue(typed.ids); - ps.Add(p); + #pragma warning disable CS0618 // list-expansion: this *is* the library usage + global::Dapper.SqlMapper.PackListParameters(cmd.Command!, "ids", typed.ids); + #pragma warning restore CS0618 } public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) { var typed = Cast(args, static () => new { ids = default(int[])! }); // expected shape var ps = cmd.Parameters; - ps[0].Value = AsValue(typed.ids); } diff --git a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.cs index 1be948cd..0bdef036 100644 --- a/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/TsqlTips.output.netfx.cs @@ -287,19 +287,15 @@ public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? { var typed = Cast(args, static () => new { ids = default(int[])! }); // expected shape var ps = cmd.Parameters; - global::System.Data.Common.DbParameter p; - p = cmd.CreateParameter(); - p.ParameterName = "ids"; - p.Direction = global::System.Data.ParameterDirection.Input; - p.Value = AsValue(typed.ids); - ps.Add(p); + #pragma warning disable CS0618 // list-expansion: this *is* the library usage + global::Dapper.SqlMapper.PackListParameters(cmd.Command!, "ids", typed.ids); + #pragma warning restore CS0618 } public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) { var typed = Cast(args, static () => new { ids = default(int[])! }); // expected shape var ps = cmd.Parameters; - ps[0].Value = AsValue(typed.ids); }