Skip to content
Draft
1 change: 1 addition & 0 deletions MapDataReader.Benchmarks/MapDataReader.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion MapDataReader.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public void MapDatareader_ViaDapper()
public void MapDataReader_ViaMapaDataReader()
{
var dr = _dt.CreateDataReader();
var list = dr.ToTestClass();
var list = dr.To<TestClass>();
}

static DataTable _dt;
Expand Down
2 changes: 1 addition & 1 deletion MapDataReader.Tests/MapDataReader.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions MapDataReader.Tests/TestActualCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public void TestDatatReader()
dt.Rows.Add(123, "ggg", true, 3213, 123, date, TimeSpan.FromSeconds(123), new byte[] { 3, 2, 1 });
dt.Rows.Add(3, "fgdk", false, 11123, 321, date, TimeSpan.FromSeconds(123), new byte[] { 5, 6, 7, 8 });

var list = dt.CreateDataReader().ToMyObject();
var list = dt.CreateDataReader().To<MyObject>();

Assert.IsTrue(list.Count == 2);

Expand Down Expand Up @@ -198,7 +198,7 @@ public void TestDatatReader()

dt2.Rows.Add(true, "alex", 123);

list = dt2.CreateDataReader().ToMyObject(); //should not throw exception
list = dt2.CreateDataReader().To<MyObject>(); //should not throw exception

Assert.IsTrue(list[0].Id == 123);
Assert.IsTrue(list[0].Name == "alex");
Expand Down
1 change: 1 addition & 0 deletions MapDataReader/MapDataReader.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageTags>aot;source-generator</PackageTags>
<Description>Super fast mapping of DataReader to custom objects</Description>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
206 changes: 136 additions & 70 deletions MapDataReader/MapperGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,113 +8,176 @@

namespace MapDataReader
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
/// <summary>
/// An attribute used to mark a class for which a data reader mapper will be generated.
/// </summary>
/// <remarks>
/// The auto-generated mappers will help in mapping data from a data reader to the class properties.
/// </remarks>
[AttributeUsage(AttributeTargets.Class)]
public class GenerateDataReaderMapperAttribute : Attribute
{
}

