From 21b383ac77000c99305413a2ec5411d373d0ab51 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Fri, 26 Jul 2024 11:18:28 +0200 Subject: [PATCH] feature: support DbString in DapperAOT (#119) * start * add dbstring example to query * create test * start processing * support DbString! * last checks * regenerate for netfx * fix DAP44 error appeared in refactor * pre-review fixes * change DbString configuration method declaration * address initial comments * DbStringHelpers to file class * include `file` via preprocessor directive * only in dapper AOT * move to Dapper.AOT.Test.Integration.csproj * setup the infra for test * test is executed targeting docker db * remove debug * some other tries * Revert "some other tries" This reverts commit a95b6ff4e8b209b27a1a5d9c1f9bf4fa3be50ad1. * Revert "remove debug" This reverts commit 44e37d04db1c42b4aa59bf6224bb123798d62cff. * Revert "test is executed targeting docker db" This reverts commit c5a4fee9e22b65c4bb769670cfab8b3beb57a4e2. * Revert "setup the infra for test" This reverts commit 63fd1a95bbe6672823ad19b959d187c499781bb7. * Revert "move to Dapper.AOT.Test.Integration.csproj" This reverts commit e80464847eb93880d6b4c36227a32897281b32f8. * address PR comments * adjust a test * use global:: for DapperSpecialType --- docs/rules/DAP048.md | 44 +++ .../DapperAnalyzer.Diagnostics.cs | 1 + .../CodeAnalysis/DapperAnalyzer.cs | 77 ++++-- .../DapperInterceptorGenerator.cs | 48 +++- .../IncludedGenerationExtensions.cs | 7 + .../CodeAnalysis/GeneratorContext.cs | 28 ++ .../CodeAnalysis/ParseState.cs | 1 + .../TypeAccessorInterceptorGenerator.cs | 4 +- ...uteWriter.cs => PreGeneratedCodeWriter.cs} | 36 ++- .../Dapper.AOT.Analyzers.csproj | 5 +- .../InGeneration/DapperHelpers.cs | 44 +++ .../InterceptsLocationAttribute.cs | 0 .../IncludedGeneration.cs | 12 + .../Internal/Inspection.cs | 30 +++ test/Dapper.AOT.Test/Dapper.AOT.Test.csproj | 4 +- .../InGeneration/DbStringHelpersTests.cs | 35 +++ .../Interceptors/DbString.input.cs | 50 ++++ .../Interceptors/DbString.output.cs | 250 ++++++++++++++++++ .../Interceptors/DbString.output.netfx.cs | 250 ++++++++++++++++++ .../Interceptors/DbString.output.netfx.txt | 4 + .../Interceptors/DbString.output.txt | 4 + test/Dapper.AOT.Test/Verifiers/DAP048.cs | 50 ++++ 22 files changed, 927 insertions(+), 57 deletions(-) create mode 100644 docs/rules/DAP048.md create mode 100644 src/Dapper.AOT.Analyzers/CodeAnalysis/Extensions/IncludedGenerationExtensions.cs create mode 100644 src/Dapper.AOT.Analyzers/CodeAnalysis/GeneratorContext.cs rename src/Dapper.AOT.Analyzers/CodeAnalysis/Writers/{InterceptorsLocationAttributeWriter.cs => PreGeneratedCodeWriter.cs} (52%) create mode 100644 src/Dapper.AOT.Analyzers/InGeneration/DapperHelpers.cs rename src/Dapper.AOT.Analyzers/{ => InGeneration}/InterceptsLocationAttribute.cs (100%) create mode 100644 src/Dapper.AOT.Analyzers/IncludedGeneration.cs create mode 100644 test/Dapper.AOT.Test/InGeneration/DbStringHelpersTests.cs create mode 100644 test/Dapper.AOT.Test/Interceptors/DbString.input.cs create mode 100644 test/Dapper.AOT.Test/Interceptors/DbString.output.cs create mode 100644 test/Dapper.AOT.Test/Interceptors/DbString.output.netfx.cs create mode 100644 test/Dapper.AOT.Test/Interceptors/DbString.output.netfx.txt create mode 100644 test/Dapper.AOT.Test/Interceptors/DbString.output.txt create mode 100644 test/Dapper.AOT.Test/Verifiers/DAP048.cs diff --git a/docs/rules/DAP048.md b/docs/rules/DAP048.md new file mode 100644 index 00000000..6bf5fca2 --- /dev/null +++ b/docs/rules/DAP048.md @@ -0,0 +1,44 @@ +# DAP048 + +[DbString](https://github.com/DapperLib/Dapper/blob/main/Dapper/DbString.cs) causes heap allocations, but achieves the same as +[DbValueAttribute](https://github.com/DapperLib/DapperAOT/blob/main/src/Dapper.AOT/DbValueAttribute.cs). + +Bad: + +``` c# +public void DapperCode(DbConnection conn) +{ + var sql = "SELECT COUNT(*) FROM Foo WHERE Name = @Name;"; + var cars = conn.Query(sql, + new + { + Name = new DbString + { + Value = "MyFoo", + IsFixedLength = false, + Length = 5, + IsAnsi = true + } + }); +} +``` + +Good: + +``` c# +public void DapperCode(DbConnection conn) +{ + var sql = "SELECT COUNT(*) FROM Foo WHERE Name = @Name;"; + var cars = conn.Query(sql, + new MyPoco + { + Name = "MyFoo" + }); +} + +class MyPoco +{ + [DbValue(Length = 5, DbType = DbType.AnsiStringFixedLength)] // specify properties here + public string Name { get; set; } +} +``` diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs index 54e231d9..72a44453 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.Diagnostics.cs @@ -55,6 +55,7 @@ public static readonly DiagnosticDescriptor CancellationDuplicated = LibraryWarning("DAP045", "Duplicate cancellation", "Multiple parameter values cannot define cancellation"), AmbiguousProperties = LibraryWarning("DAP046", "Ambiguous properties", "Properties have same name '{0}' after normalization and can be conflated"), AmbiguousFields = LibraryWarning("DAP047", "Ambiguous fields", "Fields have same name '{0}' after normalization and can be conflated"), + MoveFromDbString = LibraryWarning("DAP048", "Move from DbString to DbValue", "DbString achieves the same as [DbValue] does. Use it instead."), // 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 f66c60aa..3b2681fd 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperAnalyzer.cs @@ -191,7 +191,7 @@ private void ValidateDapperMethod(in OperationAnalysisContext ctx, IOperation sq } // check the types - var resultType = invoke.GetResultType(flags); + var resultType = invoke.GetResultType(flags); if (resultType is not null && IdentifyDbType(resultType, out _) is null) // don't warn if handled as an inbuilt { var resultMap = MemberMap.CreateForResults(resultType, location); @@ -250,34 +250,9 @@ private void ValidateDapperMethod(in OperationAnalysisContext ctx, IOperation sq { _ = AdditionalCommandState.Parse(GetSymbol(parseState, invoke), parameters, onDiagnostic); } - if (parameters is not null) - { - if (flags.HasAny(OperationFlags.DoNotGenerate)) // using vanilla Dapper mode - { - if (parameters.Members.Any(s => s.IsCancellation) || IsCancellationToken(parameters.ElementType)) - { - ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.CancellationNotSupported, parameters.Location)); - } - } - else - { - bool first = true; - foreach(var member in parameters.Members) - { - if (member.IsCancellation) - { - if (first) - { - first = false; - } - else - { - ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.CancellationDuplicated, member.GetLocation())); - } - } - } - } - } + + ValidateParameters(parameters, flags, onDiagnostic); + var args = SharedGetParametersToInclude(parameters, ref flags, sql, onDiagnostic, out var parseFlags); ValidateSql(ctx, sqlSource, GetModeFlags(flags), SqlParameters.From(args), location); @@ -850,6 +825,50 @@ enum ParameterMode ? null : new(rowCountHint, rowCountHintMember?.Member.Name, batchSize, cmdProps); } + static void ValidateParameters(MemberMap? parameters, OperationFlags flags, Action onDiagnostic) + { + if (parameters is null) return; + + var usingVanillaDapperMode = flags.HasAny(OperationFlags.DoNotGenerate); // using vanilla Dapper mode + if (usingVanillaDapperMode) + { + if (parameters.Members.Any(s => s.IsCancellation) || IsCancellationToken(parameters.ElementType)) + { + onDiagnostic(Diagnostic.Create(Diagnostics.CancellationNotSupported, parameters.Location)); + } + } + + var isFirstCancellation = true; + foreach (var member in parameters.Members) + { + ValidateCancellationTokenParameter(member); + ValidateDbStringParameter(member); + } + + void ValidateDbStringParameter(ElementMember member) + { + if (usingVanillaDapperMode) + { + // reporting ONLY in Dapper AOT + return; + } + + if (member.DapperSpecialType == DapperSpecialType.DbString) + { + onDiagnostic(Diagnostic.Create(Diagnostics.MoveFromDbString, member.GetLocation())); + } + } + + void ValidateCancellationTokenParameter(ElementMember member) + { + if (!usingVanillaDapperMode && member.IsCancellation) + { + if (isFirstCancellation) isFirstCancellation = false; + else onDiagnostic(Diagnostic.Create(Diagnostics.CancellationDuplicated, member.GetLocation())); + } + } + } + static void ValidateMembers(MemberMap memberMap, Action onDiagnostic) { if (memberMap.Members.Length == 0) diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs index 83fd1be8..702feb08 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs @@ -16,8 +16,10 @@ using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Runtime.InteropServices.ComTypes; using System.Text; using System.Threading; +using static Dapper.Internal.Inspection; namespace Dapper.CodeAnalysis; @@ -423,16 +425,15 @@ internal void Generate(in GenerateState ctx) WriteRowFactory(ctx, sb, pair.Type, pair.Index); } - foreach (var tuple in factories) { WriteCommandFactory(ctx, baseCommandFactory, sb, tuple.Type, tuple.Index, tuple.Map, tuple.CacheCount, tuple.AdditionalCommandState); } sb.Outdent().Outdent(); // ends our generated file-scoped class and the namespace - - var interceptsLocationWriter = new InterceptorsLocationAttributeWriter(sb); - interceptsLocationWriter.Write(ctx.Compilation); + + var preGeneratedCodeWriter = new PreGeneratedCodeWriter(sb, ctx.Compilation); + preGeneratedCodeWriter.Write(ctx.GeneratorContext.IncludedGenerationTypes); ctx.AddSource((ctx.Compilation.AssemblyName ?? "package") + ".generated.cs", sb.ToString()); ctx.ReportDiagnostic(Diagnostic.Create(Diagnostics.InterceptorsGenerated, null, callSiteCount, ctx.Nodes.Length, methodIndex, factories.Count(), readers.Count())); @@ -490,11 +491,11 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory else { sb.Append("public override void AddParameters(in global::Dapper.UnifiedCommand cmd, ").Append(declaredType).Append(" args)").Indent().NewLine(); - WriteArgs(type, sb, WriteArgsMode.Add, map, ref flags); + WriteArgs(in ctx, type, sb, WriteArgsMode.Add, map, ref flags); sb.Outdent().NewLine(); sb.Append("public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, ").Append(declaredType).Append(" args)").Indent().NewLine(); - WriteArgs(type, sb, WriteArgsMode.Update, map, ref flags); + WriteArgs(in ctx, type, sb, WriteArgsMode.Update, map, ref flags); sb.Outdent().NewLine(); if ((flags & (WriteArgsFlags.NeedsRowCount | WriteArgsFlags.NeedsPostProcess)) != 0) @@ -507,11 +508,11 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory sb.Append("public override void PostProcess(in global::Dapper.UnifiedCommand cmd, ").Append(declaredType).Append(" args, int rowCount)").Indent().NewLine(); if ((flags & WriteArgsFlags.NeedsPostProcess) != 0) { - WriteArgs(type, sb, WriteArgsMode.PostProcess, map, ref flags); + WriteArgs(in ctx, type, sb, WriteArgsMode.PostProcess, map, ref flags); } if ((flags & WriteArgsFlags.NeedsRowCount) != 0) { - WriteArgs(type, sb, WriteArgsMode.SetRowCount, map, ref flags); + WriteArgs(in ctx, type, sb, WriteArgsMode.SetRowCount, map, ref flags); } if (baseFactory != DapperBaseCommandFactory) { @@ -524,7 +525,7 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory { sb.Append("public override global::System.Threading.CancellationToken GetCancellationToken(").Append(declaredType).Append(" args)") .Indent().NewLine(); - WriteArgs(type, sb, WriteArgsMode.GetCancellationToken, map, ref flags); + WriteArgs(in ctx, type, sb, WriteArgsMode.GetCancellationToken, map, ref flags); sb.Outdent().NewLine(); } } @@ -966,7 +967,7 @@ enum WriteArgsMode GetCancellationToken } - private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteArgsMode mode, string map, ref WriteArgsFlags flags) + private static void WriteArgs(in GenerateState ctx, ITypeSymbol? parameterType, CodeWriter sb, WriteArgsMode mode, string map, ref WriteArgsFlags flags) { if (parameterType is null) { @@ -1073,8 +1074,19 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr switch (mode) { case WriteArgsMode.Add: - sb.Append("p = cmd.CreateParameter();").NewLine() - .Append("p.ParameterName = ").AppendVerbatimLiteral(member.DbName).Append(";").NewLine(); + sb.Append("p = cmd.CreateParameter();").NewLine(); + sb.Append("p.ParameterName = ").AppendVerbatimLiteral(member.DbName).Append(";").NewLine(); + + if (member.DapperSpecialType is DapperSpecialType.DbString) + { + ctx.GeneratorContext.IncludeGenerationType(IncludedGeneration.DbStringHelpers); + + sb.Append("global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(p, ") + .Append(source).Append(".").Append(member.DbName).Append(");").NewLine(); + + sb.Append("ps.Add(p);").NewLine(); // dont forget to add parameter to command parameters collection + break; + } var dbType = member.GetDbType(out _); var size = member.TryGetValue("Size"); @@ -1149,6 +1161,18 @@ private static void WriteArgs(ITypeSymbol? parameterType, CodeWriter sb, WriteAr } break; case WriteArgsMode.Update: + if (member.DapperSpecialType is DapperSpecialType.DbString) + { + ctx.GeneratorContext.IncludeGenerationType(IncludedGeneration.DbStringHelpers); + + sb.Append("global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter") + .Append("(ps[").Append(parameterIndex).Append("], ") + .Append(source).Append(".").Append(member.CodeName) + .Append(");").NewLine(); + + break; + } + sb.Append("ps["); if ((flags & WriteArgsFlags.NeedsTest) != 0) sb.AppendVerbatimLiteral(member.DbName); else sb.Append(parameterIndex); diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/Extensions/IncludedGenerationExtensions.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/Extensions/IncludedGenerationExtensions.cs new file mode 100644 index 00000000..1e371f0f --- /dev/null +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/Extensions/IncludedGenerationExtensions.cs @@ -0,0 +1,7 @@ +namespace Dapper.CodeAnalysis.Extensions +{ + internal static class IncludedGenerationExtensions + { + public static bool HasAny(this IncludedGeneration value, IncludedGeneration flag) => (value & flag) != 0; + } +} diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/GeneratorContext.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/GeneratorContext.cs new file mode 100644 index 00000000..db8aa9fd --- /dev/null +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/GeneratorContext.cs @@ -0,0 +1,28 @@ +namespace Dapper.CodeAnalysis +{ + /// + /// Contains data about current generation run. + /// + internal class GeneratorContext + { + /// + /// Specifies which generation types should be included in the output. + /// + public IncludedGeneration IncludedGenerationTypes { get; private set; } + + public GeneratorContext() + { + // set default included generation types here + IncludedGenerationTypes = IncludedGeneration.InterceptsLocationAttribute; + } + + /// + /// Adds another generation type to the list of already included types. + /// + /// another generation type to include in the output + public void IncludeGenerationType(IncludedGeneration anotherType) + { + IncludedGenerationTypes |= anotherType; + } + } +} diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/ParseState.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/ParseState.cs index a1b2ea09..ee738995 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/ParseState.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/ParseState.cs @@ -82,6 +82,7 @@ public GenerateState(SourceProductionContext ctx, in (Compilation Compilation, I private readonly GenerateContextProxy? proxy; public readonly ImmutableArray Nodes; public readonly Compilation Compilation; + public readonly GeneratorContext GeneratorContext = new(); internal void ReportDiagnostic(Diagnostic diagnostic) { diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/TypeAccessorInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/TypeAccessorInterceptorGenerator.cs index 966ccc60..3c806806 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/TypeAccessorInterceptorGenerator.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/TypeAccessorInterceptorGenerator.cs @@ -164,8 +164,8 @@ void ReportDiagnosticInUsages(DiagnosticDescriptor diagnosticDescriptor) } }); - var interceptsLocWriter = new InterceptorsLocationAttributeWriter(codeWriter); - interceptsLocWriter.Write(state.Compilation); + var preGenerator = new PreGeneratedCodeWriter(codeWriter, state.Compilation); + preGenerator.Write(IncludedGeneration.InterceptsLocationAttribute); context.AddSource((state.Compilation.AssemblyName ?? "package") + ".generated.cs", sb.GetSourceText()); } diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/Writers/InterceptorsLocationAttributeWriter.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/Writers/PreGeneratedCodeWriter.cs similarity index 52% rename from src/Dapper.AOT.Analyzers/CodeAnalysis/Writers/InterceptorsLocationAttributeWriter.cs rename to src/Dapper.AOT.Analyzers/CodeAnalysis/Writers/PreGeneratedCodeWriter.cs index 360e415b..94f7891d 100644 --- a/src/Dapper.AOT.Analyzers/CodeAnalysis/Writers/InterceptorsLocationAttributeWriter.cs +++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/Writers/PreGeneratedCodeWriter.cs @@ -1,27 +1,41 @@ -using Dapper.Internal; +using Dapper.CodeAnalysis.Extensions; +using Dapper.Internal; using Microsoft.CodeAnalysis; namespace Dapper.CodeAnalysis.Writers { - internal struct InterceptorsLocationAttributeWriter + internal struct PreGeneratedCodeWriter { + readonly Compilation _compilation; readonly CodeWriter _codeWriter; - public InterceptorsLocationAttributeWriter(CodeWriter codeWriter) + public PreGeneratedCodeWriter( + CodeWriter codeWriter, + Compilation compilation) { _codeWriter = codeWriter; + _compilation = compilation; } - /// - /// Writes the "InterceptsLocationAttribute" to inner . - /// - /// Does so only when "InterceptsLocationAttribute" is NOT visible by . - public void Write(Compilation compilation) + public void Write(IncludedGeneration includedGenerations) { - var attrib = compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.InterceptsLocationAttribute"); - if (!IsAvailable(attrib, compilation)) + if (includedGenerations.HasAny(IncludedGeneration.InterceptsLocationAttribute)) { - _codeWriter.NewLine().Append(Resources.ReadString("Dapper.InterceptsLocationAttribute.cs")); + WriteInterceptsLocationAttribute(); + } + + if (includedGenerations.HasAny(IncludedGeneration.DbStringHelpers)) + { + _codeWriter.NewLine().Append(Resources.ReadString("Dapper.InGeneration.DapperHelpers.cs")); + } + } + + void WriteInterceptsLocationAttribute() + { + var attrib = _compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.InterceptsLocationAttribute"); + if (!IsAvailable(attrib, _compilation)) + { + _codeWriter.NewLine().Append(Resources.ReadString("Dapper.InGeneration.InterceptsLocationAttribute.cs")); } static bool IsAvailable(INamedTypeSymbol? type, Compilation compilation) diff --git a/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj b/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj index 2145e967..47f8010a 100644 --- a/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj +++ b/src/Dapper.AOT.Analyzers/Dapper.AOT.Analyzers.csproj @@ -16,9 +16,10 @@ - - + + + DapperInterceptorGenerator.cs diff --git a/src/Dapper.AOT.Analyzers/InGeneration/DapperHelpers.cs b/src/Dapper.AOT.Analyzers/InGeneration/DapperHelpers.cs new file mode 100644 index 00000000..217d9da8 --- /dev/null +++ b/src/Dapper.AOT.Analyzers/InGeneration/DapperHelpers.cs @@ -0,0 +1,44 @@ +namespace Dapper.Aot.Generated +{ + /// + /// Contains helpers to properly handle + /// +#if !DAPPERAOT_INTERNAL + file +#endif + static class DbStringHelpers + { + public static void ConfigureDbStringDbParameter( + global::System.Data.Common.DbParameter dbParameter, + global::Dapper.DbString? dbString) + { + if (dbString is null) + { + dbParameter.Value = global::System.DBNull.Value; + return; + } + + // repeating logic from Dapper: + // https://github.com/DapperLib/Dapper/blob/52160dc44699ec7eb5ad57d0dddc6ded4662fcb9/Dapper/DbString.cs#L71 + if (dbString.Length == -1 && dbString.Value is not null && dbString.Value.Length <= global::Dapper.DbString.DefaultLength) + { + dbParameter.Size = global::Dapper.DbString.DefaultLength; + } + else + { + dbParameter.Size = dbString.Length; + } + + dbParameter.DbType = dbString switch + { + { IsAnsi: true, IsFixedLength: true } => global::System.Data.DbType.AnsiStringFixedLength, + { IsAnsi: true, IsFixedLength: false } => global::System.Data.DbType.AnsiString, + { IsAnsi: false, IsFixedLength: true } => global::System.Data.DbType.StringFixedLength, + { IsAnsi: false, IsFixedLength: false } => global::System.Data.DbType.String, + _ => dbParameter.DbType + }; + + dbParameter.Value = dbString.Value as object ?? global::System.DBNull.Value; + } + } +} \ No newline at end of file diff --git a/src/Dapper.AOT.Analyzers/InterceptsLocationAttribute.cs b/src/Dapper.AOT.Analyzers/InGeneration/InterceptsLocationAttribute.cs similarity index 100% rename from src/Dapper.AOT.Analyzers/InterceptsLocationAttribute.cs rename to src/Dapper.AOT.Analyzers/InGeneration/InterceptsLocationAttribute.cs diff --git a/src/Dapper.AOT.Analyzers/IncludedGeneration.cs b/src/Dapper.AOT.Analyzers/IncludedGeneration.cs new file mode 100644 index 00000000..dc540e71 --- /dev/null +++ b/src/Dapper.AOT.Analyzers/IncludedGeneration.cs @@ -0,0 +1,12 @@ +using System; + +namespace Dapper +{ + [Flags] + internal enum IncludedGeneration + { + None = 0, + InterceptsLocationAttribute = 1 << 0, + DbStringHelpers = 1 << 1, + } +} diff --git a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs index e95a2229..00c4d359 100644 --- a/src/Dapper.AOT.Analyzers/Internal/Inspection.cs +++ b/src/Dapper.AOT.Analyzers/Internal/Inspection.cs @@ -421,6 +421,13 @@ public enum ElementMemberKind Cancellation = 1 << 2, } + [Flags] + internal enum DapperSpecialType + { + None = 0, + DbString = 1 << 0, + } + internal static bool IsCancellationToken(ITypeSymbol? type) => type is INamedTypeSymbol { @@ -488,6 +495,24 @@ public string DbName public bool IsCancellation => (Kind & ElementMemberKind.Cancellation) != 0; public bool HasDbValueAttribute => _dbValue is not null; + public DapperSpecialType DapperSpecialType + { + get + { + if (CodeType is { + Name: "DbString", + TypeKind: TypeKind.Class, + ContainingNamespace: + { + Name: "Dapper", + IsGlobalNamespace: true + } + }) return DapperSpecialType.DbString; + + return DapperSpecialType.None; + } + } + public T? TryGetValue(string memberName) where T : struct => TryGetAttributeValue(_dbValue, memberName, out T value) ? value : null; @@ -1097,6 +1122,11 @@ public static ITypeSymbol MakeNonNullable(ITypeSymbol type) readerMethod = null; return DbType.DateTimeOffset; } + if (type.Name == "DbString" && type.ContainingNamespace is { Name: "Dapper", ContainingNamespace.IsGlobalNamespace: true }) + { + readerMethod = null; + return DbType.String; + } readerMethod = null; return null; } diff --git a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj index 10da715a..67a758e5 100644 --- a/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj +++ b/test/Dapper.AOT.Test/Dapper.AOT.Test.csproj @@ -12,9 +12,11 @@ - + + + $([System.String]::Copy(%(Filename)).Replace('.output', '.input.cs')) diff --git a/test/Dapper.AOT.Test/InGeneration/DbStringHelpersTests.cs b/test/Dapper.AOT.Test/InGeneration/DbStringHelpersTests.cs new file mode 100644 index 00000000..bcaa4e2c --- /dev/null +++ b/test/Dapper.AOT.Test/InGeneration/DbStringHelpersTests.cs @@ -0,0 +1,35 @@ +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using Xunit; + +namespace Dapper.AOT.Test.InGeneration +{ + + public class DbStringHelpersTests + { + [Theory] + [InlineData(false, false, "qweqwe", DbType.String, 60)] + [InlineData(false, true, "qweqwe", DbType.StringFixedLength, 60)] + [InlineData(true, false, "qweqwe", DbType.AnsiString, 60)] + [InlineData(true, true, "qweqwe", DbType.AnsiStringFixedLength, 60)] + public void ConfigureDbString_ShouldProperlySetupDbParameter(bool isAnsi, bool isFixedLength, string dbStringValue, DbType expectedDbType, int expectedSize) + { + var param = CreateDbParameter(); + var dbString = new DbString + { + IsAnsi = isAnsi, + IsFixedLength = isFixedLength, + Value = dbStringValue, + Length = 60 + }; + + Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(param, dbString); + + Assert.Equal(expectedSize, param.Size); + Assert.Equal(expectedDbType, param.DbType); + } + + DbParameter CreateDbParameter() => new SqlParameter(); + } +} diff --git a/test/Dapper.AOT.Test/Interceptors/DbString.input.cs b/test/Dapper.AOT.Test/Interceptors/DbString.input.cs new file mode 100644 index 00000000..b646a479 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/DbString.input.cs @@ -0,0 +1,50 @@ +using Dapper; +using System.Data.Common; +using System.Threading.Tasks; + +[module: DapperAot] + +public static class Foo +{ + static async Task SomeCode(DbConnection connection) + { + _ = await connection.QueryAsync("select * from Orders where name = @Name and id = @Id", new + { + Name = new DbString + { + Value = "myOrder", + IsFixedLength = false, + Length = 7, + IsAnsi = true + }, + + Id = 123 + }); + + _ = await connection.QueryAsync("select * from Orders where name = @Name and id = @Id", new QueryModel + { + Name = new DbString + { + Value = "myOrder", + IsFixedLength = false, + Length = 7, + IsAnsi = true + }, + + Id = 123 + }); + } + + public class QueryModel + { + public DbString Name { get; set; } + public int Id { get; set; } + } + + public class Product + { + public int ProductId { get; set; } + public string Name { get; set; } + public string ProductNumber { get; set; } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/DbString.output.cs b/test/Dapper.AOT.Test/Interceptors/DbString.output.cs new file mode 100644 index 00000000..61d5b059 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/DbString.output.cs @@ -0,0 +1,250 @@ +#nullable enable +namespace Dapper.AOT // interceptors must be in a known namespace +{ + file static class DapperGeneratedInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DbString.input.cs", 11, 30)] + internal static global::System.Threading.Tasks.Task> QueryAsync0(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, Async, TypedResult, HasParameters, Buffered, Text, BindResultsByName, KnownParameters + // takes parameter: + // parameter map: Id Name + // returns data: global::Foo.Product + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.AsEnumerableAsync( + global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory0.Instance).QueryBufferedAsync(param, RowFactory0.Instance)); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DbString.input.cs", 24, 30)] + internal static global::System.Threading.Tasks.Task> QueryAsync1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, Async, TypedResult, HasParameters, Buffered, Text, BindResultsByName, KnownParameters + // takes parameter: global::Foo.QueryModel + // parameter map: Id Name + // returns data: global::Foo.Product + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.AsEnumerableAsync( + global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).QueryBufferedAsync((global::Foo.QueryModel)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 2521315361U when NormalizedEquals(name, "productid"): + token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible + break; + case 2369371622U when NormalizedEquals(name, "name"): + token = type == typeof(string) ? 1 : 4; + break; + case 1133313085U when NormalizedEquals(name, "productnumber"): + token = type == typeof(string) ? 2 : 5; + break; + + } + tokens[i] = token; + columnOffset++; + + } + return null; + } + public override global::Foo.Product Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) + { + global::Foo.Product result = new(); + foreach (var token in tokens) + { + switch (token) + { + case 0: + result.ProductId = reader.GetInt32(columnOffset); + break; + case 3: + result.ProductId = GetValue(reader, columnOffset); + break; + case 1: + result.Name = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 4: + result.Name = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 2: + result.ProductNumber = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 5: + result.ProductNumber = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + + } + columnOffset++; + + } + return result; + + } + + } + + private sealed class CommandFactory0 : CommonCommandFactory // + { + internal static readonly CommandFactory0 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { Name = default(global::Dapper.DbString)!, Id = default(int) }); // expected shape + var ps = cmd.Parameters; + global::System.Data.Common.DbParameter p; + p = cmd.CreateParameter(); + p.ParameterName = "Name"; + global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(p, typed.Name); + ps.Add(p); + + p = cmd.CreateParameter(); + p.ParameterName = "Id"; + p.DbType = global::System.Data.DbType.Int32; + p.Direction = global::System.Data.ParameterDirection.Input; + p.Value = AsValue(typed.Id); + ps.Add(p); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { Name = default(global::Dapper.DbString)!, Id = default(int) }); // expected shape + var ps = cmd.Parameters; + global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(ps[0], typed.Name); + ps[1].Value = AsValue(typed.Id); + + } + public override bool CanPrepare => true; + + } + + private sealed class CommandFactory1 : CommonCommandFactory + { + internal static readonly CommandFactory1 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, global::Foo.QueryModel args) + { + var ps = cmd.Parameters; + global::System.Data.Common.DbParameter p; + p = cmd.CreateParameter(); + p.ParameterName = "Name"; + global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(p, args.Name); + ps.Add(p); + + p = cmd.CreateParameter(); + p.ParameterName = "Id"; + p.DbType = global::System.Data.DbType.Int32; + p.Direction = global::System.Data.ParameterDirection.Input; + p.Value = AsValue(args.Id); + ps.Add(p); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, global::Foo.QueryModel args) + { + var ps = cmd.Parameters; + global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(ps[0], args.Name); + ps[1].Value = AsValue(args.Id); + + } + public override bool CanPrepare => true; + + } + + + } +} +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; + } + } +} +namespace Dapper.Aot.Generated +{ + /// + /// Contains helpers to properly handle + /// +#if !DAPPERAOT_INTERNAL + file +#endif + static class DbStringHelpers + { + public static void ConfigureDbStringDbParameter( + global::System.Data.Common.DbParameter dbParameter, + global::Dapper.DbString? dbString) + { + if (dbString is null) + { + dbParameter.Value = global::System.DBNull.Value; + return; + } + + // repeating logic from Dapper: + // https://github.com/DapperLib/Dapper/blob/52160dc44699ec7eb5ad57d0dddc6ded4662fcb9/Dapper/DbString.cs#L71 + if (dbString.Length == -1 && dbString.Value is not null && dbString.Value.Length <= global::Dapper.DbString.DefaultLength) + { + dbParameter.Size = global::Dapper.DbString.DefaultLength; + } + else + { + dbParameter.Size = dbString.Length; + } + + dbParameter.DbType = dbString switch + { + { IsAnsi: true, IsFixedLength: true } => global::System.Data.DbType.AnsiStringFixedLength, + { IsAnsi: true, IsFixedLength: false } => global::System.Data.DbType.AnsiString, + { IsAnsi: false, IsFixedLength: true } => global::System.Data.DbType.StringFixedLength, + { IsAnsi: false, IsFixedLength: false } => global::System.Data.DbType.String, + _ => dbParameter.DbType + }; + + dbParameter.Value = dbString.Value as object ?? global::System.DBNull.Value; + } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/DbString.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/DbString.output.netfx.cs new file mode 100644 index 00000000..61d5b059 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/DbString.output.netfx.cs @@ -0,0 +1,250 @@ +#nullable enable +namespace Dapper.AOT // interceptors must be in a known namespace +{ + file static class DapperGeneratedInterceptors + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DbString.input.cs", 11, 30)] + internal static global::System.Threading.Tasks.Task> QueryAsync0(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, Async, TypedResult, HasParameters, Buffered, Text, BindResultsByName, KnownParameters + // takes parameter: + // parameter map: Id Name + // returns data: global::Foo.Product + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.AsEnumerableAsync( + global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory0.Instance).QueryBufferedAsync(param, RowFactory0.Instance)); + + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("Interceptors\\DbString.input.cs", 24, 30)] + internal static global::System.Threading.Tasks.Task> QueryAsync1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType) + { + // Query, Async, TypedResult, HasParameters, Buffered, Text, BindResultsByName, KnownParameters + // takes parameter: global::Foo.QueryModel + // parameter map: Id Name + // returns data: global::Foo.Product + global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql)); + global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text); + global::System.Diagnostics.Debug.Assert(param is not null); + + return global::Dapper.DapperAotExtensions.AsEnumerableAsync( + global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory1.Instance).QueryBufferedAsync((global::Foo.QueryModel)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 2521315361U when NormalizedEquals(name, "productid"): + token = type == typeof(int) ? 0 : 3; // two tokens for right-typed and type-flexible + break; + case 2369371622U when NormalizedEquals(name, "name"): + token = type == typeof(string) ? 1 : 4; + break; + case 1133313085U when NormalizedEquals(name, "productnumber"): + token = type == typeof(string) ? 2 : 5; + break; + + } + tokens[i] = token; + columnOffset++; + + } + return null; + } + public override global::Foo.Product Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan tokens, int columnOffset, object? state) + { + global::Foo.Product result = new(); + foreach (var token in tokens) + { + switch (token) + { + case 0: + result.ProductId = reader.GetInt32(columnOffset); + break; + case 3: + result.ProductId = GetValue(reader, columnOffset); + break; + case 1: + result.Name = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 4: + result.Name = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + case 2: + result.ProductNumber = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset); + break; + case 5: + result.ProductNumber = reader.IsDBNull(columnOffset) ? (string?)null : GetValue(reader, columnOffset); + break; + + } + columnOffset++; + + } + return result; + + } + + } + + private sealed class CommandFactory0 : CommonCommandFactory // + { + internal static readonly CommandFactory0 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { Name = default(global::Dapper.DbString)!, Id = default(int) }); // expected shape + var ps = cmd.Parameters; + global::System.Data.Common.DbParameter p; + p = cmd.CreateParameter(); + p.ParameterName = "Name"; + global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(p, typed.Name); + ps.Add(p); + + p = cmd.CreateParameter(); + p.ParameterName = "Id"; + p.DbType = global::System.Data.DbType.Int32; + p.Direction = global::System.Data.ParameterDirection.Input; + p.Value = AsValue(typed.Id); + ps.Add(p); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args) + { + var typed = Cast(args, static () => new { Name = default(global::Dapper.DbString)!, Id = default(int) }); // expected shape + var ps = cmd.Parameters; + global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(ps[0], typed.Name); + ps[1].Value = AsValue(typed.Id); + + } + public override bool CanPrepare => true; + + } + + private sealed class CommandFactory1 : CommonCommandFactory + { + internal static readonly CommandFactory1 Instance = new(); + public override void AddParameters(in global::Dapper.UnifiedCommand cmd, global::Foo.QueryModel args) + { + var ps = cmd.Parameters; + global::System.Data.Common.DbParameter p; + p = cmd.CreateParameter(); + p.ParameterName = "Name"; + global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(p, args.Name); + ps.Add(p); + + p = cmd.CreateParameter(); + p.ParameterName = "Id"; + p.DbType = global::System.Data.DbType.Int32; + p.Direction = global::System.Data.ParameterDirection.Input; + p.Value = AsValue(args.Id); + ps.Add(p); + + } + public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, global::Foo.QueryModel args) + { + var ps = cmd.Parameters; + global::Dapper.Aot.Generated.DbStringHelpers.ConfigureDbStringDbParameter(ps[0], args.Name); + ps[1].Value = AsValue(args.Id); + + } + public override bool CanPrepare => true; + + } + + + } +} +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; + } + } +} +namespace Dapper.Aot.Generated +{ + /// + /// Contains helpers to properly handle + /// +#if !DAPPERAOT_INTERNAL + file +#endif + static class DbStringHelpers + { + public static void ConfigureDbStringDbParameter( + global::System.Data.Common.DbParameter dbParameter, + global::Dapper.DbString? dbString) + { + if (dbString is null) + { + dbParameter.Value = global::System.DBNull.Value; + return; + } + + // repeating logic from Dapper: + // https://github.com/DapperLib/Dapper/blob/52160dc44699ec7eb5ad57d0dddc6ded4662fcb9/Dapper/DbString.cs#L71 + if (dbString.Length == -1 && dbString.Value is not null && dbString.Value.Length <= global::Dapper.DbString.DefaultLength) + { + dbParameter.Size = global::Dapper.DbString.DefaultLength; + } + else + { + dbParameter.Size = dbString.Length; + } + + dbParameter.DbType = dbString switch + { + { IsAnsi: true, IsFixedLength: true } => global::System.Data.DbType.AnsiStringFixedLength, + { IsAnsi: true, IsFixedLength: false } => global::System.Data.DbType.AnsiString, + { IsAnsi: false, IsFixedLength: true } => global::System.Data.DbType.StringFixedLength, + { IsAnsi: false, IsFixedLength: false } => global::System.Data.DbType.String, + _ => dbParameter.DbType + }; + + dbParameter.Value = dbString.Value as object ?? global::System.DBNull.Value; + } + } +} \ No newline at end of file diff --git a/test/Dapper.AOT.Test/Interceptors/DbString.output.netfx.txt b/test/Dapper.AOT.Test/Interceptors/DbString.output.netfx.txt new file mode 100644 index 00000000..b5a7d042 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/DbString.output.netfx.txt @@ -0,0 +1,4 @@ +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 2 of 2 possible call-sites using 2 interceptors, 2 commands and 1 readers diff --git a/test/Dapper.AOT.Test/Interceptors/DbString.output.txt b/test/Dapper.AOT.Test/Interceptors/DbString.output.txt new file mode 100644 index 00000000..b5a7d042 --- /dev/null +++ b/test/Dapper.AOT.Test/Interceptors/DbString.output.txt @@ -0,0 +1,4 @@ +Generator produced 1 diagnostics: + +Hidden DAP000 L1 C1 +Dapper.AOT handled 2 of 2 possible call-sites using 2 interceptors, 2 commands and 1 readers diff --git a/test/Dapper.AOT.Test/Verifiers/DAP048.cs b/test/Dapper.AOT.Test/Verifiers/DAP048.cs new file mode 100644 index 00000000..886a6574 --- /dev/null +++ b/test/Dapper.AOT.Test/Verifiers/DAP048.cs @@ -0,0 +1,50 @@ +using Dapper.CodeAnalysis; +using System.Threading.Tasks; +using Xunit; +using static Dapper.CodeAnalysis.DapperAnalyzer; + +namespace Dapper.AOT.Test.Verifiers; + +public class DAP048 : Verifier +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public Task MoveFromDbString(bool dapperAotEnabled) => CSVerifyAsync($$""" + using Dapper; + using System; + using System.Linq; + using System.Data.Common; + using System.Threading; + using System.Threading.Tasks; + + [DapperAot({{dapperAotEnabled.ToString().ToLower()}})] + class WithoutAot + { + public void DapperCode(DbConnection conn) + { + var sql = "SELECT * FROM Cars WHERE Name = @Name;"; + var cars = conn.Query(sql, + new + { + {|#0:Name|} = new DbString + { + Value = "MyCar", + IsFixedLength = false, + Length = 5, + IsAnsi = true + } + }); + } + } + + public class Car + { + public string Name { get; set; } + public int Speed { get; set; } + } + """, + DefaultConfig, + dapperAotEnabled ? [ Diagnostic(Diagnostics.MoveFromDbString).WithLocation(0) ] : [] + ); +} \ No newline at end of file