diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs index a051eaba..332017f2 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs @@ -484,6 +484,7 @@ internal static Location SharedParseArgsAndFlags(in ParseState ctx, IInvocationO argExpression = null; sql = null; bool? buffered = null; + // check the args foreach (var arg in op.Arguments) { @@ -524,6 +525,11 @@ internal static Location SharedParseArgsAndFlags(in ParseState ctx, IInvocationO case "cnn": case "commandTimeout": case "transaction": + case "reader": + case "startIndex": + case "length": + case "returnNullIfFirstMissing": + case "concreteType" when arg.Value is IDefaultValueOperation || (arg.ConstantValue.HasValue && arg.ConstantValue.Value is null): // nothing to do break; case "commandType": diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Multi.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Multi.cs index 6fe60419..7f47f49f 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Multi.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Multi.cs @@ -48,8 +48,7 @@ void WriteMultiExecExpression(ITypeSymbol elementType, string castType) { sb.Append(", cancellationToken: ").Append(Forward(methodParameters, "cancellationToken")); } - sb.Append(");"); - sb.NewLine().Outdent().NewLine().NewLine(); + sb.Append(");").NewLine(); } void WriteBatchCommandArguments(ITypeSymbol elementType) @@ -91,7 +90,7 @@ void WriteBatchCommandArguments(ITypeSymbol elementType) // commandFactory if (flags.HasAny(OperationFlags.HasParameters)) { - var index = factories.GetIndex(elementType, map, cache, true, additionalCommandState, out var subIndex); + var index = factories.GetIndex(elementType, map, cache, additionalCommandState, out var subIndex); sb.Append("CommandFactory").Append(index).Append(".Instance").Append(subIndex); } else diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Single.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Single.cs index a43e4524..bac9032d 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Single.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.Single.cs @@ -7,7 +7,7 @@ namespace Dapper.CodeAnalysis; public sealed partial class DapperInterceptorGenerator { - void WriteSingleImplementation( + static void WriteSingleImplementation( CodeWriter sb, IMethodSymbol method, ITypeSymbol? resultType, @@ -54,7 +54,7 @@ void WriteSingleImplementation( sb.Append(", ").Append(Forward(methodParameters, "commandTimeout")).Append(HasParam(methodParameters, "commandTimeout") ? ".GetValueOrDefault()" : "").Append(", "); if (flags.HasAny(OperationFlags.HasParameters)) { - var index = factories.GetIndex(parameterType!, map, cache, false, additionalCommandState, out var subIndex); + var index = factories.GetIndex(parameterType!, map, cache, additionalCommandState, out var subIndex); sb.Append("CommandFactory").Append(index).Append(".Instance").Append(subIndex); } else @@ -92,83 +92,7 @@ void WriteSingleImplementation( break; } } - if (IsInbuilt(resultType, out var helper)) - { - sb.Append("global::Dapper.RowFactory.Inbuilt.").Append(helper); - } - else - { - sb.Append("RowFactory").Append(readers.GetIndex(resultType!)).Append(".Instance"); - } - - static bool IsInbuilt(ITypeSymbol? type, out string? helper) - { - if (type is null || type.TypeKind == TypeKind.Dynamic) - { - helper = "Dynamic"; - return true; - } - if (type.SpecialType == SpecialType.System_Object) - { - helper = "Object"; - return true; - } - if (Inspection.IdentifyDbType(type, out _) is not null) - { - bool nullable = type.IsValueType && type.NullableAnnotation == NullableAnnotation.Annotated; - helper = (nullable ? "NullableValue<" : "Value<") + CodeWriter.GetTypeName( - nullable ? Inspection.MakeNonNullable(type) : type) + ">()"; - return true; - } - if (type is INamedTypeSymbol { Arity: 0 }) - { - if (type is - { - TypeKind: TypeKind.Interface, - Name: "IDataRecord", - ContainingType: null, - ContainingNamespace: - { - Name: "Data", - ContainingNamespace: - { - Name: "System", - ContainingNamespace.IsGlobalNamespace: true - } - } - }) - { - helper = "IDataRecord"; - return true; - } - if (type is - { - TypeKind: TypeKind.Class, - Name: "DbDataRecord", - ContainingType: null, - ContainingNamespace: - { - Name: "Common", - ContainingNamespace: - { - Name: "Data", - ContainingNamespace: - { - Name: "System", - ContainingNamespace.IsGlobalNamespace: true - } - } - } - }) - { - helper = "DbDataRecord"; - return true; - } - } - helper = null; - return false; - - } + sb.AppendReader(resultType, readers); } else if (flags.HasAny(OperationFlags.Execute)) { @@ -227,7 +151,7 @@ static bool IsInbuilt(ITypeSymbol? type, out string? helper) sb.Append("!"); } } - sb.Append(";").NewLine().Outdent().NewLine().NewLine(); + sb.Append(";").NewLine(); static CodeWriter WriteTypedArg(CodeWriter sb, ITypeSymbol? parameterType) { diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs index 62efc6ca..b0ca9fbc 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs @@ -41,7 +41,10 @@ public override void Initialize(IncrementalGeneratorInitializationContext contex } // very fast and light-weight; we'll worry about the rest later from the semantic tree - internal static bool IsCandidate(string methodName) => methodName.StartsWith("Execute") || methodName.StartsWith("Query"); + internal static bool IsCandidate(string methodName) => + methodName.StartsWith("Execute") + || methodName.StartsWith("Query") + || methodName.StartsWith("GetRowParser"); internal bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) { @@ -213,6 +216,8 @@ private void Generate(SourceProductionContext ctx, (Compilation Compilation, Imm } const string DapperBaseCommandFactory = "global::Dapper.CommandFactory"; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Allow expectation of state")] internal void Generate(in GenerateState ctx) { if (!CheckPrerequisites(ctx)) // also reports per-item diagnostics @@ -221,7 +226,7 @@ internal void Generate(in GenerateState ctx) return; } - var dbCommandTypes = IdentifyDbCommandTypes(ctx.Compilation, out var needsCommandPrep, out bool frameworkHasBatchAPI); + var dbCommandTypes = IdentifyDbCommandTypes(ctx.Compilation, out var needsCommandPrep); bool allowUnsafe = ctx.Compilation.Options is CSharpCompilationOptions cSharp && cSharp.AllowUnsafe; var sb = new CodeWriter().Append("#nullable enable").NewLine() @@ -293,18 +298,22 @@ internal void Generate(in GenerateState ctx) var commandTypeMode = flags & (OperationFlags.Text | OperationFlags.StoredProcedure | OperationFlags.TableDirect); var methodParameters = grp.Key.Method.Parameters; string? fixedSql = null; - if (flags.HasAny(OperationFlags.IncludeLocation)) - { - var origin = grp.Single(); - fixedSql = origin.Sql; // expect exactly one SQL - sb.Append("global::System.Diagnostics.Debug.Assert(sql == ") - .AppendVerbatimLiteral(fixedSql).Append(");").NewLine(); - var path = origin.Location.GetMappedLineSpan(); - fixedSql = $"-- {path.Path}#{path.StartLinePosition.Line + 1}\r\n{fixedSql}"; - } - else + + if (HasParam(methodParameters, "sql")) { - sb.Append("global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql));").NewLine(); + if (flags.HasAny(OperationFlags.IncludeLocation)) + { + var origin = grp.Single(); + fixedSql = origin.Sql; // expect exactly one SQL + sb.Append("global::System.Diagnostics.Debug.Assert(sql == ") + .AppendVerbatimLiteral(fixedSql).Append(");").NewLine(); + var path = origin.Location.GetMappedLineSpan(); + fixedSql = $"-- {path.Path}#{path.StartLinePosition.Line + 1}\r\n{fixedSql}"; + } + else + { + sb.Append("global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql));").NewLine(); + } } if (HasParam(methodParameters, "commandType")) { @@ -320,12 +329,28 @@ internal void Generate(in GenerateState ctx) sb.Append("global::System.Diagnostics.Debug.Assert(buffered is ").Append((flags & OperationFlags.Buffered) != 0).Append(");").NewLine(); } - sb.Append("global::System.Diagnostics.Debug.Assert(param is ").Append(flags.HasAny(OperationFlags.HasParameters) ? "not " : "").Append("null);").NewLine().NewLine(); + if (HasParam(methodParameters, "param")) + { + sb.Append("global::System.Diagnostics.Debug.Assert(param is ").Append(flags.HasAny(OperationFlags.HasParameters) ? "not " : "").Append("null);").NewLine(); + } + + if (HasParam(methodParameters, "concreteType")) + { + sb.Append("global::System.Diagnostics.Debug.Assert(concreteType is null);").NewLine(); + } - if (!TryWriteMultiExecImplementation(sb, flags, commandTypeMode, parameterType, grp.Key.ParameterMap, grp.Key.UniqueLocation is not null, methodParameters, factories, fixedSql, additionalCommandState)) + sb.NewLine(); + + if (flags.HasAny(OperationFlags.GetRowParser)) + { + WriteGetRowParser(sb, resultType, readers); + } + else if (!TryWriteMultiExecImplementation(sb, flags, commandTypeMode, parameterType, grp.Key.ParameterMap, grp.Key.UniqueLocation is not null, methodParameters, factories, fixedSql, additionalCommandState)) { WriteSingleImplementation(sb, method, resultType, flags, commandTypeMode, parameterType, grp.Key.ParameterMap, grp.Key.UniqueLocation is not null, methodParameters, factories, readers, fixedSql, additionalCommandState); } + + sb.Outdent().NewLine().NewLine(); } var baseCommandFactory = GetCommandFactory(ctx.Compilation, out var canConstruct) ?? DapperBaseCommandFactory; @@ -384,7 +409,7 @@ internal void Generate(in GenerateState ctx) foreach (var tuple in factories) { - WriteCommandFactory(ctx, baseCommandFactory, sb, tuple.Type, tuple.Index, tuple.Map, tuple.CacheCount, tuple.AdditionalCommandState, tuple.SupportBatch && frameworkHasBatchAPI); + WriteCommandFactory(ctx, baseCommandFactory, sb, tuple.Type, tuple.Index, tuple.Map, tuple.CacheCount, tuple.AdditionalCommandState); } sb.Outdent().Outdent(); // ends our generated file-scoped class and the namespace @@ -396,7 +421,13 @@ internal void Generate(in GenerateState ctx) ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.InterceptorsGenerated, null, callSiteCount, ctx.Nodes.Length, methodIndex, factories.Count(), readers.Count())); } - private static void WriteCommandFactory(in GenerateState ctx, string baseFactory, CodeWriter sb, ITypeSymbol type, int index, string map, int cacheCount, AdditionalCommandState? additionalCommandState, bool supportBatch) + private static void WriteGetRowParser(CodeWriter sb, ITypeSymbol? resultType, in RowReaderState readers) + { + sb.Append("return ").AppendReader(resultType, readers) + .Append(".GetRowParser(reader, startIndex, length, returnNullIfFirstMissing);").NewLine(); + } + + private static void WriteCommandFactory(in GenerateState ctx, string baseFactory, CodeWriter sb, ITypeSymbol type, int index, string map, int cacheCount, AdditionalCommandState? additionalCommandState) { var declaredType = type.IsAnonymousType ? "object?" : CodeWriter.GetTypeName(type); sb.Append("private ").Append(cacheCount <= 1 ? "sealed" : "abstract").Append(" class CommandFactory").Append(index).Append(" : ") @@ -467,11 +498,6 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory } sb.Outdent().NewLine(); } - - if (supportBatch) - { - sb.Append("public override bool SupportBatch => true;").NewLine(); - } } if ((flags & WriteArgsFlags.CanPrepare) != 0) @@ -1174,17 +1200,15 @@ private enum SpecialCommandFlags InitialLONGFetchSize = 1 << 1, } - private static ImmutableArray IdentifyDbCommandTypes(Compilation compilation, out bool needsPrepare, out bool hasBatchAPI) + private static ImmutableArray IdentifyDbCommandTypes(Compilation compilation, out bool needsPrepare) { needsPrepare = false; var dbCommand = compilation.GetTypeByMetadataName("System.Data.Common.DbCommand"); if (dbCommand is null) { // if we can't find DbCommand, we're out of luck - hasBatchAPI = false; return ImmutableArray.Empty; } - hasBatchAPI = compilation.GetTypeByMetadataName("System.Data.Common.DbBatch") is not null; var pending = new Queue(); foreach (var assemblyName in compilation.References) diff --git a/src/Dapper.AOT.Analyzers/Internal/CodeWriter.cs b/src/Dapper.AOT.Analyzers/Internal/CodeWriter.cs index 3ced576d..5e4b6feb 100644 --- a/src/Dapper.AOT.Analyzers/Internal/CodeWriter.cs +++ b/src/Dapper.AOT.Analyzers/Internal/CodeWriter.cs @@ -319,4 +319,85 @@ public string ToStringRecycle() Interlocked.Exchange(ref s_Spare, this); return s; } + + internal CodeWriter AppendReader(ITypeSymbol? resultType, RowReaderState readers) + { + if (IsInbuilt(resultType, out var helper)) + { + return Append("global::Dapper.RowFactory.Inbuilt.").Append(helper); + } + else + { + return Append("RowFactory").Append(readers.GetIndex(resultType!)).Append(".Instance"); + } + + static bool IsInbuilt(ITypeSymbol? type, out string? helper) + { + if (type is null || type.TypeKind == TypeKind.Dynamic) + { + helper = "Dynamic"; + return true; + } + if (type.SpecialType == SpecialType.System_Object) + { + helper = "Object"; + return true; + } + if (Inspection.IdentifyDbType(type, out _) is not null) + { + bool nullable = type.IsValueType && type.NullableAnnotation == NullableAnnotation.Annotated; + helper = (nullable ? "NullableValue<" : "Value<") + CodeWriter.GetTypeName( + nullable ? Inspection.MakeNonNullable(type) : type) + ">()"; + return true; + } + if (type is INamedTypeSymbol { Arity: 0 }) + { + if (type is + { + TypeKind: TypeKind.Interface, + Name: "IDataRecord", + ContainingType: null, + ContainingNamespace: + { + Name: "Data", + ContainingNamespace: + { + Name: "System", + ContainingNamespace.IsGlobalNamespace: true + } + } + }) + { + helper = "IDataRecord"; + return true; + } + if (type is + { + TypeKind: TypeKind.Class, + Name: "DbDataRecord", + ContainingType: null, + ContainingNamespace: + { + Name: "Common", + ContainingNamespace: + { + Name: "Data", + ContainingNamespace: + { + Name: "System", + ContainingNamespace.IsGlobalNamespace: true + } + } + } + }) + { + helper = "DbDataRecord"; + return true; + } + } + helper = null; + return false; + + } + } } diff --git a/src/Dapper.AOT.Analyzers/Internal/CommandFactoryState.cs b/src/Dapper.AOT.Analyzers/Internal/CommandFactoryState.cs index bc9878c4..836968bb 100644 --- a/src/Dapper.AOT.Analyzers/Internal/CommandFactoryState.cs +++ b/src/Dapper.AOT.Analyzers/Internal/CommandFactoryState.cs @@ -6,12 +6,12 @@ namespace Dapper.Internal; -internal readonly struct CommandFactoryState : IEnumerable<(ITypeSymbol Type, string Map, int Index, int CacheCount, bool SupportBatch, AdditionalCommandState? AdditionalCommandState)> +internal readonly struct CommandFactoryState : IEnumerable<(ITypeSymbol Type, string Map, int Index, int CacheCount, AdditionalCommandState? AdditionalCommandState)> { public CommandFactoryState(Compilation compilation) => systemObject = compilation.GetSpecialType(SpecialType.System_Object); private readonly ITypeSymbol systemObject; - private readonly Dictionary<(ITypeSymbol Type, string Map, bool Cached, AdditionalCommandState? AdditionalCommandState), (int Index, int CacheCount, bool SupportBatch)> parameterTypes = new(ParameterTypeMapComparer.Instance); + private readonly Dictionary<(ITypeSymbol Type, string Map, bool Cached, AdditionalCommandState? AdditionalCommandState), (int Index, int CacheCount)> parameterTypes = new(ParameterTypeMapComparer.Instance); public int Count() { @@ -24,15 +24,15 @@ public int Count() return total; } - public IEnumerator<(ITypeSymbol Type, string Map, int Index, int CacheCount, bool SupportBatch, AdditionalCommandState? AdditionalCommandState)> GetEnumerator() + public IEnumerator<(ITypeSymbol Type, string Map, int Index, int CacheCount, AdditionalCommandState? AdditionalCommandState)> GetEnumerator() { // retain discovery order - return parameterTypes.OrderBy(x => x.Value.Index).Select(x => (x.Key.Type, x.Key.Map, x.Value.Index, x.Value.CacheCount, x.Value.SupportBatch, x.Key.AdditionalCommandState)).GetEnumerator(); + return parameterTypes.OrderBy(x => x.Value.Index).Select(x => (x.Key.Type, x.Key.Map, x.Value.Index, x.Value.CacheCount, x.Key.AdditionalCommandState)).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public int GetIndex(ITypeSymbol type, string map, bool cache, bool supportBatch, AdditionalCommandState? additionalCommandState, out int? subIndex) + public int GetIndex(ITypeSymbol type, string map, bool cache, AdditionalCommandState? additionalCommandState, out int? subIndex) { if (string.IsNullOrWhiteSpace(map) && type.IsReferenceType) { @@ -47,14 +47,10 @@ public int GetIndex(ITypeSymbol type, string map, bool cache, bool supportBatch, if (cache) { subIndex = value.CacheCount; - parameterTypes[key] = new(index, value.CacheCount + 1, value.SupportBatch || supportBatch); + parameterTypes[key] = new(index, value.CacheCount + 1); } else { - if (supportBatch && !value.SupportBatch) - { // trigger batch mode on - parameterTypes[key] = new(index, value.CacheCount, true); - } subIndex = null; } } @@ -62,7 +58,7 @@ public int GetIndex(ITypeSymbol type, string map, bool cache, bool supportBatch, { index = parameterTypes.Count; subIndex = cache ? 0 : null; - parameterTypes.Add(key, (index, cache ? 1 : 0, supportBatch)); + parameterTypes.Add(key, (index, cache ? 1 : 0)); } return index; } diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index cd61ee07..d0f9a690 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -918,7 +918,7 @@ public static bool IsDapperMethod(this IInvocationOperation operation, out Opera case "Query": flags |= OperationFlags.Query; if (method.Arity > 1) flags |= OperationFlags.NotAotSupported; - return true; + break; case "QueryAsync": case "QueryUnbufferedAsync": flags |= method.Name.Contains("Unbuffered") ? OperationFlags.Unbuffered : OperationFlags.Buffered; @@ -942,21 +942,26 @@ public static bool IsDapperMethod(this IInvocationOperation operation, out Opera case "QueryMultiple": case "QueryMultipleAsync": flags |= OperationFlags.Query | OperationFlags.QueryMultiple | OperationFlags.NotAotSupported; - return true; + break; case "Execute": case "ExecuteAsync": flags |= OperationFlags.Execute; if (method.Arity != 0) flags |= OperationFlags.NotAotSupported; - return true; + break; case "ExecuteScalar": case "ExecuteScalarAsync": flags |= OperationFlags.Execute | OperationFlags.Scalar; if (method.Arity >= 2) flags |= OperationFlags.NotAotSupported; - return true; + break; + case "GetRowParser": + flags |= OperationFlags.GetRowParser; + if (method.Arity != 1) flags |= OperationFlags.NotAotSupported; + break; default: flags = OperationFlags.NotAotSupported; - return true; + break; } + return true; } public static bool HasAny(this OperationFlags value, OperationFlags testFor) => (value & testFor) != 0; public static bool HasAll(this OperationFlags value, OperationFlags testFor) => (value & testFor) == testFor; @@ -1243,5 +1248,7 @@ enum OperationFlags IncludeLocation = 1 << 20, // include -- SomeFile.cs#40 when possible KnownParameters = 1 << 21, QueryMultiple = 1 << 22, - NotAotSupported = 1 << 23, + GetRowParser = 1 << 23, + + NotAotSupported = 1 << 31, } diff --git a/src/Dapper.AOT/CommandFactory.cs b/src/Dapper.AOT/CommandFactory.cs index d5b0df1e..ea0e0dda 100644 --- a/src/Dapper.AOT/CommandFactory.cs +++ b/src/Dapper.AOT/CommandFactory.cs @@ -242,11 +242,4 @@ public virtual void UpdateParameters(in UnifiedCommand command, T args) /// public virtual bool RequirePostProcess => false; - -#if NET6_0_OR_GREATER - /// - /// Indicates whether this instance supports the API. - /// - public virtual bool SupportBatch => false; -#endif } \ No newline at end of file diff --git a/src/Dapper.AOT/CommandT.Batch.cs b/src/Dapper.AOT/CommandT.Batch.cs index 199431a6..65e10286 100644 --- a/src/Dapper.AOT/CommandT.Batch.cs +++ b/src/Dapper.AOT/CommandT.Batch.cs @@ -204,7 +204,7 @@ private int ExecuteMultiSequential(ReadOnlySpan source) #if NET6_0_OR_GREATER [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool UseBatch(int batchSize) => batchSize != 0 && connection is { CanCreateBatch: true } && commandFactory is { SupportBatch: true }; + private bool UseBatch(int batchSize) => batchSize != 0 && connection is { CanCreateBatch: true }; private DbBatchCommand AddCommand(ref UnifiedCommand state, TArgs args) { diff --git a/src/Dapper.AOT/CommandT.Query.cs b/src/Dapper.AOT/CommandT.Query.cs index f2ae8831..9b206bb4 100644 --- a/src/Dapper.AOT/CommandT.Query.cs +++ b/src/Dapper.AOT/CommandT.Query.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Data; -using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -47,7 +46,7 @@ public List QueryBuffered(TArgs args, [DapperAot] RowFactory? } else { - results = new(); + results = []; } // consume entire results (avoid unobserved TDS error messages) @@ -86,7 +85,7 @@ public async Task> QueryBufferedAsync(TArgs args, [DapperAot] R } else { - results = new(); + results = []; } // consume entire results (avoid unobserved TDS error messages) diff --git a/src/Dapper.AOT/RowFactory.cs b/src/Dapper.AOT/RowFactory.cs index aab5e031..99c9fa0e 100644 --- a/src/Dapper.AOT/RowFactory.cs +++ b/src/Dapper.AOT/RowFactory.cs @@ -1,13 +1,13 @@ using Dapper.Internal; using System; using System.Buffers; +using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using System.Threading; using System.Runtime.InteropServices; -using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Dapper; @@ -118,6 +118,34 @@ internal static Span Lease(int fieldCount, ref int[]? lease) return new Span(lease, 0, fieldCount); #endif } + + + [StructLayout(LayoutKind.Explicit, Size = 4 * sizeof(int))] + private protected struct TokenBuffer4 + { + [FieldOffset(0)] + public int PayloadStart; + [FieldOffset(0)] + private unsafe fixed int tokens[4]; + } + + [StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(int))] + private protected struct TokenBuffer8 + { + [FieldOffset(0)] + public int PayloadStart; + [FieldOffset(0)] + private unsafe fixed int tokens[8]; + } + + [StructLayout(LayoutKind.Explicit, Size = 16 * sizeof(int))] + private protected struct TokenBuffer16 + { + [FieldOffset(0)] + public int PayloadStart; + [FieldOffset(0)] + private unsafe fixed int tokens[16]; + } } /// @@ -218,5 +246,149 @@ public ValueTask ReadSingleAsync(DbDataReader reader, CancellationToken cance } return result; } + + /// + /// Gets the row parser for a specific row on a data reader. This allows for type switching every row based on, for example, a TypeId column. + /// You could return a collection of the base type but have each more specific. + /// + /// The data reader to get the parser for the current row from. + /// The start column index of the object (default: 0). + /// The length of columns to read (default: -1 = all fields following startIndex). + /// Return null if the value of the first column is null. + /// A parser for this specific object from this row. + public Func GetRowParser(DbDataReader reader, + int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false) + { + if (length < 0) // use everything remaining + { + length = reader.FieldCount - startIndex; + } + if (length == 0) + { + returnNullIfFirstMissing = false; // no "first" to ask about! + } + RowParserState state = length switch + { + 0 => new EmptyRowParserState(this, startIndex, length, returnNullIfFirstMissing), +#if NETCOREAPP3_1_OR_GREATER + <= 4 => new FixedSizeRowParserState4(this, startIndex, length, returnNullIfFirstMissing), + <= 8 => new FixedSizeRowParserState8(this, startIndex, length, returnNullIfFirstMissing), + <= 16 => new FixedSizeRowParserState16(this, startIndex, length, returnNullIfFirstMissing), +#endif + _ => new LargeRowParserState(this, startIndex, length, returnNullIfFirstMissing), + }; + state.Tokenize(reader); + return state.Parse; + } + + private abstract class RowParserState + { + // manual capture state for row reader; initialized with tokenized column metadata, then + // reused between different rows + protected RowParserState(RowFactory rowFactory, int columnOffset, bool returnNullIfFirstMissing) + { + this.rowFactory = rowFactory; + if (columnOffset < 0) Throw(); + columnOffsetAndReturnNullIfFirstMissing = columnOffset | (returnNullIfFirstMissing ? MSB : 0); + + static void Throw() => throw new ArgumentOutOfRangeException(nameof(columnOffset)); + } + + const int MSB = 1 << 31; + + private int ColumnOffset => columnOffsetAndReturnNullIfFirstMissing & ~MSB; + private bool ReturnNullIfFirstMissing => (columnOffsetAndReturnNullIfFirstMissing & MSB) != 0; + + private readonly RowFactory rowFactory; + private readonly int columnOffsetAndReturnNullIfFirstMissing; + private object? state; + protected abstract Span Tokens { get; } + + protected abstract ReadOnlySpan ReadOnlyTokens { get; } + public void Tokenize(DbDataReader reader) + => state = rowFactory.Tokenize(reader, Tokens, ColumnOffset); + + protected static void ValidateLength(int length, int max) + { + if (length < 0 || length > max) Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(length)); + } + + public T Parse(DbDataReader reader) + { + if (ReturnNullIfFirstMissing && reader.IsDBNull(ColumnOffset)) + { + return default!; + } + return rowFactory.Read(reader, ReadOnlyTokens, ColumnOffset, state); + } + } + + private sealed class EmptyRowParserState : RowParserState + { + public EmptyRowParserState(RowFactory rowFactory, int columnOffset, int length, bool returnNullIfFirstMissing) + : base(rowFactory, columnOffset, returnNullIfFirstMissing) + { + ValidateLength(length, 0); + } + + protected override Span Tokens => default; + protected override ReadOnlySpan ReadOnlyTokens => default; + } + +#if NETCOREAPP3_1_OR_GREATER // can optimize using spans over a payload struct + private sealed class FixedSizeRowParserState4 : RowParserState + { + private readonly int length; + private TokenBuffer4 tokenBuffer; + public FixedSizeRowParserState4(RowFactory rowFactory, int columnOffset, int length, bool returnNullIfFirstMissing) + : base(rowFactory, columnOffset, returnNullIfFirstMissing) + { + ValidateLength(length, 4); + this.length = length; + } + protected override Span Tokens => MemoryMarshal.CreateSpan(ref tokenBuffer.PayloadStart, length); + protected override ReadOnlySpan ReadOnlyTokens => MemoryMarshal.CreateReadOnlySpan(ref tokenBuffer.PayloadStart, length); + } + private sealed class FixedSizeRowParserState8 : RowParserState + { + private readonly int length; + private TokenBuffer8 tokenBuffer; + public FixedSizeRowParserState8(RowFactory rowFactory, int columnOffset, int length, bool returnNullIfFirstMissing) + : base(rowFactory, columnOffset, returnNullIfFirstMissing) + { + ValidateLength(length, 8); + this.length = length; + } + protected override Span Tokens => MemoryMarshal.CreateSpan(ref tokenBuffer.PayloadStart, length); + protected override ReadOnlySpan ReadOnlyTokens => MemoryMarshal.CreateReadOnlySpan(ref tokenBuffer.PayloadStart, length); + } + private sealed class FixedSizeRowParserState16 : RowParserState + { + private readonly int length; + private TokenBuffer16 tokenBuffer; + public FixedSizeRowParserState16(RowFactory rowFactory, int columnOffset, int length, bool returnNullIfFirstMissing) + : base(rowFactory, columnOffset, returnNullIfFirstMissing) + { + ValidateLength(length, 16); + this.length = length; + } + protected override Span Tokens => MemoryMarshal.CreateSpan(ref tokenBuffer.PayloadStart, length); + protected override ReadOnlySpan ReadOnlyTokens => MemoryMarshal.CreateReadOnlySpan(ref tokenBuffer.PayloadStart, length); + } +#endif + + private sealed class LargeRowParserState : RowParserState // fallback to arrays if nothing else possible + { + private readonly int[] tokens; + public LargeRowParserState(RowFactory rowFactory, int columnOffset, int length, bool returnNullIfFirstMissing) + : base(rowFactory, columnOffset, returnNullIfFirstMissing) + { + ValidateLength(length, 0X7FEFFFFF); + tokens = new int[length]; + } + protected override Span Tokens => new(tokens); + protected override ReadOnlySpan ReadOnlyTokens => new(tokens); + } } diff --git a/test/Dapper.AOT.Test/DapperApiTests.cs b/test/Dapper.AOT.Test/DapperApiTests.cs index a1205a91..2e3d3bf1 100644 --- a/test/Dapper.AOT.Test/DapperApiTests.cs +++ b/test/Dapper.AOT.Test/DapperApiTests.cs @@ -31,6 +31,6 @@ public void DiscoveredMethodsAreExpected() var candidates = string.Join(",", methods.Where(DapperInterceptorGenerator.IsCandidate)); Log.WriteLine(candidates); - Assert.Equal("Execute,ExecuteAsync,ExecuteReader,ExecuteReaderAsync,ExecuteScalar,ExecuteScalar<,ExecuteScalarAsync,ExecuteScalarAsync<,Query,Query<,QueryAsync,QueryAsync<,QueryFirst,QueryFirst<,QueryFirstAsync,QueryFirstAsync<,QueryFirstOrDefault,QueryFirstOrDefault<,QueryFirstOrDefaultAsync,QueryFirstOrDefaultAsync<,QueryMultiple,QueryMultipleAsync,QuerySingle,QuerySingle<,QuerySingleAsync,QuerySingleAsync<,QuerySingleOrDefault,QuerySingleOrDefault<,QuerySingleOrDefaultAsync,QuerySingleOrDefaultAsync<" + (IsNetFx ? "" : ",QueryUnbufferedAsync,QueryUnbufferedAsync<"), candidates); + Assert.Equal("Execute,ExecuteAsync,ExecuteReader,ExecuteReaderAsync,ExecuteScalar,ExecuteScalar<,ExecuteScalarAsync,ExecuteScalarAsync<,GetRowParser,GetRowParser<,Query,Query<,QueryAsync,QueryAsync<,QueryFirst,QueryFirst<,QueryFirstAsync,QueryFirstAsync<,QueryFirstOrDefault,QueryFirstOrDefault<,QueryFirstOrDefaultAsync,QueryFirstOrDefaultAsync<,QueryMultiple,QueryMultipleAsync,QuerySingle,QuerySingle<,QuerySingleAsync,QuerySingleAsync<,QuerySingleOrDefault,QuerySingleOrDefault<,QuerySingleOrDefaultAsync,QuerySingleOrDefaultAsync<" + (IsNetFx ? "" : ",QueryUnbufferedAsync,QueryUnbufferedAsync<"), candidates); } } diff --git a/test/Dapper.AOT.Test/Integration/BatchTests.cs b/test/Dapper.AOT.Test/Integration/BatchTests.cs index 81a651f0..d154bdce 100644 --- a/test/Dapper.AOT.Test/Integration/BatchTests.cs +++ b/test/Dapper.AOT.Test/Integration/BatchTests.cs @@ -4,7 +4,6 @@ using System.Collections.Immutable; using System.Collections.ObjectModel; using System.Data; -using System.Data.Common; using System.Linq; using Xunit; @@ -18,7 +17,7 @@ public class BatchTests : IDisposable public BatchTests(SqlClientFixture database) => connection = database.CreateConnection(); - private Command Batch => connection.Command("insert AotIntegrationBatchTests (Name) values (@name)", handler: CustomHandler.Instance); + private Command Batch => connection.Command("insert " + SqlClientFixture.AotIntegrationBatchTests + "(Name) values (@name)", handler: CustomHandler.Instance); [SkippableTheory] [InlineData(-1)] diff --git a/test/Dapper.AOT.Test/Integration/DynamicTests.cs b/test/Dapper.AOT.Test/Integration/DynamicTests.cs index 88dec479..46553881 100644 --- a/test/Dapper.AOT.Test/Integration/DynamicTests.cs +++ b/test/Dapper.AOT.Test/Integration/DynamicTests.cs @@ -15,7 +15,7 @@ public class DynamicTests : IDisposable [SkippableFact] public void CanAccessDynamicData() { - var wilma = connection.Command("select * from AotIntegrationDynamicTests where Name = 'Wilma';", handler: CommandFactory.Simple) + var wilma = connection.Command("select * from " + SqlClientFixture.AotIntegrationDynamicTests + " where Name = 'Wilma';", handler: CommandFactory.Simple) .QuerySingle(null, RowFactory.Inbuilt.Dynamic); Assert.NotNull(wilma); Assert.Equal("Wilma", (string)wilma.Name); diff --git a/test/Dapper.AOT.Test/Integration/ManualGridReaderTests.cs b/test/Dapper.AOT.Test/Integration/ManualGridReaderTests.cs index 1e5a99e2..eade42b9 100644 --- a/test/Dapper.AOT.Test/Integration/ManualGridReaderTests.cs +++ b/test/Dapper.AOT.Test/Integration/ManualGridReaderTests.cs @@ -7,7 +7,6 @@ namespace Dapper.AOT.Test.Integration; [Collection(SharedSqlClient.Collection)] -[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1861:Avoid constant arrays as arguments", Justification = "Clearer for test")] public class ManualGridReaderTests : IDisposable { private readonly SqlConnection connection; @@ -31,7 +30,7 @@ public void VanillaUsage() Assert.Equal(123, reader.ReadSingle()); Assert.Equal("abc", reader.ReadSingle()); Assert.Equal(new[] { null, "def", "ghi" }, reader.Read(buffered: true).ToArray()); - Assert.Equal(new[] { 456, 789 }, reader.Read(buffered: false).ToArray()); + Assert.Equal([456, 789], reader.Read(buffered: false).ToArray()); Assert.True(reader.IsConsumed); AssertClosed(); } @@ -48,7 +47,7 @@ public async Task VanillaUsageAsync() Assert.Equal(123, await reader.ReadSingleAsync()); Assert.Equal("abc", await reader.ReadSingleAsync()); Assert.Equal(new[] { null, "def", "ghi" }, (await reader.ReadAsync(buffered: true)).ToArray()); - Assert.Equal(new[] { 456, 789 }, (await reader.ReadAsync(buffered: false)).ToArray()); + Assert.Equal([456, 789], (await reader.ReadAsync(buffered: false)).ToArray()); Assert.True(reader.IsConsumed); AssertClosed(); } @@ -62,7 +61,7 @@ public void BasicUsage() Assert.Equal(123, ((AotGridReader)reader).ReadSingle(RowFactory.Inbuilt.Value())); Assert.Equal("abc", ((AotGridReader)reader).ReadSingle(RowFactory.Inbuilt.Value())); Assert.Equal(new[] { null, "def", "ghi" }, ((AotGridReader)reader).Read(buffered: true, RowFactory.Inbuilt.Value()).ToArray()); - Assert.Equal(new[] { 456, 789 }, ((AotGridReader)reader).Read(buffered: false, RowFactory.Inbuilt.Value()).ToArray()); + Assert.Equal([456, 789], ((AotGridReader)reader).Read(buffered: false, RowFactory.Inbuilt.Value()).ToArray()); Assert.True(reader.IsConsumed); AssertClosed(); } @@ -79,7 +78,7 @@ public async Task BasicUsageAsync() Assert.Equal(123, await ((AotGridReader)reader).ReadSingleAsync(RowFactory.Inbuilt.Value())); Assert.Equal("abc", await ((AotGridReader)reader).ReadSingleAsync(RowFactory.Inbuilt.Value())); Assert.Equal(new[] { null, "def", "ghi" }, (await ((AotGridReader)reader).ReadAsync(buffered: true, RowFactory.Inbuilt.Value())).ToArray()); - Assert.Equal(new[] { 456, 789 }, (await ((AotGridReader)reader).ReadAsync(buffered: false, RowFactory.Inbuilt.Value())).ToArray()); + Assert.Equal([456, 789], (await ((AotGridReader)reader).ReadAsync(buffered: false, RowFactory.Inbuilt.Value())).ToArray()); Assert.True(reader.IsConsumed); AssertClosed(); } @@ -93,7 +92,7 @@ public void BasicUsageWithOnTheFlyIdentity() Assert.Equal(123, reader.ReadSingle()); Assert.Equal("abc", reader.ReadSingle()); Assert.Equal(new[] { null, "def", "ghi" }, reader.Read(buffered: true).ToArray()); - Assert.Equal(new[] { 456, 789 }, reader.Read(buffered: false).ToArray()); + Assert.Equal([456, 789], reader.Read(buffered: false).ToArray()); Assert.True(reader.IsConsumed); AssertClosed(); } @@ -110,7 +109,7 @@ public async Task BasicUsageWithOnTheFlyIdentityAsync() Assert.Equal(123, await reader.ReadSingleAsync()); Assert.Equal("abc", await reader.ReadSingleAsync()); Assert.Equal(new[] { null, "def", "ghi" }, (await reader.ReadAsync(buffered: true)).ToArray()); - Assert.Equal(new[] { 456, 789 }, (await reader.ReadAsync(buffered: false)).ToArray()); + Assert.Equal([456, 789], (await reader.ReadAsync(buffered: false)).ToArray()); Assert.True(reader.IsConsumed); AssertClosed(); } diff --git a/test/Dapper.AOT.Test/Integration/QueryTests.cs b/test/Dapper.AOT.Test/Integration/QueryTests.cs index 11d9eb61..7241759c 100644 --- a/test/Dapper.AOT.Test/Integration/QueryTests.cs +++ b/test/Dapper.AOT.Test/Integration/QueryTests.cs @@ -25,7 +25,7 @@ public class QueryTests : IDisposable private void AssertClosed() => Assert.Equal(System.Data.ConnectionState.Closed, connection.State); - private void AssertExpectedTypedRow(Foo? row, int expected) + private static void AssertExpectedTypedRow(Foo? row, int expected) { if (expected == 0) { @@ -38,7 +38,7 @@ private void AssertExpectedTypedRow(Foo? row, int expected) Assert.Equal("abc", row.Name); } } - private void AssertExpectedDynamicRow(dynamic? row, int expected) + private static void AssertExpectedDynamicRow(dynamic? row, int expected) { if (expected == 0) { @@ -52,7 +52,7 @@ private void AssertExpectedDynamicRow(dynamic? row, int expected) } } - private void AssertExpectedTypedRows(List rows, int expected) + private static void AssertExpectedTypedRows(List rows, int expected) { Assert.Equal(expected, rows.Count); if (expected > 0) @@ -71,7 +71,7 @@ private void AssertExpectedTypedRows(List rows, int expected) } } - private void AssertExpectedDynamicRows(List rows, int expected) + private static void AssertExpectedDynamicRows(List rows, int expected) { Assert.Equal(expected, rows.Count); if (expected > 0) @@ -107,6 +107,7 @@ public void QueryBufferedTypedSync(int count) [InlineData(0)] [InlineData(1)] [InlineData(2)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0028:Simplify collection initialization", Justification = "Confidence in what is happening")] public void QueryNonBufferedTypedSync(int count) { AssertClosed(); @@ -135,6 +136,7 @@ public async Task QueryBufferedTypedAsync(int count) [InlineData(0)] [InlineData(1)] [InlineData(2)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0028:Simplify collection initialization", Justification = "Confidence in what is happening")] public async Task QueryNonBufferedTypedAsync(int count) { AssertClosed(); @@ -163,6 +165,7 @@ public void QueryBufferedDynamicSync(int count) [InlineData(0)] [InlineData(1)] [InlineData(2)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0028:Simplify collection initialization", Justification = "Confidence in what is happening")] public void QueryNonBufferedDynamicSync(int count) { AssertClosed(); @@ -191,6 +194,7 @@ public async Task QueryBufferedDynamicAsync(int count) [InlineData(0)] [InlineData(1)] [InlineData(2)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0028:Simplify collection initialization", Justification = "Confidence in what is happening")] public async Task QueryNonBufferedDynamicAsync(int count) { AssertClosed(); diff --git a/test/Dapper.AOT.Test/Integration/SqlClientFixture.cs b/test/Dapper.AOT.Test/Integration/SqlClientFixture.cs index 36d2f116..13e341c7 100644 --- a/test/Dapper.AOT.Test/Integration/SqlClientFixture.cs +++ b/test/Dapper.AOT.Test/Integration/SqlClientFixture.cs @@ -1,5 +1,6 @@ using Microsoft.Data.SqlClient; using System; +using System.Linq.Expressions; using Xunit; namespace Dapper.AOT.Test.Integration; @@ -15,34 +16,46 @@ public sealed class SqlClientFixture : IDisposable const string connectionString = "Server=.;Database=AdventureWorks2022;Trusted_Connection=True;TrustServerCertificate=True;Connection Timeout=2"; private readonly bool canConnect; private readonly string? message; + +#if NETFRAMEWORK + const string FrameworkSuffix = "_NETFX"; +#else + const string FrameworkSuffix = "_NETCA"; +#endif + public const string AotIntegrationDynamicTests = nameof(AotIntegrationDynamicTests) + FrameworkSuffix; + public const string AotIntegrationBatchTests = nameof(AotIntegrationBatchTests) + FrameworkSuffix; public SqlClientFixture() { try { using SqlConnection connection = new("Server=.;Database=AdventureWorks2022;Trusted_Connection=True;TrustServerCertificate=True;Connection Timeout=2"); connection.Open(); - try - { - connection.Execute( - """ - drop table AotIntegrationBatchTests; - drop table AotIntegrationDynamicTests; - """); - } - catch { } - connection.Execute( - """ - create table AotIntegrationBatchTests(Id int not null identity(1,1), Name nvarchar(200) not null); - create table AotIntegrationDynamicTests(Id int not null identity(1,1), Name nvarchar(200) not null); - insert AotIntegrationDynamicTests (Name) values ('Fred'), ('Barney'), ('Wilma'), ('Betty'); + BlindTry($"drop table {AotIntegrationBatchTests};"); + BlindTry($"drop table {AotIntegrationDynamicTests};"); + BlindTry($"create table {AotIntegrationBatchTests}(Id int not null identity(1,1), Name nvarchar(200) not null);"); + BlindTry($"create table {AotIntegrationDynamicTests}(Id int not null identity(1,1), Name nvarchar(200) not null);"); + + connection.Execute($""" + truncate table {AotIntegrationDynamicTests}; + insert {AotIntegrationDynamicTests} (Name) values ('Fred'), ('Barney'), ('Wilma'), ('Betty'); """); + canConnect = true; + + void BlindTry(string sql) + { + try + { + connection.Execute(sql); + } + catch { } // swallow + } } catch (Exception ex) { // unable to guarantee working fixture canConnect = false; - message = ex.Message; + message += ex.Message + Environment.NewLine; } } public void Dispose() { } diff --git a/test/Dapper.AOT.Test/Interceptors/ExecuteBatch.output.cs b/test/Dapper.AOT.Test/Interceptors/ExecuteBatch.output.cs index 848b7353..b9b1efed 100644 --- a/test/Dapper.AOT.Test/Interceptors/ExecuteBatch.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/ExecuteBatch.output.cs @@ -186,7 +186,6 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, glob ps[2].Value = AsValue(args.Z); } - public override bool SupportBatch => true; public override bool CanPrepare => true; } @@ -223,7 +222,6 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje ps[1].Value = AsValue(typed.bar); } - public override bool SupportBatch => true; public override bool CanPrepare => true; } @@ -249,7 +247,6 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, glob ps[0].Value = AsValue(args.X); } - public override bool SupportBatch => true; public override bool CanPrepare => true; } diff --git a/test/Dapper.AOT.Test/Interceptors/GetRowParser.input.cs b/test/Dapper.AOT.Test/Interceptors/GetRowParser.input.cs new file mode 100644 index 00000000..b6349dd6 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/GetRowParser.input.cs @@ -0,0 +1,55 @@ +using Dapper; +using System; +using System.Data.Common; + +#nullable enable + +[DapperAot] +class ShowRowReaderUsage +{ + public void DoSomething(DbDataReader reader) + { + var parser = reader.GetRowParser(); + while (reader.Read()) + { + var obj = parser(reader); + Console.WriteLine($"{obj.Id}: {obj.Name}"); + } + } + + public void DoSomethingSpecifyingConcreteType(DbDataReader reader) + { + var parser = reader.GetRowParser(concreteType: typeof(HazNameId)); + while (reader.Read()) + { + var obj = parser(reader); + Console.WriteLine($"{obj.Id}: {obj.Name}"); + } + } + + public void DoSomethingDynamic(DbDataReader reader) + { + var parser = reader.GetRowParser(); + while (reader.Read()) + { + var obj = parser(reader); + Console.WriteLine($"{obj.Id}: {obj.Name}"); + } + } + + public void DoSomethingTypeBased(DbDataReader reader) + { + var parser = reader.GetRowParser(typeof(HazNameId)); + while (reader.Read()) + { + var obj = (HazNameId)parser(reader); + Console.WriteLine($"{obj.Id}: {obj.Name}"); + } + } +} + +public class HazNameId +{ + public string? Name { get; set; } + public int Id { get; set; } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/GetRowParser.output.cs b/test/Dapper.AOT.Test/Interceptors/GetRowParser.output.cs new file mode 100644 index 00000000..f207ea7a --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/GetRowParser.output.cs @@ -0,0 +1,123 @@ +#nullable enable +namespace Dapper.AOT // interceptors must be in a known namespace +{ + file static class DapperGeneratedInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\GetRowParser.input.cs", 12, 29)] + internal static global::System.Func GetRowParser0(this global::System.Data.Common.DbDataReader reader, global::System.Type? concreteType, int startIndex, int length, bool returnNullIfFirstMissing) + { + // TypedResult, GetRowParser + // returns data: global::HazNameId + global::System.Diagnostics.Debug.Assert(concreteType is null); + + return RowFactory0.Instance.GetRowParser(reader, startIndex, length, returnNullIfFirstMissing); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\GetRowParser.input.cs", 32, 29)] + internal static global::System.Func GetRowParser1(this global::System.Data.Common.DbDataReader reader, global::System.Type? concreteType, int startIndex, int length, bool returnNullIfFirstMissing) + { + // TypedResult, GetRowParser + // returns data: dynamic + global::System.Diagnostics.Debug.Assert(concreteType is null); + + return global::Dapper.RowFactory.Inbuilt.Dynamic.GetRowParser(reader, startIndex, length, returnNullIfFirstMissing); + + } + + 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 RowFactory0 : global::Dapper.RowFactory + { + internal static readonly RowFactory0 Instance = new(); + private RowFactory0() {} + public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) + { + for (int i = 0; i < tokens.Length; i++) + { + int token = -1; + var name = reader.GetName(columnOffset); + var type = reader.GetFieldType(columnOffset); + switch (NormalizedHash(name)) + { + case 2369371622U when NormalizedEquals(name, "name"): + token = type == typeof(string) ? 0 : 2; // two tokens for right-typed and type-flexible + break; + case 926444256U when NormalizedEquals(name, "id"): + token = type == typeof(int) ? 1 : 3; + break; + + } + tokens[i] = token; + columnOffset++; + + } + return null; + } + public override global::HazNameId Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) + { + global::HazNameId result = new(); + foreach (var token in tokens) + { + switch (token) + { + case 0: + result.Name = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 2: + result.Name = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 1: + result.Id = reader.GetInt32(columnOffset); + break; + case 3: + result.Id = GetValue(reader, columnOffset); + break; + + } + columnOffset++; + + } + return result; + + } + + } + + + } +} +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/GetRowParser.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/GetRowParser.output.netfx.cs new file mode 100644 index 00000000..f207ea7a --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/GetRowParser.output.netfx.cs @@ -0,0 +1,123 @@ +#nullable enable +namespace Dapper.AOT // interceptors must be in a known namespace +{ + file static class DapperGeneratedInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\GetRowParser.input.cs", 12, 29)] + internal static global::System.Func GetRowParser0(this global::System.Data.Common.DbDataReader reader, global::System.Type? concreteType, int startIndex, int length, bool returnNullIfFirstMissing) + { + // TypedResult, GetRowParser + // returns data: global::HazNameId + global::System.Diagnostics.Debug.Assert(concreteType is null); + + return RowFactory0.Instance.GetRowParser(reader, startIndex, length, returnNullIfFirstMissing); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\GetRowParser.input.cs", 32, 29)] + internal static global::System.Func GetRowParser1(this global::System.Data.Common.DbDataReader reader, global::System.Type? concreteType, int startIndex, int length, bool returnNullIfFirstMissing) + { + // TypedResult, GetRowParser + // returns data: dynamic + global::System.Diagnostics.Debug.Assert(concreteType is null); + + return global::Dapper.RowFactory.Inbuilt.Dynamic.GetRowParser(reader, startIndex, length, returnNullIfFirstMissing); + + } + + 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 RowFactory0 : global::Dapper.RowFactory + { + internal static readonly RowFactory0 Instance = new(); + private RowFactory0() {} + public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) + { + for (int i = 0; i < tokens.Length; i++) + { + int token = -1; + var name = reader.GetName(columnOffset); + var type = reader.GetFieldType(columnOffset); + switch (NormalizedHash(name)) + { + case 2369371622U when NormalizedEquals(name, "name"): + token = type == typeof(string) ? 0 : 2; // two tokens for right-typed and type-flexible + break; + case 926444256U when NormalizedEquals(name, "id"): + token = type == typeof(int) ? 1 : 3; + break; + + } + tokens[i] = token; + columnOffset++; + + } + return null; + } + public override global::HazNameId Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) + { + global::HazNameId result = new(); + foreach (var token in tokens) + { + switch (token) + { + case 0: + result.Name = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 2: + result.Name = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 1: + result.Id = reader.GetInt32(columnOffset); + break; + case 3: + result.Id = GetValue(reader, columnOffset); + break; + + } + columnOffset++; + + } + return result; + + } + + } + + + } +} +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/GetRowParser.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/GetRowParser.output.netfx.txt new file mode 100644 index 00000000..39fa3d1a --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/GetRowParser.output.netfx.txt @@ -0,0 +1,4 @@ +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 2 of 2 possible call-sites using 2 interceptors, 0 commands and 1 readers diff --git a/test/Dapper.AOT.Test/Interceptors/GetRowParser.output.txt b/test/Dapper.AOT.Test/Interceptors/GetRowParser.output.txt new file mode 100644 index 00000000..39fa3d1a --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/GetRowParser.output.txt @@ -0,0 +1,4 @@ +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 2 of 2 possible call-sites using 2 interceptors, 0 commands and 1 readers diff --git a/test/Dapper.AOT.Test/Interceptors/NonConstant.output.cs b/test/Dapper.AOT.Test/Interceptors/NonConstant.output.cs index f374a6fa..8087a65e 100644 --- a/test/Dapper.AOT.Test/Interceptors/NonConstant.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/NonConstant.output.cs @@ -115,7 +115,6 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje } } - public override bool SupportBatch => true; public override bool CanPrepare => true; } diff --git a/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.cs b/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.cs index 72bf21da..85aba665 100644 --- a/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/SqlDetection.output.cs @@ -113,7 +113,6 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje ps[0].Value = AsValue(typed.B); } - public override bool SupportBatch => true; public override bool CanPrepare => true; } diff --git a/test/Dapper.AOT.Test/TestCommon/GeneratorWrapper.cs b/test/Dapper.AOT.Test/TestCommon/GeneratorWrapper.cs index 301cd6f5..bc877747 100644 --- a/test/Dapper.AOT.Test/TestCommon/GeneratorWrapper.cs +++ b/test/Dapper.AOT.Test/TestCommon/GeneratorWrapper.cs @@ -36,6 +36,7 @@ public GenerationState(DapperInterceptorGenerator inner) } private readonly DapperInterceptorGenerator inner; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0028:Simplify collection initialization", Justification = "This is fine")] private readonly ConcurrentBag _bag = new(); public void OnCompilationEnd(CompilationAnalysisContext context) => inner.Generate(new GenerateState(GenerateContextProxy.Create(context, _bag.ToImmutableArray())));