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..726bd1e 100644
--- a/MapDataReader.Benchmarks/Program.cs
+++ b/MapDataReader.Benchmarks/Program.cs
@@ -63,10 +63,30 @@ public void MapDatareader_ViaDapper()
}
[Benchmark]
- public void MapDataReader_ViaMapaDataReader()
+ public void MapDataReader_ViaMapDataReader()
{
var dr = _dt.CreateDataReader();
- var list = dr.ToTestClass();
+ var list = dr.To();
+ }
+
+ [Benchmark]
+ public void MapDataReader_ViaManualMap()
+ {
+ var dr = _dt.CreateDataReader();
+
+ var list = new List();
+ while (dr.Read())
+ {
+ list.Add(new TestClass
+ {
+ String1 = dr["String1"] as string,
+ String2 = dr["String2"] as string,
+ String3 = dr["String3"] as string,
+ Int = dr.GetInt32(3),
+ Int2 = dr.GetInt32(4),
+ IntNullable = dr["IntNullable"] as int?
+ });
+ }
}
static DataTable _dt;
@@ -93,14 +113,14 @@ public static void Setup()
}
}
- [GenerateDataReaderMapper]
+ [GenerateDataReaderMapper(AccessModifier = "internal")]
public class TestClass
{
public string String1 { get; set; }
public string String2 { get; set; }
public string String3 { get; set; }
- public string Int { get; set; }
- public string Int2 { get; set; }
+ public int Int { get; set; }
+ public int Int2 { get; set; }
public int? IntNullable { get; set; }
}
}
\ No newline at end of file
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..ee4a496 100644
--- a/MapDataReader.Tests/TestActualCode.cs
+++ b/MapDataReader.Tests/TestActualCode.cs
@@ -3,6 +3,7 @@
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Reflection;
using System.Text;
using System.Threading.Tasks;
@@ -147,7 +148,7 @@ public void TestStringAssign()
}
[TestMethod]
- public void TestDatatReader()
+ public void TestDataReader()
{
//create datatable with test data
var dt = new DataTable();
@@ -165,7 +166,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 +199,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");
@@ -227,6 +228,24 @@ public void TestWrongProperty()
o.SetPropertyByName("Name", 123); //try to assign string prop to int
Assert.IsTrue(o.Name == null); //wrong type. should be null
}
+
+ [TestMethod]
+ public void TestInternalAccessModifier()
+ {
+ var type = typeof(TestClassInternalExtensions);
+ var method = type.GetMethod("To", BindingFlags.Static | BindingFlags.NonPublic);
+
+ Assert.IsNotNull(method, "Expected method 'To' to be 'internal'.");
+ }
+
+ [TestMethod]
+ public void TestInternalAccessModifierNamed()
+ {
+ var type = typeof(TestClassInternalNamedExtensions);
+ var method = type.GetMethod("To", BindingFlags.Static | BindingFlags.NonPublic);
+
+ Assert.IsNotNull(method, "Expected method 'To' to be 'internal'.");
+ }
}
public class BaseClass
@@ -239,5 +258,17 @@ public class ChildClass : BaseClass
{
public string Name { get; set; }
}
+
+ [GenerateDataReaderMapper("internal")]
+ internal class TestClassInternal
+ {
+ public int Id { get; set; }
+ }
+
+ [GenerateDataReaderMapper(AccessModifier = "internal")]
+ internal class TestClassInternalNamed
+ {
+ public int Id { get; set; }
+ }
}
diff --git a/MapDataReader.Tests/TestGenerator.cs b/MapDataReader.Tests/TestGenerator.cs
index 083790b..8ae70aa 100644
--- a/MapDataReader.Tests/TestGenerator.cs
+++ b/MapDataReader.Tests/TestGenerator.cs
@@ -29,6 +29,29 @@ public class MyClass
public decimal Price {get;set;}
}
}
+";
+ var src = GetAndCheckOutputSource(userSource);
+ }
+
+ [TestMethod]
+ public void TestAccessModifier()
+ {
+ string userSource = @"
+using MapDataReader;
+
+namespace MyCode
+{
+ [GenerateDataReaderMapper(AccessModifier = ""internal"")]
+ public class MyClass
+ {
+ public string Name {get;set;}
+ public int Size {get;set;}
+ public bool Enabled {get;set;}
+ public System.DateTime Created {get;set;}
+ public System.DateTimeOffset Offset {get;set;}
+ public decimal Price {get;set;}
+ }
+}
";
var src = GetAndCheckOutputSource(userSource);
}
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..8d16112 100644
--- a/MapDataReader/MapperGenerator.cs
+++ b/MapDataReader/MapperGenerator.cs
@@ -8,17 +8,64 @@
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
{
+ ///
+ /// Gets or sets the access modifier for the generated methods.
+ ///
+ ///
+ /// A string representing the access modifier (e.g., "public", "internal") for the generated methods.
+ ///
+ public string AccessModifier { get; set; }
+
+ ///
+ public GenerateDataReaderMapperAttribute()
+ {
+ AccessModifier = "public";
+ }
+
+ ///
+ /// Change the access modifier of the generated methods.
+ /// See for more information.
+ ///
+ ///
+ public GenerateDataReaderMapperAttribute(string access = "public")
+ {
+ AccessModifier = access;
+ }
}
+ ///
+ /// 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 +73,16 @@ public void Execute(GeneratorExecutionContext context)
.GetSemanticModel(typeNode.SyntaxTree)
.GetDeclaredSymbol(typeNode);
+ if (typeNodeSymbol is null)
+ {
+ continue;
+ }
+
var allProperties = typeNodeSymbol.GetAllSettableProperties();
- var src = $@"
+ var accessModifier = GetAccessModifer(typeNode);
+
+ var src = $$"""
//
#pragma warning disable 8019 //disable 'unnecessary using directive' warning
using System;
@@ -36,89 +90,157 @@ 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);
- }}
+ namespace MapDataReader;
+
+ ///
+ /// MapDataReader extension methods
+ ///
+ /// {{typeNode.Identifier}}
+ {{accessModifier}} static class {{typeNode.Identifier}}Extensions
+ {
+ ///
+ /// Fast compile-time method for setting a property value by name
+ ///
+ /// {{typeNode.Identifier}}
+ {{accessModifier}} 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 =>
+ 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}}
+ {{accessModifier}} static List<{{typeNodeSymbol.FullName()}}> To{{typeNode.Identifier}}(this IDataReader dr)
+ {
+ return dr.To<{{typeNodeSymbol.FullName()}}>();
+ }
+
+ ///
+ /// Map the data reader to {{typeNode.Identifier}}
+ ///
+ /// {{typeNode.Identifier}}
+ {{accessModifier}} 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());
}
+
+ private string GetAccessModifer(ClassDeclarationSyntax typeNode)
+ {
+ // Retrieve the attribute list
+ var attributeList = typeNode.AttributeLists
+ .SelectMany(al => al.Attributes)
+ .FirstOrDefault(attr => attr.Name.ToString() == "GenerateDataReaderMapper");
+
+ if (attributeList?.ArgumentList == null)
+ return "public";
+
+ var arguments = attributeList.ArgumentList.Arguments;
+
+ if (arguments.Count == 0)
+ return "public";
+
+ if (arguments.Count == 1)
+ {
+ var argumentExpr = arguments[0].Expression as LiteralExpressionSyntax;
+ return argumentExpr?.Token.ValueText ?? "public";
+ }
+
+ foreach (var argument in arguments)
+ {
+ // Check if the argument is a named argument
+ if (argument is { NameEquals.Name.Identifier.Text: "AccessModifier" })
+ {
+ var argumentExpr = argument.Expression as LiteralExpressionSyntax;
+ return argumentExpr?.Token.ValueText ?? "public";
+ }
+ }
+
+ return "public";
+ }
}
internal class TargetTypeTracker : ISyntaxContextReceiver
@@ -127,25 +249,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 +286,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..6a497cc 100644
--- a/README.md
+++ b/README.md
@@ -36,19 +36,41 @@ 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.
+### Access Modifier: `public` or `internal`
+
+You can now specify the access modifer to be used with the mapping methods. By default, the methods will be `public` for backwards compatability.
+
+For example, to prevent exposure outside your assembly you'd set it to `internal`. This would hide the mapping methods outside your model project:
+
+```csharp
+[GenerateDataReaderMapper("internal")]
+public class MyClass
+{
+ public int ID { get; set; }
+...
+```
+
## Bonus API: `SetPropertyByName`
This package also adds a super fast `SetPropertyByName` extension method generated at compile time for your class.
@@ -77,10 +99,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();