diff --git a/src/MapTo/MapToGenerator.cs b/src/MapTo/MapToGenerator.cs
index 56a2358..1d9c71a 100644
--- a/src/MapTo/MapToGenerator.cs
+++ b/src/MapTo/MapToGenerator.cs
@@ -20,8 +20,9 @@ namespace MapTo
///
public void Execute(GeneratorExecutionContext context)
{
- AddMapFromAttribute(context);
-
+ AddAttribute(context, SourceBuilder.GenerateMapFromAttribute());
+ AddAttribute(context, SourceBuilder.GenerateIgnorePropertyAttribute());
+
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any())
{
AddGeneratedMappingsClasses(context, receiver.CandidateClasses);
@@ -44,11 +45,8 @@ namespace MapTo
}
}
- private static void AddMapFromAttribute(GeneratorExecutionContext context)
- {
- var (source, hintName) = SourceBuilder.GenerateMapFromAttribute();
- context.AddSource(hintName, source);
- }
+ private static void AddAttribute(GeneratorExecutionContext context, (string source, string hintName) attribute)
+ => context.AddSource(attribute.hintName, attribute.source);
private static INamedTypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classSyntax, SemanticModel model)
{
@@ -104,7 +102,10 @@ namespace MapTo
return sourceTypeSymbol
.GetAllMembersOfType()
.Select(p => p.Name)
- .Intersect(classSymbol.GetAllMembersOfType().Select(p => p.Name))
+ .Intersect(classSymbol
+ .GetAllMembersOfType()
+ .Where(p => p.GetAttributes().All(a => a.AttributeClass?.Name != SourceBuilder.IgnorePropertyAttributeName))
+ .Select(p => p.Name))
.ToImmutableArray();
}
}
diff --git a/src/MapTo/SourceBuilder.cs b/src/MapTo/SourceBuilder.cs
index fcefe5a..8c3f4df 100644
--- a/src/MapTo/SourceBuilder.cs
+++ b/src/MapTo/SourceBuilder.cs
@@ -1,10 +1,7 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
+using System.Linq;
using System.Text;
using MapTo.Extensions;
using MapTo.Models;
-using Microsoft.CodeAnalysis;
namespace MapTo
{
@@ -12,33 +9,49 @@ namespace MapTo
{
internal const string NamespaceName = "MapTo";
internal const string MapFromAttributeName = "MapFrom";
- private const int Indent1 = 4; //" ";
- private const int Indent2 = Indent1 * 2; // " ";
- private const int Indent3 = Indent1 * 3; // " ";
+ internal const string IgnorePropertyAttributeName = "IgnoreProperty";
+ internal const string GeneratedFilesHeader = "// ";
+
+ private const int Indent1 = 4;
+ private const int Indent2 = Indent1 * 2;
+ private const int Indent3 = Indent1 * 3;
internal static (string source, string hintName) GenerateMapFromAttribute()
{
- const string source = @"
+ var source = $@"{GeneratedFilesHeader}
using System;
namespace MapTo
-{
+{{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class MapFromAttribute : Attribute
- {
- public MapFromAttribute(Type sourceType)
- {
+ public sealed class {MapFromAttributeName}Attribute : Attribute
+ {{
+ public {MapFromAttributeName}Attribute(Type sourceType)
+ {{
SourceType = sourceType;
- }
+ }}
- public Type SourceType { get; }
- }
-}
-";
+ public Type SourceType {{ get; }}
+ }}
+}}";
return (source, $"{MapFromAttributeName}Attribute.g.cs");
}
+ internal static (string source, string hintName) GenerateIgnorePropertyAttribute()
+ {
+ var source = $@"{GeneratedFilesHeader}
+using System;
+
+namespace MapTo
+{{
+ [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
+ public sealed class {IgnorePropertyAttributeName}Attribute : Attribute {{ }}
+}}";
+
+ return (source, $"{IgnorePropertyAttributeName}Attribute.g.cs");
+ }
+
internal static (string source, string hintName) GenerateSource(MapModel model)
{
var builder = new StringBuilder();
@@ -141,7 +154,6 @@ namespace MapTo
}
private static StringBuilder AppendFileHeader(this StringBuilder builder) =>
- builder
- .AppendLine("// ");
+ builder.AppendLine(GeneratedFilesHeader);
}
}
\ No newline at end of file
diff --git a/test/MapTo.Tests/CompilerServices/IsExternalInit.cs b/test/MapTo.Tests/CompilerServices/IsExternalInit.cs
new file mode 100644
index 0000000..e750e2f
--- /dev/null
+++ b/test/MapTo.Tests/CompilerServices/IsExternalInit.cs
@@ -0,0 +1,16 @@
+// ReSharper disable UnusedType.Global
+// ReSharper disable CheckNamespace
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.ComponentModel;
+
+namespace System.Runtime.CompilerServices
+{
+ ///
+ /// Reserved to be used by the compiler for tracking metadata.
+ /// This class should not be used by developers in source code.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ internal static class IsExternalInit { }
+}
\ No newline at end of file
diff --git a/test/MapTo.Tests/Tests.cs b/test/MapTo.Tests/Tests.cs
index aa5423e..0f4499a 100644
--- a/test/MapTo.Tests/Tests.cs
+++ b/test/MapTo.Tests/Tests.cs
@@ -1,5 +1,7 @@
+using System;
using System.Linq;
using System.Text;
+using MapTo.Extensions;
using MapToTests;
using Microsoft.CodeAnalysis;
using Shouldly;
@@ -10,6 +12,10 @@ namespace MapTo.Tests
{
public class Tests
{
+ private const int Indent1 = 4;
+ private const int Indent2 = Indent1 * 2;
+ private const int Indent3 = Indent1 * 3;
+
public Tests(ITestOutputHelper output)
{
_output = output;
@@ -17,54 +23,100 @@ namespace MapTo.Tests
private readonly ITestOutputHelper _output;
- private const string ExpectedAttribute = @"
+ private static readonly string ExpectedAttribute = $@"{SourceBuilder.GeneratedFilesHeader}
using System;
namespace MapTo
-{
+{{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class MapFromAttribute : Attribute
- {
+ {{
public MapFromAttribute(Type sourceType)
- {
+ {{
SourceType = sourceType;
- }
+ }}
- public Type SourceType { get; }
- }
-}
-";
+ public Type SourceType {{ get; }}
+ }}
+}}";
- private static string GetSourceText(bool includeAttributeNamespace = false, string sourceClassNamespace = "Test.Models")
+ private record SourceGeneratorOptions(
+ bool UseMapToNamespace = false,
+ string SourceClassNamespace = "Test.Models",
+ int ClassPropertiesCount = 3,
+ int SourceClassPropertiesCount = 3,
+ Action PropertyBuilder = null,
+ Action SourcePropertyBuilder = null);
+
+ private static string GetSourceText(SourceGeneratorOptions options = null)
{
+ const string ns = "Test";
+ options ??= new SourceGeneratorOptions();
+ var hasDifferentSourceNamespace = options.SourceClassNamespace != ns;
var builder = new StringBuilder();
- builder.AppendLine($@"
-{(includeAttributeNamespace ? string.Empty : "using MapTo;")}
-namespace Test
-{{
- {(sourceClassNamespace != "Test" && !includeAttributeNamespace ? $"using {sourceClassNamespace};" : string.Empty)}
+
+ if (options.UseMapToNamespace)
+ {
+ builder.AppendFormat("using {0};", SourceBuilder.NamespaceName).AppendLine();
+ }
- {(includeAttributeNamespace ? "[MapTo.MapFrom(typeof(Baz))]" : "[MapFrom(typeof(Baz))]")}
- public partial class Foo
- {{
- public int Prop1 {{ get; set; }}
- public int Prop2 {{ get; }}
- public int Prop3 {{ get; }}
- }}
-}}
-");
+ builder
+ .AppendFormat("using {0};", options.SourceClassNamespace)
+ .AppendLine()
+ .AppendLine();
- builder.AppendLine($@"
-namespace {sourceClassNamespace}
-{{
- public class Baz
- {{
- public int Prop1 {{ get; set; }}
- public int Prop2 {{ get; }}
- public int Prop3 {{ get; set; }}
- }}
-}}
-");
+ builder
+ .AppendFormat("namespace {0}", ns)
+ .AppendOpeningBracket();
+
+ if (hasDifferentSourceNamespace && options.UseMapToNamespace)
+ {
+ builder
+ .PadLeft(Indent1)
+ .AppendFormat("using {0};", options.SourceClassNamespace)
+ .AppendLine()
+ .AppendLine();
+ }
+
+ builder
+ .PadLeft(Indent1)
+ .AppendLine(options.UseMapToNamespace ? "[MapTo.MapFrom(typeof(Baz))]" : "[MapFrom(typeof(Baz))]")
+ .PadLeft(Indent1).Append("public partial class Foo")
+ .AppendOpeningBracket(Indent1);
+
+ for (var i = 1; i <= options.ClassPropertiesCount; i++)
+ {
+ builder
+ .PadLeft(Indent2)
+ .AppendLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}");
+ }
+
+ options.PropertyBuilder?.Invoke(builder);
+
+ builder
+ .AppendClosingBracket(Indent1, padNewLine: false)
+ .AppendClosingBracket()
+ .AppendLine()
+ .AppendLine();
+
+ builder
+ .AppendFormat("namespace {0}", options.SourceClassNamespace)
+ .AppendOpeningBracket()
+ .PadLeft(Indent1).Append("public class Baz")
+ .AppendOpeningBracket(Indent1);
+
+ for (var i = 1; i <= options.SourceClassPropertiesCount; i++)
+ {
+ builder
+ .PadLeft(Indent2)
+ .AppendLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}");
+ }
+
+ options.SourcePropertyBuilder?.Invoke(builder);
+
+ builder
+ .AppendClosingBracket(Indent1, padNewLine: false)
+ .AppendClosingBracket();
return builder.ToString();
}
@@ -126,7 +178,6 @@ namespace Test
// Assert
diagnostics.ShouldBeSuccessful();
- compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
}
@@ -190,7 +241,6 @@ namespace Test
// Assert
diagnostics.ShouldBeSuccessful();
- compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
}
@@ -205,15 +255,18 @@ namespace Test
// Assert
diagnostics.ShouldBeSuccessful();
- compilation.SyntaxTrees.ShouldContain(s => s.ToString() == ExpectedAttribute);
- compilation.SyntaxTrees.Select(s => s.ToString()).Where(s => s != string.Empty && s != ExpectedAttribute).ShouldBeEmpty();
+ compilation.SyntaxTrees
+ .Select(s => s.ToString())
+ .Where(s => !string.IsNullOrWhiteSpace(s.ToString()))
+ .All(s => s.Contains(": Attribute"))
+ .ShouldBeTrue();
}
[Fact]
public void When_SourceTypeHasDifferentNamespace_Should_NotAddToUsings()
{
// Arrange
- var source = GetSourceText(sourceClassNamespace: "Bazaar");
+ var source = GetSourceText(new SourceGeneratorOptions(SourceClassNamespace: "Bazaar"));
const string expectedResult = @"
//
@@ -227,7 +280,6 @@ namespace Test
// Assert
diagnostics.ShouldBeSuccessful();
- compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
}
@@ -255,7 +307,6 @@ namespace Test
// Assert
diagnostics.ShouldBeSuccessful();
- compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim());
}
@@ -277,7 +328,6 @@ namespace Test
// Assert
diagnostics.ShouldBeSuccessful();
- compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim());
}
@@ -302,8 +352,66 @@ namespace Test
// Assert
diagnostics.ShouldBeSuccessful();
- compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim());
}
+
+ [Fact]
+ public void VerifyIgnorePropertyAttribute()
+ {
+ // Arrange
+ const string source = "";
+ var expectedAttribute = $@"
+{SourceBuilder.GeneratedFilesHeader}
+using System;
+
+namespace MapTo
+{{
+ [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
+ public sealed class IgnorePropertyAttribute : Attribute {{ }}
+}}
+".Trim();
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.ShouldContain(c => c.ToString() == expectedAttribute);
+ }
+
+ [Fact]
+ public void When_IgnorePropertyAttributeIsSpecified_Should_NotGenerateMappingsForThatProperty()
+ {
+ // Arrange
+ var source = GetSourceText(new SourceGeneratorOptions(
+ UseMapToNamespace: true,
+ PropertyBuilder: builder =>
+ {
+ builder
+ .PadLeft(Indent2).AppendLine("[IgnoreProperty]")
+ .PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }");
+ },
+ SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }")));
+
+ var expectedResult = @"
+ public partial class Foo
+ {
+ public Foo(Test.Models.Baz baz)
+ {
+ if (baz == null) throw new ArgumentNullException(nameof(baz));
+
+ Prop1 = baz.Prop1;
+ Prop2 = baz.Prop2;
+ Prop3 = baz.Prop3;
+ }
+".Trim();
+
+ // Act
+ var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
+
+ // Assert
+ diagnostics.ShouldBeSuccessful();
+ compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult);
+ }
}
}
\ No newline at end of file
diff --git a/test/TestConsoleApp/ViewModels/UserViewModel.cs b/test/TestConsoleApp/ViewModels/UserViewModel.cs
index 0b56b07..404c759 100644
--- a/test/TestConsoleApp/ViewModels/UserViewModel.cs
+++ b/test/TestConsoleApp/ViewModels/UserViewModel.cs
@@ -7,6 +7,7 @@ namespace TestConsoleApp.ViewModels
{
public string FirstName { get; }
+ // [IgnoreProerty]
public string LastName { get; }
}
}
\ No newline at end of file