Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/rules/DAP050.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# DAP050

Duplicate classes have been registered as type handlers for the same type,
meaning it's not possible to determine which to use when handling the type.
Note type handlers can be registered at the assembly and module level, so
ensure the type used for the `TValue` parameter in the attribute is only
specified once.

Bad:

``` c#
[module: TypeHandler<MyClass, MyHandler1>]
[module: TypeHandler<MyClass, MyHandler2>]
```

Good:

``` c#
[module: TypeHandler<MyClass, MyHandler>]
```
18 changes: 18 additions & 0 deletions docs/rules/DAP051.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# DAP051

TypeHandler attribute points to a type handler class (TTypeHandler in TypeHandler<TValue, TTypeHandler>) that is not a valid named type,
or cannot be constructed as required by Dapper AOT.
The type handler must be a concrete (non-abstract) class and must have a public parameterless constructor.

Bad:

``` c#
[module: TypeHandler<MyClass, IMyHandler>]
[module: TypeHandler<MyClass, MyAbstractHandler>]
```

Good:

``` c#
[module: TypeHandler<MyClass, MyHandler>]
```
26 changes: 26 additions & 0 deletions docs/typehandlers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Type Handlers

Dapper AOT provides a mechanism to customize how specific .NET types are mapped to and from database values.
This is achieved through Type Handlers, which allow you to define custom serialization for parameters sent
to the database and custom deserialization for values read from query results.
This is similar to SqlMapper.TypeHandler in vanilla Dapper, but with a specific AOT-compatible registration process.

To register your custom type handler, you use either an assembly or module level attribute.
Both have the same effect.
This attribute tells Dapper AOT which .NET type (TValue) your handler processes
and which class (TTypeHandler) is responsible for that processing.

``` csharp
// Example using a module-level attribute
using Dapper;

[module: TypeHandler<YourDotNetType, YourCustomTypeHandler>]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I have a query that required certain set of typeHandlers,
And other do not,
Then how to restore them, and then add if and when needed ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, this PR does not currently allow for switching or disabling type handlers on the fly.
This is definitely a feature that could be added, though.


// Example using an assembly-level attribute (often in AssemblyInfo.cs or a shared file)
// [assembly: TypeHandler<YourDotNetType, YourCustomTypeHandler>]
```

Your custom type handler class must:
* Inherit from Dapper.TypeHandler<TValue>, where TValue is the .NET type it handles.
* Have a public parameterless constructor (new()).
* Implement (override) the necessary virtual methods for your specific conversion needs.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ internal static readonly DiagnosticDescriptor
LanguageVersionTooLow = LibraryWarning("DAP004", "Language version too low", "Interceptors require at least C# version 11"),

CommandPropertyNotFound = LibraryWarning("DAP033", "Command property not found", "Command property {0}.{1} was not found or was not valid; attribute will be ignored"),
CommandPropertyReserved = LibraryWarning("DAP034", "Command property reserved", "Command property {1} is reserved for internal usage; attribute will be ignored");
CommandPropertyReserved = LibraryWarning("DAP034", "Command property reserved", "Command property {1} is reserved for internal usage; attribute will be ignored"),

DuplicateTypeHandlers = LibraryError("DAP050", "Duplicate type handlers", "Type {0} has multiple type handlers registered"),
InvalidTypeHandlerSymbol = LibraryError("DAP051", "Invalid type handler symbol", "Type handler symbol {0} is not a valid named type; attribute will be ignored");
}
}
228 changes: 180 additions & 48 deletions src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs

Large diffs are not rendered by default.