/// <summary>
/// A source generator responsible for creating mapping extensions that allow for setting properties of a class
/// based on the property name using data from a data reader.
/// </summary>
/// <remarks>
/// This generator scans for classes marked with specific attributes and generates an extension method
/// that facilitates setting properties by their names.
/// </remarks>
[Generator]
public class MapperGenerator : ISourceGenerator
{
private const string Newline = @"
";

/// <summary>
/// Executes the source generation logic, which scans for types needing generation,
/// processes their properties, and generates the corresponding source code for mapping extensions.
/// </summary>
public void Execute(GeneratorExecutionContext context)
{
var targetTypeTracker = context.SyntaxContextReceiver as TargetTypeTracker;
if (context.SyntaxContextReceiver is not TargetTypeTracker targetTypeTracker)
{
return;
}

foreach (var typeNode in targetTypeTracker.TypesNeedingGening)
{
var typeNodeSymbol = context.Compilation
.GetSemanticModel(typeNode.SyntaxTree)
.GetDeclaredSymbol(typeNode);

if (typeNodeSymbol is null)
{
continue;
}

var allProperties = typeNodeSymbol.GetAllSettableProperties();

var src = $@"
var src = $$"""
// <auto-generated/>
#pragma warning disable 8019 //disable 'unnecessary using directive' warning
using System;
using System.Data;
using System.Linq;
using System.Collections.Generic; //to support List<T> etc

namespace MapDataReader
{{
public static partial class MapperExtensions
{{
public static void SetPropertyByName(this {typeNodeSymbol.FullName()} target, string name, object value)
{{
SetPropertyByUpperName(target, name.ToUpperInvariant(), value);
}}

private static void SetPropertyByUpperName(this {typeNodeSymbol.FullName()} target, string name, object value)
{{
{"\r\n" + allProperties.Select(p =>
namespace MapDataReader;

/// <summary>
/// MapDataReader extension methods
/// </summary>
/// <seealso cref="{{typeNodeSymbol.FullName()}}">{{typeNode.Identifier}}</seealso>
public static class {{typeNode.Identifier}}Extensions
{
/// <summary>
/// Fast compile-time method for setting a property value by name
/// </summary>
/// <seealso cref="{{typeNodeSymbol.FullName()}}">{{typeNode.Identifier}}</seealso>
public static void SetPropertyByName(this {{typeNodeSymbol.FullName()}} target, string name, object value)
{
SetPropertyByUpperName(target, name.ToUpperInvariant(), value);
}

private static void SetPropertyByUpperName(this {{typeNodeSymbol.FullName()}} target, string name, object value)
{ {{
Newline + allProperties.Select(p =>
{
var pTypeName = p.Type.FullName();

if (p.Type.IsReferenceType) //ref types - just cast to property type
{
return $@" if (name == ""{p.Name.ToUpperInvariant()}"") {{ target.{p.Name} = value as {pTypeName}; return; }}";
return $"\t\tif (name == \"{p.Name.ToUpperInvariant()}\") {{ target.{p.Name} = value as {pTypeName}; return; }}";
}
else if (pTypeName.EndsWith("?") && !p.Type.IsNullableEnum()) //nullable type (unless nullable Enum)

if (pTypeName.EndsWith("?") && !p.Type.IsNullableEnum()) //nullable type (unless nullable Enum)
{
var nonNullableTypeName = pTypeName.TrimEnd('?');

//do not use "as" operator becasue "as" is slow for nullable types. Use "is" and a null-check
return $@" if (name == ""{p.Name.ToUpperInvariant()}"") {{ if(value==null) target.{p.Name}=null; else if(value is {nonNullableTypeName}) target.{p.Name}=({nonNullableTypeName})value; return; }}";
// do not use "as" operator because "as" is slow for nullable types. Use "is" and a null-check
return $"\t\tif (name == \"{p.Name.ToUpperInvariant()}\") {{ if(value==null) target.{p.Name}=null; else if(value is {nonNullableTypeName}) target.{p.Name}=({nonNullableTypeName})value; return; }}";
}
else if (p.Type.TypeKind == TypeKind.Enum || p.Type.IsNullableEnum()) //enum? pre-convert to underlying type then to int, you can't cast a boxed int to enum directly. Also to support assigning "smallint" database col to int32 (for example), which does not work at first (you can't cast a boxed "byte" to "int")
{
return $@" if (value != null && name == ""{p.Name.ToUpperInvariant()}"") {{ target.{p.Name} = ({pTypeName})(value.GetType() == typeof(int) ? (int)value : (int)Convert.ChangeType(value, typeof(int))); return; }}"; //pre-convert enums to int first (after unboxing, see below)
}
else //primitive types. use Convert.ChangeType before casting. To support assigning "smallint" database col to int32 (for example), which does not work at first (you can't cast a boxed "byte" to "int")

if (p.Type.TypeKind == TypeKind.Enum || p.Type.IsNullableEnum())
{
return $@" if (value != null && name == ""{p.Name.ToUpperInvariant()}"") {{ target.{p.Name} = value.GetType() == typeof({pTypeName}) ? ({pTypeName})value : ({pTypeName})Convert.ChangeType(value, typeof({pTypeName})); return; }}";
// enum? pre-convert to underlying type then to int, you can't cast a boxed int to enum directly.
// Also to support assigning "smallint" database col to int32 (for example), which does not work at first (you can't cast a boxed "byte" to "int")
return $"\t\tif (value != null && name == \"{p.Name.ToUpperInvariant()}\") {{ target.{p.Name} = ({pTypeName})(value.GetType() == typeof(int) ? (int)value : (int)Convert.ChangeType(value, typeof(int))); return; }}"; //pre-convert enums to int first (after unboxing, see below)
}
}).StringConcat("\r\n") }

// primitive types. use Convert.ChangeType before casting.
// To support assigning "smallint" database col to int32 (for example),
// which does not work at first (you can't cast a boxed "byte" to "int")
return $"\t\tif (value != null && name == \"{p.Name.ToUpperInvariant()}\") {{ target.{p.Name} = value.GetType() == typeof({pTypeName}) ? ({pTypeName})value : ({pTypeName})Convert.ChangeType(value, typeof({pTypeName})); return; }}";
}).StringConcat(Newline)
}}
}

""";

}} //end method";

if (typeNodeSymbol.InstanceConstructors.Any(c => !c.Parameters.Any())) //has a constructor without parameters?
{
src += $@"

public static List<{typeNodeSymbol.FullName()}> To{typeNode.Identifier}(this IDataReader dr)
{{
var list = new List<{typeNodeSymbol.FullName()}>();
src += $$"""

/// <summary>
/// Map the data reader to <see cref="{{typeNodeSymbol.FullName()}}">{{typeNode.Identifier}}</see>
/// </summary>
/// <seealso cref="{{typeNodeSymbol.FullName()}}">{{typeNode.Identifier}}</seealso>
public static List<{{typeNodeSymbol.FullName()}}> To{{typeNode.Identifier}}(this IDataReader dr)
{
return dr.To<{{typeNodeSymbol.FullName()}}>();
}

/// <summary>
/// Map the data reader to <see cref="{{typeNodeSymbol.FullName()}}">{{typeNode.Identifier}}</see>
/// </summary>
/// <seealso cref="{{typeNodeSymbol.FullName()}}">{{typeNode.Identifier}}</seealso>
public static List<{{typeNodeSymbol.FullName()}}> To<T>(this IDataReader dr) where T : {{typeNodeSymbol.FullName()}}
{
var list = new List<{{typeNodeSymbol.FullName()}}>();

if (dr.Read())
{
string[] columnNames = new string[dr.FieldCount];

if (dr.Read())
{{
string[] columnNames = new string[dr.FieldCount];

for (int i = 0; i < columnNames.Length; i++)
columnNames[i] = dr.GetName(i).ToUpperInvariant();

do
{{
var result = new {typeNodeSymbol.FullName()}();
for (int i = 0; i < columnNames.Length; i++)
{{
var value = dr[i];
if (value is DBNull) value = null;
SetPropertyByUpperName(result, columnNames[i], value);
}}
list.Add(result);
}} while (dr.Read());
}}
dr.Close();
return list;
}}";
for (int i = 0; i < columnNames.Length; i++)
columnNames[i] = dr.GetName(i).ToUpperInvariant();

do
{
var result = new {{typeNodeSymbol.FullName()}}();
for (int i = 0; i < columnNames.Length; i++)
{
var value = dr[i];
if (value is DBNull) value = null;
SetPropertyByUpperName(result, columnNames[i], value);
}
list.Add(result);
} while (dr.Read());
}
dr.Close();
return list;
}

""";
}

src += "\n}"; //end class
src += "\n}"; //end namespace
// end class
src += $"{Newline}}}";

// Add the source code to the compilation
context.AddSource($"{typeNodeSymbol.Name}DataReaderMapper.g.cs", src);
}
}

/// <summary>
/// Initializes the generator. This method is called before any generation occurs and allows
/// for setting up any necessary context or registering for specific notifications.
/// </summary>
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new TargetTypeTracker());
Expand All @@ -127,25 +190,27 @@ internal class TargetTypeTracker : ISyntaxContextReceiver

