Add MapProperty attribute. Makes it possible to map to a different property.
This commit is contained in:
parent
f7a755332e
commit
acd98e72ff
|
@ -25,7 +25,8 @@ namespace MapTo
|
||||||
.AddSource(ref context, MapFromAttributeSource.Generate(options))
|
.AddSource(ref context, MapFromAttributeSource.Generate(options))
|
||||||
.AddSource(ref context, IgnorePropertyAttributeSource.Generate(options))
|
.AddSource(ref context, IgnorePropertyAttributeSource.Generate(options))
|
||||||
.AddSource(ref context, ITypeConverterSource.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())
|
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any())
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,19 +24,24 @@ namespace MapTo
|
||||||
|
|
||||||
TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataName(ITypeConverterSource.FullyQualifiedName)
|
TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataName(ITypeConverterSource.FullyQualifiedName)
|
||||||
?? throw new TypeLoadException($"Unable to find '{ITypeConverterSource.FullyQualifiedName}' type.");
|
?? 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; }
|
private Compilation Compilation { get; }
|
||||||
|
|
||||||
public INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; }
|
|
||||||
|
|
||||||
public INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; }
|
|
||||||
|
|
||||||
public MappingModel? Model { get; private set; }
|
public MappingModel? Model { get; private set; }
|
||||||
|
|
||||||
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
|
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
|
||||||
|
|
||||||
public INamedTypeSymbol IgnorePropertyAttributeTypeSymbol { get; }
|
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)
|
internal static MappingContext Create(Compilation compilation, ClassDeclarationSyntax classSyntax, SourceGenerationOptions sourceGenerationOptions)
|
||||||
{
|
{
|
||||||
|
@ -102,7 +107,7 @@ namespace MapTo
|
||||||
|
|
||||||
foreach (var property in classProperties)
|
foreach (var property in classProperties)
|
||||||
{
|
{
|
||||||
var sourceProperty = sourceProperties.FindProperty(property);
|
var sourceProperty = FindSourceProperty(context, sourceProperties, property);
|
||||||
if (sourceProperty is null)
|
if (sourceProperty is null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
@ -138,12 +143,23 @@ namespace MapTo
|
||||||
converterParameters.AddRange(GetTypeConverterParameters(typeConverterAttribute));
|
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();
|
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)
|
private static INamedTypeSymbol? GetTypeConverterBaseInterface(MappingContext context, ITypeSymbol converterTypeSymbol, IPropertySymbol property, IPropertySymbol sourceProperty)
|
||||||
{
|
{
|
||||||
return converterTypeSymbol.AllInterfaces
|
return converterTypeSymbol.AllInterfaces
|
||||||
|
|
|
@ -7,7 +7,11 @@ namespace MapTo
|
||||||
{
|
{
|
||||||
internal record SourceCode(string Text, string HintName);
|
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 (
|
internal record MappingModel (
|
||||||
SourceGenerationOptions Options,
|
SourceGenerationOptions Options,
|
||||||
|
|
|
@ -70,7 +70,7 @@ namespace MapTo.Sources
|
||||||
{
|
{
|
||||||
if (property.TypeConverter is null)
|
if (property.TypeConverter is null)
|
||||||
{
|
{
|
||||||
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.Name};");
|
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -384,7 +384,13 @@ namespace Test
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
const string source = "";
|
const string source = "";
|
||||||
var expectedTypes = new[] { IgnorePropertyAttributeSource.AttributeName, MapFromAttributeSource.AttributeName, ITypeConverterSource.InterfaceName };
|
var expectedTypes = new[]
|
||||||
|
{
|
||||||
|
IgnorePropertyAttributeSource.AttributeName,
|
||||||
|
MapFromAttributeSource.AttributeName,
|
||||||
|
ITypeConverterSource.InterfaceName,
|
||||||
|
MapPropertyAttributeSource.AttributeName
|
||||||
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
|
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
|
||||||
|
@ -542,7 +548,7 @@ namespace MapTo
|
||||||
diagnostics.ShouldBeSuccessful();
|
diagnostics.ShouldBeSuccessful();
|
||||||
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
|
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VerifyMapTypeConverterAttribute()
|
public void VerifyMapTypeConverterAttribute()
|
||||||
{
|
{
|
||||||
|
@ -578,7 +584,7 @@ namespace MapTo
|
||||||
diagnostics.ShouldBeSuccessful();
|
diagnostics.ShouldBeSuccessful();
|
||||||
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
|
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VerifyMapTypeConverterAttributeWithNullableOptionOn()
|
public void VerifyMapTypeConverterAttributeWithNullableOptionOn()
|
||||||
{
|
{
|
||||||
|
@ -759,6 +765,73 @@ namespace Test
|
||||||
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax);
|
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")
|
private static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo")
|
||||||
{
|
{
|
||||||
return syntaxTree.GetRoot()
|
return syntaxTree.GetRoot()
|
||||||
|
|
Loading…
Reference in New Issue