Add MapProperty attribute. Makes it possible to map to a different property.

This commit is contained in:
Mohammadreza Taikandi 2021-01-29 08:28:43 +00:00
parent f7a755332e
commit acd98e72ff
6 changed files with 166 additions and 12 deletions

View File

@ -25,7 +25,8 @@ namespace MapTo
.AddSource(ref context, MapFromAttributeSource.Generate(options))
.AddSource(ref context, IgnorePropertyAttributeSource.Generate(options))
.AddSource(ref context, ITypeConverterSource.Generate(options))
.AddSource(ref context, MapTypeConverterAttributeSource.Generate(options));
.AddSource(ref context, MapTypeConverterAttributeSource.Generate(options))
.AddSource(ref context, MapPropertyAttributeSource.Generate(options));
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any())
{

View File

@ -24,19 +24,24 @@ namespace MapTo
TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataName(ITypeConverterSource.FullyQualifiedName)
?? throw new TypeLoadException($"Unable to find '{ITypeConverterSource.FullyQualifiedName}' type.");
MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataName(MapPropertyAttributeSource.FullyQualifiedName)
?? throw new TypeLoadException($"Unable to find '{MapPropertyAttributeSource.FullyQualifiedName}' type.");
}
private Compilation Compilation { get; }
public INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; }
public INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; }
public MappingModel? Model { get; private set; }
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
public INamedTypeSymbol IgnorePropertyAttributeTypeSymbol { get; }
public INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; }
public INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; }
public INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; }
internal static MappingContext Create(Compilation compilation, ClassDeclarationSyntax classSyntax, SourceGenerationOptions sourceGenerationOptions)
{
@ -102,7 +107,7 @@ namespace MapTo
foreach (var property in classProperties)
{
var sourceProperty = sourceProperties.FindProperty(property);
var sourceProperty = FindSourceProperty(context, sourceProperties, property);
if (sourceProperty is null)
{
continue;
@ -138,12 +143,23 @@ namespace MapTo
converterParameters.AddRange(GetTypeConverterParameters(typeConverterAttribute));
}
mappedProperties.Add(new MappedProperty(property.Name, converterFullyQualifiedName, converterParameters.ToImmutableArray()));
mappedProperties.Add(new MappedProperty(property.Name, converterFullyQualifiedName, converterParameters.ToImmutableArray(), sourceProperty.Name));
}
return mappedProperties.ToImmutableArray();
}
private static IPropertySymbol? FindSourceProperty(MappingContext context, IEnumerable<IPropertySymbol> sourceProperties, IPropertySymbol property)
{
var propertyName = property
.GetAttribute(context.MapPropertyAttributeTypeSymbol)
?.NamedArguments
.SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
.Value.Value as string ?? property.Name;
return sourceProperties.SingleOrDefault(p => p.Name == propertyName);
}
private static INamedTypeSymbol? GetTypeConverterBaseInterface(MappingContext context, ITypeSymbol converterTypeSymbol, IPropertySymbol property, IPropertySymbol sourceProperty)
{
return converterTypeSymbol.AllInterfaces

View File

@ -7,7 +7,11 @@ namespace MapTo
{
internal record SourceCode(string Text, string HintName);
internal record MappedProperty(string Name, string? TypeConverter, ImmutableArray<string> TypeConverterParameters);
internal record MappedProperty(
string Name,
string? TypeConverter,
ImmutableArray<string> TypeConverterParameters,
string SourcePropertyName);
internal record MappingModel (
SourceGenerationOptions Options,

View File

@ -70,7 +70,7 @@ namespace MapTo.Sources
{
if (property.TypeConverter is null)
{
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.Name};");
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};");
}
else
{

View File

@ -0,0 +1,60 @@
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapPropertyAttributeSource
{
internal const string AttributeName = "MapProperty";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal const string SourcePropertyNamePropertyName = "SourcePropertyName";
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine()
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies the mapping behavior of annotated property.")
.WriteLine("/// </summary>")
.WriteLine("/// <remarks>")
.WriteLine($"/// {AttributeClassName} has a number of uses:")
.WriteLine("/// <list type=\"bullet\">")
.WriteLine("/// <item><description>By default properties with same name will get mapped. This attribute allows the names to be different.</description></item>")
.WriteLine("/// <item><description>Indicates that a property should be mapped when member serialization is set to opt-in.</description></item>")
.WriteLine("/// </list>")
.WriteLine("/// </remarks>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Gets or sets the property name of the object to mapping from.")
.WriteLine("/// </summary>");
}
builder
.WriteLine($"public string{options.NullableReferenceSyntax} {SourcePropertyNamePropertyName} {{ get; set; }}")
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
}
}
}

View File

@ -384,7 +384,13 @@ namespace Test
{
// Arrange
const string source = "";
var expectedTypes = new[] { IgnorePropertyAttributeSource.AttributeName, MapFromAttributeSource.AttributeName, ITypeConverterSource.InterfaceName };
var expectedTypes = new[]
{
IgnorePropertyAttributeSource.AttributeName,
MapFromAttributeSource.AttributeName,
ITypeConverterSource.InterfaceName,
MapPropertyAttributeSource.AttributeName
};
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
@ -542,7 +548,7 @@ namespace MapTo
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
}
[Fact]
public void VerifyMapTypeConverterAttribute()
{
@ -578,7 +584,7 @@ namespace MapTo
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void VerifyMapTypeConverterAttributeWithNullableOptionOn()
{
@ -759,6 +765,73 @@ namespace Test
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax);
}
[Theory]
[InlineData(NullableContextOptions.Disable)]
[InlineData(NullableContextOptions.Enable)]
public void VerifyMapPropertyAttribute(NullableContextOptions nullableContextOptions)
{
// Arrange
const string source = "";
var nullableSyntax = nullableContextOptions == NullableContextOptions.Enable ? "?" : string.Empty;
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
{(nullableContextOptions == NullableContextOptions.Enable ? $"#nullable enable{Environment.NewLine}": string.Empty)}
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public sealed class MapPropertyAttribute : Attribute
{{
public string{nullableSyntax} SourcePropertyName {{ get; set; }}
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: nullableContextOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void When_MapPropertyFound_Should_UseItToMapToSourceProperty()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.PadLeft(Indent2).AppendLine("[MapProperty(SourcePropertyName = nameof(Baz.Prop3))]")
.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }")));
var expectedResult = @"
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;
Prop4 = baz.Prop3;
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult);
}
private static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo")
{
return syntaxTree.GetRoot()