diff --git a/MapDataReader.Benchmarks/MapDataReader.Benchmarks.csproj b/MapDataReader.Benchmarks/MapDataReader.Benchmarks.csproj
index 1073b53..44cba17 100644
--- a/MapDataReader.Benchmarks/MapDataReader.Benchmarks.csproj
+++ b/MapDataReader.Benchmarks/MapDataReader.Benchmarks.csproj
@@ -5,6 +5,7 @@
net6.0
enable
enable
+ latest
diff --git a/MapDataReader.Benchmarks/Program.cs b/MapDataReader.Benchmarks/Program.cs
index d402e0c..369cc90 100644
--- a/MapDataReader.Benchmarks/Program.cs
+++ b/MapDataReader.Benchmarks/Program.cs
@@ -66,7 +66,7 @@ public void MapDatareader_ViaDapper()
public void MapDataReader_ViaMapaDataReader()
{
var dr = _dt.CreateDataReader();
- var list = dr.ToTestClass();
+ var list = dr.To();
}
static DataTable _dt;
diff --git a/MapDataReader.Tests/MapDataReader.Tests.csproj b/MapDataReader.Tests/MapDataReader.Tests.csproj
index b14646f..033eb5a 100644
--- a/MapDataReader.Tests/MapDataReader.Tests.csproj
+++ b/MapDataReader.Tests/MapDataReader.Tests.csproj
@@ -4,8 +4,8 @@
net6.0
enable
enable
-
false
+ latest
diff --git a/MapDataReader.Tests/TestActualCode.cs b/MapDataReader.Tests/TestActualCode.cs
index 76ccaca..10ab964 100644
--- a/MapDataReader.Tests/TestActualCode.cs
+++ b/MapDataReader.Tests/TestActualCode.cs
@@ -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();
Assert.IsTrue(list.Count == 2);
@@ -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(); //should not throw exception
Assert.IsTrue(list[0].Id == 123);
Assert.IsTrue(list[0].Name == "alex");
diff --git a/MapDataReader/MapDataReader.csproj b/MapDataReader/MapDataReader.csproj
index 7fda524..286c9e4 100644
--- a/MapDataReader/MapDataReader.csproj
+++ b/MapDataReader/MapDataReader.csproj
@@ -14,6 +14,7 @@
aot;source-generator
Super fast mapping of DataReader to custom objects
True
+ latest
diff --git a/MapDataReader/MapperGenerator.cs b/MapDataReader/MapperGenerator.cs
index 07e3b91..2af5c4d 100644
--- a/MapDataReader/MapperGenerator.cs
+++ b/MapDataReader/MapperGenerator.cs
@@ -8,17 +8,41 @@
namespace MapDataReader
{
- [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
+ ///
+ /// An attribute used to mark a class for which a data reader mapper will be generated.
+ ///
+ ///
+ /// The auto-generated mappers will help in mapping data from a data reader to the class properties.
+ ///
+ [AttributeUsage(AttributeTargets.Class)]
public class GenerateDataReaderMapperAttribute : Attribute
{
}
+ ///
+ /// 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.
+ ///
+ ///
+ /// This generator scans for classes marked with specific attributes and generates an extension method
+ /// that facilitates setting properties by their names.
+ ///
[Generator]
public class MapperGenerator : ISourceGenerator
{
+ private const string Newline = @"
+";
+
+ ///
+ /// Executes the source generation logic, which scans for types needing generation,
+ /// processes their properties, and generates the corresponding source code for mapping extensions.
+ ///
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)
{
@@ -26,9 +50,14 @@ public void Execute(GeneratorExecutionContext context)
.GetSemanticModel(typeNode.SyntaxTree)
.GetDeclaredSymbol(typeNode);
+ if (typeNodeSymbol is null)
+ {
+ continue;
+ }
+
var allProperties = typeNodeSymbol.GetAllSettableProperties();
- var src = $@"
+ var src = $$"""
//
#pragma warning disable 8019 //disable 'unnecessary using directive' warning
using System;
@@ -36,85 +65,119 @@ public void Execute(GeneratorExecutionContext context)
using System.Linq;
using System.Collections.Generic; //to support List 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;
+
+ ///
+ /// MapDataReader extension methods
+ ///
+ /// {{typeNode.Identifier}}
+ public static class {{typeNode.Identifier}}Extensions
+ {
+ ///
+ /// Fast compile-time method for setting a property value by name
+ ///
+ /// {{typeNode.Identifier}}
+ 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 += $$"""
+
+ ///
+ /// Map the data reader to {{typeNode.Identifier}}
+ ///
+ /// {{typeNode.Identifier}}
+ public static List<{{typeNodeSymbol.FullName()}}> To{{typeNode.Identifier}}(this IDataReader dr)
+ {
+ return dr.To<{{typeNodeSymbol.FullName()}}>();
+ }
+
+ ///
+ /// Map the data reader to {{typeNode.Identifier}}
+ ///
+ /// {{typeNode.Identifier}}
+ public static List<{{typeNodeSymbol.FullName()}}> To(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);
}
}
+ ///
+ /// Initializes the generator. This method is called before any generation occurs and allows
+ /// for setting up any necessary context or registering for specific notifications.
+ ///
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new TargetTypeTracker());
@@ -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 source, string separator) => string.Join(separator, source);
- // returns all properties with public setters
+ ///
+ /// Returns all properties with public setters
+ ///
internal static IEnumerable GetAllSettableProperties(this ITypeSymbol typeSymbol)
{
var result = typeSymbol
@@ -162,18 +227,19 @@ internal static IEnumerable GetAllSettableProperties(this IType
return result;
}
- //checks if type is a nullable num
+ ///
+ /// Checks if type is a nullable Enum
+ ///
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"
- )
+ && symbol is INamedTypeSymbol { IsValueType: true, IsGenericType: true } namedType
+ && namedType.ConstructedFrom.ToDisplayString() == "System.Nullable")
+ {
return namedType.TypeArguments[0].TypeKind == TypeKind.Enum;
+ }
return false;
}
diff --git a/README.md b/README.md
index ab78f76..cb83e31 100644
--- a/README.md
+++ b/README.md
@@ -36,19 +36,35 @@ public class MyClass
var dataReader = new SqlCommand("SELECT * FROM MyTable", connection).ExecuteReader();
-List results = dataReader.ToMyClass(); // "ToMyClass" method is generated at compile time
+List results = dataReader.To(); // Generic method
+
+// or
+
+List results = dataReader.ToMyClass(); // Direct method
```
Some notes for the above
-* The `ToMyClass()` method above - is an `IDataReader` extension method generated at compile time. You can even "go to definition" in Visual Studio and examine its code.
-* The naming convention is `ToCLASSNAME()` we can't use generics here, since `` is not part of method signatures in C# (considered in later versions of C#). If you find a prettier way - please contribute!
-* Maps properies with public setters only.
+* The `To()` method above - is an `IDataReader` extension method generated at compile time.
+ * The naming convention is `To()` where `T` is your class name marked with `[GenerateDataReaderMapper]`, e.g. `MyClass`.
+ * Thanks to [@5andr0](https://github.com/5andr0) for the suggestion of how to add the generic version of this method.
+* The `ToMyClass()` method is functionally identical to the `To()` method but maintained for backwards compatability.
+ * The naming convention is `ToCLASSNAME()`
+ * You can even "go to definition" in Visual Studio and examine the code for either of these two methods.
+* Maps properties with public setters only.
* The datareader is being closed after mapping, so don't reuse it.
* Supports `enum` properties based on `int` and other implicit casting (sometimes a DataReader may decide to return `byte` for small integer database value, and it maps to `int` perfectly via some unboxing magic)
* Properly maps `DBNull` to `null`.
* Complex-type properties may not work.
+### Legacy Mapping Method
+
+The `To()` method has been added to unify the method calls, however the previous version of this method is maintained for now.
+
+```csharp
+List results = dataReader.ToMyClass();
+```
+
## Bonus API: `SetPropertyByName`
This package also adds a super fast `SetPropertyByName` extension method generated at compile time for your class.
@@ -77,10 +93,10 @@ If you're already using the awesome [Dapper ORM](https://github.com/DapperLib/Da
public static List Query(this SqlConnection cn, string sql, object parameters = null)
{
if (typeof(T) == typeof(MyClass)) //our own class that we marked with attribute?
- return cn.ExecuteReader(sql, parameters).ToMyClass() as List; //use MapDataReader
+ return cn.ExecuteReader(sql, parameters).To() as List; //use MapDataReader
if (typeof(T) == typeof(AnotherClass)) //another class we have enabled?
- return cn.ExecuteReader(sql, parameters).ToAnotherClass() as List; //again
+ return cn.ExecuteReader(sql, parameters).To() as List; //again
//fallback to Dapper by default
return SqlMapper.Query(cn, sql, parameters).AsList();