public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is ClassDeclarationSyntax cdecl)
if (cdecl.IsDecoratedWithAttribute("GenerateDataReaderMapper"))
TypesNeedingGening = TypesNeedingGening.Add(cdecl);
if (context.Node is not ClassDeclarationSyntax classDec) return;

if (classDec.IsDecoratedWithAttribute("GenerateDataReaderMapper"))
TypesNeedingGening = TypesNeedingGening.Add(classDec);
}
}

internal static class Helpers
{
internal static bool IsDecoratedWithAttribute(this TypeDeclarationSyntax cdecl, string attributeName) =>
cdecl.AttributeLists
internal static bool IsDecoratedWithAttribute(this TypeDeclarationSyntax typeDec, string attributeName) =>
typeDec.AttributeLists
.SelectMany(x => x.Attributes)
.Any(x => x.Name.ToString().Contains(attributeName));


internal static string FullName(this ITypeSymbol typeSymbol) => typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

internal static string StringConcat(this IEnumerable<string> source, string separator) => string.Join(separator, source);

// returns all properties with public setters
/// <summary>
/// Returns all properties with public setters
/// </summary>
internal static IEnumerable<IPropertySymbol> GetAllSettableProperties(this ITypeSymbol typeSymbol)
{
var result = typeSymbol
Expand All @@ -162,18 +227,19 @@ internal static IEnumerable<IPropertySymbol> GetAllSettableProperties(this IType
return result;
}

//checks if type is a nullable num
/// <summary>
/// Checks if type is a nullable Enum
/// </summary>
internal static bool IsNullableEnum(this ITypeSymbol symbol)
{
//tries to get underlying non-nullable type from nullable type
//and then check if it's Enum
if (symbol.NullableAnnotation == NullableAnnotation.Annotated
&& symbol is INamedTypeSymbol namedType
&& namedType.IsValueType
&& namedType.IsGenericType
&& namedType.ConstructedFrom?.ToDisplayString() == "System.Nullable<T>"
)
&& symbol is INamedTypeSymbol { IsValueType: true, IsGenericType: true } namedType
&& namedType.ConstructedFrom.ToDisplayString() == "System.Nullable<T>")
{
return namedType.TypeArguments[0].TypeKind == TypeKind.Enum;
}

return false;
}
Expand Down
Loading