From 3856a722cfaaca931241eb35e90564b50fc25986 Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 13:56:01 +0100 Subject: [PATCH 01/17] feat: added new benchmarks for comparison to manual mapping --- MapDataReader.Benchmarks/Program.cs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/MapDataReader.Benchmarks/Program.cs b/MapDataReader.Benchmarks/Program.cs index d402e0c..dd18ef9 100644 --- a/MapDataReader.Benchmarks/Program.cs +++ b/MapDataReader.Benchmarks/Program.cs @@ -63,12 +63,32 @@ public void MapDatareader_ViaDapper() } [Benchmark] - public void MapDataReader_ViaMapaDataReader() + public void MapDataReader_ViaMapDataReader() { var dr = _dt.CreateDataReader(); var list = dr.ToTestClass(); } + [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; [GlobalSetup] @@ -99,8 +119,8 @@ 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 From ab4f9279969da0ceff28ba5c86bc9644d1a6f28a Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 15:59:19 +0100 Subject: [PATCH 02/17] feat: added AccessModifier property to the GenerateDataReaderMapper attribute --- MapDataReader/MapperGenerator.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MapDataReader/MapperGenerator.cs b/MapDataReader/MapperGenerator.cs index 07e3b91..ad16e52 100644 --- a/MapDataReader/MapperGenerator.cs +++ b/MapDataReader/MapperGenerator.cs @@ -11,6 +11,17 @@ namespace MapDataReader [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class GenerateDataReaderMapperAttribute : Attribute { + public string AccessModifier { get; set; } + + public GenerateDataReaderMapperAttribute() + { + AccessModifier = "public"; + } + + public GenerateDataReaderMapperAttribute(string access = "public") + { + AccessModifier = access; + } } [Generator] From 8dab45155f8e4dc88b025b4e002a74cf56ad9c71 Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 15:59:33 +0100 Subject: [PATCH 03/17] feat: added method to get the access modifier value --- MapDataReader/MapperGenerator.cs | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/MapDataReader/MapperGenerator.cs b/MapDataReader/MapperGenerator.cs index ad16e52..1c72fcd 100644 --- a/MapDataReader/MapperGenerator.cs +++ b/MapDataReader/MapperGenerator.cs @@ -130,6 +130,44 @@ 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 AttributeArgumentSyntax attributeArgument) + { + var nameEquals = attributeArgument.NameEquals; + if (nameEquals?.Name.Identifier.Text == "AccessModifier") + { + var argumentExpr = argument.Expression as LiteralExpressionSyntax; + return argumentExpr?.Token.ValueText ?? "public"; + } + } + } + + return "public"; + } } internal class TargetTypeTracker : ISyntaxContextReceiver From 7d7e83b589cbf1916091abce431018f78f2201b7 Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 15:59:47 +0100 Subject: [PATCH 04/17] feat: using the access modifier for SetPropertyByName and To methods --- MapDataReader/MapperGenerator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/MapDataReader/MapperGenerator.cs b/MapDataReader/MapperGenerator.cs index 1c72fcd..6863dee 100644 --- a/MapDataReader/MapperGenerator.cs +++ b/MapDataReader/MapperGenerator.cs @@ -39,6 +39,8 @@ public void Execute(GeneratorExecutionContext context) var allProperties = typeNodeSymbol.GetAllSettableProperties(); + var accessModifier = GetAccessModifer(typeNode); + var src = $@" // #pragma warning disable 8019 //disable 'unnecessary using directive' warning @@ -51,7 +53,7 @@ namespace MapDataReader {{ public static partial class MapperExtensions {{ - public static void SetPropertyByName(this {typeNodeSymbol.FullName()} target, string name, object value) + {accessModifier} static void SetPropertyByName(this {typeNodeSymbol.FullName()} target, string name, object value) {{ SetPropertyByUpperName(target, name.ToUpperInvariant(), value); }} @@ -90,7 +92,7 @@ private static void SetPropertyByUpperName(this {typeNodeSymbol.FullName()} targ { src += $@" - public static List<{typeNodeSymbol.FullName()}> To{typeNode.Identifier}(this IDataReader dr) + {accessModifier} static List<{typeNodeSymbol.FullName()}> To{typeNode.Identifier}(this IDataReader dr) {{ var list = new List<{typeNodeSymbol.FullName()}>(); From 238644154548142d7ad81621f9887406b292bad8 Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 16:00:09 +0100 Subject: [PATCH 05/17] test: added tests for the new access modifer Tests added to test the generation and usage --- MapDataReader.Benchmarks/Program.cs | 2 +- MapDataReader.Tests/TestActualCode.cs | 31 +++++++++++++++++++++++++++ MapDataReader.Tests/TestGenerator.cs | 23 ++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/MapDataReader.Benchmarks/Program.cs b/MapDataReader.Benchmarks/Program.cs index d402e0c..bf3afe3 100644 --- a/MapDataReader.Benchmarks/Program.cs +++ b/MapDataReader.Benchmarks/Program.cs @@ -93,7 +93,7 @@ public static void Setup() } } - [GenerateDataReaderMapper] + [GenerateDataReaderMapper(AccessModifier = "internal")] public class TestClass { public string String1 { get; set; } diff --git a/MapDataReader.Tests/TestActualCode.cs b/MapDataReader.Tests/TestActualCode.cs index 76ccaca..19a2054 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; @@ -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(MapperExtensions); + var method = type.GetMethod("ToTestClassInternal", BindingFlags.Static | BindingFlags.NonPublic); + + Assert.IsNotNull(method, "Expected method 'ToTestClassInternal' to be 'internal'."); + } + + [TestMethod] + public void TestInternalAccessModifierNamed() + { + var type = typeof(MapperExtensions); + var method = type.GetMethod("ToTestClassInternalNamed", BindingFlags.Static | BindingFlags.NonPublic); + + Assert.IsNotNull(method, "Expected method 'ToTestClassInternalNamed' 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); } From acee9ce4b2984c61087ed94830698ad3da9205e9 Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 16:00:53 +0100 Subject: [PATCH 06/17] refactor: fixed typo --- MapDataReader.Tests/TestActualCode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MapDataReader.Tests/TestActualCode.cs b/MapDataReader.Tests/TestActualCode.cs index 19a2054..2c71391 100644 --- a/MapDataReader.Tests/TestActualCode.cs +++ b/MapDataReader.Tests/TestActualCode.cs @@ -148,7 +148,7 @@ public void TestStringAssign() } [TestMethod] - public void TestDatatReader() + public void TestDataReader() { //create datatable with test data var dt = new DataTable(); From a8b48bfcae12b01d7ccc1c282d63ce6a8e086194 Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 16:54:30 +0100 Subject: [PATCH 07/17] feat: updated to latest C# language --- MapDataReader.Benchmarks/MapDataReader.Benchmarks.csproj | 1 + MapDataReader.Tests/MapDataReader.Tests.csproj | 2 +- MapDataReader/MapDataReader.csproj | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) 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.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/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 From 7bb7ab3faf01551313b3ed5bb2df559b475d284c Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 16:55:12 +0100 Subject: [PATCH 08/17] fix: added null and type checking --- MapDataReader/MapperGenerator.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/MapDataReader/MapperGenerator.cs b/MapDataReader/MapperGenerator.cs index 6863dee..b647cc6 100644 --- a/MapDataReader/MapperGenerator.cs +++ b/MapDataReader/MapperGenerator.cs @@ -29,7 +29,10 @@ public class MapperGenerator : ISourceGenerator { 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) { @@ -37,6 +40,11 @@ public void Execute(GeneratorExecutionContext context) .GetSemanticModel(typeNode.SyntaxTree) .GetDeclaredSymbol(typeNode); + if (typeNodeSymbol is null) + { + continue; + } + var allProperties = typeNodeSymbol.GetAllSettableProperties(); var accessModifier = GetAccessModifer(typeNode); From c0fcfe5a6954121e9d0f92de73bcca23a2f4c3ba Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 17:13:07 +0100 Subject: [PATCH 09/17] feat: updated to use string literals and generic methods --- MapDataReader/MapperGenerator.cs | 119 +++++++++++++++++-------------- 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/MapDataReader/MapperGenerator.cs b/MapDataReader/MapperGenerator.cs index b647cc6..8be6450 100644 --- a/MapDataReader/MapperGenerator.cs +++ b/MapDataReader/MapperGenerator.cs @@ -48,8 +48,8 @@ public void Execute(GeneratorExecutionContext context) var allProperties = typeNodeSymbol.GetAllSettableProperties(); var accessModifier = GetAccessModifer(typeNode); - - var src = $@" + + var src = $$""" // #pragma warning disable 8019 //disable 'unnecessary using directive' warning using System; @@ -57,79 +57,88 @@ public void Execute(GeneratorExecutionContext context) using System.Linq; using System.Collections.Generic; //to support List etc - namespace MapDataReader - {{ - public static partial class MapperExtensions - {{ - {accessModifier} static void SetPropertyByName(this {typeNodeSymbol.FullName()} target, string name, object value) - {{ - SetPropertyByUpperName(target, name.ToUpperInvariant(), value); - }} + namespace MapDataReader; - private static void SetPropertyByUpperName(this {typeNodeSymbol.FullName()} target, string name, object value) - {{ - {"\r\n" + allProperties.Select(p => + {{accessModifier}} static class {{typeNode.Identifier}}Extensions + { + {{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) + { {{ + 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; }}"; - } - 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) + // 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 //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 += $@" - - {accessModifier} static List<{typeNodeSymbol.FullName()}> To{typeNode.Identifier}(this IDataReader dr) - {{ - var list = new List<{typeNodeSymbol.FullName()}>(); + src += $$""" + + {{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); From 4c5cae334a0683b31cc3ecd8e3cc79645ff385ca Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 17:13:38 +0100 Subject: [PATCH 10/17] refactor: updated usages to use new syntax --- MapDataReader.Benchmarks/Program.cs | 2 +- MapDataReader.Tests/TestActualCode.cs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/MapDataReader.Benchmarks/Program.cs b/MapDataReader.Benchmarks/Program.cs index bf3afe3..1427c37 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/TestActualCode.cs b/MapDataReader.Tests/TestActualCode.cs index 2c71391..ee4a496 100644 --- a/MapDataReader.Tests/TestActualCode.cs +++ b/MapDataReader.Tests/TestActualCode.cs @@ -166,7 +166,7 @@ public void TestDataReader() 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); @@ -199,7 +199,7 @@ public void TestDataReader() 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"); @@ -232,19 +232,19 @@ public void TestWrongProperty() [TestMethod] public void TestInternalAccessModifier() { - var type = typeof(MapperExtensions); - var method = type.GetMethod("ToTestClassInternal", BindingFlags.Static | BindingFlags.NonPublic); + var type = typeof(TestClassInternalExtensions); + var method = type.GetMethod("To", BindingFlags.Static | BindingFlags.NonPublic); - Assert.IsNotNull(method, "Expected method 'ToTestClassInternal' to be 'internal'."); + Assert.IsNotNull(method, "Expected method 'To' to be 'internal'."); } [TestMethod] public void TestInternalAccessModifierNamed() { - var type = typeof(MapperExtensions); - var method = type.GetMethod("ToTestClassInternalNamed", BindingFlags.Static | BindingFlags.NonPublic); + var type = typeof(TestClassInternalNamedExtensions); + var method = type.GetMethod("To", BindingFlags.Static | BindingFlags.NonPublic); - Assert.IsNotNull(method, "Expected method 'ToTestClassInternalNamed' to be 'internal'."); + Assert.IsNotNull(method, "Expected method 'To' to be 'internal'."); } } From c0e4227911fd10a0071784ca973e5584b294ac04 Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 17:14:43 +0100 Subject: [PATCH 11/17] docs: added xmldoc statements for methods --- MapDataReader/MapperGenerator.cs | 47 ++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/MapDataReader/MapperGenerator.cs b/MapDataReader/MapperGenerator.cs index 8be6450..1474d8b 100644 --- a/MapDataReader/MapperGenerator.cs +++ b/MapDataReader/MapperGenerator.cs @@ -8,25 +8,58 @@ 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) { if (context.SyntaxContextReceiver is not TargetTypeTracker targetTypeTracker) @@ -145,6 +178,10 @@ private static void SetPropertyByUpperName(this {{typeNodeSymbol.FullName()}} ta } } + /// + /// 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()); @@ -213,7 +250,9 @@ internal static bool IsDecoratedWithAttribute(this TypeDeclarationSyntax cdecl, 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 @@ -230,7 +269,9 @@ 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 From f1fe8e6b6874856e7f342c4881c0e86f4c2ff049 Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 17:15:17 +0100 Subject: [PATCH 12/17] refactor: updated some syntax to use latest c# features --- MapDataReader/MapperGenerator.cs | 37 ++++++++++++++------------------ 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/MapDataReader/MapperGenerator.cs b/MapDataReader/MapperGenerator.cs index 1474d8b..78d8535 100644 --- a/MapDataReader/MapperGenerator.cs +++ b/MapDataReader/MapperGenerator.cs @@ -196,9 +196,9 @@ private string GetAccessModifer(ClassDeclarationSyntax typeNode) if (attributeList?.ArgumentList == null) return "public"; - + var arguments = attributeList.ArgumentList.Arguments; - + if (arguments.Count == 0) return "public"; @@ -211,17 +211,13 @@ private string GetAccessModifer(ClassDeclarationSyntax typeNode) foreach (var argument in arguments) { // Check if the argument is a named argument - if (argument is AttributeArgumentSyntax attributeArgument) + if (argument is { NameEquals.Name.Identifier.Text: "AccessModifier" }) { - var nameEquals = attributeArgument.NameEquals; - if (nameEquals?.Name.Identifier.Text == "AccessModifier") - { - var argumentExpr = argument.Expression as LiteralExpressionSyntax; - return argumentExpr?.Token.ValueText ?? "public"; - } + var argumentExpr = argument.Expression as LiteralExpressionSyntax; + return argumentExpr?.Token.ValueText ?? "public"; } } - + return "public"; } } @@ -232,20 +228,20 @@ 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); @@ -277,12 +273,11 @@ 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; } From 72ca54037e6d4f6cfd112fcf529ea11ed0df2939 Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 17:31:39 +0100 Subject: [PATCH 13/17] docs: added additional xmldoc statements for the generated code --- MapDataReader/MapperGenerator.cs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/MapDataReader/MapperGenerator.cs b/MapDataReader/MapperGenerator.cs index 78d8535..f48aeb1 100644 --- a/MapDataReader/MapperGenerator.cs +++ b/MapDataReader/MapperGenerator.cs @@ -91,9 +91,17 @@ public void Execute(GeneratorExecutionContext context) using System.Collections.Generic; //to support List etc 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); @@ -140,6 +148,20 @@ private static void SetPropertyByUpperName(this {{typeNodeSymbol.FullName()}} ta { src += $$""" + /// + /// Map the data reader to {{typeNode.Identifier}} + /// + /// {{typeNode.Identifier}} + [Obsolete("Use To instead, this will be removed in future versions.")] + {{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()}}>(); From 7ab1312d31fdb6b4bb0b8095eead41a5a5bb2579 Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 17:36:36 +0100 Subject: [PATCH 14/17] docs: updated readme with the call to the newer and older mapping method --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ab78f76..c23eacd 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ 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(); ``` Some notes for the above @@ -49,6 +49,14 @@ Some notes for the above * 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. From 42b5809c7b5d5a71c57f2d71de606824fc50604b Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 2 Oct 2024 17:47:02 +0100 Subject: [PATCH 15/17] docs: updated documentation with details of the new access modifier --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index ab78f76..21c89e8 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,20 @@ Some notes for the above * 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. From 95aa1f3c1b744ff0a52ab5086d8c7fba791e799b Mon Sep 17 00:00:00 2001 From: Amy Date: Sun, 6 Oct 2024 12:23:50 +0100 Subject: [PATCH 16/17] refactor: removed the obsolete attribute --- MapDataReader/MapperGenerator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MapDataReader/MapperGenerator.cs b/MapDataReader/MapperGenerator.cs index f48aeb1..8d16112 100644 --- a/MapDataReader/MapperGenerator.cs +++ b/MapDataReader/MapperGenerator.cs @@ -152,7 +152,6 @@ private static void SetPropertyByUpperName(this {{typeNodeSymbol.FullName()}} ta /// Map the data reader to {{typeNode.Identifier}} /// /// {{typeNode.Identifier}} - [Obsolete("Use To instead, this will be removed in future versions.")] {{accessModifier}} static List<{{typeNodeSymbol.FullName()}}> To{{typeNode.Identifier}}(this IDataReader dr) { return dr.To<{{typeNodeSymbol.FullName()}}>(); From 20a1296d4b316f7b46525c114c783c9815d64e7a Mon Sep 17 00:00:00 2001 From: Amy Date: Sun, 6 Oct 2024 12:32:43 +0100 Subject: [PATCH 17/17] docs: updated the documentation --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4637b1f..6a497cc 100644 --- a/README.md +++ b/README.md @@ -36,27 +36,27 @@ public class MyClass var dataReader = new SqlCommand("SELECT * FROM MyTable", connection).ExecuteReader(); -List results = dataReader.To(); +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(); -``` - ### 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. @@ -99,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();