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 diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs index 3044453c..bcf006b9 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 f45413f9..8c693e7b 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs @@ -206,18 +206,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/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs index 9db48a12..4fb7d9db 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs @@ -654,7 +654,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(); @@ -664,8 +664,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(); @@ -720,7 +725,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 @@ -736,12 +742,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++; @@ -802,7 +810,7 @@ void WriteReadMethod() if (useDeferredConstruction) { - // create instance using constructor. like + // create instance using constructor or factory method. like // ``` // return new Type(member0, member1, member2, ...) // { @@ -810,23 +818,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) @@ -837,8 +867,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 b71c4512..9955f192 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; @@ -418,6 +418,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) { @@ -435,14 +440,22 @@ 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); @@ -470,15 +483,87 @@ public override bool Equals(object obj) => obj is ElementMember other } } + [Flags] public enum ConstructorResult { NoneFound, SuccessSingleExplicit, SuccessSingleImplicit, FailMultipleExplicit, - FailMultipleImplicit, + FailMultipleImplicit + } + + public enum FactoryMethodResult + { + NoneFound, + SuccessSingleExplicit, + SuccessSingleImplicit, + FailMultipleExplicit, + FailMultipleImplicit } + /// + /// 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) + { + factoryMethod = null; + if (typeSymbol is null) + { + return FactoryMethodResult.NoneFound; + } + + 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 (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; + } + else + { + if (standardFactoryMethod is not null) + { + factoryMethod = standardFactoryMethod; // pointing to first found method for diagnostic + 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; + } + /// /// Builds a collection of type constructors, which are NOT: /// a) parameterless @@ -582,7 +667,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) { @@ -596,7 +683,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 @@ -638,6 +726,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; @@ -652,7 +744,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; } @@ -663,18 +755,18 @@ 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); + 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/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/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs b/src/Dapper.AOT.Analyzers/Internal/Roslyn/TypeSymbolExtensions.cs index 9620998e..d4b68d18 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 string? GetTypeDisplayName(this ITypeSymbol? typeSymbol) { if (typeSymbol is null) return null; 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 98% rename from test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.cs rename to test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.cs index e0aca6ea..fbf95e3a 100644 --- a/test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.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/QueryCustomConstruction.output.txt b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.txt similarity index 100% rename from test/Dapper.AOT.Test/Interceptors/QueryCustomConstruction.output.txt rename to test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithConstructor.output.txt 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..7a787e8b --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.input.cs @@ -0,0 +1,64 @@ +using Dapper; +using System.Data.Common; + +[module: DapperAot] + +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"); + } + + public class PublicPropertiesNoConstructor + { + public int X { get; set; } + public string Y { get; set; } + public double? Z { get; set; } + + [ExplicitConstructor] + public static PublicPropertiesNoConstructor Construct(int x, string y, double? z) + => new PublicPropertiesNoConstructor { X = x, Y = y, Z = z }; + } + + public class MultipleDapperAotFactoryMethods + { + public int X { get; set; } + public string Y { get; set; } + public double? Z { get; set; } + + [ExplicitConstructor] + public static MultipleDapperAotFactoryMethods Construct(int x, string y, double? z) + => new MultipleDapperAotFactoryMethods { X = x, Y = y, Z = z }; + + [ExplicitConstructor] + 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 new file mode 100644 index 00000000..61865a22 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.cs @@ -0,0 +1,364 @@ +#nullable enable +namespace Dapper.AOT // interceptors must be in a known namespace +{ + 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, 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); + + } + + [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); + + } + + [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); + + } + + [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); + + } + + 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) + { + 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.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) + { + 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; + + } + + } + + 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 +{ + // 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..cc29be50 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/QueryCustomConstructionWithFactoryMethod.output.txt @@ -0,0 +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/Internal/Roslyn/TypeSymbolExtensionTests.cs b/test/Dapper.AOT.Test/Internal/Roslyn/TypeSymbolExtensionTests.cs index af11cccc..9e2792e5 100644 --- a/test/Dapper.AOT.Test/Internal/Roslyn/TypeSymbolExtensionTests.cs +++ b/test/Dapper.AOT.Test/Internal/Roslyn/TypeSymbolExtensionTests.cs @@ -10,13 +10,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); @@ -203,41 +222,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; @@ -255,6 +261,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 { 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