├── .github └── workflows │ └── MapperGenerator-CI.yml ├── .gitignore ├── MapperGenerator.Tests ├── Helper │ └── GeneratorTestHelper.cs ├── MapperGenerator.Tests.csproj └── MapperGeneratorTest.cs ├── MapperGenerator ├── Extensions │ └── SyntaxExtensions.cs ├── Generators │ └── MapperGenerator.cs └── MapperGenerator.csproj └── README.md /.github/workflows/MapperGenerator-CI.yml: -------------------------------------------------------------------------------- 1 | name: MapperGenerator-CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'README.md' 9 | - '.github/**' 10 | 11 | env: 12 | solution: '**/*.sln' 13 | buildPlatform: Any CPU 14 | buildConfiguration: Release 15 | jobs: 16 | build: 17 | runs-on: windows-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - run: dotnet build MapperGenerator/MapperGenerator.csproj --configuration ${{ env.buildConfiguration }} 21 | - run: dotnet test MapperGenerator.Tests/MapperGenerator.Tests.csproj 22 | 23 | # Publish 24 | - name: publish on version change 25 | id: publish_nuget 26 | uses: rohith/publish-nuget@v2 27 | with: 28 | # Filepath of the project to be packaged, relative to root of repository 29 | PROJECT_FILE_PATH: MapperGenerator/MapperGenerator.csproj 30 | 31 | # NuGet package id, used for version detection & defaults to project name 32 | # PACKAGE_NAME: Core 33 | 34 | # Filepath with version info, relative to root of repository & defaults to PROJECT_FILE_PATH 35 | # VERSION_FILE_PATH: Directory.Build.props 36 | 37 | # Regex pattern to extract version info in a capturing group 38 | # VERSION_REGEX: ^\s*(.*)<\/Version>\s*$ 39 | 40 | # Flag to toggle git tagging, enabled by default 41 | TAG_COMMIT: true 42 | 43 | # Format of the git tag, [*] gets replaced with actual version 44 | TAG_FORMAT: v* 45 | 46 | # API key to authenticate with NuGet server 47 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 48 | 49 | # NuGet server uri hosting the packages, defaults to https://api.nuget.org 50 | NUGET_SOURCE: https://api.nuget.org 51 | 52 | # Flag to toggle pushing symbols along with nuget package to the server, disabled by default 53 | INCLUDE_SYMBOLS: false 54 | 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This .gitignore file was automatically created by Microsoft(R) Visual Studio. 3 | ################################################################################ 4 | 5 | /MapperGenerator/.vs/MapperGenerator/v16 6 | /MapperGenerator/obj 7 | /MapperGenerator/MapperGenerator.sln 8 | /MapperGenerator/bin 9 | /MapperGenerator/MapperGenerator.csproj.user 10 | /MapperGenerator/.vs/MapperGenerator/DesignTimeBuild 11 | /MapperGenerator/MapperGenerator.sln.DotSettings.user 12 | /MapperGenerator.Tests/bin/ 13 | /MapperGenerator.Tests/obj 14 | -------------------------------------------------------------------------------- /MapperGenerator.Tests/Helper/GeneratorTestHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CSharp; 9 | 10 | namespace MapperGenerator.Tests.Helper 11 | { 12 | internal class GeneratorTestHelper 13 | { 14 | internal static (Compilation, ImmutableArray diagnostics) RunGenerators( 15 | Compilation originCompilation, 16 | params ISourceGenerator[] generators) 17 | { 18 | CreateDriver(originCompilation, generators).RunGeneratorsAndUpdateCompilation(originCompilation, 19 | out var resultCompilation, out var diagnostics); 20 | return (resultCompilation, diagnostics); 21 | } 22 | 23 | internal static Compilation CreateCompilation(params string[] source) 24 | { 25 | var dd = typeof(Enumerable).GetTypeInfo().Assembly.Location; 26 | var coreDir = Directory.GetParent(dd) ?? throw new Exception("Couldn't find location of coredir"); 27 | 28 | var references = GetReferneces(coreDir); 29 | 30 | var syntaxTrees = source.Select(x => 31 | CSharpSyntaxTree.ParseText(x, new CSharpParseOptions(LanguageVersion.Preview))); 32 | return CSharpCompilation.Create( 33 | "compilation", 34 | syntaxTrees, 35 | references, 36 | new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); 37 | } 38 | 39 | internal static Assembly GetAssemblyFromCompilation(Compilation resultCompilation) 40 | { 41 | using var stream = new MemoryStream(); 42 | resultCompilation.Emit(stream); 43 | var assembly = Assembly.Load(stream.ToArray()); 44 | return assembly; 45 | } 46 | 47 | private static GeneratorDriver CreateDriver(Compilation c, params ISourceGenerator[] generators) 48 | { 49 | var parseOptions = (CSharpParseOptions) c.SyntaxTrees.First().Options; 50 | 51 | return CSharpGeneratorDriver.Create( 52 | ImmutableArray.Create(generators), 53 | ImmutableArray.Empty, 54 | parseOptions); 55 | } 56 | 57 | private static PortableExecutableReference[] GetReferneces(DirectoryInfo coreDir) 58 | { 59 | var references = new[] 60 | { 61 | MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), 62 | MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location), 63 | MetadataReference.CreateFromFile(typeof(Dictionary<,>).GetTypeInfo().Assembly.Location), 64 | MetadataReference.CreateFromFile($"{coreDir.FullName}{Path.DirectorySeparatorChar}mscorlib.dll"), 65 | MetadataReference.CreateFromFile($"{coreDir.FullName}{Path.DirectorySeparatorChar}System.Runtime.dll"), 66 | MetadataReference.CreateFromFile( 67 | $"{coreDir.FullName}{Path.DirectorySeparatorChar}System.Collections.dll"), 68 | }; 69 | return references; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /MapperGenerator.Tests/MapperGenerator.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /MapperGenerator.Tests/MapperGeneratorTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using MapperGenerator.Tests.Helper; 8 | using Microsoft.CodeAnalysis; 9 | using Microsoft.CodeAnalysis.CSharp; 10 | using NUnit.Framework; 11 | 12 | namespace MapperGenerator.Tests 13 | { 14 | public class MapperGeneratorTest 15 | { 16 | [SetUp] 17 | public void Setup() 18 | { 19 | } 20 | 21 | [Test] 22 | public void General_Property_Mapping() 23 | { 24 | #region Produce two mapping class text 25 | 26 | const string sourceText = @" 27 | namespace Sample.Entities 28 | { 29 | public class Person 30 | { 31 | public int Id { get; set; } 32 | public string Name { get; set; } 33 | } 34 | } 35 | "; 36 | 37 | const string targetText = @" 38 | using MapperGenerator; 39 | using Sample.Entities; 40 | namespace Sample.Models 41 | { 42 | [Mapping(typeof(Person))] 43 | public class PersonViewModel 44 | { 45 | public int Id { get; set; } 46 | public string Name { get; set; } 47 | } 48 | } 49 | "; 50 | 51 | #endregion 52 | 53 | var originCompilation = GeneratorTestHelper.CreateCompilation(sourceText, targetText); 54 | var (resultCompilation, generatorDiagnostics) = 55 | GeneratorTestHelper.RunGenerators(originCompilation, new Generators.MapperGenerator()); 56 | 57 | // verify no errors or warnings are returned 58 | Assert.IsEmpty(generatorDiagnostics); 59 | Assert.IsEmpty(resultCompilation.GetDiagnostics()); 60 | 61 | // compile and get an assembly along with our methods. 62 | var assembly = GeneratorTestHelper.GetAssemblyFromCompilation(resultCompilation); 63 | var mapperType = assembly.GetType("MapperGenerator.Mapper"); 64 | var generalMappingMethod = 65 | mapperType?.GetMethod("MapToPersonViewModel"); // this one is added via the generator 66 | var extensionMappingMethod = mapperType?.GetMethod("ToPersonViewModel"); // this is in our source 67 | 68 | Assert.NotNull(generalMappingMethod); 69 | Assert.NotNull(extensionMappingMethod); 70 | 71 | //var mapper = Activator.CreateInstance(mapperType); 72 | var personType = assembly.GetType("Sample.Entities.Person"); 73 | var person = Activator.CreateInstance(personType); 74 | personType.GetProperty("Id")?.SetValue(person, 1); 75 | personType.GetProperty("Name")?.SetValue(person, "Roberson"); 76 | 77 | 78 | var result = generalMappingMethod.Invoke(null, new[] {person}); 79 | 80 | var expectedType = assembly.GetType("Sample.Models.PersonViewModel"); 81 | Assert.AreEqual(expectedType, result.GetType()); 82 | 83 | var actualId = expectedType.GetProperty("Id").GetValue(result); 84 | var actualName = expectedType.GetProperty("Name").GetValue(result); 85 | 86 | Assert.AreEqual(1, actualId); 87 | Assert.AreEqual("Roberson", actualName); 88 | } 89 | 90 | [Test] 91 | public void Property_Mismatch_Throw_Diagnostic_Error() 92 | { 93 | #region Produce two mapping class text 94 | 95 | const string sourceText = @" 96 | namespace Sample.Entities 97 | { 98 | public class Person 99 | { 100 | public int Id { get; set; } 101 | public string Name { get; set; } 102 | } 103 | } 104 | "; 105 | 106 | const string targetText = @" 107 | using MapperGenerator; 108 | using Sample.Entities; 109 | namespace Sample.Models 110 | { 111 | [Mapping(typeof(Person))] 112 | public class PersonViewModel 113 | { 114 | public int Sn { get; set; } 115 | public string Name { get; set; } 116 | } 117 | } 118 | "; 119 | 120 | #endregion 121 | 122 | var originCompilation = GeneratorTestHelper.CreateCompilation(sourceText, targetText); 123 | var (_, generatorDiagnostics) = 124 | GeneratorTestHelper.RunGenerators(originCompilation, 125 | new Generators.MapperGenerator()); 126 | var diagnostic = generatorDiagnostics.FirstOrDefault(); 127 | 128 | 129 | Assert.AreEqual("MPERR001", diagnostic.Id); 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /MapperGenerator/Extensions/SyntaxExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | 7 | namespace MapperGenerator.Extensions 8 | { 9 | public static class SyntaxExtensions 10 | { 11 | public static IEnumerable<(string propertyType, string propertyName, PropertyDeclarationSyntax propertySyntax)> GetProperties(this ClassDeclarationSyntax classSyntax, SemanticModel semanticModel) 12 | { 13 | (string propertyType, string propertyName, PropertyDeclarationSyntax) GetPropertyInfo( 14 | PropertyDeclarationSyntax propertySyntax, SemanticModel model) 15 | { 16 | var declaredSymbol = model.GetDeclaredSymbol(propertySyntax); 17 | var propertyType = declaredSymbol.Type.ToString(); 18 | var propertyName = declaredSymbol.Name; 19 | 20 | return (propertyType, propertyName, propertySyntax); 21 | } 22 | 23 | var propertySyntaxes = classSyntax.SyntaxTree.GetRoot().DescendantNodes().OfType(); 24 | return propertySyntaxes.Select(prop 25 | => GetPropertyInfo(prop, semanticModel)); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /MapperGenerator/Generators/MapperGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | using MapperGenerator.Extensions; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CSharp; 9 | using Microsoft.CodeAnalysis.CSharp.Syntax; 10 | using Microsoft.CodeAnalysis.Text; 11 | 12 | namespace MapperGenerator.Generators 13 | { 14 | [Generator] 15 | public class MapperGenerator : ISourceGenerator 16 | { 17 | private const string MappingAttributeText = @" 18 | using System; 19 | namespace MapperGenerator 20 | { 21 | public class MappingAttribute : Attribute 22 | { 23 | public MappingAttribute(Type targetType) 24 | { 25 | this.TargetType = targetType; 26 | } 27 | 28 | public Type TargetType { get; set; } 29 | } 30 | }"; 31 | 32 | public void Initialize(GeneratorInitializationContext context) 33 | { 34 | #region Manually toggle debugger 35 | //Debugger.Launch(); 36 | #endregion 37 | //context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); 38 | } 39 | 40 | public void Execute(GeneratorExecutionContext context) 41 | { 42 | context.AddSource("MapperAttribute", SourceText.From(MappingAttributeText, Encoding.UTF8)); 43 | 44 | //Create a new compilation that contains the attribute 45 | var options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions; 46 | var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(MappingAttributeText, Encoding.UTF8), options)); 47 | 48 | var allNodes = compilation.SyntaxTrees.SelectMany(s => s.GetRoot().DescendantNodes()); 49 | var allAttributes = allNodes.Where((d) => d.IsKind(SyntaxKind.Attribute)).OfType(); 50 | var attributes = allAttributes.Where(d => d.Name.ToString() == "Mapping" 51 | || d.Name.ToString() == "Mapper.Mapping").ToImmutableArray(); 52 | var allClasses = compilation.SyntaxTrees. 53 | SelectMany(x => x.GetRoot().DescendantNodes().OfType()); 54 | 55 | 56 | var sourceBuilder = new StringBuilder(@" 57 | // 58 | namespace MapperGenerator 59 | { 60 | public static class Mapper 61 | {"); 62 | foreach (AttributeSyntax attr in attributes) 63 | { 64 | if (attr.ArgumentList is null) throw new Exception("Can't be null here"); 65 | 66 | #region Get Mapping Source Class Info 67 | 68 | //todo: add diagnostic when ArgumentList is null 69 | //get type of mapping target from constructor argument 70 | var mappedTypeArgSyntax = attr.ArgumentList.Arguments.First(); 71 | var mappedTypeArgSyntaxExpr = mappedTypeArgSyntax.Expression.NormalizeWhitespace().ToFullString(); 72 | 73 | var sourceClassName = GetContentInParentheses(mappedTypeArgSyntaxExpr); 74 | var sourceClassSyntax = allClasses.First(x => x.Identifier.ToString() == sourceClassName); 75 | var sourceClassModel = compilation.GetSemanticModel(sourceClassSyntax.SyntaxTree); 76 | var sourceClassNamedTypeSymbol = ModelExtensions.GetDeclaredSymbol(sourceClassModel, sourceClassSyntax); 77 | var sourceClassFullName = sourceClassNamedTypeSymbol.OriginalDefinition.ToString(); 78 | var sourceClassProperties = sourceClassSyntax.GetProperties(sourceClassModel); 79 | 80 | #endregion 81 | 82 | #region Get Mapping Target Class Info 83 | 84 | var targetClassSyntax = attr.SyntaxTree.GetRoot().DescendantNodes().OfType().Last(); 85 | var targetClassModel = compilation.GetSemanticModel(attr.SyntaxTree); 86 | var targetClassNamedTypeSymbol = ModelExtensions.GetDeclaredSymbol(targetClassModel, targetClassSyntax); 87 | var targetClassFullName = targetClassNamedTypeSymbol.OriginalDefinition.ToString(); 88 | var targetClassName = targetClassFullName.Split('.').Last(); 89 | var targetClassProperties = targetClassSyntax.GetProperties(targetClassModel); 90 | 91 | #endregion 92 | 93 | #region Create diagnostic erroes if any property of target doesn't match to source properties. 94 | 95 | //source class properties should match all of target class properties 96 | //should use same name and type of property 97 | var targetPropertiesMatchedResult = targetClassProperties.Select(target => new { 98 | TargetPropertyName = target.propertyName, 99 | TargetPropertySyntax = target.propertySyntax, 100 | IsMatched = sourceClassProperties.Any(source => 101 | source.propertyName == target.propertyName && 102 | source.propertyType == target.propertyType) 103 | }); 104 | if (targetPropertiesMatchedResult.Any(x => x.IsMatched == false)) 105 | { 106 | foreach (var target in targetPropertiesMatchedResult.Where(x => x.IsMatched == false)) 107 | { 108 | var diagnosticDescriptor = new DiagnosticDescriptor("MPERR001", "Property mapping error", 109 | $"{targetClassName}.{target.TargetPropertyName} couldn't match to {sourceClassName}, please check if the name and type of properties are the same.", "source generator", 110 | DiagnosticSeverity.Error, true); 111 | var diagnostic = Diagnostic.Create(diagnosticDescriptor, target.TargetPropertySyntax.GetLocation()); 112 | context.ReportDiagnostic(diagnostic); 113 | } 114 | break; 115 | } 116 | 117 | #endregion 118 | 119 | #region Build mapper method 120 | 121 | sourceBuilder.Append(@$" 122 | public static {targetClassFullName} MapTo{targetClassName}({sourceClassFullName} source) 123 | {{ 124 | var target = new {targetClassFullName}();"); 125 | 126 | foreach (var (_, propertyName, _) in targetClassProperties) 127 | { 128 | sourceBuilder.Append(@$" 129 | target.{propertyName} = source.{propertyName};"); 130 | } 131 | 132 | 133 | sourceBuilder.Append(@" 134 | return target; 135 | } 136 | "); 137 | 138 | 139 | sourceBuilder.Append(@$" 140 | public static {targetClassFullName} To{targetClassName}(this {sourceClassFullName} source) 141 | {{ 142 | var target = new {targetClassFullName}();"); 143 | 144 | foreach (var (_, propertyName, _) in targetClassProperties) 145 | { 146 | sourceBuilder.Append(@$" 147 | target.{propertyName} = source.{propertyName};"); 148 | } 149 | 150 | 151 | sourceBuilder.Append(@" 152 | return target; 153 | } 154 | "); 155 | 156 | #endregion 157 | 158 | } 159 | sourceBuilder.Append(@" 160 | } 161 | }"); 162 | 163 | context.AddSource("Mapper", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); 164 | 165 | } 166 | 167 | private string GetContentInParentheses(string value) 168 | { 169 | var match = Regex.Match(value, @"\(([^)]*)\)"); 170 | return match.Groups[1].Value; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /MapperGenerator/MapperGenerator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | latest 5 | true 6 | false 7 | 0.0.2 8 | Roberson Liou 9 | A sample mapper created by source generator for .NET 10 | MIT 11 | https://github.com/robersonliou/MapperGenerator 12 | https://github.com/robersonliou/MapperGenerator 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![MapperGenerator-CI](https://github.com/robersonliou/MapperGenerator/workflows/MapperGenerator-CI/badge.svg) 2 | [![Nuget (with prereleases)](https://img.shields.io/nuget/vpre/MapperGenerator)](https://www.nuget.org/packages/MapperGenerator) 3 | # MapperGenerator 4 | 5 | A sample mapper using source generator for .NET 6 | 7 | ## Nuget 8 | #### URL 9 | https://www.nuget.org/packages/MapperGenerator/ 10 | 11 | #### Add package refernece to `.csproj` 12 | ``` xml 13 | 14 | ``` 15 | 16 | ## Getting Started 17 | 18 | #### 1. Prepare two classes for mapping. Here we create a `PersonEntity` and a `PersonViewModel`. 19 | 20 | _PersonEntity.cs_ 21 | ```csharp 22 | public class PersonEntity 23 | { 24 | public int Id { get; set; } 25 | public string Name { get; set; } 26 | } 27 | ``` 28 | 29 | _PersonViewModel.cs_ 30 | ```csharp 31 | public class PersonViewModel 32 | { 33 | public int Id { get; set; } 34 | public string Name { get; set; } 35 | } 36 | ``` 37 | 38 | > For now we only support fully matched mapping, you need to define same type and naming of properties between two related classes. 39 | 40 | #### 2. Plug `MappingAttribute` to the mapping target. 41 | 42 | _PersonViewModel.cs_ 43 | ```csharp 44 | [Mapping(typeof(PersonEntity))] 45 | public class PersonViewModel 46 | { 47 | public int Id { get; set; } 48 | public string Name { get; set; } 49 | } 50 | ``` 51 | 52 | #### 3. Add namespace `MapperGenerator` to the entry class (ex: `Program.cs/Main` ) 53 | 54 | #### 4. Then source generator will automatically generate `Mapper.cs` which inculded two mapping method. 55 | 56 | _Mapper.cs_ 57 | ```csharp 58 | // 59 | using System; 60 | namespace MapperGenerator 61 | { 62 | public static class Mapper 63 | { 64 | public static MyConsumedApp.Models.PersonViewModel MapToPersonViewModel(MyConsumedApp.Entities.Person source) 65 | { 66 | var target = new MyConsumedApp.Models.PersonViewModel(); 67 | target.Id = source.Id; 68 | target.Name = source.Name; 69 | return target; 70 | } 71 | 72 | public static MyConsumedApp.Models.PersonViewModel ToPersonViewModel(this MyConsumedApp.Entities.Person source) 73 | { 74 | var target = new MyConsumedApp.Models.PersonViewModel(); 75 | target.Id = source.Id; 76 | target.Name = source.Name; 77 | return target; 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | #### 5. Test result 84 | ```csharp 85 | var personEntity = new Person 86 | { 87 | Id = 1, 88 | Name = "Roberson" 89 | }; 90 | 91 | //static mapping method. 92 | var vm1 = Mapper.MapToPersonViewModel(personEntity); 93 | 94 | //extension method. 95 | var vm2 = personEntity.ToPersonViewModel(); 96 | ``` 97 | 98 | ## Welcome to feedback 99 | 100 | Currently it's just a prototype mapper sample created by source generator, welcome to contribute or giving any sugesstion. 101 | --------------------------------------------------------------------------------