Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added generic methods and updated C# version #11

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
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