30 changes: 24 additions & 6 deletions src/Dapper.AOT.Analyzers/CodeAnalysis/ParseState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,26 @@ public GenerateState(GenerateContextProxy proxy)
Nodes = proxy.Nodes;
ctx = default;
this.proxy = proxy;
TypeHandlerRegistry = proxy.TypeHandlerRegistry;
TypeHandlers = proxy.TypeHandlers;
}
public GenerateState(SourceProductionContext ctx, in (Compilation Compilation, ImmutableArray<SourceState> Nodes) state)
public GenerateState(SourceProductionContext ctx, Compilation compilation, ImmutableArray<SourceState> nodes,
IImmutableDictionary<ITypeSymbol, ITypeSymbol> typeHandlers, TypeHandlerInstanceRegistry typeHandlerRegistry)
{
Compilation = state.Compilation;
Nodes = state.Nodes;
Compilation = compilation;
Nodes = nodes;
this.ctx = ctx;
proxy = null;
TypeHandlers = typeHandlers;
TypeHandlerRegistry = typeHandlerRegistry;
}
private readonly SourceProductionContext ctx;
private readonly GenerateContextProxy? proxy;
public readonly ImmutableArray<SourceState> Nodes;
public readonly Compilation Compilation;
public readonly GeneratorContext GeneratorContext = new();
public readonly IImmutableDictionary<ITypeSymbol, ITypeSymbol> TypeHandlers;
public readonly TypeHandlerInstanceRegistry TypeHandlerRegistry;

internal void ReportDiagnostic(Diagnostic diagnostic)
{
Expand Down Expand Up @@ -121,9 +128,12 @@ internal abstract class GenerateContextProxy
{
public abstract Compilation Compilation { get; }
public abstract ImmutableArray<SourceState> Nodes { get; }
public abstract TypeHandlerInstanceRegistry TypeHandlerRegistry { get; }
public abstract IImmutableDictionary<ITypeSymbol, ITypeSymbol> TypeHandlers { get; }

public static GenerateContextProxy Create(in CompilationAnalysisContext context, ImmutableArray<SourceState> nodes)
=> new CompilationAnalysisContextProxy(in context, nodes);
public static GenerateContextProxy Create(in CompilationAnalysisContext context, ImmutableArray<SourceState> nodes,
TypeHandlerInstanceRegistry typeHandlerRegistry, IImmutableDictionary<ITypeSymbol, ITypeSymbol> typeHandlers)
=> new CompilationAnalysisContextProxy(in context, nodes, typeHandlerRegistry, typeHandlers);

internal virtual void AddSource(string hintName, string text) { }
internal virtual void ReportDiagnostic(Diagnostic diagnostic) { }
Expand All @@ -132,8 +142,14 @@ private sealed class CompilationAnalysisContextProxy : GenerateContextProxy
{
private readonly CompilationAnalysisContext context;
private readonly ImmutableArray<SourceState> nodes;
public CompilationAnalysisContextProxy(in CompilationAnalysisContext context, ImmutableArray<SourceState> nodes)
private readonly TypeHandlerInstanceRegistry typeHandlerRegistry;
private readonly IImmutableDictionary<ITypeSymbol, ITypeSymbol> typeHandlers;

public CompilationAnalysisContextProxy(in CompilationAnalysisContext context, ImmutableArray<SourceState> nodes,
TypeHandlerInstanceRegistry typeHandlerRegistry, IImmutableDictionary<ITypeSymbol, ITypeSymbol> typeHandlers)
{
this.typeHandlerRegistry = typeHandlerRegistry;
this.typeHandlers = typeHandlers;
this.context = context;
this.nodes = nodes;
}
Expand All @@ -142,5 +158,7 @@ internal override void ReportDiagnostic(Diagnostic diagnostic)
=> context.ReportDiagnostic(diagnostic);
public override Compilation Compilation => context.Compilation;
public override ImmutableArray<SourceState> Nodes => nodes;
public override TypeHandlerInstanceRegistry TypeHandlerRegistry => typeHandlerRegistry;
public override IImmutableDictionary<ITypeSymbol, ITypeSymbol> TypeHandlers => typeHandlers;
}
}
30 changes: 26 additions & 4 deletions src/Dapper.AOT.Analyzers/Internal/CodeWriter.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace Dapper.Internal;

Expand Down Expand Up @@ -155,6 +157,23 @@ private void AppendAsValueTuple(ITypeSymbol value)
}
}

public CodeWriter AppendTypeHandlers(IEnumerable<(string PropertyName, string TypeHandlerFullName)> typeHandlers)
{
if (typeHandlers == null || !typeHandlers.Any())
{
return this;
}

Append("#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.").NewLine();
foreach (var (propertyName, typeHandlerFullName) in typeHandlers)
{
Append($"private static {typeHandlerFullName}? {propertyName.Replace("__Handler", "__handler")};").NewLine();
Append($"private static {typeHandlerFullName} {propertyName} => {propertyName.Replace("__Handler", "__handler")} ??= new {typeHandlerFullName}();").NewLine();
}
Append("#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.").NewLine();
return this;
}

