From 6208174d0c83550c4400b3fbc4fb0bc2bb56fcbe Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 8 Sep 2023 23:13:20 +0200 Subject: [PATCH 1/9] skeleton --- .../DapperInterceptorGenerator.cs | 15 +-- .../Internal/Inspection.cs | 78 ++++++++++- .../Internal/Roslyn/TypeSymbolExtensions.cs | 18 ++- test/Dapper.AOT.Test/Dapper.AOT.Test.csproj | 11 ++ .../GlobalFetchSize.output.netfx.cs | 6 - .../GlobalFetchSize.output.netfx.txt | 5 +- ...ustomConstructionWithConstructor.input.cs} | 0 ...stomConstructionWithConstructor.output.cs} | 24 ++-- ...tomConstructionWithConstructor.output.txt} | 2 +- ...stomConstructionWithFactoryMethod.input.cs | 23 ++++ ...tomConstructionWithFactoryMethod.output.cs | 121 ++++++++++++++++++ ...omConstructionWithFactoryMethod.output.txt | 4 + .../Roslyn/TypeSymbolExtensionTests.cs | 79 +++++++----- 13 files changed, 320 insertions(+), 66 deletions(-) rename test/Dapper.AOT.Test/Interceptors/{QueryCustomConstruction.input.cs => QueryCustomConstructionWithConstructor.input.cs} (100%) rename test/Dapper.AOT.Test/Interceptors/{QueryCustomConstruction.output.cs => QueryCustomConstructionWithConstructor.output.cs} (97%) rename test/Dapper.AOT.Test/Interceptors/{QueryCustomConstruction.output.txt => QueryCustomConstructionWithConstructor.output.txt} (74%) create mode 100644 test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs create mode 100644 test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs create mode 100644 test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs index 9ab8d6ee..2e20410f 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs @@ -1291,22 +1291,21 @@ static bool IsReserved(string name) private static void WriteRowFactory(SourceProductionContext context, CodeWriter sb, ITypeSymbol type, int index) { - var hasExplicitConstructor = Inspection.TryGetSingleCompatibleDapperAotConstructor(type, out var constructor, out var errorDiagnostic); - if (!hasExplicitConstructor && errorDiagnostic is not null) + var hasDapperConstructor = Inspection.TryGetSingleCompatibleDapperAotConstructor(type, out var constructor, out var constructorSearchErrorDiagnostic); + var hasDapperFactoryMethod = Inspection.TryGetSingleCompatibleDapperAotFactoryMethod(type, out var factoryMethod, out var factorySearchErrorDiagnostic); + if (!hasDapperConstructor && constructorSearchErrorDiagnostic is not null) { - context.ReportDiagnostic(errorDiagnostic); - + context.ReportDiagnostic(constructorSearchErrorDiagnostic); // error is emitted, but we still generate default RowFactory to not emit more errors for this type WriteRowFactoryHeader(); WriteRowFactoryFooter(); - return; } var members = Inspection.GetMembers(type, dapperAotConstructor: constructor).ToImmutableArray(); var membersCount = members.Length; - if (membersCount == 0 && !hasExplicitConstructor) + if (membersCount == 0 && !hasDapperConstructor) { // there are so settable members + there is no constructor to use context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UserTypeNoSettableMembersFound, type.Locations.First(), type.ToDisplayString())); @@ -1320,7 +1319,7 @@ private static void WriteRowFactory(SourceProductionContext context, CodeWriter var hasInitOnlyMembers = members.Any(member => member.IsInitOnly); var hasGetOnlyMembers = members.Any(member => member.IsGettable && !member.IsSettable && !member.IsInitOnly); - var useDeferredConstruction = hasExplicitConstructor || hasInitOnlyMembers || hasGetOnlyMembers; + var useDeferredConstruction = hasDapperConstructor || hasInitOnlyMembers || hasGetOnlyMembers; WriteRowFactoryHeader(); @@ -1467,7 +1466,7 @@ void WriteReadMethod() // ``` sb.Append("return new ").Append(type); - if (hasExplicitConstructor && constructorArgumentsOrdered.Count != 0) + if (hasDapperConstructor && constructorArgumentsOrdered.Count != 0) { // write `(member0, member1, member2, ...)` part of constructor sb.Append('('); diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index 1a312636..a46c47e6 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -118,6 +118,21 @@ public static bool IsDapperAttribute(AttributeData attrib) } }; + public static bool HasDapperAotEnabledAttribute(ISymbol? symbol) + { + var dapperAotAttribute = GetDapperAttribute(symbol, Types.DapperAotAttribute); + + // no attribute at all + if (dapperAotAttribute is null) return false; + + // `[DapperAot]` + if (dapperAotAttribute.ConstructorArguments.Length == 0) return true; + + // `[DapperAot(true)]` + var typedArg = dapperAotAttribute.ConstructorArguments.First(); + return (typedArg.Value is true); + } + public static AttributeData? GetDapperAttribute(ISymbol? symbol, string attributeName) { if (symbol is not null) @@ -396,6 +411,19 @@ public override bool Equals(object obj) => obj is ElementMember other public Location? GetLocation() => Member.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax()?.GetLocation(); } + public static bool TryGetSingleCompatibleDapperAotFactoryMethod( + ITypeSymbol? typeSymbol, + out IMethodSymbol? factoryMethod, + out Diagnostic? errorDiagnostic) + { + errorDiagnostic = null; + factoryMethod = null!; + + var (standardFactories, dapperAotFactories) = ChooseDapperAotCompatibleFactoryMethods(typeSymbol); + + return false; + } + /// /// Chooses a single constructor of type which to use for type's instances creation. /// @@ -446,6 +474,52 @@ public static bool TryGetSingleCompatibleDapperAotConstructor( constructor = null!; return false; } + + private static (IReadOnlyCollection standardFactories, IReadOnlyCollection dapperAotFactories) ChooseDapperAotCompatibleFactoryMethods(ITypeSymbol? typeSymbol) + { + bool FilterFactoryMethods(IMethodSymbol methodSymbol) + => methodSymbol is { IsStatic: true, DeclaredAccessibility: Accessibility.Public } + && SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, typeSymbol); + + var methodSymbols = typeSymbol.GetMethods(filter: FilterFactoryMethods); + // TODO general question: do I need to avoid another enumeration here? cast to array? + if (methodSymbols?.Any() == false) + { + return (standardFactories: Array.Empty(), dapperAotFactories: Array.Empty()); + } + + var standardFactories = new List(); + var dapperAotFactories = new List(); + foreach (var methodSymbol in methodSymbols) + { + // not taking into an account parameterless methods + if (methodSymbol.Parameters.Length == 0) continue; + + var dapperAotAttribute = HasDapperAotEnabledAttribute(methodSymbol); + if (dapperAotAttribute is null) + { + // picking constructor which is not marked with [DapperAot] attribute at all + standardFactories.Add(methodSymbol); + continue; + } + + if (dapperAotAttribute.ConstructorArguments.Length == 0) + { + // picking constructor which is marked with [DapperAot] attribute without arguments (its enabled by default) + dapperAotFactories.Add(methodSymbol); + continue; + } + + var typedArg = dapperAotAttribute.ConstructorArguments.First(); + if (typedArg.Value is true) + { + // picking constructor which is marked with explicit [DapperAot(true)] + dapperAotFactories.Add(methodSymbol); + } + } + + return (standardFactories, dapperAotFactories); + } /// /// Builds a collection of type constructors, which are NOT: @@ -481,7 +555,7 @@ private static (IReadOnlyCollection standardConstructors, IReadOn // not taking into an account parameterless constructors if (constructorMethodSymbol.Parameters.Length == 0) continue; - var dapperAotAttribute= GetDapperAttribute(constructorMethodSymbol, Types.DapperAotAttribute); + var dapperAotAttribute = GetDapperAttribute(constructorMethodSymbol, Types.DapperAotAttribute); if (dapperAotAttribute is null) { // picking constructor which is not marked with [DapperAot] attribute at all @@ -497,7 +571,7 @@ private static (IReadOnlyCollection standardConstructors, IReadOn } var typedArg = dapperAotAttribute.ConstructorArguments.First(); - if (typedArg.Value is bool isAot && isAot) + if (typedArg.Value is true) { // picking constructor which is marked with explicit [DapperAot(true)] dapperAotEnabledCtors.Add(constructorMethodSymbol); diff --git a/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs b/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs index ded85ca4..9cffd8bf 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs @@ -1,11 +1,27 @@ -using Microsoft.CodeAnalysis; +using System; +using Microsoft.CodeAnalysis; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Dapper.Internal.Roslyn; internal static class TypeSymbolExtensions { + public static IEnumerable? GetMethods( + this ITypeSymbol? typeSymbol, + Func? filter = null) + { + if (typeSymbol is null) yield break; + + foreach (var methodSymbol in typeSymbol.GetMembers() + .OfType() + .Where(m => m.MethodKind is MethodKind.Ordinary or MethodKind.DeclareMethod)) + { + if (filter is null || filter(methodSymbol)) yield return methodSymbol; + } + } + public static bool TryGetConstructors(this ITypeSymbol? typeSymbol, out ImmutableArray? constructors) { constructors = null; diff --git a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj index a42a6c35..ad96698c 100644 --- a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj +++ b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj @@ -19,6 +19,17 @@ $([System.String]::Copy(%(Filename)).Replace('.output.netfx', '.input.cs')) + + PreserveNewest + + + PreserveNewest + QueryCustomConstructionWithConstructor.input.cs + + + PreserveNewest + QueryCustomConstructionWithConstructor.input.cs + diff --git a/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.netfx.cs index c4eaaa99..9e2f21c0 100644 --- a/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.netfx.cs +++ b/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.netfx.cs @@ -40,12 +40,6 @@ private sealed class RowFactory0 : global::Dapper.RowFactory tokens, int columnOffset, object? state) - { - global::SomeApp.SomeQueryType result = new(); - return result; - - } } diff --git a/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.netfx.txt index 5f233c5d..72ee7731 100644 --- a/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.netfx.txt +++ b/test/Dapper.AOT.Test/Interceptors/GlobalFetchSize.output.netfx.txt @@ -1,4 +1,7 @@ -Generator produced 1 diagnostics: +Generator produced 2 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 1 of 1 enabled call-sites using 1 interceptors, 1 commands and 1 readers + +Error DAP037 Interceptors/GlobalFetchSize.input.cs L18 C18 +Type 'SomeApp.SomeQueryType' has no settable members (fields or properties) diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.input.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.input.cs similarity index 100% rename from test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.input.cs rename to test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.input.cs diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs similarity index 97% rename from test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.cs rename to test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs index af9efa63..290b384a 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs @@ -1,7 +1,7 @@ #nullable enable file static class DapperGeneratedInterceptors { - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 10, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 10, 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, Buffered, StoredProcedure @@ -15,7 +15,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 11, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 11, 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, Buffered, StoredProcedure @@ -29,7 +29,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 12, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 12, 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, Buffered, StoredProcedure @@ -43,7 +43,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 13, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 13, 24)] internal static global::System.Collections.Generic.IEnumerable Query3(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, Buffered, StoredProcedure @@ -57,7 +57,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 14, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 14, 24)] internal static global::System.Collections.Generic.IEnumerable Query4(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, Buffered, StoredProcedure @@ -71,7 +71,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 15, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 15, 24)] internal static global::System.Collections.Generic.IEnumerable Query5(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, Buffered, StoredProcedure @@ -85,7 +85,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 16, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 16, 24)] internal static global::System.Collections.Generic.IEnumerable Query6(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, Buffered, StoredProcedure @@ -99,7 +99,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 17, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 17, 24)] internal static global::System.Collections.Generic.IEnumerable Query7(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, Buffered, StoredProcedure @@ -113,7 +113,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 18, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 18, 24)] internal static global::System.Collections.Generic.IEnumerable Query8(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, Buffered, StoredProcedure @@ -127,7 +127,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 19, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 19, 24)] internal static global::System.Collections.Generic.IEnumerable Query9(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, Buffered, StoredProcedure @@ -141,7 +141,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 20, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 20, 24)] internal static global::System.Collections.Generic.IEnumerable Query10(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, Buffered, StoredProcedure @@ -155,7 +155,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 21, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 21, 24)] internal static global::System.Collections.Generic.IEnumerable Query11(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, Buffered, StoredProcedure diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.txt b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.txt similarity index 74% rename from test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.txt rename to test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.txt index b32c1530..2221ac2d 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.txt @@ -3,5 +3,5 @@ Generator produced 2 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 12 of 12 enabled call-sites using 12 interceptors, 0 commands and 12 readers -Error DAP035 Interceptors/QueryCustomConstruction.input.cs L132 C16 +Error DAP035 Interceptors/QueryCustomConstructionWithConstructor.input.cs L132 C16 Only one constructor can be Dapper.AOT enabled per type 'Foo.MultipleDapperAotCtors' diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs new file mode 100644 index 00000000..ebe031cb --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs @@ -0,0 +1,23 @@ +using Dapper; +using System.Data.Common; + +[module: DapperAot] + +public static class Foo +{ + static void SomeCode(DbConnection connection, string bar, bool isBuffered) + { + _ = connection.Query("def"); + } + + public class PublicPropertiesNoConstructor + { + public int X { get; set; } + public string Y { get; set; } + public double? Z { get; set; } + + [DapperAot(true)] + public static PublicPropertiesNoConstructor Construct(int x, string y, double? z) + => new PublicPropertiesNoConstructor { X = x, Y = y, Z = z }; + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs new file mode 100644 index 00000000..74bf8991 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs @@ -0,0 +1,121 @@ +#nullable enable +file static class DapperGeneratedInterceptors +{ + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 10, 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, Buffered, StoredProcedure + // returns data: global::Foo.PublicPropertiesNoConstructor + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.StoredProcedure, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QueryBuffered(param, RowFactory0.Instance); + + } + + 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 4245442695U when NormalizedEquals(name, "x"): + token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible + break; + case 4228665076U when NormalizedEquals(name, "y"): + token = type == typeof(string) ? 1 : 4; + break; + case 4278997933U when NormalizedEquals(name, "z"): + token = type == typeof(double) ? 2 : 5; + break; + + } + tokens[i] = token; + columnOffset++; + + } + return null; + } + public override global::Foo.PublicPropertiesNoConstructor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) + { + global::Foo.PublicPropertiesNoConstructor result = new(); + foreach (var token in tokens) + { + switch (token) + { + case 0: + result.X = reader.GetInt32(columnOffset); + break; + case 3: + result.X = GetValue(reader, columnOffset); + break; + case 1: + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 4: + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 2: + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + break; + case 5: + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : 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/QueryCustomConstructionWithFactoryMethod.output.txt b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt new file mode 100644 index 00000000..3783f0a6 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt @@ -0,0 +1,4 @@ +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 1 of 1 enabled call-sites using 1 interceptors, 0 commands and 1 readers diff --git a/test/Dapper.AOT.Test/Internal/Roslyn/TypeSymbolExtensionTests.cs b/test/Dapper.AOT.Test/Internal/Roslyn/TypeSymbolExtensionTests.cs index 4ebb2425..fabef2fb 100644 --- a/test/Dapper.AOT.Test/Internal/Roslyn/TypeSymbolExtensionTests.cs +++ b/test/Dapper.AOT.Test/Internal/Roslyn/TypeSymbolExtensionTests.cs @@ -9,13 +9,32 @@ namespace Dapper.Internal.Roslyn { public class TypeSymbolExtensionsTests { + [Fact] + public void GetMethods_ProperlyPassesTypeMethods() + { + var text = BuildDapperCodeText( + """ + _ = connection.Execute("def", new Customer()); + """, + withFactoryMethod: true + ); + + var argumentOperation = GetInvocationArgumentOperation(text); + var typeSymbol = GetConversionTypeSymbol(argumentOperation); + + var result = typeSymbol.GetMethods()!.ToArray(); + Assert.Single(result); + } + [Fact] public void CheckTypeUsage_WithCustomConstructor() { - var text = BuildDapperCodeTextWithConstructor( - """ - _ = connection.Execute("def", new Customer()); - """); + var text = BuildDapperCodeText( + """ + _ = connection.Execute("def", new Customer()); + """, + withConstructor: true + ); var argumentOperation = GetInvocationArgumentOperation(text); var typeSymbol = GetConversionTypeSymbol(argumentOperation); @@ -202,41 +221,28 @@ static IArgumentOperation GetInvocationArgumentOperation(string text, int invoca return arg; } - static string BuildDapperCodeTextWithConstructor(string implementation) => $$""" - using Dapper; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Data; - using System.Data.Common; - - public static class Foo + private const string ConstructorSnippet = + """ + public Customer(int x, string y, double? z) { - static void SomeCode(DbConnection connection, string bar) - { - {{implementation}} - } - public class Customer - { - public int X { get; set; } - public string Y; - public double? Z { get; set; } - - public Customer(int x, string y, double? z) - { - X = x; - Y = y; - Z = z; - } - } - public enum State - { - Active, - Disabled - } + X = x; + Y = y; + Z = z; + } + """; + private const string FactoryMethodSnippet = + """ + public static Customer Create(int x, string y, double? z) + { + return new Customer { X = x, Y = y, Z = z }; } """; - static string BuildDapperCodeText(string implementation) => $$""" + static string BuildDapperCodeText( + string implementation, + bool withConstructor = false, + bool withFactoryMethod = false) + => $$""" using Dapper; using System.Collections.Generic; using System.Collections.Immutable; @@ -254,6 +260,9 @@ public class Customer public int X { get; set; } public string Y; public double? Z { get; set; } + + {{(withConstructor ? ConstructorSnippet : string.Empty)}} + {{(withFactoryMethod ? FactoryMethodSnippet : string.Empty)}} } public enum State { From 0a43f91b4b5b73df121b04347d318d8cc317eb45 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sun, 10 Sep 2023 22:36:53 +0200 Subject: [PATCH 2/9] implement factory methods standard cases --- .../AnalyzerReleases.Unshipped.md | 1 + .../DapperInterceptorGenerator.cs | 627 +++++++++++++----- .../CodeAnalysis/Diagnostics.cs | 2 + .../Internal/Inspection.cs | 140 ++-- ...ustomConstructionWithConstructor.output.cs | 21 +- ...stomConstructionWithFactoryMethod.input.cs | 16 + ...tomConstructionWithFactoryMethod.output.cs | 39 +- ...omConstructionWithFactoryMethod.output.txt | 7 +- 8 files changed, 589 insertions(+), 264 deletions(-) diff --git a/src/Dapper.AOT.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapper.AOT.Analyzers/AnalyzerReleases.Unshipped.md index 29551ff7..636ddcfd 100644 --- a/src/Dapper.AOT.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Dapper.AOT.Analyzers/AnalyzerReleases.Unshipped.md @@ -43,6 +43,7 @@ DAP034 | Library | Warning | Diagnostics DAP035 | Library | Error | Diagnostics DAP036 | Library | Error | Diagnostics DAP037 | Library | Error | Diagnostics +DAP038 | Library | Error | Diagnostics DAP100 | Library | Error | Diagnostics DAP101 | Library | Error | Diagnostics DAP102 | Library | Error | Diagnostics diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs index 2e20410f..0f7323c5 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs @@ -38,18 +38,20 @@ public sealed partial class DapperInterceptorGenerator : InterceptorGeneratorBas public override void Initialize(IncrementalGeneratorInitializationContext context) { var nodes = context.SyntaxProvider.CreateSyntaxProvider(PreFilter, Parse) - .Where(x => x is not null) - .Select((x, _) => x!); + .Where(x => x is not null) + .Select((x, _) => x!); var combined = context.CompilationProvider.Combine(nodes.Collect()); context.RegisterImplementationSourceOutput(combined, Generate); } // 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"); private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) { - if (node is InvocationExpressionSyntax ie && ie.ChildNodes().FirstOrDefault() is MemberAccessExpressionSyntax ma) + if (node is InvocationExpressionSyntax ie && + ie.ChildNodes().FirstOrDefault() is MemberAccessExpressionSyntax ma) { return IsCandidate(ma.Name.ToString()); } @@ -64,6 +66,7 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) { return null; } + var methodKind = IsSupportedDapperMethod(op, out var flags); switch (methodKind) { @@ -83,6 +86,7 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) { loc = ma.ChildNodes().Skip(1).FirstOrDefault()?.GetLocation(); } + loc ??= op.Syntax.GetLocation(); if (loc is null) { @@ -97,7 +101,8 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) if (methodKind == DapperMethodKind.DapperUnsupported) { flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.UnsupportedMethod, loc, GetSignature(op.TargetMethod))); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.UnsupportedMethod, loc, GetSignature(op.TargetMethod))); } } else @@ -136,12 +141,14 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) { sql = s; } + break; case "buffered": if (TryGetConstantValue(arg, out bool b)) { buffered = b; } + break; case "param": if (arg.Value is not IDefaultValueOperation) @@ -151,9 +158,11 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) { expr = conv.Operand; } + paramType = expr?.Type; flags |= OperationFlags.HasParameters; } + break; case "cnn": case "commandTimeout": @@ -167,7 +176,9 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) { case null when !string.IsNullOrWhiteSpace(sql): // if no spaces: interpret as stored proc, else: text - flags |= sql!.Trim().IndexOf(' ') < 0 ? OperationFlags.StoredProcedure : OperationFlags.Text; + flags |= sql!.Trim().IndexOf(' ') < 0 + ? OperationFlags.StoredProcedure + : OperationFlags.Text; break; case null: break; // flexible @@ -184,24 +195,31 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) flags |= OperationFlags.DoNotGenerate; if (dapperEnabled) { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.UnexpectedCommandType, arg.Syntax.GetLocation())); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.UnexpectedCommandType, arg.Syntax.GetLocation())); } + break; } } + break; default: if (dapperEnabled && !HasAny(flags, OperationFlags.DoNotGenerate)) { flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.UnexpectedArgument, arg.Syntax.GetLocation(), arg.Parameter?.Name)); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.UnexpectedArgument, arg.Syntax.GetLocation(), + arg.Parameter?.Name)); } + break; } } if (!string.IsNullOrWhiteSpace(sql) && HasAny(flags, OperationFlags.Text) - && Inspection.IsEnabled(ctx, op, Types.IncludeLocationAttribute, out _, cancellationToken)) + && Inspection.IsEnabled(ctx, op, Types.IncludeLocationAttribute, out _, + cancellationToken)) { flags |= OperationFlags.IncludeLocation; } @@ -218,9 +236,12 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) if (HasAny(flags, OperationFlags.DoNotGenerate)) { // extra checks specific to Dapper vanilla - if (resultTuple && Inspection.IsEnabled(ctx, op, Types.BindTupleByNameAttribute, out _, cancellationToken)) - { // Dapper vanilla supports bind-by-position for tuples; warn if bind-by-name is enabled - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DapperLegacyBindNameTupleResults, loc)); + if (resultTuple && + Inspection.IsEnabled(ctx, op, Types.BindTupleByNameAttribute, out _, cancellationToken)) + { + // Dapper vanilla supports bind-by-position for tuples; warn if bind-by-name is enabled + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.DapperLegacyBindNameTupleResults, loc)); } } else @@ -229,17 +250,21 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) if (Inspection.InvolvesGenericTypeParameter(resultType)) { flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.GenericTypeParameter, loc, resultType!.ToDisplayString())); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.GenericTypeParameter, loc, resultType!.ToDisplayString())); } else if (resultTuple) { - if (Inspection.IsEnabled(ctx, op, Types.BindTupleByNameAttribute, out var defined, cancellationToken)) + if (Inspection.IsEnabled(ctx, op, Types.BindTupleByNameAttribute, out var defined, + cancellationToken)) { flags |= OperationFlags.BindTupleResultByName; } + if (!defined) { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DapperAotAddBindTupleByName, loc)); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.DapperAotAddBindTupleByName, loc)); } // but not implemented currently! @@ -249,7 +274,9 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) else if (!Inspection.IsPublicOrAssemblyLocal(resultType, ctx, out var failing)) { flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.NonPublicType, loc, failing!.ToDisplayString(), Inspection.NameAccessibility(failing))); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.NonPublicType, loc, failing!.ToDisplayString(), + Inspection.NameAccessibility(failing))); } } } @@ -271,13 +298,16 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) // extra checks specific to DapperAOT if (paramTuple) { - if (Inspection.IsEnabled(ctx, op, Types.BindTupleByNameAttribute, out var defined, cancellationToken)) + if (Inspection.IsEnabled(ctx, op, Types.BindTupleByNameAttribute, out var defined, + cancellationToken)) { flags |= OperationFlags.BindTupleParameterByName; } + if (!defined) { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DapperAotAddBindTupleByName, loc)); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.DapperAotAddBindTupleByName, loc)); } // but not implemented currently! @@ -287,7 +317,8 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) else if (Inspection.InvolvesGenericTypeParameter(paramType)) { flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.GenericTypeParameter, loc, paramType!.ToDisplayString())); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.GenericTypeParameter, loc, paramType!.ToDisplayString())); } else if (Inspection.IsMissingOrObjectOrDynamic(paramType)) { @@ -297,13 +328,16 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) else if (!Inspection.IsPublicOrAssemblyLocal(paramType, ctx, out var failing)) { flags |= OperationFlags.DoNotGenerate; - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.NonPublicType, loc, failing!.ToDisplayString(), Inspection.NameAccessibility(failing))); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.NonPublicType, loc, failing!.ToDisplayString(), + Inspection.NameAccessibility(failing))); } } } // perform SQL inspection - var parameterMap = BuildParameterMap(ctx, op, sql, flags, paramType, loc, ref diagnostics, sqlSyntax, out var parseFlags, cancellationToken); + var parameterMap = BuildParameterMap(ctx, op, sql, flags, paramType, loc, ref diagnostics, sqlSyntax, + out var parseFlags, cancellationToken); // if we have a good parser *and* the SQL isn't borked: check for obvious query/exec mismatch if ((parseFlags & (ParseFlags.Reliable | ParseFlags.SyntaxError)) == ParseFlags.Reliable) @@ -316,6 +350,7 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) // definitely have a query Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.ExecuteCommandWithQuery, loc)); } + break; case OperationFlags.Query: case OperationFlags.Execute | OperationFlags.Scalar: @@ -324,6 +359,7 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) // definitely do not have a query Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.QueryCommandMissingQuery, loc)); } + break; } } @@ -332,7 +368,8 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) { bool canBeCached = true; // need fixed text, command-type and parameters to be reusable - if (string.IsNullOrWhiteSpace(sql) || parameterMap == "?" || !HasAny(flags, OperationFlags.StoredProcedure | OperationFlags.TableDirect | OperationFlags.Text)) + if (string.IsNullOrWhiteSpace(sql) || parameterMap == "?" || !HasAny(flags, + OperationFlags.StoredProcedure | OperationFlags.TableDirect | OperationFlags.Text)) { canBeCached = false; } @@ -340,10 +377,12 @@ private bool PreFilter(SyntaxNode node, CancellationToken cancellationToken) if (!canBeCached) flags &= ~OperationFlags.CacheCommand; } - var estimatedRowCount = AdditionalCommandState.Parse(Inspection.GetSymbol(ctx, op, cancellationToken), paramType, ref diagnostics); + var estimatedRowCount = AdditionalCommandState.Parse(Inspection.GetSymbol(ctx, op, cancellationToken), + paramType, ref diagnostics); CheckCallValidity(op, flags, ref diagnostics); - return new SourceState(loc, op.TargetMethod, flags, sql, resultType, paramType, parameterMap, estimatedRowCount, diagnostics); + return new SourceState(loc, op.TargetMethod, flags, sql, resultType, paramType, parameterMap, estimatedRowCount, + diagnostics); //static bool HasDiagnostic(object? diagnostics, DiagnosticDescriptor diagnostic) //{ @@ -375,6 +414,7 @@ static bool TryGetConstantValueWithSyntax(IArgumentOperation op, out T? value syntax = op.Syntax; return true; } + var val = op.Value; // work through any implict/explicit conversion steps while (val is IConversionOperation conv) @@ -415,13 +455,18 @@ static bool TryGetConstantValueWithSyntax(IArgumentOperation op, out T? value return true; } } - catch { } + catch + { + } + value = default!; syntax = null; return false; } - static string BuildParameterMap(in GeneratorSyntaxContext ctx, IInvocationOperation op, string? sql, OperationFlags flags, ITypeSymbol? parameterType, Location loc, ref object? diagnostics, SyntaxNode? sqlSyntax, out ParseFlags parseFlags, CancellationToken cancellationToken) + static string BuildParameterMap(in GeneratorSyntaxContext ctx, IInvocationOperation op, string? sql, + OperationFlags flags, ITypeSymbol? parameterType, Location loc, ref object? diagnostics, + SyntaxNode? sqlSyntax, out ParseFlags parseFlags, CancellationToken cancellationToken) { // if command-type is known statically to be stored procedure etc: pass everything if (HasAny(flags, OperationFlags.StoredProcedure | OperationFlags.TableDirect)) @@ -429,6 +474,7 @@ static string BuildParameterMap(in GeneratorSyntaxContext ctx, IInvocationOperat parseFlags = HasAny(flags, OperationFlags.StoredProcedure) ? ParseFlags.MaybeQuery : ParseFlags.Query; return HasAny(flags, OperationFlags.HasParameters) ? "*" : ""; } + // if command-type or command is not known statically: defer decision if (!HasAny(flags, OperationFlags.Text) || string.IsNullOrWhiteSpace(sql)) { @@ -454,9 +500,9 @@ static string BuildParameterMap(in GeneratorSyntaxContext ctx, IInvocationOperat proc.Execute(sql!); parseFlags = proc.Flags; paramNames = (from var in proc.Variables - where var.IsParameter - select var.Name.StartsWith("@") ? var.Name.Substring(1) : var.Name - ).ToImmutableHashSet(); + where var.IsParameter + select var.Name.StartsWith("@") ? var.Name.Substring(1) : var.Name + ).ToImmutableHashSet(); diagnostics = proc.DiagnosticsObject; } catch (Exception ex) @@ -464,6 +510,7 @@ select var.Name.StartsWith("@") ? var.Name.Substring(1) : var.Name Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.SqlError, loc, ex.Message)); goto default; // some internal failure } + break; default: paramNames = SqlTools.GetUniqueParameters(sql, out parseFlags); @@ -476,6 +523,7 @@ select var.Name.StartsWith("@") ? var.Name.Substring(1) : var.Name { Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.SqlParametersNotDetected, loc)); } + return ""; } @@ -486,8 +534,10 @@ select var.Name.StartsWith("@") ? var.Name.Substring(1) : var.Name { Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.NoParametersSupplied, loc)); } + return ""; } + if (HasAny(flags, OperationFlags.HasParameters) && Inspection.IsMissingOrObjectOrDynamic(elementType)) { // unknown parameter type; defer decision @@ -508,26 +558,33 @@ select var.Name.StartsWith("@") ? var.Name.Substring(1) : var.Name } else { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DuplicateRowCount, loc, member.CodeName, rowCountMember)); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.DuplicateRowCount, loc, member.CodeName, rowCountMember)); } + if (member.HasDbValueAttribute) { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.RowCountDbValue, loc, member.CodeName)); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.RowCountDbValue, loc, member.CodeName)); } } + if (member.Kind != Inspection.ElementMemberKind.None) { continue; // not treated as parameters for naming etc purposes } + var dbName = member.DbName; if (memberDbToCodeNames.TryGetValue(dbName, out var existing)) { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DuplicateParameter, loc, member.CodeName, existing, dbName)); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.DuplicateParameter, loc, member.CodeName, existing, dbName)); } else { memberDbToCodeNames.Add(dbName, member.CodeName); } + if (member.Direction == ParameterDirection.ReturnValue) { if (returnCodeMember is null) @@ -536,7 +593,8 @@ select var.Name.StartsWith("@") ? var.Name.Substring(1) : var.Name } else { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.DuplicateReturn, loc, member.CodeName, returnCodeMember)); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.DuplicateReturn, loc, member.CodeName, returnCodeMember)); } } } @@ -553,42 +611,47 @@ select var.Name.StartsWith("@") ? var.Name.Substring(1) : var.Name // we can only consider this an error if we're confident in how well we parsed the input if ((parseFlags & ParseFlags.Reliable) != 0) { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.SqlParameterNotBound, loc, sqlParamName, CodeWriter.GetTypeName(elementType))); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.SqlParameterNotBound, loc, sqlParamName, + CodeWriter.GetTypeName(elementType))); } } } + if ((parseFlags & ParseFlags.Return) != 0 && returnCodeMember is not null) { WithSpace(ref sb).Append(returnCodeMember); } + return sb is null ? "" : sb.ToString(); - static StringBuilder WithSpace(ref StringBuilder? sb) => sb is null ? (sb = new()) : (sb.Length == 0 ? sb : sb.Append(' ')); + static StringBuilder WithSpace(ref StringBuilder? sb) => + sb is null ? (sb = new()) : (sb.Length == 0 ? sb : sb.Append(' ')); } } private void CheckCallValidity(IInvocationOperation op, OperationFlags flags, ref object? diagnostics) { if (HasAny(flags, OperationFlags.Query) && !HasAny(flags, OperationFlags.SingleRow) - && op.Parent is IArgumentOperation arg - && arg.Parent is IInvocationOperation parent && parent.TargetMethod is - { - IsExtensionMethod: true, - Parameters.Length: 1, Arity: 1, ContainingType: - { - Name: nameof(Enumerable), - ContainingType: null, - ContainingNamespace: - { - Name: "Linq", - ContainingNamespace: - { - Name: "System", - ContainingNamespace.IsGlobalNamespace: true - } - } - } - } target) + && op.Parent is IArgumentOperation arg + && arg.Parent is IInvocationOperation parent && parent.TargetMethod is + { + IsExtensionMethod: true, + Parameters.Length: 1, Arity: 1, ContainingType: + { + Name: nameof(Enumerable), + ContainingType: null, + ContainingNamespace: + { + Name: "Linq", + ContainingNamespace: + { + Name: "System", + ContainingNamespace.IsGlobalNamespace: true + } + } + } + } target) { string? preferred = parent.TargetMethod.Name switch { @@ -600,21 +663,29 @@ private void CheckCallValidity(IInvocationOperation op, OperationFlags flags, re }; if (preferred is not null) { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.UseSingleRowQuery, parent.Syntax.GetLocation(), preferred, parent.TargetMethod.Name)); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.UseSingleRowQuery, parent.Syntax.GetLocation(), preferred, + parent.TargetMethod.Name)); } else if (parent.TargetMethod.Name == nameof(Enumerable.ToList)) { - Diagnostics.Add(ref diagnostics, Diagnostic.Create(Diagnostics.UseQueryAsList, parent.Syntax.GetLocation())); + Diagnostics.Add(ref diagnostics, + Diagnostic.Create(Diagnostics.UseQueryAsList, parent.Syntax.GetLocation())); } } } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0042:Deconstruct variable declaration", Justification = "Readability is fine as-is")] - private static SqlSyntax IdentifySqlSyntax(in GeneratorSyntaxContext ctx, IInvocationOperation op, out bool caseSensitive, + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0042:Deconstruct variable declaration", + Justification = "Readability is fine as-is")] + private static SqlSyntax IdentifySqlSyntax(in GeneratorSyntaxContext ctx, IInvocationOperation op, + out bool caseSensitive, CancellationToken cancellationToken) { caseSensitive = false; - if (op.Arguments[0].Value is IConversionOperation conv && conv.Operand.Type is INamedTypeSymbol { Arity: 0, ContainingType: null } type) + if (op.Arguments[0].Value is IConversionOperation conv && conv.Operand.Type is INamedTypeSymbol + { + Arity: 0, ContainingType: null + } type) { var ns = type.ContainingNamespace; foreach (var candidate in KnownConnectionTypes) @@ -632,7 +703,8 @@ private static SqlSyntax IdentifySqlSyntax(in GeneratorSyntaxContext ctx, IInvoc // get fom [SqlSyntax(...)] hint var attrib = Inspection.GetClosestDapperAttribute(ctx, op, Types.SqlSyntaxAttribute, cancellationToken); - if (attrib is not null && attrib.ConstructorArguments.Length == 1 && attrib.ConstructorArguments[0].Value is int i) + if (attrib is not null && attrib.ConstructorArguments.Length == 1 && + attrib.ConstructorArguments[0].Value is int i) { return (SqlSyntax)i; } @@ -652,24 +724,27 @@ static bool AssertAndAscend(ref INamespaceSymbol ns, string? expected) ns = ns.ContainingNamespace; return true; } + return false; } } } - private static readonly ImmutableArray<(string? Namespace2, string? Namespace1, string Namespace0, string Connection, SqlSyntax Syntax)> KnownConnectionTypes = new[] - { - ("System", "Data", "SqlClient", "SqlConnection", SqlSyntax.SqlServer), - ("Microsoft", "Data", "SqlClient", "SqlConnection", SqlSyntax.SqlServer), + private static readonly + ImmutableArray<(string? Namespace2, string? Namespace1, string Namespace0, string Connection, SqlSyntax Syntax)> + KnownConnectionTypes = new[] + { + ("System", "Data", "SqlClient", "SqlConnection", SqlSyntax.SqlServer), + ("Microsoft", "Data", "SqlClient", "SqlConnection", SqlSyntax.SqlServer), - (null, null, "Npgsql", "NpgsqlConnection", SqlSyntax.PostgreSql), + (null, null, "Npgsql", "NpgsqlConnection", SqlSyntax.PostgreSql), - ("MySql", "Data", "MySqlClient", "MySqlConnection", SqlSyntax.MySql), + ("MySql", "Data", "MySqlClient", "MySqlConnection", SqlSyntax.MySql), - ("Oracle", "DataAccess", "Client", "OracleConnection", SqlSyntax.Oracle), + ("Oracle", "DataAccess", "Client", "OracleConnection", SqlSyntax.Oracle), - ("Microsoft", "Data", "Sqlite", "SqliteConnection", SqlSyntax.SQLite), - }.ToImmutableArray(); + ("Microsoft", "Data", "Sqlite", "SqliteConnection", SqlSyntax.SQLite), + }.ToImmutableArray(); enum DapperMethodKind { @@ -686,19 +761,24 @@ static DapperMethodKind IsSupportedDapperMethod(IInvocationOperation operation, { return DapperMethodKind.NotDapper; } + var type = method.ContainingType; - if (type is not { Name: "SqlMapper", ContainingNamespace: { Name: "Dapper", ContainingNamespace.IsGlobalNamespace: true } }) + if (type is not + { Name: "SqlMapper", ContainingNamespace: { Name: "Dapper", ContainingNamespace.IsGlobalNamespace: true } }) { return DapperMethodKind.NotDapper; } + if (method.Name.EndsWith("Async")) { flags |= OperationFlags.Async; } + if (method.IsGenericMethod) { flags |= OperationFlags.TypedResult; } + switch (method.Name) { case "Query": @@ -792,15 +872,18 @@ enum OperationFlags } } } + var name = CodeWriter.GetTypeName(type); var trimGeneric = name.LastIndexOf('<'); if (trimGeneric >= 0) { name = name.Substring(0, trimGeneric); } + return name; } } + canConstruct = true; // we mean the default Dapper one, which can be constructed return null; } @@ -811,7 +894,8 @@ private static string GetSignature(IMethodSymbol method, bool deconstruct = true return method.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat); } - private bool CheckPrerequisites(SourceProductionContext ctx, (Compilation Compilation, ImmutableArray Nodes) state, out int enabledCount) + private bool CheckPrerequisites(SourceProductionContext ctx, + (Compilation Compilation, ImmutableArray Nodes) state, out int enabledCount) { enabledCount = 0; if (!state.Nodes.IsDefaultOrEmpty) @@ -854,6 +938,7 @@ private bool CheckPrerequisites(SourceProductionContext ctx, (Compilation Compil } } } + if (disabledCount == state.Nodes.Length) { ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.DapperAotNotEnabled, null)); @@ -868,10 +953,12 @@ private bool CheckPrerequisites(SourceProductionContext ctx, (Compilation Compil return errorCount == 0; } + return false; // nothing to validate - so: nothing to do, quick exit } - private void Generate(SourceProductionContext ctx, (Compilation Compilation, ImmutableArray Nodes) state) + private void Generate(SourceProductionContext ctx, + (Compilation Compilation, ImmutableArray Nodes) state) { if (!CheckPrerequisites(ctx, state, out int enabledCount)) // also reports per-item diagnostics { @@ -889,7 +976,8 @@ private void Generate(SourceProductionContext ctx, (Compilation Compilation, Imm var factories = new CommandFactoryState(state.Compilation); var readers = new RowReaderState(); - foreach (var grp in state.Nodes.Where(x => !HasAny(x.Flags, OperationFlags.DoNotGenerate)).GroupBy(x => x.Group(), CommonComparer.Instance)) + foreach (var grp in state.Nodes.Where(x => !HasAny(x.Flags, OperationFlags.DoNotGenerate)) + .GroupBy(x => x.Group(), CommonComparer.Instance)) { // first, try to resolve the helper method that we're going to use for this var (flags, method, parameterType, parameterMap, _, additionalCommandState) = grp.Key; @@ -901,7 +989,8 @@ private void Generate(SourceProductionContext ctx, (Compilation Compilation, Imm var loc = op.Location.GetLineSpan(); var start = loc.StartLinePosition; sb.Append("[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(") - .AppendVerbatimLiteral(loc.Path).Append(", ").Append(start.Line + 1).Append(", ").Append(start.Character + 1).Append(")]").NewLine(); + .AppendVerbatimLiteral(loc.Path).Append(", ").Append(start.Line + 1).Append(", ") + .Append(start.Character + 1).Append(")]").NewLine(); usageCount++; } @@ -909,6 +998,7 @@ private void Generate(SourceProductionContext ctx, (Compilation Compilation, Imm { continue; // empty group? } + callSiteCount += usageCount; // declare the method @@ -922,12 +1012,14 @@ private void Generate(SourceProductionContext ctx, (Compilation Compilation, Imm else if (method.IsExtensionMethod) sb.Append("this "); sb.Append(parameters[i].Type).Append(" ").Append(parameters[i].Name); } + sb.Append(")").Indent().NewLine(); sb.Append("// ").Append(flags.ToString()).NewLine(); if (HasAny(flags, OperationFlags.HasParameters)) { sb.Append("// takes parameter: ").Append(parameterType).NewLine(); } + if (!string.IsNullOrWhiteSpace(grp.Key.ParameterMap)) { sb.Append("// parameter map: ").Append(grp.Key.ParameterMap switch @@ -937,6 +1029,7 @@ private void Generate(SourceProductionContext ctx, (Compilation Compilation, Imm _ => grp.Key.ParameterMap, }).NewLine(); } + ITypeSymbol? resultType = null; if (HasAny(flags, OperationFlags.TypedResult)) { @@ -945,7 +1038,8 @@ private void Generate(SourceProductionContext ctx, (Compilation Compilation, Imm } // assertions - var commandTypeMode = flags & (OperationFlags.Text | OperationFlags.StoredProcedure | OperationFlags.TableDirect); + var commandTypeMode = + flags & (OperationFlags.Text | OperationFlags.StoredProcedure | OperationFlags.TableDirect); var methodParameters = grp.Key.Method.Parameters; string? fixedSql = null; if (HasAny(flags, OperationFlags.IncludeLocation)) @@ -961,25 +1055,33 @@ private void Generate(SourceProductionContext ctx, (Compilation Compilation, Imm { sb.Append("global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql));").NewLine(); } + if (HasParam(methodParameters, "commandType")) { if (commandTypeMode != 0) { - sb.Append("global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.") - .Append(commandTypeMode.ToString()).Append(");").NewLine(); + sb.Append( + "global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.") + .Append(commandTypeMode.ToString()).Append(");").NewLine(); } } - if (HasAny(flags, OperationFlags.Buffered | OperationFlags.Unbuffered) && HasParam(methodParameters, "buffered")) + if (HasAny(flags, OperationFlags.Buffered | OperationFlags.Unbuffered) && + HasParam(methodParameters, "buffered")) { - sb.Append("global::System.Diagnostics.Debug.Assert(buffered is ").Append((flags & OperationFlags.Buffered) != 0).Append(");").NewLine(); + 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(HasAny(flags, OperationFlags.HasParameters) ? "not " : "").Append("null);").NewLine().NewLine(); + sb.Append("global::System.Diagnostics.Debug.Assert(param is ") + .Append(HasAny(flags, OperationFlags.HasParameters) ? "not " : "").Append("null);").NewLine().NewLine(); - if (!TryWriteMultiExecImplementation(sb, flags, commandTypeMode, parameterType, grp.Key.ParameterMap, grp.Key.UniqueLocation is not null, methodParameters, factories, fixedSql, additionalCommandState)) + 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); + WriteSingleImplementation(sb, method, resultType, flags, commandTypeMode, parameterType, + grp.Key.ParameterMap, grp.Key.UniqueLocation is not null, methodParameters, factories, readers, + fixedSql, additionalCommandState); } } @@ -988,33 +1090,42 @@ private void Generate(SourceProductionContext ctx, (Compilation Compilation, Imm if (needsCommandPrep || !canConstruct) { // at least one command-type needs special handling; do that - sb.Append("private class CommonCommandFactory : ").Append(baseCommandFactory).Append("").Indent().NewLine(); + sb.Append("private class CommonCommandFactory : ").Append(baseCommandFactory).Append("").Indent() + .NewLine(); if (needsCommandPrep) { - sb.Append("public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, T args)").Indent().NewLine() - .Append("var cmd = base.GetCommand(connection, sql, commandType, args);"); + sb.Append( + "public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, T args)") + .Indent().NewLine() + .Append("var cmd = base.GetCommand(connection, sql, commandType, args);"); int cmdTypeIndex = 0; foreach (var type in dbCommandTypes) { var flags = GetSpecialCommandFlags(type); if (flags != SpecialCommandFlags.None) { - sb.NewLine().Append("// apply special per-provider command initialization logic for ").Append(type.Name).NewLine() - .Append(cmdTypeIndex == 0 ? "" : "else ").Append("if (cmd is ").Append(type).Append(" cmd").Append(cmdTypeIndex).Append(")").Indent().NewLine(); + sb.NewLine().Append("// apply special per-provider command initialization logic for ") + .Append(type.Name).NewLine() + .Append(cmdTypeIndex == 0 ? "" : "else ").Append("if (cmd is ").Append(type).Append(" cmd") + .Append(cmdTypeIndex).Append(")").Indent().NewLine(); if ((flags & SpecialCommandFlags.BindByName) != 0) { sb.Append("cmd").Append(cmdTypeIndex).Append(".BindByName = true;").NewLine(); } + if ((flags & SpecialCommandFlags.InitialLONGFetchSize) != 0) { sb.Append("cmd").Append(cmdTypeIndex).Append(".InitialLONGFetchSize = -1;").NewLine(); } + sb.Outdent().NewLine(); cmdTypeIndex++; } } + sb.Append("return cmd;").Outdent().NewLine(); } + sb.Outdent().NewLine(); baseCommandFactory = "CommonCommandFactory"; } @@ -1028,8 +1139,10 @@ private void Generate(SourceProductionContext ctx, (Compilation Compilation, Imm } else { - sb.Append("private static readonly ").Append(baseCommandFactory).Append(" DefaultCommandFactory = new();").NewLine(); + sb.Append("private static readonly ").Append(baseCommandFactory) + .Append(" DefaultCommandFactory = new();").NewLine(); } + sb.NewLine(); foreach (var pair in readers) @@ -1039,7 +1152,8 @@ private void Generate(SourceProductionContext ctx, (Compilation Compilation, Imm foreach (var tuple in factories) { - WriteCommandFactory(ctx, baseCommandFactory, sb, tuple.Type, tuple.Index, tuple.Map, tuple.CacheCount, tuple.AdditionalCommandState); + WriteCommandFactory(ctx, baseCommandFactory, sb, tuple.Type, tuple.Index, tuple.Map, tuple.CacheCount, + tuple.AdditionalCommandState); } sb.Outdent(); // ends our generated file-scoped class @@ -1047,19 +1161,24 @@ private void Generate(SourceProductionContext ctx, (Compilation Compilation, Imm var interceptsLocationWriter = new InterceptorsLocationAttributeWriter(sb); interceptsLocationWriter.Write(state.Compilation); - ctx.AddSource((state.Compilation.AssemblyName ?? "package") + ".generated.cs", SourceText.From(sb.ToString(), Encoding.UTF8)); - ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.InterceptorsGenerated, null, callSiteCount, enabledCount, methodIndex, factories.Count(), readers.Count())); + ctx.AddSource((state.Compilation.AssemblyName ?? "package") + ".generated.cs", + SourceText.From(sb.ToString(), Encoding.UTF8)); + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.InterceptorsGenerated, null, callSiteCount, enabledCount, + methodIndex, factories.Count(), readers.Count())); } - private static void WriteCommandFactory(SourceProductionContext ctx, string baseFactory, CodeWriter sb, ITypeSymbol type, int index, string map, int cacheCount, AdditionalCommandState? additionalCommandState) + private static void WriteCommandFactory(SourceProductionContext 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(" : ") + sb.Append("private ").Append(cacheCount <= 1 ? "sealed" : "abstract").Append(" class CommandFactory") + .Append(index).Append(" : ") .Append(baseFactory).Append("<").Append(declaredType).Append(">"); if (type.IsAnonymousType) { sb.Append(" // ").Append(type); // give the reader a clue } + sb.Indent().NewLine(); @@ -1067,20 +1186,24 @@ private static void WriteCommandFactory(SourceProductionContext ctx, string base { case 0: // default instance - sb.Append("internal static readonly CommandFactory").Append(index).Append(" Instance = new();").NewLine(); + sb.Append("internal static readonly CommandFactory").Append(index).Append(" Instance = new();") + .NewLine(); break; case 1: // default instance, but we named it slightly differently because we were expecting more trouble - sb.Append("internal static readonly CommandFactory").Append(index).Append(" Instance0 = new();").NewLine(); + sb.Append("internal static readonly CommandFactory").Append(index).Append(" Instance0 = new();") + .NewLine(); break; default: // per-usage concrete sub-type - sb.Append("// these represent different call-sites (and most likely all have different SQL etc)").NewLine(); + sb.Append("// these represent different call-sites (and most likely all have different SQL etc)") + .NewLine(); for (int i = 0; i < cacheCount; i++) { sb.Append("internal static readonly CommandFactory").Append(index).Append(".Cached").Append(i) .Append(" Instance").Append(i).Append(" = new();").NewLine(); } + sb.NewLine(); break; } @@ -1092,25 +1215,29 @@ private static void WriteCommandFactory(SourceProductionContext ctx, string base } else { - sb.Append("public override void AddParameters(global::System.Data.Common.DbCommand cmd, ").Append(declaredType).Append(" args)").Indent().NewLine(); + sb.Append("public override void AddParameters(global::System.Data.Common.DbCommand cmd, ") + .Append(declaredType).Append(" args)").Indent().NewLine(); WriteArgs(type, sb, WriteArgsMode.Add, map, ref flags); sb.Outdent().NewLine(); - sb.Append("public override void UpdateParameters(global::System.Data.Common.DbCommand cmd, ").Append(declaredType).Append(" args)").Indent().NewLine(); + sb.Append("public override void UpdateParameters(global::System.Data.Common.DbCommand cmd, ") + .Append(declaredType).Append(" args)").Indent().NewLine(); WriteArgs(type, sb, WriteArgsMode.Update, map, ref flags); sb.Outdent().NewLine(); if ((flags & WriteArgsFlags.NeedsPostProcess) != 0) { - sb.Append("public override void PostProcess(global::System.Data.Common.DbCommand cmd, ").Append(declaredType).Append(" args)").Indent().NewLine(); + sb.Append("public override void PostProcess(global::System.Data.Common.DbCommand cmd, ") + .Append(declaredType).Append(" args)").Indent().NewLine(); WriteArgs(type, sb, WriteArgsMode.PostProcess, map, ref flags); sb.Outdent().NewLine(); } if ((flags & WriteArgsFlags.NeedsRowCount) != 0) { - sb.Append("public override void PostProcess(global::System.Data.Common.DbCommand cmd, ").Append(declaredType).Append(" args, int rowCount)").Indent().NewLine(); + sb.Append("public override void PostProcess(global::System.Data.Common.DbCommand cmd, ") + .Append(declaredType).Append(" args, int rowCount)").Indent().NewLine(); WriteArgs(type, sb, WriteArgsMode.SetRowCount, map, ref flags); sb.Append("PostProcess(cmd, args);"); sb.Outdent().NewLine(); @@ -1127,7 +1254,9 @@ private static void WriteCommandFactory(SourceProductionContext ctx, string base if ((flags & WriteArgsFlags.NeedsTest) != 0) { // I hope to never see this, but I'd rather know than not - sb.Append("#error writing cache, but per-parameter test is needed; this isn't your fault - please report this! for now, mark the offending usage with [CacheCommand(false)]").NewLine(); + sb.Append( + "#error writing cache, but per-parameter test is needed; this isn't your fault - please report this! for now, mark the offending usage with [CacheCommand(false)]") + .NewLine(); } // provide overrides to fetch/store cached commands @@ -1143,9 +1272,16 @@ private static void WriteCommandFactory(SourceProductionContext ctx, string base } else { - sb.Indent(false).NewLine().Append(" => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);").Outdent(false); + sb.Indent(false).NewLine() + .Append( + " => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);") + .Outdent(false); } - sb.NewLine().NewLine().Append("public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);").NewLine(); + + sb.NewLine().NewLine() + .Append( + "public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);") + .NewLine(); if (cacheCount == 1) { @@ -1153,12 +1289,16 @@ private static void WriteCommandFactory(SourceProductionContext ctx, string base } else { - sb.Append("protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}").NewLine().NewLine(); + sb.Append("protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}").NewLine() + .NewLine(); for (int i = 0; i < cacheCount; i++) { - sb.Append("internal sealed class Cached").Append(i).Append(" : CommandFactory").Append(index).Indent().NewLine() - .Append("protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;").NewLine() + sb.Append("internal sealed class Cached").Append(i).Append(" : CommandFactory").Append(index) + .Indent().NewLine() + .Append( + "protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;") + .NewLine() .Append("private static global::System.Data.Common.DbCommand? s_Storage;").NewLine() .Outdent().NewLine(); } @@ -1166,7 +1306,8 @@ private static void WriteCommandFactory(SourceProductionContext ctx, string base } else if (additionalCommandState is not null && additionalCommandState.HasCommandProperties) { - WriteGetCommandHeader(sb, declaredType).Indent().NewLine().Append("var cmd = base.GetCommand(connection, sql, commandType, args);"); + WriteGetCommandHeader(sb, declaredType).Indent().NewLine() + .Append("var cmd = base.GetCommand(connection, sql, commandType, args);"); WriteCommandProperties(ctx, sb, "cmd", additionalCommandState.CommandProperties); sb.NewLine().Append("return cmd;").Outdent(); } @@ -1174,12 +1315,15 @@ private static void WriteCommandFactory(SourceProductionContext ctx, string base sb.Outdent().NewLine().NewLine(); static CodeWriter WriteGetCommandHeader(CodeWriter sb, string declaredType) => sb.NewLine() - .Append("public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,").Indent(false).NewLine() + .Append( + "public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,") + .Indent(false).NewLine() .Append("string sql, global::System.Data.CommandType commandType, ") .Append(declaredType).Append(" args)").Outdent(false); } - private static void WriteCommandProperties(SourceProductionContext ctx, CodeWriter sb, string source, ImmutableArray properties, int index = 0) + private static void WriteCommandProperties(SourceProductionContext ctx, CodeWriter sb, string source, + ImmutableArray properties, int index = 0) { foreach (var grp in properties.GroupBy(x => x.CommandType, SymbolEqualityComparer.Default)) { @@ -1206,14 +1350,17 @@ private static void WriteCommandProperties(SourceProductionContext ctx, CodeWrit { if (IsReserved(prop.Name)) { - ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.CommandPropertyReserved, prop.Location, prop.Name)); + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.CommandPropertyReserved, prop.Location, + prop.Name)); continue; } else if (!HasPublicSettableInstanceMember(type, prop.Name)) { - ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.CommandPropertyNotFound, prop.Location, type.Name, prop.Name)); + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.CommandPropertyNotFound, prop.Location, + type.Name, prop.Name)); continue; } + if (firstForType && !isDbCmd) { sb.NewLine().Append("if (cmd is ").Append(type).Append(" cmd").Append(index).Append(")").Indent(); @@ -1242,8 +1389,10 @@ private static void WriteCommandProperties(SourceProductionContext ctx, CodeWrit sb.Append(Convert.ToString(prop.Value, CultureInfo.InvariantCulture)); break; } + sb.Append(";").NewLine(); } + if (!firstForType && !isDbCmd) // at least one was emitted; close the type test { sb.Outdent(); @@ -1255,7 +1404,8 @@ static bool HasPublicSettableInstanceMember(ITypeSymbol type, string name) { foreach (var member in type.GetMembers()) { - if (member.IsStatic || member.Name != name || member.DeclaredAccessibility != Accessibility.Public) continue; + if (member.IsStatic || member.Name != name || + member.DeclaredAccessibility != Accessibility.Public) continue; switch (member.Kind) { case SymbolKind.Field when member is IFieldSymbol field: return field.IsReadOnly; @@ -1263,6 +1413,7 @@ static bool HasPublicSettableInstanceMember(ITypeSymbol type, string name) default: return false; } } + return false; } @@ -1302,26 +1453,37 @@ private static void WriteRowFactory(SourceProductionContext context, CodeWriter return; } - var members = Inspection.GetMembers(type, dapperAotConstructor: constructor).ToImmutableArray(); + if (!hasDapperFactoryMethod && factorySearchErrorDiagnostic is not null) + { + context.ReportDiagnostic(factorySearchErrorDiagnostic); + // error is emitted, but we still generate default RowFactory to not emit more errors for this type + WriteRowFactoryHeader(); + WriteRowFactoryFooter(); + return; + } + + var members = Inspection + .GetMembers(type, dapperAotConstructor: constructor, dapperAotFactoryMethod: factoryMethod) + .ToImmutableArray(); var membersCount = members.Length; - if (membersCount == 0 && !hasDapperConstructor) + if (membersCount == 0 && !hasDapperConstructor && !hasDapperFactoryMethod) { - // there are so settable members + there is no constructor to use - context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UserTypeNoSettableMembersFound, type.Locations.First(), type.ToDisplayString())); + // there are so settable members + there is no way to construct an instance + context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UserTypeNoSettableMembersFound, + type.Locations.First(), type.ToDisplayString())); // error is emitted, but we still generate default RowFactory to not emit more errors for this type WriteRowFactoryHeader(); WriteRowFactoryFooter(); - return; } var hasInitOnlyMembers = members.Any(member => member.IsInitOnly); - var hasGetOnlyMembers = members.Any(member => member.IsGettable && !member.IsSettable && !member.IsInitOnly); - var useDeferredConstruction = hasDapperConstructor || hasInitOnlyMembers || hasGetOnlyMembers; + var hasGetOnlyMembers = members.Any(member => member is { IsGettable: true, IsSettable: false, IsInitOnly: false }); + var useDeferredConstruction = hasDapperConstructor || hasDapperFactoryMethod || hasInitOnlyMembers || hasGetOnlyMembers; - WriteRowFactoryHeader(); + WriteRowFactoryHeader(); WriteTokenizeMethod(); WriteReadMethod(); @@ -1330,11 +1492,13 @@ private static void WriteRowFactory(SourceProductionContext context, CodeWriter void WriteRowFactoryHeader() { - sb.Append("private sealed class RowFactory").Append(index).Append(" : global::Dapper.RowFactory").Append("<").Append(type).Append(">") - .Indent().NewLine() - .Append("internal static readonly RowFactory").Append(index).Append(" Instance = new();").NewLine() - .Append("private RowFactory").Append(index).Append("() {}").NewLine(); + sb.Append("private sealed class RowFactory").Append(index).Append(" : global::Dapper.RowFactory") + .Append("<").Append(type).Append(">") + .Indent().NewLine() + .Append("internal static readonly RowFactory").Append(index).Append(" Instance = new();").NewLine() + .Append("private RowFactory").Append(index).Append("() {}").NewLine(); } + void WriteRowFactoryFooter() { sb.Outdent().NewLine().NewLine(); @@ -1342,7 +1506,9 @@ void WriteRowFactoryFooter() void WriteTokenizeMethod() { - sb.Append("public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset)").Indent().NewLine(); + sb.Append( + "public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset)") + .Indent().NewLine(); sb.Append("for (int i = 0; i < tokens.Length; i++)").Indent().NewLine() .Append("int token = -1;").NewLine() .Append("var name = reader.GetName(columnOffset);").NewLine() @@ -1356,25 +1522,33 @@ void WriteTokenizeMethod() sb.Append("case ").Append(StringHashing.NormalizedHash(dbName)) .Append(" when NormalizedEquals(name, ") .AppendVerbatimLiteral(StringHashing.Normalize(dbName)).Append("):").Indent(false).NewLine() - .Append("token = type == typeof(").Append(Inspection.MakeNonNullable(member.CodeType)).Append(") ? ").Append(token) + .Append("token = type == typeof(").Append(Inspection.MakeNonNullable(member.CodeType)) + .Append(") ? ").Append(token) .Append(" : ").Append(token + membersCount).Append(";") .Append(token == 0 ? " // two tokens for right-typed and type-flexible" : "").NewLine() .Append("break;").Outdent(false).NewLine(); token++; } + sb.Outdent().NewLine() .Append("tokens[i] = token;").NewLine() .Append("columnOffset++;").NewLine(); sb.Outdent().NewLine().Append("return null;").Outdent().NewLine(); } + void WriteReadMethod() { const string DeferredConstructionVariableName = "value"; - sb.Append("public override ").Append(type).Append(" Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state)").Indent().NewLine(); + sb.Append("public override ").Append(type) + .Append( + " Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state)") + .Indent().NewLine(); int token = 0; var constructorArgumentsOrdered = new SortedList(); + var factoryMethodArgumentsOrdered = new SortedList(); + if (useDeferredConstruction) { // dont create an instance now, but define the variables to create an instance later like @@ -1387,16 +1561,21 @@ void WriteReadMethod() { var variableName = DeferredConstructionVariableName + token; - if (CouldBeNullable(member.CodeType)) sb.Append(CodeWriter.GetTypeName(member.CodeType.WithNullableAnnotation(NullableAnnotation.Annotated))); + if (CouldBeNullable(member.CodeType)) + sb.Append(CodeWriter.GetTypeName( + member.CodeType.WithNullableAnnotation(NullableAnnotation.Annotated))); else sb.Append(CodeWriter.GetTypeName(member.CodeType)); sb.Append(' ').Append(variableName).Append(" = default;").NewLine(); - // filling in the constructor arguments in first iteration through members - // will be used afterwards to create the instance + // will be used for deferred construction if (member.ConstructorParameterOrder is not null) { constructorArgumentsOrdered.Add(member.ConstructorParameterOrder.Value, variableName); } + if (member.FactoryMethodParameterOrder is not null) + { + factoryMethodArgumentsOrdered.Add(member.FactoryMethodParameterOrder.Value, variableName); + } token++; } @@ -1405,11 +1584,12 @@ void WriteReadMethod() { // we are not using a constructor, so we need to create an instance now sb.Append(type.NullableAnnotation == NullableAnnotation.Annotated - ? type.WithNullableAnnotation(NullableAnnotation.None) : type).Append(" result = new();").NewLine(); + ? type.WithNullableAnnotation(NullableAnnotation.None) + : type).Append(" result = new();").NewLine(); } sb.Append("foreach (var token in tokens)").Indent().NewLine() - .Append("switch (token)").Indent().NewLine(); + .Append("switch (token)").Indent().NewLine(); token = 0; foreach (var member in members) @@ -1417,7 +1597,9 @@ void WriteReadMethod() var memberType = member.CodeType; member.GetDbType(out var readerMethod); - var nullCheck = CouldBeNullable(memberType) ? $"reader.IsDBNull(columnOffset) ? ({CodeWriter.GetTypeName(memberType.WithNullableAnnotation(NullableAnnotation.Annotated))})null : " : ""; + var nullCheck = CouldBeNullable(memberType) + ? $"reader.IsDBNull(columnOffset) ? ({CodeWriter.GetTypeName(memberType.WithNullableAnnotation(NullableAnnotation.Annotated))})null : " + : ""; sb.Append("case ").Append(token).Append(":").NewLine().Indent(false); // write `result.X = ` or `member0 = ` @@ -1435,20 +1617,20 @@ void WriteReadMethod() sb.Append("reader.").Append(readerMethod).Append("(columnOffset);"); } - + sb.NewLine().Append("break;").NewLine().Outdent(false) .Append("case ").Append(token + membersCount).Append(":").NewLine().Indent(false); // write `result.X = ` or `member0 = ` if (useDeferredConstruction) sb.Append(DeferredConstructionVariableName).Append(token); else sb.Append("result.").Append(member.CodeName); - + sb.Append(" = ") .Append(nullCheck) .Append("GetValue<") .Append(Inspection.MakeNonNullable(memberType)).Append(">(reader, columnOffset);").NewLine() .Append("break;").NewLine().Outdent(false); - + token++; } @@ -1456,7 +1638,8 @@ void WriteReadMethod() if (useDeferredConstruction) { - // create instance using constructor. like + // create instance using deferred construction. + // 1) in case of constructor: // ``` // return new Type(member0, member1, member2, ...) // { @@ -1464,35 +1647,65 @@ void WriteReadMethod() // SettableMember2 = member4, // } // ``` + // 2) in case of factoryMethod: + // ``` + // return Type(member0, member1, member2, ...) + // ``` + + if (hasDapperFactoryMethod) WriteFactoryMethod(); + else WriteConstructor(); + sb.Append(";").Outdent(); - sb.Append("return new ").Append(type); - if (hasDapperConstructor && constructorArgumentsOrdered.Count != 0) + void WriteConstructor() { - // write `(member0, member1, member2, ...)` part of constructor - sb.Append('('); - foreach (var constructorArg in constructorArgumentsOrdered) + sb.Append("return new ").Append(type); + if (constructorArgumentsOrdered.Count != 0) { - sb.Append(constructorArg.Value).Append(", "); + // write `(member0, member1, member2, ...)` part of constructor + sb.Append('('); + foreach (var constructorArg in constructorArgumentsOrdered) + { + sb.Append(constructorArg.Value).Append(", "); + } + + sb.RemoveLast(2); // remove last ', ' generated in the loop + sb.Append(')'); + } + + // if all members are constructor arguments, no need to set them again + if (constructorArgumentsOrdered.Count != members.Length) + { + sb.Indent().NewLine(); + token = -1; + foreach (var member in members) + { + token++; + if (member.ConstructorParameterOrder is not null) + continue; // already used in constructor arguments + sb.Append(member.CodeName).Append(" = ").Append(DeferredConstructionVariableName) + .Append(token).Append(',').NewLine(); + } + + sb.Outdent(withScope: false).Append("}"); } - sb.RemoveLast(2); // remove last ', ' generated in the loop - sb.Append(')'); } - // if all members are constructor arguments, no need to set them again - if (constructorArgumentsOrdered.Count != members.Length) + void WriteFactoryMethod() { - sb.Indent().NewLine(); - token = -1; - foreach (var member in members) + sb.Append("return ").Append(type).Append('.').Append(factoryMethod.Name); + if (factoryMethodArgumentsOrdered.Count != 0) { - token++; - if (member.ConstructorParameterOrder is not null) continue; // already used in constructor arguments - sb.Append(member.CodeName).Append(" = ").Append(DeferredConstructionVariableName).Append(token).Append(',').NewLine(); + // write `(member0, member1, member2, ...)` part of constructor + sb.Append('('); + foreach (var arg in factoryMethodArgumentsOrdered) + { + sb.Append(arg.Value).Append(", "); + } + + sb.RemoveLast(2); // remove last ', ' generated in the loop + sb.Append(')'); } - sb.Outdent(withScope: false).Append("}"); } - - sb.Append(";").Outdent(); } else { @@ -1518,11 +1731,14 @@ enum WriteArgsFlags enum WriteArgsMode { - Add, Update, PostProcess, + Add, + Update, + PostProcess, SetRowCount } - private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteArgsMode mode, string map, ref WriteArgsFlags flags) + private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteArgsMode mode, string map, + ref WriteArgsFlags flags) { if (parameterType is null) { @@ -1539,7 +1755,8 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr } if (mode == WriteArgsMode.Add) - { // we'll calculate this; assume we can, and claw backwards from there + { + // we'll calculate this; assume we can, and claw backwards from there flags |= WriteArgsFlags.CanPrepare; } @@ -1555,6 +1772,7 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr sb.Append(source).Append(".").Append(member.CodeName).Append(" = rowCount;").NewLine(); } } + if (mode == WriteArgsMode.SetRowCount || member.IsRowCount) { // row-count mode *only* does the above, and row-count members are *only* @@ -1566,6 +1784,7 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr { continue; // not required } + var direction = member.Direction; if (mode == WriteArgsMode.PostProcess) { @@ -1590,6 +1809,7 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr sb.Append("global::System.Data.Common.DbParameter p;").NewLine(); break; } + first = false; } else if (mode == WriteArgsMode.Add) @@ -1603,12 +1823,16 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr // add is seeing this for the first time if (firstTest) { - sb.Append("var sql = cmd.CommandText;").NewLine().Append("var commandType = cmd.CommandType;").NewLine(); + sb.Append("var sql = cmd.CommandText;").NewLine().Append("var commandType = cmd.CommandType;") + .NewLine(); flags |= WriteArgsFlags.NeedsTest; firstTest = false; } - sb.Append("if (Include(sql, commandType, ").AppendVerbatimLiteral(member.DbName).Append("))").Indent().NewLine(); + + sb.Append("if (Include(sql, commandType, ").AppendVerbatimLiteral(member.DbName).Append("))").Indent() + .NewLine(); } + switch (mode) { case WriteArgsMode.Add: @@ -1619,7 +1843,8 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr var size = member.TryGetValue("Size"); if (dbType is not null) { - sb.Append("p.DbType = global::System.Data.DbType.").Append(dbType.GetValueOrDefault().ToString()).Append(";").NewLine(); + sb.Append("p.DbType = global::System.Data.DbType.") + .Append(dbType.GetValueOrDefault().ToString()).Append(";").NewLine(); if (size is null) { switch (dbType.GetValueOrDefault()) @@ -1638,6 +1863,7 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr // string/binary args to have a size, but: we've set that) flags &= ~WriteArgsFlags.CanPrepare; } + AppendDbParameterSetting(sb, "Size", size); AppendDbParameterSetting(sb, "Precision", member.TryGetValue("Precision")); AppendDbParameterSetting(sb, "Scale", member.TryGetValue("Scale")); @@ -1654,12 +1880,14 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr { case ParameterDirection.Input: case ParameterDirection.InputOutput: - sb.Append("AsValue(").Append(source).Append(".").Append(member.CodeName).Append(");").NewLine(); + sb.Append("AsValue(").Append(source).Append(".").Append(member.CodeName).Append(");") + .NewLine(); break; default: sb.Append("global::System.DBNull.Value;").NewLine(); break; } + sb.Append("ps.Add(p);").NewLine(); switch (direction) @@ -1670,6 +1898,7 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr flags |= WriteArgsFlags.NeedsPostProcess; break; } + break; case WriteArgsMode.Update: sb.Append("ps["); @@ -1680,13 +1909,14 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr { case ParameterDirection.Input: case ParameterDirection.InputOutput: - sb.Append("AsValue(").Append(source).Append(".").Append(member.CodeName).Append(");").NewLine(); + sb.Append("AsValue(").Append(source).Append(".").Append(member.CodeName).Append(");") + .NewLine(); break; default: sb.Append("global::System.DBNull.Value;").NewLine(); break; - } + break; case WriteArgsMode.PostProcess: // we already eliminated args that we don't need to look at @@ -1698,10 +1928,12 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr break; } + if (test) { sb.Outdent().NewLine(); } + parameterIndex++; } } @@ -1713,6 +1945,7 @@ static void AppendDbParameterSetting(CodeWriter sb, string memberName, int? valu sb.Append("p.").Append(memberName).Append(" = ").Append(value.GetValueOrDefault()).Append(";").NewLine(); } } + static void AppendDbParameterSetting(CodeWriter sb, string memberName, byte? value) { if (value is not null) @@ -1737,14 +1970,17 @@ private static void AppendShapeLambda(CodeWriter sb, ITypeSymbol parameterType) { if (CodeWriter.IsGettableInstanceMember(member, out var type)) { - sb.Append(first ? " " : ", ").Append(member.Name).Append(" = default(").Append(type).Append(")"); + sb.Append(first ? " " : ", ").Append(member.Name).Append(" = default(").Append(type) + .Append(")"); if (type.IsReferenceType && type.NullableAnnotation == NullableAnnotation.None) { sb.Append("!"); } + first = false; } } + sb.Append(" }"); break; } @@ -1767,13 +2003,14 @@ private static SpecialCommandFlags GetSpecialCommandFlags(ITypeSymbol type) break; } } + return flags; static bool IsSettableInstanceProperty(ISymbol? symbol, SpecialType type) => symbol is IPropertySymbol prop && prop.DeclaredAccessibility == Accessibility.Public - && prop.SetMethod is { DeclaredAccessibility: Accessibility.Public } - && prop.Type.SpecialType == type - && !prop.IsIndexer && !prop.IsStatic; + && prop.SetMethod is { DeclaredAccessibility: Accessibility.Public } + && prop.Type.SpecialType == type + && !prop.IsIndexer && !prop.IsStatic; } [Flags] @@ -1793,6 +2030,7 @@ private ImmutableArray IdentifyDbCommandTypes(Compilation compilati // if we can't find DbCommand, we're out of luck return ImmutableArray.Empty; } + var pending = new Queue(); foreach (var assemblyName in compilation.References) { @@ -1808,6 +2046,7 @@ private ImmutableArray IdentifyDbCommandTypes(Compilation compilati pending.Enqueue(ns); } } + var found = new HashSet(SymbolEqualityComparer.Default); while (pending.Count != 0) { @@ -1821,7 +2060,8 @@ private ImmutableArray IdentifyDbCommandTypes(Compilation compilati break; case ITypeSymbol type: // only interested in public non-static classes - if (!type.IsStatic && type.TypeKind == TypeKind.Class && type.DeclaredAccessibility == Accessibility.Public) + if (!type.IsStatic && type.TypeKind == TypeKind.Class && + type.DeclaredAccessibility == Accessibility.Public) { // note we're not checking for nested types; that seems incredibly unlikely for ADO.NET types if (IsDerived(type, dbCommand)) @@ -1829,6 +2069,7 @@ private ImmutableArray IdentifyDbCommandTypes(Compilation compilati found.Add(type); } } + break; } } @@ -1842,6 +2083,7 @@ private ImmutableArray IdentifyDbCommandTypes(Compilation compilati break; // only need at least one } } + return found.ToImmutableArray(); static bool IsDerived(ITypeSymbol? type, ITypeSymbol baseType) @@ -1854,6 +2096,7 @@ static bool IsDerived(ITypeSymbol? type, ITypeSymbol baseType) return true; } } + return false; } } @@ -1870,6 +2113,7 @@ sealed class SourceState public ITypeSymbol? ResultType { get; } public ITypeSymbol? ParameterType { get; } public AdditionalCommandState? AdditionalCommandState { get; } + public SourceState(Location location, IMethodSymbol method, OperationFlags flags, string? sql, ITypeSymbol? resultType, ITypeSymbol? parameterType, string parameterMap, AdditionalCommandState? additionalCommandState, object? diagnostics = null) @@ -1900,25 +2144,37 @@ public SourceState(Location location, IMethodSymbol method, OperationFlags flags _ => throw new IndexOutOfRangeException(nameof(index)), }; - public (OperationFlags Flags, IMethodSymbol Method, ITypeSymbol? ParameterType, string ParameterMap, Location? UniqueLocation, AdditionalCommandState? AdditionalCommandState) Group() - => new(Flags, Method, ParameterType, ParameterMap, (Flags & (OperationFlags.CacheCommand | OperationFlags.IncludeLocation)) == 0 ? null : Location, AdditionalCommandState); + public (OperationFlags Flags, IMethodSymbol Method, ITypeSymbol? ParameterType, string ParameterMap, Location? + UniqueLocation, AdditionalCommandState? AdditionalCommandState) Group() + => new(Flags, Method, ParameterType, ParameterMap, + (Flags & (OperationFlags.CacheCommand | OperationFlags.IncludeLocation)) == 0 ? null : Location, + AdditionalCommandState); } - private sealed class CommonComparer : LocationComparer, IEqualityComparer<(OperationFlags Flags, IMethodSymbol Method, ITypeSymbol? ParameterType, string ParameterMap, Location? UniqueLocation, AdditionalCommandState? AdditionalCommandState)> + + private sealed class CommonComparer : LocationComparer, + IEqualityComparer<(OperationFlags Flags, IMethodSymbol Method, ITypeSymbol? ParameterType, string ParameterMap, + Location? UniqueLocation, AdditionalCommandState? AdditionalCommandState)> { public static readonly CommonComparer Instance = new(); - private CommonComparer() { } - public bool Equals( - - (OperationFlags Flags, IMethodSymbol Method, ITypeSymbol? ParameterType, string ParameterMap, Location? UniqueLocation, AdditionalCommandState? AdditionalCommandState) x, - (OperationFlags Flags, IMethodSymbol Method, ITypeSymbol? ParameterType, string ParameterMap, Location? UniqueLocation, AdditionalCommandState? AdditionalCommandState) y) => x.Flags == y.Flags - && x.ParameterMap == y.ParameterMap - && SymbolEqualityComparer.Default.Equals(x.Method, y.Method) - && SymbolEqualityComparer.Default.Equals(x.ParameterType, y.ParameterType) - && x.UniqueLocation == y.UniqueLocation - && Equals(x.AdditionalCommandState, y.AdditionalCommandState); + private CommonComparer() + { + } - public int GetHashCode((OperationFlags Flags, IMethodSymbol Method, ITypeSymbol? ParameterType, string ParameterMap, Location? UniqueLocation, AdditionalCommandState? AdditionalCommandState) obj) + public bool Equals( + (OperationFlags Flags, IMethodSymbol Method, ITypeSymbol? ParameterType, string ParameterMap, Location? + UniqueLocation, AdditionalCommandState? AdditionalCommandState) x, + (OperationFlags Flags, IMethodSymbol Method, ITypeSymbol? ParameterType, string ParameterMap, Location? + UniqueLocation, AdditionalCommandState? AdditionalCommandState) y) => x.Flags == y.Flags + && x.ParameterMap == y.ParameterMap + && SymbolEqualityComparer.Default.Equals(x.Method, y.Method) + && SymbolEqualityComparer.Default.Equals(x.ParameterType, y.ParameterType) + && x.UniqueLocation == y.UniqueLocation + && Equals(x.AdditionalCommandState, y.AdditionalCommandState); + + public int GetHashCode( + (OperationFlags Flags, IMethodSymbol Method, ITypeSymbol? ParameterType, string ParameterMap, Location? + UniqueLocation, AdditionalCommandState? AdditionalCommandState) obj) { var hash = (int)obj.Flags; hash *= -47; @@ -1930,16 +2186,19 @@ public int GetHashCode((OperationFlags Flags, IMethodSymbol Method, ITypeSymbol? { hash += SymbolEqualityComparer.Default.GetHashCode(obj.ParameterType); } + hash *= -47; if (obj.UniqueLocation is not null) { hash += obj.UniqueLocation.GetHashCode(); } + hash *= -47; if (obj.AdditionalCommandState is not null) { hash += obj.AdditionalCommandState.GetHashCode(); } + return hash; } } diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/Diagnostics.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/Diagnostics.cs index f03d252c..84e8ae8b 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/Diagnostics.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/Diagnostics.cs @@ -84,6 +84,8 @@ internal static readonly DiagnosticDescriptor "Type has more than 1 constructor, please, either mark one constructor with [DapperAot] or reduce amount of constructors", Category.Library, DiagnosticSeverity.Error, true), UserTypeNoSettableMembersFound = new("DAP037", "No settable members exist for user type", "Type '{0}' has no settable members (fields or properties)", Category.Library, DiagnosticSeverity.Error, true), + TooManyDapperAotEnabledFactoryMethods = new("DAP038", "Too many Dapper.AOT enabled factory methods", + "Only one factory method can be Dapper.AOT enabled per type '{0}'", Category.Library, DiagnosticSeverity.Error, true), // TypeAccessor TypeAccessorCollectionTypeNotAllowed = new("DAP100", "TypeAccessors does not allow collection types", diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index a46c47e6..70dd24ab 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -304,23 +304,23 @@ private static bool IsCollectionType(ITypeSymbol? parameterType, out ITypeSymbol } [DebuggerDisplay("Order: {Order}; Name: {Name}")] - public readonly struct ConstructorParameter + public readonly struct MethodParameter { /// - /// Order of parameter in constructor. - /// Will be 1 for member1 in constructor(member0, member1, ...) + /// Order of parameter in method. + /// Will be 1 for member1 in method(member0, member1, ...) /// public int Order { get; } /// - /// Type of constructor parameter + /// Type of method parameter /// public ITypeSymbol Type { get; } /// - /// Name of constructor parameter + /// Name of method parameter /// public string Name { get; } - public ConstructorParameter(int order, ITypeSymbol type, string name) + public MethodParameter(int order, ITypeSymbol type, string name) { Order = order; Type = type; @@ -382,6 +382,10 @@ public readonly struct ElementMember /// Order of member in constructor parameter list (starts from 0). /// public int? ConstructorParameterOrder { get; } + /// + /// Order of member in factory method parameter list (starts from 0). + /// + public int? FactoryMethodParameterOrder { get; } public ElementMember(ISymbol member, AttributeData? dbValue, ElementMemberKind kind) { @@ -390,7 +394,15 @@ public ElementMember(ISymbol member, AttributeData? dbValue, ElementMemberKind k Kind = kind; } - public ElementMember(ISymbol member, AttributeData? dbValue, ElementMemberKind kind, bool isGettable, bool isSettable, bool isInitOnly, int? constructorParameterOrder) + public ElementMember( + ISymbol member, + AttributeData? dbValue, + ElementMemberKind kind, + bool isGettable, + bool isSettable, + bool isInitOnly, + int? constructorParameterOrder, + int? factoryMethodParameterOrder) { Member = member; _dbValue = dbValue; @@ -400,6 +412,7 @@ public ElementMember(ISymbol member, AttributeData? dbValue, ElementMemberKind k IsSettable = isSettable; IsInitOnly = isInitOnly; ConstructorParameterOrder = constructorParameterOrder; + FactoryMethodParameterOrder = factoryMethodParameterOrder; } public override int GetHashCode() => SymbolEqualityComparer.Default.GetHashCode(Member); @@ -420,7 +433,42 @@ public static bool TryGetSingleCompatibleDapperAotFactoryMethod( factoryMethod = null!; var (standardFactories, dapperAotFactories) = ChooseDapperAotCompatibleFactoryMethods(typeSymbol); + if (standardFactories.Count == 0 && dapperAotFactories.Count == 0) + { + errorDiagnostic = null; + factoryMethod = null!; + return false; + } + + // if multiple factory methods remain, and multiple are marked [DapperAot]/[DapperAot(true)], + // a generator error is emitted and no constructor is selected + if (dapperAotFactories.Count > 1) + { + // attaching diagnostic to first location of first ctor + var loc = dapperAotFactories.First().Locations.First(); + + errorDiagnostic = Diagnostic.Create(Diagnostics.TooManyDapperAotEnabledFactoryMethods, loc, typeSymbol!.ToDisplayString()); + factoryMethod = null!; + return false; + } + + if (dapperAotFactories.Count == 1) + { + errorDiagnostic = null; + factoryMethod = dapperAotFactories.First(); + return true; + } + + if (standardFactories.Count == 1) + { + errorDiagnostic = null; + factoryMethod = standardFactories.First(); + return true; + } + // we cant choose anything + errorDiagnostic = null; + factoryMethod = null!; return false; } @@ -481,8 +529,7 @@ bool FilterFactoryMethods(IMethodSymbol methodSymbol) => methodSymbol is { IsStatic: true, DeclaredAccessibility: Accessibility.Public } && SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, typeSymbol); - var methodSymbols = typeSymbol.GetMethods(filter: FilterFactoryMethods); - // TODO general question: do I need to avoid another enumeration here? cast to array? + var methodSymbols = typeSymbol.GetMethods(filter: FilterFactoryMethods)?.ToImmutableArray(); if (methodSymbols?.Any() == false) { return (standardFactories: Array.Empty(), dapperAotFactories: Array.Empty()); @@ -490,32 +537,15 @@ bool FilterFactoryMethods(IMethodSymbol methodSymbol) var standardFactories = new List(); var dapperAotFactories = new List(); - foreach (var methodSymbol in methodSymbols) + foreach (var methodSymbol in methodSymbols!) { // not taking into an account parameterless methods if (methodSymbol.Parameters.Length == 0) continue; - var dapperAotAttribute = HasDapperAotEnabledAttribute(methodSymbol); - if (dapperAotAttribute is null) - { - // picking constructor which is not marked with [DapperAot] attribute at all - standardFactories.Add(methodSymbol); - continue; - } - - if (dapperAotAttribute.ConstructorArguments.Length == 0) - { - // picking constructor which is marked with [DapperAot] attribute without arguments (its enabled by default) - dapperAotFactories.Add(methodSymbol); - continue; - } - - var typedArg = dapperAotAttribute.ConstructorArguments.First(); - if (typedArg.Value is true) - { - // picking constructor which is marked with explicit [DapperAot(true)] - dapperAotFactories.Add(methodSymbol); - } + var hasDapperAotEnabled = HasDapperAotEnabledAttribute(methodSymbol); + + if (hasDapperAotEnabled) dapperAotFactories.Add(methodSymbol); + else standardFactories.Add(methodSymbol); } return (standardFactories, dapperAotFactories); @@ -555,27 +585,10 @@ private static (IReadOnlyCollection standardConstructors, IReadOn // not taking into an account parameterless constructors if (constructorMethodSymbol.Parameters.Length == 0) continue; - var dapperAotAttribute = GetDapperAttribute(constructorMethodSymbol, Types.DapperAotAttribute); - if (dapperAotAttribute is null) - { - // picking constructor which is not marked with [DapperAot] attribute at all - standardCtors.Add(constructorMethodSymbol); - continue; - } - - if (dapperAotAttribute.ConstructorArguments.Length == 0) - { - // picking constructor which is marked with [DapperAot] attribute without arguments (its enabled by default) - dapperAotEnabledCtors.Add(constructorMethodSymbol); - continue; - } - - var typedArg = dapperAotAttribute.ConstructorArguments.First(); - if (typedArg.Value is true) - { - // picking constructor which is marked with explicit [DapperAot(true)] - dapperAotEnabledCtors.Add(constructorMethodSymbol); - } + var hasDapperAotEnabled = HasDapperAotEnabledAttribute(constructorMethodSymbol); + + if (hasDapperAotEnabled) dapperAotEnabledCtors.Add(constructorMethodSymbol); + else standardCtors.Add(constructorMethodSymbol); } return (standardCtors, dapperAotEnabledCtors); @@ -583,10 +596,14 @@ private static (IReadOnlyCollection standardConstructors, IReadOn /// /// Yields the type's members. - /// If is passed, will be used to associate element member with the constructor parameter by name (case-insensitive). /// /// type, which elements to parse - public static IEnumerable GetMembers(ITypeSymbol? elementType, IMethodSymbol? dapperAotConstructor = null) + /// If is passed, will be used to associate element member with the constructor parameter by name (case-insensitive). + /// If is passed, will be used to associate element member with the factoryMethod parameter by name (case-insensitive). + public static IEnumerable GetMembers( + ITypeSymbol? elementType, + IMethodSymbol? dapperAotConstructor = null, + IMethodSymbol? dapperAotFactoryMethod = null) { if (elementType is null) { @@ -601,7 +618,9 @@ public static IEnumerable GetMembers(ITypeSymbol? elementType, IM } else { - var constructorParameters = (dapperAotConstructor is not null) ? ParseConstructorParameters(dapperAotConstructor) : null; + var constructorParameters = (dapperAotConstructor is not null) ? ParseMethodParameters(dapperAotConstructor) : null; + var factoryMethodParameters = (dapperAotFactoryMethod is not null) ? ParseMethodParameters(dapperAotFactoryMethod) : null; + foreach (var member in elementType.GetMembers()) { // instance only, must be able to access by name @@ -638,23 +657,26 @@ public static IEnumerable GetMembers(ITypeSymbol? elementType, IM int? constructorParameterOrder = constructorParameters?.TryGetValue(member.Name, out var constructorParameter) == true ? constructorParameter.Order : null; + int? factoryMethodParameterOrder = factoryMethodParameters?.TryGetValue(member.Name, out var factoryMethodParameter) == true + ? factoryMethodParameter.Order + : null; var isGettable = CodeWriter.IsGettableInstanceMember(member, out _); var isSettable = CodeWriter.IsSettableInstanceMember(member, out _); var isInitOnly = CodeWriter.IsInitOnlyInstanceMember(member, out _); // all good, then! - yield return new(member, dbValue, kind, isGettable, isSettable, isInitOnly, constructorParameterOrder); + yield return new(member, dbValue, kind, isGettable, isSettable, isInitOnly, constructorParameterOrder, factoryMethodParameterOrder); } } - IReadOnlyDictionary ParseConstructorParameters(IMethodSymbol constructorSymbol) + IReadOnlyDictionary ParseMethodParameters(IMethodSymbol constructorSymbol) { - var parameters = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var parameters = new Dictionary(StringComparer.InvariantCultureIgnoreCase); int order = 0; foreach (var parameter in constructorSymbol.Parameters) { - parameters.Add(parameter.Name, new ConstructorParameter(order: order++, type: parameter.Type, name: parameter.Name)); + parameters.Add(parameter.Name, new MethodParameter(order: order++, type: parameter.Type, name: parameter.Name)); } return parameters; } diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs index 290b384a..e086e75c 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs @@ -828,38 +828,39 @@ private RowFactory9() {} } public override global::Foo.SingleDefaultCtor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) { - int value0 = default; - string? value1 = default; - double? value2 = default; + // TODO CHECK WHY + global::Foo.SingleDefaultCtor result = new(); foreach (var token in tokens) { switch (token) { case 0: - value0 = reader.GetInt32(columnOffset); + result.X = reader.GetInt32(columnOffset); break; case 3: - value0 = GetValue(reader, columnOffset); + result.X = GetValue(reader, columnOffset); break; case 1: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); break; case 4: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); break; case 2: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); break; case 5: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); break; } columnOffset++; } - return new global::Foo.SingleDefaultCtor(value0, value1, value2); + return result; + } + } private sealed class RowFactory10 : global::Dapper.RowFactory diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs index ebe031cb..24c19720 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs @@ -8,6 +8,7 @@ public static class Foo static void SomeCode(DbConnection connection, string bar, bool isBuffered) { _ = connection.Query("def"); + _ = connection.Query("def"); } public class PublicPropertiesNoConstructor @@ -20,4 +21,19 @@ public class PublicPropertiesNoConstructor public static PublicPropertiesNoConstructor Construct(int x, string y, double? z) => new PublicPropertiesNoConstructor { X = x, Y = y, Z = z }; } + + public class MultipleFactoryMethods + { + public int X { get; set; } + public string Y { get; set; } + public double? Z { get; set; } + + [DapperAot(true)] + public static MultipleFactoryMethods Construct(int x, string y, double? z) + => new MultipleFactoryMethods { X = x, Y = y, Z = z }; + + [DapperAot(true)] + public static MultipleFactoryMethods Construct2(int x, string y, double? z) + => new MultipleFactoryMethods { X = x, Y = y, Z = z }; + } } \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs index 74bf8991..3b26fff3 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs @@ -15,6 +15,20 @@ file static class DapperGeneratedInterceptors } + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 11, 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, Buffered, StoredProcedure + // returns data: global::Foo.MultipleFactoryMethods + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.StoredProcedure, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QueryBuffered(param, RowFactory1.Instance); + + } + 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) @@ -66,37 +80,44 @@ private RowFactory0() {} } public override global::Foo.PublicPropertiesNoConstructor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) { - global::Foo.PublicPropertiesNoConstructor result = new(); + int value0 = default; + string? value1 = default; + double? value2 = default; foreach (var token in tokens) { switch (token) { case 0: - result.X = reader.GetInt32(columnOffset); + value0 = reader.GetInt32(columnOffset); break; case 3: - result.X = GetValue(reader, columnOffset); + value0 = GetValue(reader, columnOffset); break; case 1: - result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); break; case 4: - result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); break; case 2: - result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); break; case 5: - result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); break; } columnOffset++; } - return result; - + return global::Foo.PublicPropertiesNoConstructor.Construct(value0, value1, value2); } + } + + private sealed class RowFactory1 : global::Dapper.RowFactory + { + internal static readonly RowFactory1 Instance = new(); + private RowFactory1() {} } diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt index 3783f0a6..f039ff99 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt @@ -1,4 +1,7 @@ -Generator produced 1 diagnostics: +Generator produced 2 diagnostics: Hidden DAP000 L1 C1 -Dapper.AOT handled 1 of 1 enabled call-sites using 1 interceptors, 0 commands and 1 readers +Dapper.AOT handled 2 of 2 enabled call-sites using 2 interceptors, 0 commands and 2 readers + +Error DAP038 Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs L32 C46 +Only one factory method can be Dapper.AOT enabled per type 'Foo.MultipleFactoryMethods' From 652f77bd8ff9a9a3450c9cc8f78c9b87c6e531c8 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Mon, 11 Sep 2023 12:41:32 +0200 Subject: [PATCH 3/9] more tests --- ...stomConstructionWithFactoryMethod.input.cs | 37 +++- ...tomConstructionWithFactoryMethod.output.cs | 160 ++++++++++++++++++ ...omConstructionWithFactoryMethod.output.txt | 4 +- 3 files changed, 193 insertions(+), 8 deletions(-) diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs index 24c19720..6dd83d06 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs @@ -8,7 +8,9 @@ public static class Foo static void SomeCode(DbConnection connection, string bar, bool isBuffered) { _ = connection.Query("def"); - _ = connection.Query("def"); + _ = connection.Query("def"); + _ = connection.Query("def"); + _ = connection.Query("def"); } public class PublicPropertiesNoConstructor @@ -22,18 +24,41 @@ public static PublicPropertiesNoConstructor Construct(int x, string y, double? z => new PublicPropertiesNoConstructor { X = x, Y = y, Z = z }; } - public class MultipleFactoryMethods + public class MultipleDapperAotFactoryMethods { public int X { get; set; } public string Y { get; set; } public double? Z { get; set; } [DapperAot(true)] - public static MultipleFactoryMethods Construct(int x, string y, double? z) - => new MultipleFactoryMethods { X = x, Y = y, Z = z }; + public static MultipleDapperAotFactoryMethods Construct(int x, string y, double? z) + => new MultipleDapperAotFactoryMethods { X = x, Y = y, Z = z }; [DapperAot(true)] - public static MultipleFactoryMethods Construct2(int x, string y, double? z) - => new MultipleFactoryMethods { X = x, Y = y, Z = z }; + public static MultipleDapperAotFactoryMethods Construct2(int x, string y, double? z) + => new MultipleDapperAotFactoryMethods { X = x, Y = y, Z = z }; + } + + public class SingleFactoryNotMarkedWithDapperAot + { + public int X { get; set; } + public string Y { get; set; } + public double? Z { get; set; } + + public static SingleFactoryNotMarkedWithDapperAot Construct(int x, string y, double? z) + => new SingleFactoryNotMarkedWithDapperAot { X = x, Y = y, Z = z }; + } + + public class MultipleStandardFactoryMethods + { + public int X { get; set; } + public string Y { get; set; } + public double? Z { get; set; } + + public static MultipleStandardFactoryMethods Construct1(int x, string y, double? z) + => new MultipleStandardFactoryMethods { X = x, Y = y, Z = z }; + + public static MultipleStandardFactoryMethods Construct2(int x, string y, double? z) + => new MultipleStandardFactoryMethods { X = x, Y = y, Z = z }; } } \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs index 3b26fff3..bc5115ae 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs @@ -29,6 +29,34 @@ file static class DapperGeneratedInterceptors } + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 12, 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, Buffered, StoredProcedure + // returns data: global::Foo.SingleFactoryNotMarkedWithDapperAot + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.StoredProcedure, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QueryBuffered(param, RowFactory2.Instance); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 13, 24)] + internal static global::System.Collections.Generic.IEnumerable Query3(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, Buffered, StoredProcedure + // returns data: global::Foo.MultipleStandardFactoryMethods + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is null); + + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.StoredProcedure, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QueryBuffered(param, RowFactory3.Instance); + + } + 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) @@ -121,6 +149,138 @@ private RowFactory1() {} } + private sealed class RowFactory2 : global::Dapper.RowFactory + { + internal static readonly RowFactory2 Instance = new(); + private RowFactory2() {} + 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 4245442695U when NormalizedEquals(name, "x"): + token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible + break; + case 4228665076U when NormalizedEquals(name, "y"): + token = type == typeof(string) ? 1 : 4; + break; + case 4278997933U when NormalizedEquals(name, "z"): + token = type == typeof(double) ? 2 : 5; + break; + + } + tokens[i] = token; + columnOffset++; + + } + return null; + } + public override global::Foo.SingleFactoryNotMarkedWithDapperAot Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) + { + int value0 = default; + string? value1 = default; + double? value2 = default; + foreach (var token in tokens) + { + switch (token) + { + case 0: + value0 = reader.GetInt32(columnOffset); + break; + case 3: + value0 = GetValue(reader, columnOffset); + break; + case 1: + value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 4: + value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 2: + value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + break; + case 5: + value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + break; + + } + columnOffset++; + + } + return global::Foo.SingleFactoryNotMarkedWithDapperAot.Construct(value0, value1, value2); + } + } + + private sealed class RowFactory3 : global::Dapper.RowFactory + { + internal static readonly RowFactory3 Instance = new(); + private RowFactory3() {} + 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 4245442695U when NormalizedEquals(name, "x"): + token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible + break; + case 4228665076U when NormalizedEquals(name, "y"): + token = type == typeof(string) ? 1 : 4; + break; + case 4278997933U when NormalizedEquals(name, "z"): + token = type == typeof(double) ? 2 : 5; + break; + + } + tokens[i] = token; + columnOffset++; + + } + return null; + } + public override global::Foo.MultipleStandardFactoryMethods Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) + { + global::Foo.MultipleStandardFactoryMethods result = new(); + foreach (var token in tokens) + { + switch (token) + { + case 0: + result.X = reader.GetInt32(columnOffset); + break; + case 3: + result.X = GetValue(reader, columnOffset); + break; + case 1: + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 4: + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 2: + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + break; + case 5: + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + break; + + } + columnOffset++; + + } + return result; + + } + + } + } namespace System.Runtime.CompilerServices diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt index f039ff99..9719fba3 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt @@ -1,7 +1,7 @@ Generator produced 2 diagnostics: Hidden DAP000 L1 C1 -Dapper.AOT handled 2 of 2 enabled call-sites using 2 interceptors, 0 commands and 2 readers +Dapper.AOT handled 4 of 4 enabled call-sites using 4 interceptors, 0 commands and 4 readers -Error DAP038 Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs L32 C46 +Error DAP038 Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs L34 C46 Only one factory method can be Dapper.AOT enabled per type 'Foo.MultipleFactoryMethods' From 9d6345602ee6f262ed0c818dad76d13054561fff Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Fri, 29 Sep 2023 19:32:35 +0200 Subject: [PATCH 4/9] sync main somehow :( --- .../Internal/Inspection.cs | 53 ++++++++++++++----- .../Internal/Roslyn/TypeSymbolExtensions.cs | 12 ----- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index 1417f354..b20086c8 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -331,23 +331,23 @@ private static bool IsCollectionType(ITypeSymbol? parameterType, out ITypeSymbol } [DebuggerDisplay("Order: {Order}; Name: {Name}")] - public readonly struct ConstructorParameter + readonly struct MethodParameter { /// - /// Order of parameter in constructor. - /// Will be 1 for member1 in constructor(member0, member1, ...) + /// Order of parameter in method. + /// Will be 1 for member1 in method(member0, member1, ...) /// public int Order { get; } /// - /// Type of constructor parameter + /// Type of method parameter /// public ITypeSymbol Type { get; } /// - /// Name of constructor parameter + /// Name of method parameter /// public string Name { get; } - public ConstructorParameter(int order, ITypeSymbol type, string name) + public MethodParameter(int order, ITypeSymbol type, string name) { Order = order; Type = type; @@ -412,6 +412,11 @@ public readonly struct ElementMember /// Order of member in constructor parameter list (starts from 0). /// public int? ConstructorParameterOrder { get; } + + /// + /// Order of member in factory method parameter list (starts from 0). + /// + public int? FactoryMethodParameterOrder { get; } public ElementMember(ISymbol member, AttributeData? dbValue, ElementMemberKind kind) { @@ -429,14 +434,16 @@ public enum ElementMemberFlags IsExpandable = 1 << 3, } - public ElementMember(ISymbol member, AttributeData? dbValue, ElementMemberKind kind, ElementMemberFlags flags, int? constructorParameterOrder) + public ElementMember(ISymbol member, AttributeData? dbValue, ElementMemberKind kind, ElementMemberFlags flags, int? constructorParameterOrder, int? factoryMethodParameterOrder) { - Member = member; _dbValue = dbValue; - Kind = kind; - _flags = flags; + + Member = member; + Kind = kind; + ConstructorParameterOrder = constructorParameterOrder; + FactoryMethodParameterOrder = factoryMethodParameterOrder; } public override int GetHashCode() => SymbolEqualityComparer.Default.GetHashCode(Member); @@ -473,6 +480,26 @@ public enum ConstructorResult FailMultipleImplicit, } + /// + /// Builds a collection of type factory methods, which are NOT: + /// a) parameterless + /// b) marked with [DapperAot(false)] + /// _Note:_ factory method is a 1) publicly visibly; 2) static method 3) with response type equal to containing type + /// + internal static ConstructorResult ChooseFactoryMethod(ITypeSymbol? typeSymbol, out IMethodSymbol? factoryMethod) + { + factoryMethod = null; + if (typeSymbol is not INamedTypeSymbol named) + { + return ConstructorResult.NoneFound; + } + + var staticMethods = typeSymbol.GetMethods(method => + method.IsStatic && SymbolEqualityComparer.Default.Equals(method.ReturnType, typeSymbol)); + + // TODO + } + /// /// Builds a collection of type constructors, which are NOT: /// a) parameterless @@ -662,13 +689,13 @@ internal static ImmutableArray GetMembers(bool forParameters, ITy return builder.ToImmutable(); } - static IReadOnlyDictionary ParseConstructorParameters(IMethodSymbol constructorSymbol) + static IReadOnlyDictionary ParseConstructorParameters(IMethodSymbol constructorSymbol) { - var parameters = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var parameters = new Dictionary(StringComparer.InvariantCultureIgnoreCase); int order = 0; foreach (var parameter in constructorSymbol.Parameters) { - parameters.Add(parameter.Name, new ConstructorParameter(order: order++, type: parameter.Type, name: parameter.Name)); + parameters.Add(parameter.Name, new MethodParameter(order: order++, type: parameter.Type, name: parameter.Name)); } return parameters; } diff --git a/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs b/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs index ee495265..d4b68d18 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs @@ -21,18 +21,6 @@ internal static class TypeSymbolExtensions if (filter is null || filter(methodSymbol)) yield return methodSymbol; } } - - public static bool TryGetConstructors(this ITypeSymbol? typeSymbol, out ImmutableArray? constructors) - { - constructors = null; - if (typeSymbol is not INamedTypeSymbol namedTypeSymbol) - { - return false; - } - - constructors = namedTypeSymbol.Constructors; - return true; - } public static string? GetTypeDisplayName(this ITypeSymbol? typeSymbol) { From d95196f5d95f512767c45d6323efcb98d180626f Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sun, 1 Oct 2023 16:40:28 +0200 Subject: [PATCH 5/9] factory methods --- .../DapperInterceptorGenerator.cs | 90 +++++++++---- .../Internal/Inspection.cs | 122 +++++++++++++++--- .../Internal/MemberMap.cs | 11 +- ...ustomConstructionWithConstructor.output.cs | 24 ++-- ...tomConstructionWithFactoryMethod.output.cs | 79 ++++++++++-- ...omConstructionWithFactoryMethod.output.txt | 7 +- 6 files changed, 262 insertions(+), 71 deletions(-) diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs index 6ff8fd47..b3edb514 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs @@ -638,7 +638,7 @@ private static void WriteRowFactory(in GenerateState context, CodeWriter sb, ITy var members = map.Members; - if (members.IsDefaultOrEmpty && map.Constructor is null) + if (members.IsDefaultOrEmpty && map.Constructor is null && map.FactoryMethod is null) { // error is emitted, but we still generate default RowFactory to not emit more errors for this type WriteRowFactoryHeader(); @@ -648,8 +648,13 @@ private static void WriteRowFactory(in GenerateState context, CodeWriter sb, ITy } var hasInitOnlyMembers = members.Any(member => member.IsInitOnly); - var hasGetOnlyMembers = members.Any(member => member.IsGettable && !member.IsSettable && !member.IsInitOnly); - var useDeferredConstruction = map.Constructor is not null || hasInitOnlyMembers || hasGetOnlyMembers; + var hasGetOnlyMembers = members.Any(member => member is { IsGettable: true, IsSettable: false, IsInitOnly: false }); + var useConstructorDeferred = map.Constructor is not null; + var useFactoryMethodDeferred = map.FactoryMethod is not null; + + // Implementation detail: + // constructor takes advantage over factory method. + var useDeferredConstruction = useConstructorDeferred || useFactoryMethodDeferred || hasInitOnlyMembers || hasGetOnlyMembers; WriteRowFactoryHeader(); @@ -704,7 +709,8 @@ void WriteReadMethod() sb.Append("public override ").Append(type).Append(" Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state)").Indent().NewLine(); int token = 0; - var constructorArgumentsOrdered = new SortedList(); + var deferredMethodArgumentsOrdered = new SortedList(); + if (useDeferredConstruction) { // don't create an instance now, but define the variables to create an instance later like @@ -720,12 +726,14 @@ void WriteReadMethod() if (Inspection.CouldBeNullable(member.CodeType)) sb.Append(CodeWriter.GetTypeName(member.CodeType.WithNullableAnnotation(NullableAnnotation.Annotated))); else sb.Append(CodeWriter.GetTypeName(member.CodeType)); sb.Append(' ').Append(variableName).Append(" = default;").NewLine(); - - // filling in the constructor arguments in first iteration through members - // will be used afterwards to create the instance - if (member.ConstructorParameterOrder is not null) + + if (useConstructorDeferred && member.ConstructorParameterOrder is not null) { - constructorArgumentsOrdered.Add(member.ConstructorParameterOrder.Value, variableName); + deferredMethodArgumentsOrdered.Add(member.ConstructorParameterOrder.Value, variableName); + } + else if (useFactoryMethodDeferred && member.FactoryMethodParameterOrder is not null) + { + deferredMethodArgumentsOrdered.Add(member.FactoryMethodParameterOrder.Value, variableName); } token++; @@ -786,7 +794,7 @@ void WriteReadMethod() if (useDeferredConstruction) { - // create instance using constructor. like + // create instance using constructor or factory method. like // ``` // return new Type(member0, member1, member2, ...) // { @@ -794,23 +802,45 @@ void WriteReadMethod() // SettableMember2 = member4, // } // ``` - - sb.Append("return new ").Append(type); - if (map.Constructor is not null && constructorArgumentsOrdered.Count != 0) + // or in case of factory method: + // return Type.Create(member0, member1, member2, ...) + // ``` + + if (useConstructorDeferred) { - // write `(member0, member1, member2, ...)` part of constructor - sb.Append('('); - foreach (var constructorArg in constructorArgumentsOrdered) - { - sb.Append(constructorArg.Value).Append(", "); - } - sb.RemoveLast(2); // remove last ', ' generated in the loop + // `return new Type(member0, member1, member2, ...);` + sb.Append("return new ").Append(type).Append('('); + WriteDeferredMethodArgs(); sb.Append(')'); + WriteDeferredInitialization(); + sb.Append(";").Outdent(); } - - // if all members are constructor arguments, no need to set them again - if (constructorArgumentsOrdered.Count != members.Length) + else if (useFactoryMethodDeferred) { + // `return Type.FactoryCreate(member0, member1, member2, ...);` + sb.Append("return ").Append(type) + .Append('.').Append(map.FactoryMethod!.Name).Append('('); + WriteDeferredMethodArgs(); + sb.Append(')').Append(";").Outdent(); + } + else + { + // left case is GetOnly or InitOnly - we can use only init syntax like: + // return new Type + // { + // Member1 = value1, + // Member2 = value2 + // } + sb.Append("return new ").Append(type); + WriteDeferredInitialization(); + sb.Append(";").Outdent(); + } + + void WriteDeferredInitialization() + { + // if all members are constructor arguments, no need to set them again + if (deferredMethodArgumentsOrdered!.Count == members.Length) return; + sb.Indent().NewLine(); token = -1; foreach (var member in members) @@ -821,8 +851,18 @@ void WriteReadMethod() } sb.Outdent(withScope: false).Append("}"); } - - sb.Append(";").Outdent(); + + void WriteDeferredMethodArgs() + { + if (deferredMethodArgumentsOrdered!.Count == 0) return; + + // write `member0, member1, member2, ...` part of method + foreach (var constructorArg in deferredMethodArgumentsOrdered!) + { + sb.Append(constructorArg.Value).Append(", "); + } + sb.RemoveLast(2); // remove last ', ' generated in the loop + } } else { diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index b20086c8..2fffb035 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -142,6 +142,27 @@ public static bool IsDapperAttribute(AttributeData attrib) } }; + /// + /// Returns true if either: + /// a) has implicit `[DapperAot]` attribute; + /// b) has explicit `[DapperAot(true)]` attribute; + /// Otherwise returns false + /// + public static bool HasDapperAotEnabledAttribute(ISymbol? symbol) + { + var dapperAotAttribute = GetDapperAttribute(symbol, Types.DapperAotAttribute); + + // no attribute at all + if (dapperAotAttribute is null) return false; + + // `[DapperAot]` + if (dapperAotAttribute.ConstructorArguments.Length == 0) return true; + + // `[DapperAot(true)]` + var typedArg = dapperAotAttribute.ConstructorArguments.First(); + return (typedArg.Value is true); + } + public static AttributeData? GetDapperAttribute(ISymbol? symbol, string attributeName) { if (symbol is not null) @@ -434,7 +455,13 @@ public enum ElementMemberFlags IsExpandable = 1 << 3, } - public ElementMember(ISymbol member, AttributeData? dbValue, ElementMemberKind kind, ElementMemberFlags flags, int? constructorParameterOrder, int? factoryMethodParameterOrder) + public ElementMember( + ISymbol member, + AttributeData? dbValue, + ElementMemberKind kind, + ElementMemberFlags flags, + int? constructorParameterOrder, + int? factoryMethodParameterOrder) { _dbValue = dbValue; _flags = flags; @@ -471,13 +498,23 @@ public override bool Equals(object obj) => obj is ElementMember other } } + [Flags] public enum ConstructorResult { - NoneFound, - SuccessSingleExplicit, - SuccessSingleImplicit, - FailMultipleExplicit, - FailMultipleImplicit, + NoneFound = 0, + SuccessSingleExplicit = 1 << 0, + SuccessSingleImplicit = 1 << 1, + FailMultipleExplicit = 1 << 2, + FailMultipleImplicit = 1 << 3, + } + + public enum FactoryMethodResult + { + NoneFound = 0, + SuccessSingleExplicit = 1 << 0, + SuccessSingleImplicit = 1 << 1, + FailMultipleExplicit = 1 << 2, + FailMultipleImplicit = 1 << 3, } /// @@ -486,18 +523,60 @@ public enum ConstructorResult /// b) marked with [DapperAot(false)] /// _Note:_ factory method is a 1) publicly visibly; 2) static method 3) with response type equal to containing type /// - internal static ConstructorResult ChooseFactoryMethod(ITypeSymbol? typeSymbol, out IMethodSymbol? factoryMethod) + internal static FactoryMethodResult ChooseFactoryMethod(ITypeSymbol? typeSymbol, out IMethodSymbol? factoryMethod) { factoryMethod = null; - if (typeSymbol is not INamedTypeSymbol named) + if (typeSymbol is null) { - return ConstructorResult.NoneFound; + return FactoryMethodResult.NoneFound; } - var staticMethods = typeSymbol.GetMethods(method => - method.IsStatic && SymbolEqualityComparer.Default.Equals(method.ReturnType, typeSymbol)); - - // TODO + var staticMethods = typeSymbol + .GetMethods(method => + method.IsStatic && + SymbolEqualityComparer.Default.Equals(method.ReturnType, typeSymbol) && + method.DeclaredAccessibility == Accessibility.Public + ) + ?.ToArray(); + if (staticMethods?.Length == 0) + { + return FactoryMethodResult.NoneFound; + } + + IMethodSymbol? standardFactoryMethod = null; + IMethodSymbol? dapperAotEnabledFactoryMethod = null; + foreach (var method in staticMethods!) + { + if (HasDapperAotEnabledAttribute(method)) + { + if (dapperAotEnabledFactoryMethod is not null) + { + return FactoryMethodResult.FailMultipleExplicit; + } + dapperAotEnabledFactoryMethod = method; + } + else + { + if (standardFactoryMethod is not null) + { + return FactoryMethodResult.FailMultipleImplicit; + } + standardFactoryMethod = method; + } + } + + if (dapperAotEnabledFactoryMethod is not null) + { + factoryMethod = dapperAotEnabledFactoryMethod; + return FactoryMethodResult.SuccessSingleExplicit; + } + else if (standardFactoryMethod is not null) + { + factoryMethod = standardFactoryMethod; + return FactoryMethodResult.SuccessSingleImplicit; + } + + return FactoryMethodResult.NoneFound; } /// @@ -603,7 +682,9 @@ internal static ConstructorResult ChooseConstructor(ITypeSymbol? typeSymbol, out /// If is passed, will be used to associate element member with the constructor parameter by name (case-insensitive). /// /// type, which elements to parse - internal static ImmutableArray GetMembers(bool forParameters, ITypeSymbol? elementType, IMethodSymbol? constructor) + /// pointer to single constructor available for AOT scenario + /// pointer to single factoryMethod available for AOT scenario + internal static ImmutableArray GetMembers(bool forParameters, ITypeSymbol? elementType, IMethodSymbol? constructor, IMethodSymbol? factoryMethod) { if (elementType is null) { @@ -617,7 +698,8 @@ internal static ImmutableArray GetMembers(bool forParameters, ITy { var elMembers = elementType.GetMembers(); var builder = ImmutableArray.CreateBuilder(elMembers.Length); - var constructorParameters = (constructor is not null) ? ParseConstructorParameters(constructor) : null; + var constructorParameters = (constructor is not null) ? ParseMethodParameters(constructor) : null; + var factoryMethodParameters = (factoryMethod is not null) ? ParseMethodParameters(factoryMethod) : null; foreach (var member in elMembers) { // instance only, must be able to access by name @@ -659,6 +741,10 @@ internal static ImmutableArray GetMembers(bool forParameters, ITy int? constructorParameterOrder = constructorParameters?.TryGetValue(member.Name, out var constructorParameter) == true ? constructorParameter.Order : null; + + int? factoryMethodParamOrder = factoryMethodParameters?.TryGetValue(member.Name, out var factoryMethodParam) == true + ? factoryMethodParam.Order + : null; ElementMember.ElementMemberFlags flags = ElementMember.ElementMemberFlags.None; if (CodeWriter.IsGettableInstanceMember(member, out _)) flags |= ElementMember.ElementMemberFlags.IsGettable; @@ -673,7 +759,7 @@ internal static ImmutableArray GetMembers(bool forParameters, ITy else { // needs to be writable - if (constructorParameterOrder is null && + if (constructorParameterOrder is null && factoryMethodParamOrder is null && (flags & (ElementMember.ElementMemberFlags.IsSettable | ElementMember.ElementMemberFlags.IsInitOnly)) == 0) continue; } @@ -684,12 +770,12 @@ internal static ImmutableArray GetMembers(bool forParameters, ITy } // all good, then! - builder.Add(new(member, dbValue, kind, flags, constructorParameterOrder)); + builder.Add(new(member, dbValue, kind, flags, constructorParameterOrder, factoryMethodParamOrder)); } return builder.ToImmutable(); } - static IReadOnlyDictionary ParseConstructorParameters(IMethodSymbol constructorSymbol) + static IReadOnlyDictionary ParseMethodParameters(IMethodSymbol constructorSymbol) { var parameters = new Dictionary(StringComparer.InvariantCultureIgnoreCase); int order = 0; diff --git a/src/Dapper.AOT.Analyzers/Internal/MemberMap.cs b/src/Dapper.AOT.Analyzers/Internal/MemberMap.cs index 686cac73..9726c27e 100644 --- a/src/Dapper.AOT.Analyzers/Internal/MemberMap.cs +++ b/src/Dapper.AOT.Analyzers/Internal/MemberMap.cs @@ -14,6 +14,7 @@ internal sealed class MemberMap private readonly MapFlags _flags; public IMethodSymbol? Constructor { get; } + public IMethodSymbol? FactoryMethod { get; } public bool IsUnknownParameters => (_flags & (MapFlags.IsObject | MapFlags.IsDapperDynamic)) != 0; public bool IsObject => (_flags & MapFlags.IsObject) != 0; @@ -71,8 +72,16 @@ private MemberMap(bool forParameters, Location? location, ITypeSymbol declaredTy Constructor = constructor; break; } + + switch (ChooseFactoryMethod(ElementType, out var factoryMethod)) + { + case FactoryMethodResult.SuccessSingleImplicit: + case FactoryMethodResult.SuccessSingleExplicit: + FactoryMethod = factoryMethod; + break; + } } - Members = GetMembers(forParameters, ElementType, Constructor); + Members = GetMembers(forParameters, ElementType, Constructor, FactoryMethod); } static bool IsDynamicParameters(ITypeSymbol? type) diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs index e6346e75..d67f8385 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs @@ -1,7 +1,7 @@ #nullable enable file static class DapperGeneratedInterceptors { - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 10, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 10, 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, Buffered, StoredProcedure, BindResultsByName @@ -15,7 +15,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 11, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 11, 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, Buffered, StoredProcedure, BindResultsByName @@ -29,7 +29,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 12, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 12, 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, Buffered, StoredProcedure, BindResultsByName @@ -43,7 +43,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 13, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 13, 24)] internal static global::System.Collections.Generic.IEnumerable Query3(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, Buffered, StoredProcedure, BindResultsByName @@ -57,7 +57,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 14, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 14, 24)] internal static global::System.Collections.Generic.IEnumerable Query4(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, Buffered, StoredProcedure, BindResultsByName @@ -71,7 +71,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 15, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 15, 24)] internal static global::System.Collections.Generic.IEnumerable Query5(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, Buffered, StoredProcedure, BindResultsByName @@ -85,7 +85,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 16, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 16, 24)] internal static global::System.Collections.Generic.IEnumerable Query6(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, Buffered, StoredProcedure, BindResultsByName @@ -99,7 +99,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 17, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 17, 24)] internal static global::System.Collections.Generic.IEnumerable Query7(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, Buffered, StoredProcedure, BindResultsByName @@ -113,7 +113,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 18, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 18, 24)] internal static global::System.Collections.Generic.IEnumerable Query8(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, Buffered, StoredProcedure, BindResultsByName @@ -127,7 +127,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 19, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 19, 24)] internal static global::System.Collections.Generic.IEnumerable Query9(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, Buffered, StoredProcedure, BindResultsByName @@ -141,7 +141,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 20, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 20, 24)] internal static global::System.Collections.Generic.IEnumerable Query10(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, Buffered, StoredProcedure, BindResultsByName @@ -155,7 +155,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 21, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 21, 24)] internal static global::System.Collections.Generic.IEnumerable Query11(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, Buffered, StoredProcedure, BindResultsByName diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs index bc5115ae..e978d7b6 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs @@ -2,9 +2,9 @@ file static class DapperGeneratedInterceptors { [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 10, 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) + 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, Buffered, StoredProcedure + // Query, TypedResult, Buffered, StoredProcedure, BindResultsByName // returns data: global::Foo.PublicPropertiesNoConstructor global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -16,10 +16,10 @@ file static class DapperGeneratedInterceptors } [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 11, 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) + 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, Buffered, StoredProcedure - // returns data: global::Foo.MultipleFactoryMethods + // Query, TypedResult, Buffered, StoredProcedure, BindResultsByName + // returns data: global::Foo.MultipleDapperAotFactoryMethods global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); global::System.Diagnostics.Debug.Assert(buffered is true); @@ -30,9 +30,9 @@ file static class DapperGeneratedInterceptors } [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 12, 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) + 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, Buffered, StoredProcedure + // Query, TypedResult, Buffered, StoredProcedure, BindResultsByName // returns data: global::Foo.SingleFactoryNotMarkedWithDapperAot global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -44,9 +44,9 @@ file static class DapperGeneratedInterceptors } [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 13, 24)] - internal static global::System.Collections.Generic.IEnumerable Query3(this global::System.Data.IDbConnection cnn, string sql, object param, global::System.Data.IDbTransaction transaction, bool buffered, int? commandTimeout, global::System.Data.CommandType? commandType) + internal static global::System.Collections.Generic.IEnumerable Query3(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, Buffered, StoredProcedure + // Query, TypedResult, Buffered, StoredProcedure, BindResultsByName // returns data: global::Foo.MultipleStandardFactoryMethods global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); @@ -142,10 +142,69 @@ private RowFactory0() {} } } - private sealed class RowFactory1 : global::Dapper.RowFactory + private sealed class RowFactory1 : global::Dapper.RowFactory { internal static readonly RowFactory1 Instance = new(); private RowFactory1() {} + 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 4245442695U when NormalizedEquals(name, "x"): + token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible + break; + case 4228665076U when NormalizedEquals(name, "y"): + token = type == typeof(string) ? 1 : 4; + break; + case 4278997933U when NormalizedEquals(name, "z"): + token = type == typeof(double) ? 2 : 5; + break; + + } + tokens[i] = token; + columnOffset++; + + } + return null; + } + public override global::Foo.MultipleDapperAotFactoryMethods Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) + { + global::Foo.MultipleDapperAotFactoryMethods result = new(); + foreach (var token in tokens) + { + switch (token) + { + case 0: + result.X = reader.GetInt32(columnOffset); + break; + case 3: + result.X = GetValue(reader, columnOffset); + break; + case 1: + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 4: + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 2: + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + break; + case 5: + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + break; + + } + columnOffset++; + + } + return result; + + } } diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt index 9719fba3..9ae5dfc7 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt @@ -1,7 +1,4 @@ -Generator produced 2 diagnostics: +Generator produced 1 diagnostics: Hidden DAP000 L1 C1 -Dapper.AOT handled 4 of 4 enabled call-sites using 4 interceptors, 0 commands and 4 readers - -Error DAP038 Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs L34 C46 -Only one factory method can be Dapper.AOT enabled per type 'Foo.MultipleFactoryMethods' +Dapper.AOT handled 4 of 4 possible call-sites using 4 interceptors, 0 commands and 4 readers From 7bbbe2522f9f8e7ef2a654f2979f025aa9be191e Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sun, 1 Oct 2023 16:47:13 +0200 Subject: [PATCH 6/9] cleanup after pre-review --- .../Internal/Inspection.cs | 20 +++++++++---------- test/Dapper.AOT.Test/Dapper.AOT.Test.csproj | 11 ---------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index 2fffb035..08e2f173 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -501,20 +501,20 @@ public override bool Equals(object obj) => obj is ElementMember other [Flags] public enum ConstructorResult { - NoneFound = 0, - SuccessSingleExplicit = 1 << 0, - SuccessSingleImplicit = 1 << 1, - FailMultipleExplicit = 1 << 2, - FailMultipleImplicit = 1 << 3, + NoneFound, + SuccessSingleExplicit, + SuccessSingleImplicit, + FailMultipleExplicit, + FailMultipleImplicit } public enum FactoryMethodResult { - NoneFound = 0, - SuccessSingleExplicit = 1 << 0, - SuccessSingleImplicit = 1 << 1, - FailMultipleExplicit = 1 << 2, - FailMultipleImplicit = 1 << 3, + NoneFound, + SuccessSingleExplicit, + SuccessSingleImplicit, + FailMultipleExplicit, + FailMultipleImplicit } /// diff --git a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj index f679a0ad..edf11ced 100644 --- a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj +++ b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj @@ -21,17 +21,6 @@ $([System.String]::Copy(%(Filename)).Replace('.output.netfx', '.input.cs')) - - PreserveNewest - - - PreserveNewest - QueryCustomConstructionWithConstructor.input.cs - - - PreserveNewest - QueryCustomConstructionWithConstructor.input.cs - From 5ee93e5bf33a529e9599e605b8d0f5114ece5965 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sat, 14 Oct 2023 22:26:25 +0200 Subject: [PATCH 7/9] add diagnostics + tests --- .../DapperAnalyzer.Diagnostics.cs | 3 ++ .../CodeAnalysis/DapperAnalyzer.cs | 23 +++++++++- .../Internal/Inspection.cs | 29 ++---------- ...stomConstructionWithFactoryMethod.input.cs | 6 +-- ...omConstructionWithFactoryMethod.output.txt | 20 ++++++++ test/Dapper.AOT.Test/Verifiers/DAP039.cs | 46 +++++++++++++++++++ test/Dapper.AOT.Test/Verifiers/DAP040.cs | 44 ++++++++++++++++++ test/Dapper.AOT.Test/Verifiers/DAP041.cs | 36 +++++++++++++++ 8 files changed, 177 insertions(+), 30 deletions(-) create mode 100644 test/Dapper.AOT.Test/Verifiers/DAP039.cs create mode 100644 test/Dapper.AOT.Test/Verifiers/DAP040.cs create mode 100644 test/Dapper.AOT.Test/Verifiers/DAP041.cs diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs index 06b8735b..2e423d2f 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs @@ -47,6 +47,9 @@ public static readonly DiagnosticDescriptor ConstructorAmbiguous = LibraryError("DAP036", "Ambiguous constructors", "Type '{0}' has more than 1 constructor; mark one constructor with [ExplicitConstructor] or reduce constructors"), UserTypeNoSettableMembersFound = LibraryError("DAP037", "No settable members exist for user type", "Type '{0}' has no settable fields or properties"), ValueTypeSingleFirstOrDefaultUsage = LibraryWarning("DAP038", "Value-type single row 'OrDefault' usage", "Type '{0}' is a value-type; it will not be trivial to identify missing rows from {1}"), + FactoryMethodMultipleExplicit = LibraryError("DAP039", "Multiple explicit factory methods", "Only one factory method should be marked [ExplicitConstructor] for type '{0}'"), + FactoryMethodAmbiguous = LibraryError("DAP040", "Ambiguous factory methods", "Type '{0}' has more than 1 factory method; mark one factory method with [ExplicitConstructor] or reduce factory methods"), + ConstructorOverridesFactoryMethod = LibraryWarning("DAP041", "Constructor overrides factory method", "Type '{0}' has both constructor and factory method; Constructor will be used instead of a factory method"), // SQL parse specific GeneralSqlError = SqlWarning("DAP200", "SQL error", "SQL error: {0}"), diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs index 477c42b2..e242def5 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs @@ -205,18 +205,37 @@ private void ValidateDapperMethod(in OperationAnalysisContext ctx, IOperation sq ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.ValueTypeSingleFirstOrDefaultUsage, location, resultMap.ElementType.Name, invoke.TargetMethod.Name)); } - // check for constructors on the materialized type - DiagnosticDescriptor? ctorFault = ChooseConstructor(resultMap.ElementType, out var ctor) switch + // check for constructors and factoryMethods on the materialized type + var ctorFault = ChooseConstructor(resultMap.ElementType, out var ctor) switch { ConstructorResult.FailMultipleExplicit => Diagnostics.ConstructorMultipleExplicit, ConstructorResult.FailMultipleImplicit when aotEnabled => Diagnostics.ConstructorAmbiguous, _ => null, }; + var factoryMethodFault = ChooseFactoryMethod(resultMap.ElementType, out var factoryMethod) switch + { + FactoryMethodResult.FailMultipleExplicit => Diagnostics.FactoryMethodMultipleExplicit, + FactoryMethodResult.FailMultipleImplicit when aotEnabled => Diagnostics.FactoryMethodAmbiguous, + _ => null, + }; + + // we cant use both ctor and factoryMethod, so reporting a warning that ctor is prioritized + if (ctor is not null && factoryMethod is not null) + { + var loc = factoryMethod?.Locations.FirstOrDefault() ?? resultMap.ElementType.Locations.FirstOrDefault(); + ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.ConstructorOverridesFactoryMethod, loc, resultMap.ElementType.GetDisplayString())); + } + if (ctorFault is not null) { var loc = ctor?.Locations.FirstOrDefault() ?? resultMap.ElementType.Locations.FirstOrDefault(); ctx.ReportDiagnostic(Diagnostic.Create(ctorFault, loc, resultMap.ElementType.GetDisplayString())); } + else if (factoryMethodFault is not null) + { + var loc = factoryMethod?.Locations.FirstOrDefault() ?? resultMap.ElementType.Locations.FirstOrDefault(); + ctx.ReportDiagnostic(Diagnostic.Create(factoryMethodFault, loc, resultMap.ElementType.GetDisplayString())); + } else if (resultMap.Members.IsDefaultOrEmpty && IsPublicOrAssemblyLocal(resultType, parseState, out _)) { // there are so settable members + there is no constructor to use diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index 08e2f173..a1aa03c8 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -142,27 +142,6 @@ public static bool IsDapperAttribute(AttributeData attrib) } }; - /// - /// Returns true if either: - /// a) has implicit `[DapperAot]` attribute; - /// b) has explicit `[DapperAot(true)]` attribute; - /// Otherwise returns false - /// - public static bool HasDapperAotEnabledAttribute(ISymbol? symbol) - { - var dapperAotAttribute = GetDapperAttribute(symbol, Types.DapperAotAttribute); - - // no attribute at all - if (dapperAotAttribute is null) return false; - - // `[DapperAot]` - if (dapperAotAttribute.ConstructorArguments.Length == 0) return true; - - // `[DapperAot(true)]` - var typedArg = dapperAotAttribute.ConstructorArguments.First(); - return (typedArg.Value is true); - } - public static AttributeData? GetDapperAttribute(ISymbol? symbol, string attributeName) { if (symbol is not null) @@ -518,9 +497,7 @@ public enum FactoryMethodResult } /// - /// Builds a collection of type factory methods, which are NOT: - /// a) parameterless - /// b) marked with [DapperAot(false)] + /// Tries to choose a single factory method for the type. /// _Note:_ factory method is a 1) publicly visibly; 2) static method 3) with response type equal to containing type /// internal static FactoryMethodResult ChooseFactoryMethod(ITypeSymbol? typeSymbol, out IMethodSymbol? factoryMethod) @@ -547,10 +524,11 @@ internal static FactoryMethodResult ChooseFactoryMethod(ITypeSymbol? typeSymbol, IMethodSymbol? dapperAotEnabledFactoryMethod = null; foreach (var method in staticMethods!) { - if (HasDapperAotEnabledAttribute(method)) + if (GetDapperAttribute(method, Types.ExplicitConstructorAttribute) is not null) { if (dapperAotEnabledFactoryMethod is not null) { + factoryMethod = dapperAotEnabledFactoryMethod; // pointing to first found method for diagnostic return FactoryMethodResult.FailMultipleExplicit; } dapperAotEnabledFactoryMethod = method; @@ -559,6 +537,7 @@ internal static FactoryMethodResult ChooseFactoryMethod(ITypeSymbol? typeSymbol, { if (standardFactoryMethod is not null) { + factoryMethod = standardFactoryMethod; // pointing to first found method for diagnostic return FactoryMethodResult.FailMultipleImplicit; } standardFactoryMethod = method; diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs index 6dd83d06..7a787e8b 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs @@ -19,7 +19,7 @@ public class PublicPropertiesNoConstructor public string Y { get; set; } public double? Z { get; set; } - [DapperAot(true)] + [ExplicitConstructor] public static PublicPropertiesNoConstructor Construct(int x, string y, double? z) => new PublicPropertiesNoConstructor { X = x, Y = y, Z = z }; } @@ -30,11 +30,11 @@ public class MultipleDapperAotFactoryMethods public string Y { get; set; } public double? Z { get; set; } - [DapperAot(true)] + [ExplicitConstructor] public static MultipleDapperAotFactoryMethods Construct(int x, string y, double? z) => new MultipleDapperAotFactoryMethods { X = x, Y = y, Z = z }; - [DapperAot(true)] + [ExplicitConstructor] public static MultipleDapperAotFactoryMethods Construct2(int x, string y, double? z) => new MultipleDapperAotFactoryMethods { X = x, Y = y, Z = z }; } diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt index 9ae5dfc7..cc29be50 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt @@ -1,4 +1,24 @@ +Input code has 3 diagnostics from 'Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs': + +Error CS0592 Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs L22 C10 +Attribute 'ExplicitConstructor' is not valid on this declaration type. It is only valid on 'constructor' declarations. + +Error CS0592 Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs L33 C10 +Attribute 'ExplicitConstructor' is not valid on this declaration type. It is only valid on 'constructor' declarations. + +Error CS0592 Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs L37 C10 +Attribute 'ExplicitConstructor' is not valid on this declaration type. It is only valid on 'constructor' declarations. Generator produced 1 diagnostics: Hidden DAP000 L1 C1 Dapper.AOT handled 4 of 4 possible call-sites using 4 interceptors, 0 commands and 4 readers +Output code has 3 diagnostics from 'Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs': + +Error CS0592 Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs L22 C10 +Attribute 'ExplicitConstructor' is not valid on this declaration type. It is only valid on 'constructor' declarations. + +Error CS0592 Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs L33 C10 +Attribute 'ExplicitConstructor' is not valid on this declaration type. It is only valid on 'constructor' declarations. + +Error CS0592 Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs L37 C10 +Attribute 'ExplicitConstructor' is not valid on this declaration type. It is only valid on 'constructor' declarations. diff --git a/test/Dapper.AOT.Test/Verifiers/DAP039.cs b/test/Dapper.AOT.Test/Verifiers/DAP039.cs new file mode 100644 index 00000000..e243d801 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP039.cs @@ -0,0 +1,46 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP039 : Verifier +{ + [Fact] + public Task FactoryMethodMultipleExplicit() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot] + class SomeCode + { + public void Foo(DbConnection conn) + { + _ = conn.Query("storedproc"); + _ = conn.Query("storedproc"); + _ = conn.Query("storedproc"); + } + } + class NoFactoryMethods { public int Id {get;set;} } + class SingleExplicit + { + public int A {get; private set;} + [ExplicitConstructor] public static SingleExplicit Create1(int a) => new SingleExplicit { A = a }; + public static SingleExplicit Create2(int a) => new SingleExplicit { A = a }; + } + class MultipleExplicit + { + public int A {get; private set;} + [ExplicitConstructor] public static MultipleExplicit {|#0:Create1|}(int a) => new MultipleExplicit { A = a }; + [ExplicitConstructor] public static MultipleExplicit Create2(int a) => new MultipleExplicit { A = a }; + public static MultipleExplicit Create3(int a) => new MultipleExplicit { A = a }; + } + """, + DefaultConfig, + [ + Diagnostic(Diagnostics.FactoryMethodMultipleExplicit).WithLocation(0).WithArguments("MultipleExplicit"), + ] + ); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP040.cs b/test/Dapper.AOT.Test/Verifiers/DAP040.cs new file mode 100644 index 00000000..fbeee5fd --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP040.cs @@ -0,0 +1,44 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP040 : Verifier +{ + [Fact] + public Task FactoryMethodAmbiguous() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot] + class SomeCode + { + public void Foo(DbConnection conn) + { + _ = conn.Query("storedproc"); + _ = conn.Query("storedproc"); + _ = conn.Query("storedproc"); + } + } + class NoFactoryMethods { public int Id {get;set;} } + class SingleImplicit + { + public int A {get; private set;} + public static SingleImplicit Create(int a) => new SingleImplicit { A = a }; + } + class MultipleImplicit + { + public int A {get; private set;} + public static MultipleImplicit {|#0:Create1|}(int a) => new MultipleImplicit { A = a }; + public static MultipleImplicit Create2(int a) => new MultipleImplicit { A = a }; + } + """, + DefaultConfig, + [ + Diagnostic(Diagnostics.FactoryMethodAmbiguous).WithLocation(0).WithArguments("MultipleImplicit"), + ] + ); + +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Verifiers/DAP041.cs b/test/Dapper.AOT.Test/Verifiers/DAP041.cs new file mode 100644 index 00000000..f9c866b0 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP041.cs @@ -0,0 +1,36 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP041 : Verifier +{ + [Fact] + public Task ConstructorOverridesFactoryMethod() => CSVerifyAsync(""" + using Dapper; + using System.Data.Common; + + [DapperAot] + class SomeCode + { + public void Foo(DbConnection conn) + { + _ = conn.Query("storedproc"); + } + } + class MultipleConstructionVariants + { + public int A {get; private set;} + [ExplicitConstructor] public MultipleConstructionVariants(int a) { A = a; } + [ExplicitConstructor] public static MultipleConstructionVariants {|#0:Create|}(int a) => new MultipleConstructionVariants { A = a }; + } + """, + DefaultConfig, + [ + Diagnostic(Diagnostics.ConstructorOverridesFactoryMethod).WithLocation(0).WithArguments("MultipleConstructionVariants"), + ] + ); + +} \ No newline at end of file From 151df9fdf7b0b49a0ef717193df460994c8388e0 Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Thu, 9 Nov 2023 11:48:19 +0100 Subject: [PATCH 8/9] add docs for new diagnostics --- docs/rules/DAP036.md | 2 +- docs/rules/DAP039.md | 35 +++++++++++++++++++++++++++++ docs/rules/DAP040.md | 34 ++++++++++++++++++++++++++++ docs/rules/DAP041.md | 53 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 docs/rules/DAP039.md create mode 100644 docs/rules/DAP040.md create mode 100644 docs/rules/DAP041.md diff --git a/docs/rules/DAP036.md b/docs/rules/DAP036.md index 94cb46d9..d6ba5a8f 100644 --- a/docs/rules/DAP036.md +++ b/docs/rules/DAP036.md @@ -1,4 +1,4 @@ -# DAP035 +# DAP036 Your type has multiple constructors. Vanilla Dapper would be happy to pick one based on the exact columns, but Dapper.AOT wants diff --git a/docs/rules/DAP039.md b/docs/rules/DAP039.md new file mode 100644 index 00000000..a74bad27 --- /dev/null +++ b/docs/rules/DAP039.md @@ -0,0 +1,35 @@ +# DAP039 + +_Terminology:_ **factory method** is a static method that returns an instance of a type. + +It looks like you have multiple factory methods on a type marked `[ExplicitConstructor]`; that's just confusing! +Pick one, and remove the attribute from the others. This should *probably* be the one with the most parameters. + +Bad: + +``` csharp +class MyType +{ + public int A { get; private set; } + + [ExplicitConstructor] + public static MyType Create1(int a) => new MyType { A = a }; + + [ExplicitConstructor] + public static MyType Create2(int a) => new MyType { A = a }; +} +``` + +Good: + +``` csharp +class MyType +{ + public int A { get; private set; } + + [ExplicitConstructor] + public static MyType Create1(int a) => new MyType { A = a }; + + public static MyType Create2(int a) => new MyType { A = a }; +} +``` \ No newline at end of file diff --git a/docs/rules/DAP040.md b/docs/rules/DAP040.md new file mode 100644 index 00000000..a9f57105 --- /dev/null +++ b/docs/rules/DAP040.md @@ -0,0 +1,34 @@ +# DAP040 + +_Terminology:_ **factory method** is a static method that returns an instance of a type. + +Your type has multiple factory methods. Dapper.AOT expects a type to have a single constructor to use with all data. +You might consider marking your preferred constructor with `[ExplicitConstructor]`. +This should *probably* be the one with the most parameters. + +Bad: + +``` csharp +class MyType +{ + public int A { get; private set; } + + public static MyType Create1(int a) => new MyType { A = a }; + + public static MyType Create2(int a) => new MyType { A = a }; +} +``` + +Good: + +``` csharp +class MyType +{ + public int A { get; private set; } + + [ExplicitConstructor] + public static MyType Create1(int a) => new MyType { A = a }; + + public static MyType Create2(int a) => new MyType { A = a }; +} +``` \ No newline at end of file diff --git a/docs/rules/DAP041.md b/docs/rules/DAP041.md new file mode 100644 index 00000000..978e4c2f --- /dev/null +++ b/docs/rules/DAP041.md @@ -0,0 +1,53 @@ +# DAP041 + +_Terminology:_ **factory method** is a static method that returns an instance of a type. + +Your type has both at least one _constructor_ and at least one _factory method_ marked with `[ExplicitConstructor]`. +Dapper.AOT simply **uses the constructor** in such a case. Recommended action is to remove the `[ExplicitConstructor]` attribute from one of the construction mechanisms. + +Bad: + +``` csharp +class MyType +{ + public int A { get; private set; } + + // constructor + [ExplicitConstructor] + public MyType(int a) { A = a; } + + // factory method + [ExplicitConstructor] + public static MyType Create(int a) => new MyType { A = a }; +} +``` + +Good: + +``` csharp +class MyType +{ + public int A { get; private set; } + + // constructor + [ExplicitConstructor] + public MyType(int a) { A = a; } + + public static MyType Create(int a) => new MyType { A = a }; +} +``` + +Also Good could be: + +``` csharp +class MyType +{ + public int A { get; private set; } + + public MyType(int a) { A = a; } + + // factory method + [ExplicitConstructor] + public static MyType Create(int a) => new MyType { A = a }; +} +``` \ No newline at end of file From f521921613dec5e35d018d4e48bd389abfd5fc1b Mon Sep 17 00:00:00 2001 From: Dmitrii Korolev Date: Sat, 11 Nov 2023 20:57:55 +0100 Subject: [PATCH 9/9] merge main + regen tests --- ...ustomConstructionWithConstructor.output.cs | 24 +- ...tomConstructionWithFactoryMethod.output.cs | 531 +++++++++--------- 2 files changed, 279 insertions(+), 276 deletions(-) diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs index e0aca6ea..fbf95e3a 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs @@ -3,7 +3,7 @@ namespace Dapper.AOT // interceptors must be in a known namespace { file static class DapperGeneratedInterceptors { - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 10, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 10, 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, Buffered, StoredProcedure, BindResultsByName @@ -17,7 +17,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 11, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 11, 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, Buffered, StoredProcedure, BindResultsByName @@ -31,7 +31,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 12, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 12, 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, Buffered, StoredProcedure, BindResultsByName @@ -45,7 +45,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 13, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 13, 24)] internal static global::System.Collections.Generic.IEnumerable Query3(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, Buffered, StoredProcedure, BindResultsByName @@ -59,7 +59,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 14, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 14, 24)] internal static global::System.Collections.Generic.IEnumerable Query4(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, Buffered, StoredProcedure, BindResultsByName @@ -73,7 +73,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 15, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 15, 24)] internal static global::System.Collections.Generic.IEnumerable Query5(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, Buffered, StoredProcedure, BindResultsByName @@ -87,7 +87,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 16, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 16, 24)] internal static global::System.Collections.Generic.IEnumerable Query6(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, Buffered, StoredProcedure, BindResultsByName @@ -101,7 +101,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 17, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 17, 24)] internal static global::System.Collections.Generic.IEnumerable Query7(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, Buffered, StoredProcedure, BindResultsByName @@ -115,7 +115,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 18, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 18, 24)] internal static global::System.Collections.Generic.IEnumerable Query8(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, Buffered, StoredProcedure, BindResultsByName @@ -129,7 +129,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 19, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 19, 24)] internal static global::System.Collections.Generic.IEnumerable Query9(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, Buffered, StoredProcedure, BindResultsByName @@ -143,7 +143,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 20, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 20, 24)] internal static global::System.Collections.Generic.IEnumerable Query10(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, Buffered, StoredProcedure, BindResultsByName @@ -157,7 +157,7 @@ file static class DapperGeneratedInterceptors } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstruction.input.cs", 21, 24)] + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithConstructor.input.cs", 21, 24)] internal static global::System.Collections.Generic.IEnumerable Query11(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, Buffered, StoredProcedure, BindResultsByName diff --git a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs index e978d7b6..61865a22 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs @@ -1,346 +1,349 @@ #nullable enable -file static class DapperGeneratedInterceptors +namespace Dapper.AOT // interceptors must be in a known namespace { - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 10, 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) + file static class DapperGeneratedInterceptors { - // Query, TypedResult, Buffered, StoredProcedure, BindResultsByName - // returns data: global::Foo.PublicPropertiesNoConstructor - global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); - global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); - global::System.Diagnostics.Debug.Assert(buffered is true); - global::System.Diagnostics.Debug.Assert(param is null); + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 10, 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, Buffered, StoredProcedure, BindResultsByName + // returns data: global::Foo.PublicPropertiesNoConstructor + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is null); - return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.StoredProcedure, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QueryBuffered(param, RowFactory0.Instance); + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.StoredProcedure, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QueryBuffered(param, RowFactory0.Instance); - } + } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 11, 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, Buffered, StoredProcedure, BindResultsByName - // returns data: global::Foo.MultipleDapperAotFactoryMethods - global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); - global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); - global::System.Diagnostics.Debug.Assert(buffered is true); - global::System.Diagnostics.Debug.Assert(param is null); + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 11, 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, Buffered, StoredProcedure, BindResultsByName + // returns data: global::Foo.MultipleDapperAotFactoryMethods + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is null); - return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.StoredProcedure, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QueryBuffered(param, RowFactory1.Instance); + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.StoredProcedure, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QueryBuffered(param, RowFactory1.Instance); - } + } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 12, 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, Buffered, StoredProcedure, BindResultsByName - // returns data: global::Foo.SingleFactoryNotMarkedWithDapperAot - global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); - global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); - global::System.Diagnostics.Debug.Assert(buffered is true); - global::System.Diagnostics.Debug.Assert(param is null); + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 12, 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, Buffered, StoredProcedure, BindResultsByName + // returns data: global::Foo.SingleFactoryNotMarkedWithDapperAot + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is null); - return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.StoredProcedure, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QueryBuffered(param, RowFactory2.Instance); + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.StoredProcedure, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QueryBuffered(param, RowFactory2.Instance); - } + } - [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 13, 24)] - internal static global::System.Collections.Generic.IEnumerable Query3(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, Buffered, StoredProcedure, BindResultsByName - // returns data: global::Foo.MultipleStandardFactoryMethods - global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); - global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); - global::System.Diagnostics.Debug.Assert(buffered is true); - global::System.Diagnostics.Debug.Assert(param is null); + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\QueryCustomConstructionWithFactoryMethod.input.cs", 13, 24)] + internal static global::System.Collections.Generic.IEnumerable Query3(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, Buffered, StoredProcedure, BindResultsByName + // returns data: global::Foo.MultipleStandardFactoryMethods + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.StoredProcedure); + global::System.Diagnostics.Debug.Assert(buffered is true); + global::System.Diagnostics.Debug.Assert(param is null); - return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.StoredProcedure, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QueryBuffered(param, RowFactory3.Instance); + return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.StoredProcedure, commandTimeout.GetValueOrDefault(), DefaultCommandFactory).QueryBuffered(param, RowFactory3.Instance); - } + } - 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) + private class CommonCommandFactory : global::Dapper.CommandFactory { - 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) + public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection, string sql, global::System.Data.CommandType commandType, T args) { - cmd0.BindByName = true; - cmd0.InitialLONGFetchSize = -1; + 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; } - return cmd; - } - } + } - private static readonly CommonCommandFactory DefaultCommandFactory = new(); + 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) + private sealed class RowFactory0 : global::Dapper.RowFactory { - for (int i = 0; i < tokens.Length; i++) + internal static readonly RowFactory0 Instance = new(); + private RowFactory0() {} + public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) { - int token = -1; - var name = reader.GetName(columnOffset); - var type = reader.GetFieldType(columnOffset); - switch (NormalizedHash(name)) + for (int i = 0; i < tokens.Length; i++) { - case 4245442695U when NormalizedEquals(name, "x"): - token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible - break; - case 4228665076U when NormalizedEquals(name, "y"): - token = type == typeof(string) ? 1 : 4; - break; - case 4278997933U when NormalizedEquals(name, "z"): - token = type == typeof(double) ? 2 : 5; - break; + int token = -1; + var name = reader.GetName(columnOffset); + var type = reader.GetFieldType(columnOffset); + switch (NormalizedHash(name)) + { + case 4245442695U when NormalizedEquals(name, "x"): + token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible + break; + case 4228665076U when NormalizedEquals(name, "y"): + token = type == typeof(string) ? 1 : 4; + break; + case 4278997933U when NormalizedEquals(name, "z"): + token = type == typeof(double) ? 2 : 5; + break; + + } + tokens[i] = token; + columnOffset++; } - tokens[i] = token; - columnOffset++; - + return null; } - return null; - } - public override global::Foo.PublicPropertiesNoConstructor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) - { - int value0 = default; - string? value1 = default; - double? value2 = default; - foreach (var token in tokens) + public override global::Foo.PublicPropertiesNoConstructor Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) { - switch (token) + int value0 = default; + string? value1 = default; + double? value2 = default; + foreach (var token in tokens) { - case 0: - value0 = reader.GetInt32(columnOffset); - break; - case 3: - value0 = GetValue(reader, columnOffset); - break; - case 1: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); - break; - case 4: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); - break; - case 2: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); - break; - case 5: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); - break; + switch (token) + { + case 0: + value0 = reader.GetInt32(columnOffset); + break; + case 3: + value0 = GetValue(reader, columnOffset); + break; + case 1: + value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 4: + value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 2: + value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + break; + case 5: + value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + break; + + } + columnOffset++; } - columnOffset++; - + return global::Foo.PublicPropertiesNoConstructor.Construct(value0, value1, value2); } - return global::Foo.PublicPropertiesNoConstructor.Construct(value0, value1, value2); } - } - private sealed class RowFactory1 : global::Dapper.RowFactory - { - internal static readonly RowFactory1 Instance = new(); - private RowFactory1() {} - public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) + private sealed class RowFactory1 : global::Dapper.RowFactory { - for (int i = 0; i < tokens.Length; i++) + internal static readonly RowFactory1 Instance = new(); + private RowFactory1() {} + public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) { - int token = -1; - var name = reader.GetName(columnOffset); - var type = reader.GetFieldType(columnOffset); - switch (NormalizedHash(name)) + for (int i = 0; i < tokens.Length; i++) { - case 4245442695U when NormalizedEquals(name, "x"): - token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible - break; - case 4228665076U when NormalizedEquals(name, "y"): - token = type == typeof(string) ? 1 : 4; - break; - case 4278997933U when NormalizedEquals(name, "z"): - token = type == typeof(double) ? 2 : 5; - break; + int token = -1; + var name = reader.GetName(columnOffset); + var type = reader.GetFieldType(columnOffset); + switch (NormalizedHash(name)) + { + case 4245442695U when NormalizedEquals(name, "x"): + token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible + break; + case 4228665076U when NormalizedEquals(name, "y"): + token = type == typeof(string) ? 1 : 4; + break; + case 4278997933U when NormalizedEquals(name, "z"): + token = type == typeof(double) ? 2 : 5; + break; + + } + tokens[i] = token; + columnOffset++; } - tokens[i] = token; - columnOffset++; - + return null; } - return null; - } - public override global::Foo.MultipleDapperAotFactoryMethods Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) - { - global::Foo.MultipleDapperAotFactoryMethods result = new(); - foreach (var token in tokens) + public override global::Foo.MultipleDapperAotFactoryMethods Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) { - switch (token) + global::Foo.MultipleDapperAotFactoryMethods result = new(); + foreach (var token in tokens) { - case 0: - result.X = reader.GetInt32(columnOffset); - break; - case 3: - result.X = GetValue(reader, columnOffset); - break; - case 1: - result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); - break; - case 4: - result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); - break; - case 2: - result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); - break; - case 5: - result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); - break; + switch (token) + { + case 0: + result.X = reader.GetInt32(columnOffset); + break; + case 3: + result.X = GetValue(reader, columnOffset); + break; + case 1: + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 4: + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 2: + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + break; + case 5: + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + break; + + } + columnOffset++; } - columnOffset++; + return result; } - return result; } - } - - private sealed class RowFactory2 : global::Dapper.RowFactory - { - internal static readonly RowFactory2 Instance = new(); - private RowFactory2() {} - public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) + private sealed class RowFactory2 : global::Dapper.RowFactory { - for (int i = 0; i < tokens.Length; i++) + internal static readonly RowFactory2 Instance = new(); + private RowFactory2() {} + public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) { - int token = -1; - var name = reader.GetName(columnOffset); - var type = reader.GetFieldType(columnOffset); - switch (NormalizedHash(name)) + for (int i = 0; i < tokens.Length; i++) { - case 4245442695U when NormalizedEquals(name, "x"): - token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible - break; - case 4228665076U when NormalizedEquals(name, "y"): - token = type == typeof(string) ? 1 : 4; - break; - case 4278997933U when NormalizedEquals(name, "z"): - token = type == typeof(double) ? 2 : 5; - break; + int token = -1; + var name = reader.GetName(columnOffset); + var type = reader.GetFieldType(columnOffset); + switch (NormalizedHash(name)) + { + case 4245442695U when NormalizedEquals(name, "x"): + token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible + break; + case 4228665076U when NormalizedEquals(name, "y"): + token = type == typeof(string) ? 1 : 4; + break; + case 4278997933U when NormalizedEquals(name, "z"): + token = type == typeof(double) ? 2 : 5; + break; + + } + tokens[i] = token; + columnOffset++; } - tokens[i] = token; - columnOffset++; - + return null; } - return null; - } - public override global::Foo.SingleFactoryNotMarkedWithDapperAot Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) - { - int value0 = default; - string? value1 = default; - double? value2 = default; - foreach (var token in tokens) + public override global::Foo.SingleFactoryNotMarkedWithDapperAot Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) { - switch (token) + int value0 = default; + string? value1 = default; + double? value2 = default; + foreach (var token in tokens) { - case 0: - value0 = reader.GetInt32(columnOffset); - break; - case 3: - value0 = GetValue(reader, columnOffset); - break; - case 1: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); - break; - case 4: - value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); - break; - case 2: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); - break; - case 5: - value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); - break; + switch (token) + { + case 0: + value0 = reader.GetInt32(columnOffset); + break; + case 3: + value0 = GetValue(reader, columnOffset); + break; + case 1: + value1 = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 4: + value1 = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 2: + value2 = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + break; + case 5: + value2 = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + break; + + } + columnOffset++; } - columnOffset++; - + return global::Foo.SingleFactoryNotMarkedWithDapperAot.Construct(value0, value1, value2); } - return global::Foo.SingleFactoryNotMarkedWithDapperAot.Construct(value0, value1, value2); } - } - private sealed class RowFactory3 : global::Dapper.RowFactory - { - internal static readonly RowFactory3 Instance = new(); - private RowFactory3() {} - public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) + private sealed class RowFactory3 : global::Dapper.RowFactory { - for (int i = 0; i < tokens.Length; i++) + internal static readonly RowFactory3 Instance = new(); + private RowFactory3() {} + public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span tokens, int columnOffset) { - int token = -1; - var name = reader.GetName(columnOffset); - var type = reader.GetFieldType(columnOffset); - switch (NormalizedHash(name)) + for (int i = 0; i < tokens.Length; i++) { - case 4245442695U when NormalizedEquals(name, "x"): - token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible - break; - case 4228665076U when NormalizedEquals(name, "y"): - token = type == typeof(string) ? 1 : 4; - break; - case 4278997933U when NormalizedEquals(name, "z"): - token = type == typeof(double) ? 2 : 5; - break; + int token = -1; + var name = reader.GetName(columnOffset); + var type = reader.GetFieldType(columnOffset); + switch (NormalizedHash(name)) + { + case 4245442695U when NormalizedEquals(name, "x"): + token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible + break; + case 4228665076U when NormalizedEquals(name, "y"): + token = type == typeof(string) ? 1 : 4; + break; + case 4278997933U when NormalizedEquals(name, "z"): + token = type == typeof(double) ? 2 : 5; + break; + + } + tokens[i] = token; + columnOffset++; } - tokens[i] = token; - columnOffset++; - + return null; } - return null; - } - public override global::Foo.MultipleStandardFactoryMethods Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) - { - global::Foo.MultipleStandardFactoryMethods result = new(); - foreach (var token in tokens) + public override global::Foo.MultipleStandardFactoryMethods Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) { - switch (token) + global::Foo.MultipleStandardFactoryMethods result = new(); + foreach (var token in tokens) { - case 0: - result.X = reader.GetInt32(columnOffset); - break; - case 3: - result.X = GetValue(reader, columnOffset); - break; - case 1: - result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); - break; - case 4: - result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); - break; - case 2: - result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); - break; - case 5: - result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); - break; + switch (token) + { + case 0: + result.X = reader.GetInt32(columnOffset); + break; + case 3: + result.X = GetValue(reader, columnOffset); + break; + case 1: + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 4: + result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 2: + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset); + break; + case 5: + result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue(reader, columnOffset); + break; + + } + columnOffset++; } - columnOffset++; + return result; } - return result; } - } - + } } namespace System.Runtime.CompilerServices {