public static int CountGettableInstanceMembers(ImmutableArray<ISymbol> members)
{
int count = 0;
Expand Down Expand Up @@ -238,7 +257,10 @@ public CodeWriter AppendEnumLiteral(ITypeSymbol enumType, int value)

}
public CodeWriter AppendVerbatimLiteral(string? value) => Append(
value is null ? "null" : SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(value)).ToFullString());
CreateVerbatimLiteral(value));

public static string CreateVerbatimLiteral(string? value) =>
value is null ? "null" : SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(value)).ToFullString();
public CodeWriter Append(char value)
{
Core.Append(value);
Expand Down
8 changes: 7 additions & 1 deletion src/Dapper.AOT/TypeHandlerT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Dapper;
/// when processing values of type <typeparamref name="TValue"/>
/// </summary>
[ImmutableObject(true)]
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method, AllowMultiple = true)]
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Module, AllowMultiple = true)]
public sealed class TypeHandlerAttribute<TValue, TTypeHandler> : Attribute
where TTypeHandler : TypeHandler<TValue>, new()
{}
Expand All @@ -31,4 +31,10 @@ public virtual void SetValue(DbParameter parameter, T value)
/// </summary>
public virtual T Parse(DbParameter parameter)
=> CommandUtils.As<T>(parameter.Value);

/// <summary>
/// Reads the value from the results
/// </summary>
public virtual T Read(DbDataReader reader, int columnOffset)
=> CommandUtils.As<T>(reader.GetValue(columnOffset));
}
12 changes: 6 additions & 6 deletions test/Dapper.AOT.Test/Interceptors/QueryStrictBind.output.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,16 @@ private RowFactory0() {}
case 3:
result.X = GetValue<int>(reader, columnOffset);
break;
case 2:
case 1:
result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset);
break;
case 5:
case 4:
result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue<double>(reader, columnOffset);
break;
case 4:
case 2:
result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset);
break;
case 7:
case 5:
result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue<string>(reader, columnOffset);
break;

Expand Down Expand Up @@ -161,10 +161,10 @@ private RowFactory1() {}
case 0:
result.X = reader.GetInt32(columnOffset);
break;
case 2:
case 1:
result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset);
break;
case 4:
case 2:
result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset);
break;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,16 @@ private RowFactory0() {}
case 3:
result.X = GetValue<int>(reader, columnOffset);
break;
case 2:
case 1:
result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset);
break;
case 5:
case 4:
result.Z = reader.IsDBNull(columnOffset) ? (double?)null : GetValue<double>(reader, columnOffset);
break;
case 4:
case 2:
result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset);
break;
case 7:
case 5:
result.Y = reader.IsDBNull(columnOffset) ? (string?)null : GetValue<string>(reader, columnOffset);
break;

Expand Down Expand Up @@ -161,10 +161,10 @@ private RowFactory1() {}
case 0:
result.X = reader.GetInt32(columnOffset);
break;
case 2:
case 1:
result.Z = reader.IsDBNull(columnOffset) ? (double?)null : reader.GetDouble(columnOffset);
break;
case 4:
case 2:
result.Y = reader.IsDBNull(columnOffset) ? (string?)null : reader.GetString(columnOffset);
break;

Expand Down
35 changes: 35 additions & 0 deletions test/Dapper.AOT.Test/Interceptors/TypeHandler.input.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Dapper;
using System.Data;
using System.Data.Common;

[module: DapperAot]
[module: TypeHandler<CustomClass, CustomClassTypeHandler>]

public class CustomClassTypeHandler : TypeHandler<CustomClass>
{
}

public class CustomClass
{
}

public static class Foo
{
static void SomeCode(DbConnection connection, string bar, bool isBuffered)
{
_ = connection.Query<MyType>("def");
_ = connection.Query<int>("def", new { Param = new CustomClass() });
_ = connection.Query<int>("@OutputValue = def", new CommandParameters());
}

public class CommandParameters
{
[DbValue(Direction = ParameterDirection.Output)]
public CustomClass OutputValue { get; set; }
}

public class MyType
{
public CustomClass C { get; set; }
}
}
Loading