Compare commits

...

3 Commits

Author SHA1 Message Date
code liturgy c1dd14a38d bump mapto 2022-11-18 17:40:10 +00:00
code liturgy c9b2b5a652 prepare for nuget publish 2022-11-18 16:40:29 +00:00
Wvader efdd3ada94 k 2021-12-11 21:06:58 +00:00
46 changed files with 146 additions and 2940 deletions

View File

@ -2,12 +2,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo", "src\MapTo\MapTo.csproj", "{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo", "src\MapTo\MapTo.csproj", "{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapToTests", "test\MapTo.Tests\MapTo.Tests.csproj", "{797DA57B-AC7E-468B-8799-44C5A574C0E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsoleApp", "test\TestConsoleApp\TestConsoleApp.csproj", "{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo.Integration.Tests", "test\MapTo.Integration.Tests\MapTo.Integration.Tests.csproj", "{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU

View File

@ -11,26 +11,25 @@ namespace MapTo
internal ClassMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) internal ClassMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { } : base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedProperty> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) protected override ImmutableArray<MappedProperty> GetSourceMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{ {
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray(); var sourceProperties = sourceTypeSymbol.GetAllMembers().ToArray();
return typeSymbol return typeSymbol
.GetAllMembers(!isInheritFromMappedBaseClass) .GetAllMembers(!isInheritFromMappedBaseClass)
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property)) .Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null) .Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!; .ToImmutableArray()!;
} }
protected override ImmutableArray<MappedProperty> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) protected override ImmutableArray<MappedProperty> GetTypeMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{ {
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray(); var sourceProperties = sourceTypeSymbol.GetAllMembers().ToArray();
return typeSymbol return typeSymbol
.GetAllMembers() .GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapProperty(typeSymbol, sourceProperties, property)) .Select(property => MapProperty(typeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null) .Where(mappedProperty => mappedProperty is not null)

View File

@ -27,8 +27,12 @@ namespace MapTo
internal static Diagnostic NoMatchingPropertyTypeFoundError(ISymbol property) => internal static Diagnostic NoMatchingPropertyTypeFoundError(ISymbol property) =>
Create($"{ErrorId}031", property.Locations.FirstOrDefault(), $"Cannot create a map for '{property.ToDisplayString()}' property because source and destination types are not implicitly convertible. Consider using '{MapTypeConverterAttributeSource.FullyQualifiedName}' to provide a type converter or ignore the property using '{IgnorePropertyAttributeSource.FullyQualifiedName}'."); Create($"{ErrorId}031", property.Locations.FirstOrDefault(), $"Cannot create a map for '{property.ToDisplayString()}' property because source and destination types are not implicitly convertible. Consider using '{MapTypeConverterAttributeSource.FullyQualifiedName}' to provide a type converter or ignore the property using '{IgnorePropertyAttributeSource.FullyQualifiedName}'.");
internal static Diagnostic InvalidTypeConverterGenericTypesError(ISymbol property, IPropertySymbol sourceProperty) => internal static Diagnostic InvalidTypeConverterGenericTypesError(ISymbol property, ISymbol memberSymbol)
Create($"{ErrorId}032", property.Locations.FirstOrDefault(), $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{ITypeConverterSource.InterfaceName}<{sourceProperty.Type.ToDisplayString()}, {property.GetTypeSymbol()?.ToDisplayString()}>'."); {
return Create($"{ErrorId}032", property.Locations.FirstOrDefault(), $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{ITypeConverterSource.InterfaceName}<{memberSymbol.ToString()}, {property.GetTypeSymbol()?.ToDisplayString()}>'.");
}
internal static Diagnostic ConfigurationParseError(string error) => internal static Diagnostic ConfigurationParseError(string error) =>
Create($"{ErrorId}040", Location.None, error); Create($"{ErrorId}040", Location.None, error);

View File

@ -88,7 +88,7 @@ namespace MapTo.Extensions
return builder.WriteClosingBracket(); return builder.WriteClosingBracket();
} }
private static bool IsMappedProperty(this System.Collections.Immutable.ImmutableArray<MappedProperty> properties, MappedProperty property) { private static bool IsMappedProperty(this ImmutableArray<MappedProperty> properties, MappedProperty property) {
foreach(var prop in properties) foreach(var prop in properties)
{ {
@ -98,7 +98,7 @@ namespace MapTo.Extensions
return false; return false;
} }
private static SourceBuilder TryWriteProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray<MappedProperty> properties, System.Collections.Immutable.ImmutableArray<MappedProperty>? otherProperties, private static SourceBuilder TryWriteProperties(this SourceBuilder builder, ImmutableArray<MappedProperty> properties, System.Collections.Immutable.ImmutableArray<MappedProperty>? otherProperties,
string? sourceClassParameterName, string mappingContextParameterName, bool fromUpdate) string? sourceClassParameterName, string mappingContextParameterName, bool fromUpdate)
{ {
if (fromUpdate) if (fromUpdate)

View File

@ -67,6 +67,9 @@ namespace MapTo.Extensions
case IPropertySymbol propertySymbol: case IPropertySymbol propertySymbol:
typeSymbol = propertySymbol.Type; typeSymbol = propertySymbol.Type;
return true; return true;
case IFieldSymbol fieldSymbol:
typeSymbol = fieldSymbol.Type;
return true;
case IParameterSymbol parameterSymbol: case IParameterSymbol parameterSymbol:
typeSymbol = parameterSymbol.Type; typeSymbol = parameterSymbol.Type;

View File

@ -4,18 +4,17 @@
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<AssemblyName>MapTo</AssemblyName> <AssemblyName>MapTo</AssemblyName>
<Description>An object to object mapping generator using Roslyn source generator.</Description> <Description>An object to object mapping generator using Roslyn source generator.</Description>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
<PackageId>MapTo</PackageId> <PackageId>MapTo</PackageId>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/mrtaikandi/mapto</PackageProjectUrl> <PackageProjectUrl>https://git.codeliturgy.com/P0/MapTo</PackageProjectUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageVersion>$(Version)</PackageVersion> <PackageVersion>$(Version)</PackageVersion>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<RepositoryUrl>https://github.com/mrtaikandi/mapto</RepositoryUrl> <RepositoryUrl>https://git.codeliturgy.com/P0/MapTo</RepositoryUrl>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<RootNamespace>MapTo</RootNamespace> <RootNamespace>MapTo</RootNamespace>
</PropertyGroup> </PropertyGroup>
@ -36,6 +35,9 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
<PackageReference Update="Nerdbank.GitVersioning">
<Version>3.6.65-alpha</Version>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -34,6 +34,8 @@ namespace MapTo
MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName); MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName);
MapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName); MapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName);
UseUpdateAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(UseUpdateAttributeSource.FullyQualifiedName); UseUpdateAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(UseUpdateAttributeSource.FullyQualifiedName);
JsonExtensionAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(UseUpdateAttributeSource.FullyQualifiedName);
MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName); MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis"); AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
@ -51,6 +53,8 @@ namespace MapTo
protected INamedTypeSymbol UseUpdateAttributeTypeSymbol { get; } protected INamedTypeSymbol UseUpdateAttributeTypeSymbol { get; }
protected INamedTypeSymbol JsonExtensionAttributeTypeSymbol { get; }
protected INamedTypeSymbol MappingContextTypeSymbol { get; } protected INamedTypeSymbol MappingContextTypeSymbol { get; }
protected INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; } protected INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; }
@ -99,19 +103,31 @@ namespace MapTo
} }
} }
protected IPropertySymbol? FindSourceProperty(IEnumerable<IPropertySymbol> sourceProperties, ISymbol property) protected ISymbol? FindSourceProperty(IEnumerable<ISymbol> sourceMembers, ISymbol member)
{ {
var propertyName = property var propertyName = member
.GetAttribute(MapPropertyAttributeTypeSymbol) .GetAttribute(MapPropertyAttributeTypeSymbol)
?.NamedArguments ?.NamedArguments
.SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName) .SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
.Value.Value as string ?? property.Name; .Value.Value as string ?? member.Name;
foreach(var sourceProperty in sourceMembers)
{
if(sourceProperty is IPropertySymbol propertySymbol)
{
if (propertySymbol.Name == propertyName) return sourceProperty;
}
if (sourceProperty is IFieldSymbol fieldSymbol)
{
if (fieldSymbol.Name == propertyName) return sourceProperty;
}
return sourceProperties.SingleOrDefault(p => p.Name == propertyName); }
return sourceMembers.SingleOrDefault(p => p.Name == propertyName);
} }
protected abstract ImmutableArray<MappedProperty> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); protected abstract ImmutableArray<MappedProperty> GetSourceMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected abstract ImmutableArray<MappedProperty> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); protected abstract ImmutableArray<MappedProperty> GetTypeMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected INamedTypeSymbol? GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null) => protected INamedTypeSymbol? GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null) =>
GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel); GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel);
@ -144,7 +160,12 @@ namespace MapTo
return TypeSyntax.GetAttribute("UseUpdate") != null; return TypeSyntax.GetAttribute("UseUpdate") != null;
} }
protected virtual MappedProperty? MapProperty(ISymbol sourceTypeSymbol, IReadOnlyCollection<IPropertySymbol> sourceProperties, ISymbol property) protected bool IsJsonExtension()
{
return TypeSyntax.GetAttribute("JsonExtension") != null;
}
protected virtual MappedProperty? MapProperty(ISymbol sourceTypeSymbol, IReadOnlyCollection<ISymbol> sourceProperties, ISymbol property)
{ {
var sourceProperty = FindSourceProperty(sourceProperties, property); var sourceProperty = FindSourceProperty(sourceProperties, property);
if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType)) if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType))
@ -171,6 +192,16 @@ namespace MapTo
AddUsingIfRequired(enumerableTypeArgumentType); AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType); AddUsingIfRequired(mappedSourcePropertyType);
bool isReadOnly = false;
if(property is IPropertySymbol pSymbol)
{
isReadOnly = pSymbol.IsReadOnly;
}
if (property is IFieldSymbol fSymbol)
{
isReadOnly = fSymbol.IsReadOnly;
}
return new MappedProperty( return new MappedProperty(
property.Name, property.Name,
@ -181,7 +212,7 @@ namespace MapTo
sourceProperty.Name, sourceProperty.Name,
ToQualifiedDisplayName(mappedSourcePropertyType), ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType), ToQualifiedDisplayName(enumerableTypeArgumentType),
(property as IPropertySymbol).IsReadOnly); isReadOnly);
; ;
} }
protected virtual MappedProperty? MapPropertySimple(ISymbol sourceTypeSymbol, ISymbol property) protected virtual MappedProperty? MapPropertySimple(ISymbol sourceTypeSymbol, ISymbol property)
@ -202,7 +233,16 @@ namespace MapTo
AddUsingIfRequired(enumerableTypeArgumentType); AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType); AddUsingIfRequired(mappedSourcePropertyType);
bool isReadOnly = false;
if (property is IPropertySymbol pSymbol)
{
isReadOnly = pSymbol.IsReadOnly;
}
if (property is IFieldSymbol fSymbol)
{
isReadOnly = fSymbol.IsReadOnly;
}
return new MappedProperty( return new MappedProperty(
property.Name, property.Name,
property.GetTypeSymbol().ToString(), property.GetTypeSymbol().ToString(),
@ -212,10 +252,10 @@ namespace MapTo
property.Name, property.Name,
ToQualifiedDisplayName(mappedSourcePropertyType), ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType), ToQualifiedDisplayName(enumerableTypeArgumentType),
(property as IPropertySymbol).IsReadOnly); isReadOnly);
; ;
} }
protected bool TryGetMapTypeConverter(ISymbol property, IPropertySymbol sourceProperty, out string? converterFullyQualifiedName, protected bool TryGetMapTypeConverter(ISymbol property, ISymbol sourceMember, out string? converterFullyQualifiedName,
out ImmutableArray<string> converterParameters) out ImmutableArray<string> converterParameters)
{ {
converterFullyQualifiedName = null; converterFullyQualifiedName = null;
@ -232,10 +272,10 @@ namespace MapTo
return false; return false;
} }
var baseInterface = GetTypeConverterBaseInterface(converterTypeSymbol, property, sourceProperty); var baseInterface = GetTypeConverterBaseInterface(converterTypeSymbol, property, sourceMember);
if (baseInterface is null) if (baseInterface is null)
{ {
AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, sourceProperty)); AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, sourceMember));
return false; return false;
} }
@ -310,18 +350,19 @@ namespace MapTo
var sourceTypeIdentifierName = sourceTypeSymbol.Name; var sourceTypeIdentifierName = sourceTypeSymbol.Name;
var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel); var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
var isTypeUpdatable = IsTypeUpdatable(); var isTypeUpdatable = IsTypeUpdatable();
var isJsonExtension = IsJsonExtension();
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol); var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
var mappedProperties = GetSourceMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass); var mappedProperties = GetSourceMappedMembers(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
if (!mappedProperties.Any()) /* if (!mappedProperties.Any())
{ {
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol)); AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol));
return null; return null;
} }*/
AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq"); AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq");
var allProperties = GetTypeMappedProperties(sourceTypeSymbol, typeSymbol , isTypeInheritFromMappedBaseClass); var allProperties = GetTypeMappedMembers(sourceTypeSymbol, typeSymbol , isTypeInheritFromMappedBaseClass);
return new MappingModel( return new MappingModel(
SourceGenerationOptions, SourceGenerationOptions,
@ -333,6 +374,7 @@ namespace MapTo
sourceTypeIdentifierName, sourceTypeIdentifierName,
sourceTypeSymbol.ToDisplayString(), sourceTypeSymbol.ToDisplayString(),
isTypeUpdatable, isTypeUpdatable,
isJsonExtension,
mappedProperties, mappedProperties,
allProperties, allProperties,
isTypeInheritFromMappedBaseClass, isTypeInheritFromMappedBaseClass,
@ -342,18 +384,31 @@ namespace MapTo
private INamedTypeSymbol? GetTypeConverterBaseInterface(ITypeSymbol converterTypeSymbol, ISymbol property, IPropertySymbol sourceProperty) private INamedTypeSymbol? GetTypeConverterBaseInterface(ITypeSymbol converterTypeSymbol, ISymbol property, ISymbol sourceMember)
{ {
if (!property.TryGetTypeSymbol(out var propertyType)) if (!property.TryGetTypeSymbol(out var propertyType))
{ {
return null; return null;
} }
ISymbol? type = null;
if(sourceMember is IPropertySymbol propertySymbol)
{
type = propertySymbol.Type;
}
if (sourceMember is IFieldSymbol fieldSymbol)
{
type = fieldSymbol.Type;
}
if (type == null) return null;
return converterTypeSymbol.AllInterfaces return converterTypeSymbol.AllInterfaces
.SingleOrDefault(i => .SingleOrDefault(i =>
i.TypeArguments.Length == 2 && i.TypeArguments.Length == 2 &&
SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) && SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) &&
SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) && SymbolEqualityComparer.Default.Equals(type, i.TypeArguments[0]) &&
SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1])); SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1]));
} }

View File

@ -46,6 +46,7 @@ namespace MapTo
string SourceTypeIdentifierName, string SourceTypeIdentifierName,
string SourceTypeFullName, string SourceTypeFullName,
bool IsTypeUpdatable, bool IsTypeUpdatable,
bool IsJsonExtension,
ImmutableArray<MappedProperty> SourceProperties, ImmutableArray<MappedProperty> SourceProperties,
ImmutableArray<MappedProperty> TypeProperties, ImmutableArray<MappedProperty> TypeProperties,
bool HasMappedBaseClass, bool HasMappedBaseClass,

6
src/MapTo/Nuget.Config Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="BlueWest.EfGenerator" value="https://git.codeliturgy.com/api/packages/P0/nuget/index.json" />
</packageSources>
</configuration>

View File

@ -11,7 +11,7 @@ namespace MapTo
internal RecordMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) internal RecordMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { } : base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedProperty> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) protected override ImmutableArray<MappedProperty> GetSourceMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{ {
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray(); var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol.GetMembers() return typeSymbol.GetMembers()
@ -25,7 +25,7 @@ namespace MapTo
.ToImmutableArray()!; .ToImmutableArray()!;
} }
protected override ImmutableArray<MappedProperty> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) protected override ImmutableArray<MappedProperty> GetTypeMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{ {
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray(); var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol.GetMembers() return typeSymbol.GetMembers()

View File

@ -0,0 +1,40 @@
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class JsonExtensionAttributeSource
{
internal const string AttributeName = "JsonExtension";
internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
internal static SourceCode Generate(SourceGenerationOptions options)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
if (options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine("/// Specifies that the annotated needs ToJson method")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]")
.WriteLine($"public sealed class {AttributeName}Attribute : Attribute")
.WriteOpeningBracket();
builder
.WriteClosingBracket() // class
.WriteClosingBracket(); // namespace
return new(builder.ToString(), $"{AttributeName}Attribute.g.cs");
}
}
}

View File

@ -11,25 +11,23 @@ namespace MapTo
internal StructMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) internal StructMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { } : base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedProperty> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) protected override ImmutableArray<MappedProperty> GetSourceMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{ {
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray(); var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol return typeSymbol
.GetAllMembers() .GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property)) .Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null) .Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!; .ToImmutableArray()!;
} }
protected override ImmutableArray<MappedProperty> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) protected override ImmutableArray<MappedProperty> GetTypeMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{ {
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray(); var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return sourceTypeSymbol return sourceTypeSymbol
.GetAllMembers() .GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapPropertySimple(typeSymbol, property)) .Select(property => MapPropertySimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null) .Where(mappedProperty => mappedProperty is not null)

View File

@ -1,85 +0,0 @@
using System.Linq;
using MapTo.Integration.Tests.Data.Models;
using MapTo.Integration.Tests.Data.ViewModels;
using Shouldly;
using Xunit;
namespace MapTo.Integration.Tests
{
public class CyclicReferenceTests
{
[Fact]
public void VerifySelfReference()
{
// Arrange
var manager = new Manager { Id = 1, EmployeeCode = "M001", Level = 100 };
manager.Manager = manager;
// Act
var result = manager.ToManagerViewModel();
// Assert
result.Id.ShouldBe(manager.Id);
result.EmployeeCode.ShouldBe(manager.EmployeeCode);
result.Level.ShouldBe(manager.Level);
result.Manager.ShouldBeSameAs(result);
}
[Fact]
public void VerifyNestedReference()
{
// Arrange
var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 };
var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 };
var employee1 = new Employee { Id = 200, EmployeeCode = "E001"};
var employee2 = new Employee { Id = 201, EmployeeCode = "E002"};
employee1.Manager = manager1;
employee2.Manager = manager2;
manager2.Manager = manager1;
// Act
var manager1ViewModel = manager1.ToManagerViewModel();
// Assert
manager1ViewModel.Id.ShouldBe(manager1.Id);
manager1ViewModel.Manager.ShouldBeNull();
manager1ViewModel.Employees.Count.ShouldBe(2);
manager1ViewModel.Employees[0].Id.ShouldBe(employee1.Id);
manager1ViewModel.Employees[0].Manager.ShouldBeSameAs(manager1ViewModel);
manager1ViewModel.Employees[1].Id.ShouldBe(manager2.Id);
manager1ViewModel.Employees[1].Manager.ShouldBeSameAs(manager1ViewModel);
}
[Fact]
public void VerifyNestedSelfReference()
{
// Arrange
var manager1 = new Manager { Id = 100, EmployeeCode = "M001", Level = 100 };
var manager3 = new Manager { Id = 101, EmployeeCode = "M003", Level = 100 };
var manager2 = new Manager { Id = 102, EmployeeCode = "M002", Level = 100 };
var employee1 = new Employee { Id = 200, EmployeeCode = "E001"};
var employee2 = new Employee { Id = 201, EmployeeCode = "E002"};
var employee3 = new Employee { Id = 202, EmployeeCode = "E003"};
employee1.Manager = manager1;
employee2.Manager = manager2;
employee3.Manager = manager3;
manager2.Manager = manager1;
manager3.Manager = manager2;
// Act
var manager3ViewModel = manager3.ToManagerViewModel();
// Assert
manager3ViewModel.Manager.ShouldNotBeNull();
manager3ViewModel.Manager.Id.ShouldBe(manager2.Id);
manager3ViewModel.Manager.Manager.Id.ShouldBe(manager1.Id);
manager3ViewModel.Employees.All(e => ReferenceEquals(e.Manager, manager3ViewModel)).ShouldBeTrue();
}
}
}

View File

@ -1,29 +0,0 @@
namespace MapTo.Integration.Tests.Data.Models
{
public class Employee
{
private Manager _manager;
public int Id { get; set; }
public string EmployeeCode { get; set; }
public Manager Manager
{
get => _manager;
set
{
if (value == null)
{
_manager.Employees.Remove(this);
}
else
{
value.Employees.Add(this);
}
_manager = value;
}
}
}
}

View File

@ -1,11 +0,0 @@
using System.Collections.Generic;
namespace MapTo.Integration.Tests.Data.Models
{
public class Manager : Employee
{
public int Level { get; set; }
public List<Employee> Employees { get; set; } = new();
}
}

View File

@ -1,14 +0,0 @@
using MapTo.Integration.Tests.Data.Models;
namespace MapTo.Integration.Tests.Data.ViewModels
{
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; }
}
}

View File

@ -1,13 +0,0 @@
using System.Collections.Generic;
using MapTo.Integration.Tests.Data.Models;
namespace MapTo.Integration.Tests.Data.ViewModels
{
[MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel
{
public int Level { get; set; }
public List<EmployeeViewModel> Employees { get; set; } = new();
}
}

View File

@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MapTo\MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -1,252 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Shouldly;
namespace MapTo.Tests
{
internal static class Common
{
internal const int Indent1 = 4;
internal const int Indent2 = Indent1 * 2;
internal const int Indent3 = Indent1 * 3;
internal static readonly Location IgnoreLocation = Location.None;
internal static readonly Dictionary<string, string> DefaultAnalyzerOptions = new()
{
[GeneratorExecutionContextExtensions.GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false"
};
internal static string GetSourceText(SourceGeneratorOptions? options = null)
{
const string ns = "Test";
options ??= new SourceGeneratorOptions();
var hasDifferentSourceNamespace = options.SourceClassNamespace != ns;
var builder = new SourceBuilder();
builder.WriteLine("//");
builder.WriteLine("// Test source code.");
builder.WriteLine("//");
builder.WriteLine();
options.Usings?.ForEach(s => builder.WriteLine($"using {s};"));
if (options.UseMapToNamespace)
{
builder.WriteLine($"using {Constants.RootNamespace};");
}
builder
.WriteLine($"using {options.SourceClassNamespace};")
.WriteLine()
.WriteLine();
builder
.WriteLine($"namespace {ns}")
.WriteOpeningBracket();
if (hasDifferentSourceNamespace && options.UseMapToNamespace)
{
builder
.WriteLine($"using {options.SourceClassNamespace};")
.WriteLine()
.WriteLine();
}
builder
.WriteLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]" : "[MapTo.MapFrom(typeof(Baz))]")
.WriteLine("public partial class Foo")
.WriteOpeningBracket();
for (var i = 1; i <= options.ClassPropertiesCount; i++)
{
builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}");
}
options.PropertyBuilder?.Invoke(builder);
builder
.WriteClosingBracket()
.WriteClosingBracket()
.WriteLine()
.WriteLine();
builder
.WriteLine($"namespace {options.SourceClassNamespace}")
.WriteOpeningBracket()
.WriteLine("public class Baz")
.WriteOpeningBracket();
for (var i = 1; i <= options.SourceClassPropertiesCount; i++)
{
builder.WriteLine(i % 2 == 0 ? $"public int Prop{i} {{ get; set; }}" : $"public int Prop{i} {{ get; }}");
}
options.SourcePropertyBuilder?.Invoke(builder);
builder
.WriteClosingBracket()
.WriteClosingBracket();
return builder.ToString();
}
internal static string[] GetEmployeeManagerSourceText(
Func<string>? employeeClassSource = null,
Func<string>? managerClassSource = null,
Func<string>? employeeViewModelSource = null,
Func<string>? managerViewModelSource = null,
bool useDifferentViewModelNamespace = false)
{
return new[]
{
employeeClassSource?.Invoke() ?? DefaultEmployeeClassSource(),
managerClassSource?.Invoke() ?? DefaultManagerClassSource(),
employeeViewModelSource?.Invoke() ??
DefaultEmployeeViewModelSource(useDifferentViewModelNamespace),
managerViewModelSource?.Invoke() ?? DefaultManagerViewModelSource(useDifferentViewModelNamespace)
};
static string DefaultEmployeeClassSource() =>
@"
using System;
using System.Collections.Generic;
using System.Text;
namespace Test.Data.Models
{
public class Employee
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public Manager Manager { get; set; }
}
}".Trim();
static string DefaultManagerClassSource() =>
@"using System;
using System.Collections.Generic;
using System.Text;
namespace Test.Data.Models
{
public class Manager: Employee
{
public int Level { get; set; }
public IEnumerable<Employee> Employees { get; set; } = Array.Empty<Employee>();
}
}
".Trim();
static string DefaultEmployeeViewModelSource(bool useDifferentNamespace) => useDifferentNamespace
? @"
using MapTo;
using Test.Data.Models;
using Test.ViewModels2;
namespace Test.ViewModels
{
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; }
}
}
".Trim()
: @"
using MapTo;
using Test.Data.Models;
namespace Test.ViewModels
{
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; set; }
public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; }
}
}
".Trim();
static string DefaultManagerViewModelSource(bool useDifferentNamespace) => useDifferentNamespace
? @"
using System;
using System.Collections.Generic;
using MapTo;
using Test.Data.Models;
using Test.ViewModels;
namespace Test.ViewModels2
{
[MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel
{
public int Level { get; set; }
public IEnumerable<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>();
}
}
".Trim()
: @"
using System;
using System.Collections.Generic;
using MapTo;
using Test.Data.Models;
namespace Test.ViewModels
{
[MapFrom(typeof(Manager))]
public partial class ManagerViewModel : EmployeeViewModel
{
public int Level { get; set; }
public IEnumerable<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>();
}
}".Trim();
}
internal static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo")
{
return syntaxTree.GetRoot()
.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Single(c => c.Identifier.ValueText == targetClass)
.DescendantNodes()
.OfType<PropertyDeclarationSyntax>()
.Single(p => p.Identifier.ValueText == targetPropertyName);
}
internal static IPropertySymbol GetSourcePropertySymbol(string propertyName, Compilation compilation, string targetClass = "Foo")
{
var syntaxTree = compilation.SyntaxTrees.First();
var propSyntax = GetPropertyDeclarationSyntax(syntaxTree, propertyName, targetClass);
var semanticModel = compilation.GetSemanticModel(syntaxTree);
return semanticModel.GetDeclaredSymbol(propSyntax).ShouldNotBeNull();
}
internal record SourceGeneratorOptions(
bool UseMapToNamespace = false,
string SourceClassNamespace = "Test.Models",
int ClassPropertiesCount = 3,
int SourceClassPropertiesCount = 3,
Action<SourceBuilder>? PropertyBuilder = null,
Action<SourceBuilder>? SourcePropertyBuilder = null,
IEnumerable<string>? Usings = null);
}
}

View File

@ -1,16 +0,0 @@
// 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
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit { }
}

View File

@ -1,42 +0,0 @@
using System;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
namespace MapTo.Tests.Extensions
{
internal static class RoslynExtensions
{
internal static SyntaxTree? GetGeneratedSyntaxTree(this Compilation compilation, string className) =>
compilation.SyntaxTrees.SingleOrDefault(s => s.FilePath.EndsWith($"{className}.g.cs"));
internal static string PrintSyntaxTree(this Compilation compilation)
{
var builder = new StringBuilder();
return string.Join(
Environment.NewLine,
compilation.SyntaxTrees
.Reverse()
.Select((s, i) =>
{
builder
.Clear()
.AppendLine("----------------------------------------")
.AppendFormat("File Path: \"{0}\"", s.FilePath).AppendLine()
.AppendFormat("Index: \"{0}\"", i).AppendLine()
.AppendLine();
var lines = s.ToString().Split(Environment.NewLine);
var lineNumber = 0;
foreach (var line in lines)
{
builder.AppendFormat("{0:00}: {1}", lineNumber, line).AppendLine();
lineNumber++;
}
return builder.ToString();
}));
}
}
}

View File

@ -1,88 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Shouldly;
using Xunit;
namespace MapTo.Tests.Extensions
{
internal static class ShouldlyExtensions
{
internal static void ShouldContainSource(this IEnumerable<SyntaxTree> syntaxTree, string typeName, string expectedSource, string? customMessage = null)
{
var syntax = syntaxTree
.Select(s => s.ToString().Trim())
.SingleOrDefault(s => s.Contains(typeName));
syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldBe(expectedSource, customMessage);
}
internal static void ShouldContainPartialSource(this IEnumerable<SyntaxTree> syntaxTree, string typeName, string expectedSource, string? customMessage = null)
{
var syntax = syntaxTree
.Select(s => s.ToString().Trim())
.SingleOrDefault(s => s.Contains(typeName));
syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage);
}
internal static void ShouldContainPartialSource(this SyntaxTree syntaxTree, string expectedSource, string? customMessage = null)
{
var syntax = syntaxTree.ToString();
syntax.ShouldNotBeNullOrWhiteSpace();
syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage);
}
internal static void ShouldBeSuccessful(this IEnumerable<Diagnostic> diagnostics, Compilation? compilation = null, IEnumerable<string>? ignoreDiagnosticsIds = null)
{
var actual = diagnostics
.Where(d => (ignoreDiagnosticsIds is null || ignoreDiagnosticsIds.All(i => !d.Id.StartsWith(i) )) && (d.Severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Error))
.Select(c => $"{c.Severity}: {c.Location.GetLineSpan()} - {c.GetMessage()}").ToArray();
if (!actual.Any())
{
return;
}
var builder = new StringBuilder();
builder.AppendLine("Failed");
foreach (var d in actual)
{
builder.AppendFormat("- {0}", d).AppendLine();
}
if (compilation is not null)
{
builder.AppendLine("Generated Sources:");
builder.AppendLine(compilation.PrintSyntaxTree());
}
Assert.False(true, builder.ToString());
}
internal static void ShouldNotBeSuccessful(this ImmutableArray<Diagnostic> diagnostics, Diagnostic expectedError)
{
var actualDiagnostics = diagnostics.SingleOrDefault(d => d.Id == expectedError.Id);
var compilationDiagnostics = actualDiagnostics == null ? diagnostics : diagnostics.Except(new[] { actualDiagnostics });
compilationDiagnostics.ShouldBeSuccessful();
Assert.NotNull(actualDiagnostics);
Assert.Equal(expectedError.Id, actualDiagnostics?.Id);
Assert.Equal(expectedError.Descriptor.Id, actualDiagnostics?.Descriptor.Id);
Assert.Equal(expectedError.Descriptor.Description, actualDiagnostics?.Descriptor.Description);
Assert.Equal(expectedError.Descriptor.Title, actualDiagnostics?.Descriptor.Title);
if (expectedError.Location != Location.None)
{
Assert.Equal(expectedError.Location, actualDiagnostics?.Location);
}
}
}
}

View File

@ -1,79 +0,0 @@
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Shouldly;
using Xunit;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class IgnorePropertyAttributeTests
{
[Fact]
public void VerifyIgnorePropertyAttribute()
{
// Arrange
const string source = "";
var expectedAttribute = $@"
{Constants.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, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(IgnorePropertyAttributeSource.AttributeName, expectedAttribute);
}
[Fact]
public void When_IgnorePropertyAttributeIsSpecified_Should_NotGenerateMappingsForThatProperty()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[IgnoreProperty]")
.WriteLine("public int Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
var expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
}
}

View File

@ -1,64 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using MapTo.Tests.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace MapTo.Tests.Infrastructure
{
internal static class CSharpGenerator
{
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(
string source,
bool assertCompilation = false,
IDictionary<string, string>? analyzerConfigOptions = null,
NullableContextOptions nullableContextOptions = NullableContextOptions.Disable,
LanguageVersion languageVersion = LanguageVersion.CSharp7_3) =>
GetOutputCompilation(
new[] { source },
assertCompilation,
analyzerConfigOptions,
nullableContextOptions,
languageVersion);
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(
IEnumerable<string> sources,
bool assertCompilation = false,
IDictionary<string, string>? analyzerConfigOptions = null,
NullableContextOptions nullableContextOptions = NullableContextOptions.Disable,
LanguageVersion languageVersion = LanguageVersion.CSharp7_3)
{
var references = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic && !string.IsNullOrWhiteSpace(a.Location))
.Select(a => MetadataReference.CreateFromFile(a.Location))
.ToList();
var compilation = CSharpCompilation.Create(
$"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic",
sources.Select((source, index) => CSharpSyntaxTree.ParseText(source, path: $"Test{index:00}.g.cs", options: new CSharpParseOptions(languageVersion))),
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableContextOptions));
if (assertCompilation)
{
// NB: fail tests when the injected program isn't valid _before_ running generators
compilation.GetDiagnostics().ShouldBeSuccessful();
}
var driver = CSharpGeneratorDriver.Create(
new[] { new MapToGenerator() },
optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions),
parseOptions: new CSharpParseOptions(languageVersion)
);
driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics);
generateDiagnostics.ShouldBeSuccessful(ignoreDiagnosticsIds: new[] { "MT" });
outputCompilation.GetDiagnostics().ShouldBeSuccessful(outputCompilation);
return (outputCompilation, generateDiagnostics);
}
}
}

View File

@ -1,19 +0,0 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace MapTo.Tests.Infrastructure
{
internal sealed class TestAnalyzerConfigOptions : AnalyzerConfigOptions
{
private readonly ImmutableDictionary<string, string> _backing;
public TestAnalyzerConfigOptions(IDictionary<string, string>? properties)
{
_backing = properties?.ToImmutableDictionary(KeyComparer) ?? ImmutableDictionary.Create<string, string>(KeyComparer);
}
public override bool TryGetValue(string key, out string? value) => _backing.TryGetValue(key, out value);
}
}

View File

@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace MapTo.Tests.Infrastructure
{
internal sealed class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider
{
public TestAnalyzerConfigOptionsProvider(IDictionary<string, string>? options)
{
GlobalOptions = new TestAnalyzerConfigOptions(options);
}
/// <inheritdoc />
public override AnalyzerConfigOptions GlobalOptions { get; }
/// <inheritdoc />
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException();
/// <inheritdoc />
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => throw new NotImplementedException();
}
}

View File

@ -1,202 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class MapPropertyTests
{
[Theory]
[InlineData(NullableContextOptions.Disable)]
[InlineData(NullableContextOptions.Enable)]
public void VerifyMapPropertyAttribute(NullableContextOptions nullableContextOptions)
{
// Arrange
const string source = "";
var nullableSyntax = nullableContextOptions == NullableContextOptions.Enable ? "?" : string.Empty;
var languageVersion = nullableContextOptions == NullableContextOptions.Enable ? LanguageVersion.CSharp8 : LanguageVersion.CSharp7_3;
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
{(nullableContextOptions == NullableContextOptions.Enable ? $"#nullable enable{Environment.NewLine}" : string.Empty)}
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, 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, languageVersion: languageVersion);
// 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
.WriteLine("[MapProperty(SourcePropertyName = nameof(Baz.Prop3))]")
.WriteLine("public int Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
var expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
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().ShouldContainPartialSource(expectedResult);
}
[Theory]
[MemberData(nameof(MapPropertyWithImplicitConversionFoundData))]
public void When_MapPropertyWithImplicitConversionFound_Should_UseItToMapToSourceProperty(string source, string expectedResult, LanguageVersion languageVersion)
{
// Arrange
source = source.Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
public static IEnumerable<object[]> MapPropertyWithImplicitConversionFoundData => new List<object[]>
{
new object[]
{
@"
namespace Test
{
using System.Collections.Generic;
public class InnerClass { public int Prop1 { get; set; } }
public class OuterClass
{
public int Id { get; set; }
public List<InnerClass> InnerProp { get; set; }
}
}
namespace Test.Models
{
using MapTo;
using System.Collections.Generic;
[MapFrom(typeof(Test.InnerClass))]
public partial class InnerClass { public int Prop1 { get; set; } }
[MapFrom(typeof(Test.OuterClass))]
public partial class OuterClass
{
public int Id { get; set; }
public IReadOnlyList<InnerClass> InnerProp { get; set; }
}
}
",
@"
private protected OuterClass(MappingContext context, Test.OuterClass outerClass)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (outerClass == null) throw new ArgumentNullException(nameof(outerClass));
context.Register(outerClass, this);
Id = outerClass.Id;
InnerProp = outerClass.InnerProp.Select(context.MapFromWithContext<Test.InnerClass, InnerClass>).ToList();
}
",
LanguageVersion.CSharp7_3
},
new object[]
{
@"
namespace Test
{
using System;
using System.Collections.Generic;
public class InnerClass
{
public int Id { get; set; }
public string Name { get; set; }
}
public class OuterClass
{
public int Id { get; set; }
public List<InnerClass> InnerClasses { get; set; }
public DateTime? SomeDate { get; set; }
}
}
namespace Test.Models
{
using MapTo;
using System;
using System.Collections.Generic;
[MapFrom(typeof(Test.InnerClass))]
public partial record InnerClass(int Id, string Name);
[MapFrom(typeof(Test.OuterClass))]
public partial record OuterClass(int Id, DateTime? SomeDate, IReadOnlyList<InnerClass> InnerClasses);
}
",
@"
private protected OuterClass(MappingContext context, Test.OuterClass outerClass)
: this(Id: outerClass.Id, SomeDate: outerClass.SomeDate, InnerClasses: outerClass.InnerClasses.Select(context.MapFromWithContext<Test.InnerClass, InnerClass>).ToList())
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (outerClass == null) throw new ArgumentNullException(nameof(outerClass));
context.Register(outerClass, this);
}
",
LanguageVersion.CSharp9
}
};
}
}

View File

@ -1,37 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MapTo\MapTo.csproj" />
</ItemGroup>
</Project>

View File

@ -1,601 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Shouldly;
using Xunit;
using static MapTo.Extensions.GeneratorExecutionContextExtensions;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class MapToTests
{
private static readonly string ExpectedAttribute = $@"{Constants.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; }}
}}
}}";
[Fact]
public void VerifyMapToAttribute()
{
// Arrange
const string source = "";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapFromAttributeSource.AttributeClassName, ExpectedAttribute);
}
[Fact]
public void When_FoundMatchingPropertyNameWithDifferentTypes_Should_ReportError()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder => { builder.WriteLine("public string Prop4 { get; set; }"); },
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
var expectedError = DiagnosticsFactory.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("Prop4", compilation));
diagnostics.ShouldNotBeSuccessful(expectedError);
}
[Fact]
public void When_MappingsModifierOptionIsSetToInternal_Should_GenerateThoseMethodsWithInternalAccessModifier()
{
// Arrange
var source = GetSourceText();
var configOptions = new Dictionary<string, string>
{
[GetBuildPropertyName(nameof(SourceGenerationOptions.GeneratedMethodsAccessModifier))] = "Internal",
[GetBuildPropertyName(nameof(SourceGenerationOptions.GenerateXmlDocument))] = "false"
};
var expectedExtension = @"
internal static partial class BazToFooExtensions
{
internal static Foo ToFoo(this Test.Models.Baz baz)
{
return baz == null ? null : new Foo(baz);
}
}".Trim();
var expectedFactory = @"
internal static Foo From(Test.Models.Baz baz)
{
return baz == null ? null : MappingContext.Create<Test.Models.Baz, Foo>(baz);
}".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: configOptions);
// Assert
diagnostics.ShouldBeSuccessful();
var syntaxTree = compilation.SyntaxTrees.Last().ToString();
syntaxTree.ShouldContain(expectedFactory);
syntaxTree.ShouldContain(expectedExtension);
}
[Fact]
public void When_MapToAttributeFound_Should_GenerateTheClass()
{
// Arrange
const string source = @"
using MapTo;
namespace Test
{
[MapFrom(typeof(Baz))]
public partial class Foo
{
public int Prop1 { get; set; }
}
public class Baz
{
public int Prop1 { get; set; }
}
}
";
const string expectedResult = @"
// <auto-generated />
using MapTo;
using System;
namespace Test
{
partial class Foo
{
public Foo(Test.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
}
[Fact]
public void When_MapToAttributeFoundWithoutMatchingProperties_Should_ReportError()
{
// Arrange
const string source = @"
using MapTo;
namespace Test
{
[MapFrom(typeof(Baz))]
public partial class Foo { }
public class Baz { public int Prop1 { get; set; } }
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
var fooType = compilation.GetTypeByMetadataName("Test.Foo");
fooType.ShouldNotBeNull();
var bazType = compilation.GetTypeByMetadataName("Test.Baz");
bazType.ShouldNotBeNull();
var expectedDiagnostic = DiagnosticsFactory.NoMatchingPropertyFoundError(fooType.Locations.Single(), fooType, bazType);
var error = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnostic.Id);
error.ShouldNotBeNull();
}
[Fact]
public void When_MapToAttributeWithNamespaceFound_Should_GenerateTheClass()
{
// Arrange
const string source = @"
namespace Test
{
[MapTo.MapFrom(typeof(Baz))]
public partial class Foo { public int Prop1 { get; set; } }
public class Baz { public int Prop1 { get; set; } }
}
";
const string expectedResult = @"
// <auto-generated />
using MapTo;
using System;
namespace Test
{
partial class Foo
{
public Foo(Test.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
}
[Fact]
public void When_NoMapToAttributeFound_Should_GenerateOnlyTheAttribute()
{
// Arrange
const string source = "";
var expectedTypes = new[]
{
IgnorePropertyAttributeSource.AttributeName,
MapFromAttributeSource.AttributeName,
ITypeConverterSource.InterfaceName,
MapPropertyAttributeSource.AttributeName
};
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees
.Select(s => s.ToString())
.Where(s => !string.IsNullOrWhiteSpace(s.ToString()))
.All(s => expectedTypes.Any(s.Contains))
.ShouldBeTrue();
}
[Fact]
public void When_SourceTypeHasDifferentNamespace_Should_NotAddToUsings()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(SourceClassNamespace: "Bazaar"));
const string expectedResult = @"
// <auto-generated />
using MapTo;
using System;
namespace Test
{
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
}
[Fact]
public void When_SourceTypeHasMatchingProperties_Should_CreateConstructorAndAssignSrcToDest()
{
// Arrange
var source = GetSourceText();
const string expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim());
}
[Fact]
public void When_SourceTypeHasMatchingProperties_Should_CreateFromStaticMethod()
{
// Arrange
var source = GetSourceText();
const string expectedResult = @"
public static Foo From(Test.Models.Baz baz)
{
return baz == null ? null : MappingContext.Create<Test.Models.Baz, Foo>(baz);
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim());
}
[Fact]
public void When_SourceTypeHasMatchingProperties_Should_GenerateToExtensionMethodOnSourceType()
{
// Arrange
var source = GetSourceText();
const string expectedResult = @"
public static partial class BazToFooExtensions
{
public static Foo ToFoo(this Test.Models.Baz baz)
{
return baz == null ? null : new Foo(baz);
}
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim());
}
[Fact]
public void When_HasNestedObjectPropertyTypeHasMapFromAttribute_Should_UseContinueToMap()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
SourceClassNamespace: "Test",
PropertyBuilder: b => b.WriteLine("public B InnerProp1 { get; }"),
SourcePropertyBuilder: b => b.WriteLine("public A InnerProp1 { get; }")));
source += @"
namespace Test
{
public class A { public int Prop1 { get; } }
[MapTo.MapFrom(typeof(A))]
public partial class B { public int Prop1 { get; }}
}
".Trim();
var expectedResult = @"
partial class Foo
{
public Foo(Test.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
InnerProp1 = context.MapFromWithContext<A, B>(baz.InnerProp1);
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ToArray()[^2].ShouldContainPartialSource(expectedResult);
}
[Fact]
public void When_HasNestedObjectPropertyTypeDoesNotHaveMapFromAttribute_Should_ReportError()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
SourceClassNamespace: "Test",
PropertyBuilder: b => b.WriteLine("public FooInner1 InnerProp1 { get; }"),
SourcePropertyBuilder: b => b.WriteLine("public BazInner1 InnerProp1 { get; }")));
source += @"
namespace Test
{
public class FooInner1 { public int Prop1 { get; } }
public partial class BazInner1 { public int Prop1 { get; }}
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
var expectedError = DiagnosticsFactory.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("InnerProp1", compilation));
diagnostics.ShouldNotBeSuccessful(expectedError);
}
[Fact]
public void When_HasNestedObjectPropertyTypeHasMapFromAttributeToDifferentType_Should_ReportError()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
SourceClassNamespace: "Test",
PropertyBuilder: b => b.WriteLine("public FooInner1 InnerProp1 { get; }"),
SourcePropertyBuilder: b => b.WriteLine("public BazInner1 InnerProp1 { get; }")));
source += @"
namespace Test
{
public class FooInner1 { public int Prop1 { get; } }
public class FooInner2 { public int Prop1 { get; } }
[MapTo.MapFrom(typeof(FooInner2))]
public partial class BazInner1 { public int Prop1 { get; }}
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
var expectedError = DiagnosticsFactory.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("InnerProp1", compilation));
diagnostics.ShouldNotBeSuccessful(expectedError);
}
[Fact]
public void When_SourceTypeEnumerableProperties_Should_CreateConstructorAndAssignSrcToDest()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
Usings: new[] { "System.Collections.Generic"},
PropertyBuilder: builder => builder.WriteLine("public IEnumerable<int> Prop4 { get; }"),
SourcePropertyBuilder: builder => builder.WriteLine("public IEnumerable<int> Prop4 { get; }")));
const string expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
Prop4 = baz.Prop4;
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult.Trim());
}
[Fact]
public void When_DestinationTypeHasBaseClass_Should_CallBaseConstructor()
{
// Arrange
var sources = GetEmployeeManagerSourceText();
const string expectedResult = @"
private protected ManagerViewModel(MappingContext context, Test.Data.Models.Manager manager) : base(context, manager)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (manager == null) throw new ArgumentNullException(nameof(manager));
context.Register(manager, this);
Level = manager.Level;
Employees = manager.Employees.Select(context.MapFromWithContext<Test.Data.Models.Employee, EmployeeViewModel>).ToList();
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
[Fact]
public void When_SourceTypeHasEnumerablePropertiesWithMapFromAttribute_Should_CreateANewEnumerableWithMappedObjects()
{
// Arrange
var sources = GetEmployeeManagerSourceText();
const string expectedResult = @"
// <auto-generated />
using MapTo;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Test.ViewModels
{
partial class ManagerViewModel
{
public ManagerViewModel(Test.Data.Models.Manager manager)
: this(new MappingContext(), manager) { }
private protected ManagerViewModel(MappingContext context, Test.Data.Models.Manager manager) : base(context, manager)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (manager == null) throw new ArgumentNullException(nameof(manager));
context.Register(manager, this);
Level = manager.Level;
Employees = manager.Employees.Select(context.MapFromWithContext<Test.Data.Models.Employee, EmployeeViewModel>).ToList();
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
[Fact]
public void When_SourceTypeHasEnumerablePropertiesWithMapFromAttributeInDifferentNamespaces_Should_CreateANewEnumerableWithMappedObjectsAndImportNamespace()
{
// Arrange
var sources = GetEmployeeManagerSourceText(useDifferentViewModelNamespace: true);
const string expectedResult = @"
using MapTo;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Test.ViewModels2
{
partial class ManagerViewModel
{
public ManagerViewModel(Test.Data.Models.Manager manager)
: this(new MappingContext(), manager) { }
private protected ManagerViewModel(MappingContext context, Test.Data.Models.Manager manager) : base(context, manager)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (manager == null) throw new ArgumentNullException(nameof(manager));
context.Register(manager, this);
Level = manager.Level;
Employees = manager.Employees.Select(context.MapFromWithContext<Test.Data.Models.Employee, Test.ViewModels.EmployeeViewModel>).ToList();
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
}
}

View File

@ -1,283 +0,0 @@
using System.Linq;
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class MapTypeConverterTests
{
[Fact]
public void VerifyMapTypeConverterAttribute()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class MapTypeConverterAttribute : Attribute
{{
public MapTypeConverterAttribute(Type converter, object[] converterParameters = null)
{{
Converter = converter;
ConverterParameters = converterParameters;
}}
public Type Converter {{ get; }}
public object[] ConverterParameters {{ get; }}
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void VerifyMapTypeConverterAttributeWithNullableOptionOn()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
#nullable enable
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class MapTypeConverterAttribute : Attribute
{{
public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null)
{{
Converter = converter;
ConverterParameters = converterParameters;
}}
public Type Converter {{ get; }}
public object[]? ConverterParameters {{ get; }}
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void VerifyTypeConverterInterface()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
namespace MapTo
{{
public interface ITypeConverter<in TSource, out TDestination>
{{
TDestination Convert(TSource source, object[] converterParameters);
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
}
[Fact]
public void VerifyTypeConverterInterfaceWithNullableOptionOn()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
#nullable enable
namespace MapTo
{{
public interface ITypeConverter<in TSource, out TDestination>
{{
TDestination Convert(TSource source, object[]? converterParameters);
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
}
[Fact]
public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterAndItsParametersToAssignProperties()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[MapTypeConverter(typeof(Prop4Converter), new object[]{\"G\", 'C', 10})]")
.WriteLine("public string Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public long Prop4 { get; set; }")));
source += @"
namespace Test
{
using MapTo;
public class Prop4Converter: ITypeConverter<long, string>
{
public string Convert(long source, object[] converterParameters) => source.ToString(converterParameters[0] as string);
}
}
";
const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, new object[] { \"G\", 'C', 10 });";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax);
}
[Fact]
public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterToAssignProperties()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[MapTypeConverter(typeof(Prop4Converter))]")
.WriteLine("public long Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }")));
source += @"
namespace Test
{
using MapTo;
public class Prop4Converter: ITypeConverter<string, long>
{
public long Convert(string source, object[] converterParameters) => long.Parse(source);
}
}
";
const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, null);";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedSyntax);
}
[Fact]
public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder => { builder.WriteLine("public long Prop4 { get; set; }"); },
SourcePropertyBuilder: builder => builder.WriteLine("public int Prop4 { get; set; }")));
var expectedResult = @"
partial class Foo
{
public Foo(Test.Models.Baz baz)
: this(new MappingContext(), baz) { }
private protected Foo(MappingContext context, Test.Models.Baz baz)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (baz == null) throw new ArgumentNullException(nameof(baz));
context.Register(baz, this);
Prop1 = baz.Prop1;
Prop2 = baz.Prop2;
Prop3 = baz.Prop3;
Prop4 = baz.Prop4;
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Last().ShouldContainPartialSource(expectedResult);
}
[Fact]
public void When_FoundMatchingPropertyNameWithIncorrectConverterType_ShouldReportError()
{
// Arrange
var source = GetSourceText(new SourceGeneratorOptions(
true,
PropertyBuilder: builder =>
{
builder
.WriteLine("[IgnoreProperty]")
.WriteLine("public long IgnoreMe { get; set; }")
.WriteLine("[MapTypeConverter(typeof(Prop4Converter))]")
.WriteLine("public long Prop4 { get; set; }");
},
SourcePropertyBuilder: builder => builder.WriteLine("public string Prop4 { get; set; }")));
source += @"
namespace Test
{
using MapTo;
public class Prop4Converter: ITypeConverter<string, int>
{
public int Convert(string source, object[] converterParameters) => int.Parse(source);
}
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
var expectedError = DiagnosticsFactory.InvalidTypeConverterGenericTypesError(GetSourcePropertySymbol("Prop4", compilation), GetSourcePropertySymbol("Prop4", compilation, "Baz"));
diagnostics.ShouldNotBeSuccessful(expectedError);
}
}
}

View File

@ -1,562 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class MappedClassesTests
{
private readonly ITestOutputHelper _output;
public MappedClassesTests(ITestOutputHelper output)
{
_output = output;
}
[Theory]
[MemberData(nameof(SecondaryConstructorCheckData))]
public void When_SecondaryConstructorExists_Should_NotGenerateOne(string source, LanguageVersion languageVersion)
{
// Arrange
source = source.Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
// Assert
diagnostics.ShouldBeSuccessful();
compilation
.GetGeneratedSyntaxTree("DestinationClass")
.ShouldNotBeNull()
.GetRoot()
.DescendantNodes()
.OfType<ConstructorDeclarationSyntax>()
.Count()
.ShouldBe(1);
}
public static IEnumerable<object[]> SecondaryConstructorCheckData => new List<object[]>
{
new object[]
{
@"
using MapTo;
namespace Test.Data.Models
{
public class SourceClass { public string Prop1 { get; set; } }
[MapFrom(typeof(SourceClass))]
public partial class DestinationClass
{
public DestinationClass(SourceClass source) : this(new MappingContext(), source) { }
public string Prop1 { get; set; }
}
}
",
LanguageVersion.CSharp7_3
},
new object[]
{
@"
using MapTo;
namespace Test.Data.Models
{
public record SourceClass(string Prop1);
[MapFrom(typeof(SourceClass))]
public partial record DestinationClass(string Prop1)
{
public DestinationClass(SourceClass source) : this(new MappingContext(), source) { }
}
}
",
LanguageVersion.CSharp9
}
};
[Theory]
[MemberData(nameof(SecondaryCtorWithoutPrivateCtorData))]
public void When_SecondaryConstructorExistsButDoNotReferencePrivateConstructor_Should_ReportError(string source, LanguageVersion languageVersion)
{
// Arrange
source = source.Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
// Assert
var constructorSyntax = compilation.SyntaxTrees
.First()
.GetRoot()
.DescendantNodes()
.OfType<ConstructorDeclarationSyntax>()
.Single();
diagnostics.ShouldNotBeSuccessful(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
}
public static IEnumerable<object[]> SecondaryCtorWithoutPrivateCtorData => new List<object[]>
{
new object[]
{
@"
using MapTo;
namespace Test.Data.Models
{
public class SourceClass { public string Prop1 { get; set; } }
[MapFrom(typeof(SourceClass))]
public partial class DestinationClass
{
public DestinationClass(SourceClass source) { }
public string Prop1 { get; set; }
}
}
",
LanguageVersion.CSharp7_3
},
new object[]
{
@"
using MapTo;
namespace Test.Data.Models
{
public record SourceClass(string Prop1);
[MapFrom(typeof(SourceClass))]
public partial record DestinationClass(string Prop1)
{
public DestinationClass(SourceClass source) : this(""invalid"") { }
}
}
",
LanguageVersion.CSharp9
}
};
[Fact]
public void When_PropertyNameIsTheSameAsClassName_Should_MapAccordingly()
{
// Arrange
var source = @"
namespace Sale
{
public class Sale { public Sale Prop1 { get; set; } }
}
namespace SaleModel
{
using MapTo;
using Sale;
[MapFrom(typeof(Sale))]
public partial class SaleModel
{
[MapProperty(SourcePropertyName = nameof(global::Sale.Sale.Prop1))]
public Sale Sale { get; set; }
}
}
".Trim();
// Act
var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
}
[Theory]
[MemberData(nameof(SameSourceAndDestinationTypeNameData))]
public void When_SourceAndDestinationNamesAreTheSame_Should_MapAccordingly(string source, LanguageVersion languageVersion)
{
// Arrange
source = source.Trim();
// Act
var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
// Assert
diagnostics.ShouldBeSuccessful();
}
public static IEnumerable<object> SameSourceAndDestinationTypeNameData => new List<object>
{
new object[]
{
@"
namespace Test
{
public class TypeName { public int Prop2 { get; set; } }
}
namespace Test2
{
using MapTo;
[MapFrom(typeof(Test.TypeName))]
public partial class TypeName
{
[MapProperty(SourcePropertyName=""Prop2"")]
public int Prop1 { get; set; }
}
}",
LanguageVersion.CSharp7_3
},
new object[]
{
@"
namespace Test
{
public record TypeName(int Prop2);
}
namespace Test2
{
using MapTo;
[MapFrom(typeof(Test.TypeName))]
public partial record TypeName([MapProperty(SourcePropertyName=""Prop2"")] int Prop1);
}",
LanguageVersion.CSharp9
},
new object[]
{
@"
namespace Test
{
using System.Collections.Generic;
public class SourceType2 { public int Id { get; set; } }
public class SourceType
{
public int Id { get; set; }
public List<SourceType2> Prop1 { get; set; }
}
}
namespace Test2
{
using MapTo;
using System.Collections.Generic;
[MapFrom(typeof(Test.SourceType2))]
public partial class SourceType2 { public int Id { get; set; } }
[MapFrom(typeof(Test.SourceType))]
public partial class SourceType
{
public int Id { get; set; }
public IReadOnlyList<SourceType2> Prop1 { get; set; }
}
}",
LanguageVersion.CSharp7_3
},
new object[]
{
@"
namespace Test
{
using System.Collections.Generic;
public record SourceType(int Id, List<SourceType2> Prop1);
public record SourceType2(int Id);
}
namespace Test2
{
using MapTo;
using System.Collections.Generic;
[MapFrom(typeof(Test.SourceType2))]
public partial record SourceType2(int Id);
[MapFrom(typeof(Test.SourceType))]
public partial record SourceType(int Id, IReadOnlyList<SourceType2> Prop1);
}",
LanguageVersion.CSharp9
},
new object[]
{
@"
namespace Test
{
using System.Collections.Generic;
public record SourceType1(int Id);
public record SourceType2(int Id, List<SourceType1> Prop1);
}
namespace Test
{
using MapTo;
using System.Collections.Generic;
[MapFrom(typeof(Test.SourceType1))]
public partial record SourceType3(int Id);
[MapFrom(typeof(Test.SourceType2))]
public partial record SourceType4(int Id, IReadOnlyList<SourceType3> Prop1);
}",
LanguageVersion.CSharp9
}
};
[Theory]
[MemberData(nameof(VerifyMappedTypesData))]
public void VerifyMappedTypes(string[] sources, LanguageVersion languageVersion)
{
// Arrange
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(sources, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: languageVersion);
// Assert
diagnostics.ShouldBeSuccessful();
_output.WriteLine(compilation.PrintSyntaxTree());
}
public static IEnumerable<object[]> VerifyMappedTypesData => new List<object[]>
{
new object[] { new[] { MainSourceClass, NestedSourceClass, MainDestinationClass, NestedDestinationClass }, LanguageVersion.CSharp7_3 },
new object[] { new[] { MainSourceRecord, NestedSourceRecord, MainDestinationRecord, NestedDestinationRecord }, LanguageVersion.CSharp9 },
new object[]
{
new[]
{
@"
namespace Test.Classes.Classes1
{
public class Class1
{
public int Id { get; set; }
public string Name { get; set; }
}
}",
@"
using System;
using System.Collections.Generic;
using Test.Classes.Classes1;
namespace Test.Classes.Classes2
{
public class Class2
{
public int Id { get; set; }
public List<Class1> Genres { get; set; }
public DateTime? ReleaseDate { get; set; }
}
}",
@"
using MapTo;
using System;
using System.Collections.Generic;
using TC = Test.Classes;
namespace Tests.Records
{
[MapFrom(typeof(Test.Classes.Classes1.Class1))]
public partial record Class1(int Id, string Name);
[MapFrom(typeof(Test.Classes.Classes2.Class2))]
public partial record Class2(int Id, IReadOnlyList<Class1> Genres);
}"
},
LanguageVersion.CSharp9
}
};
[Fact]
public void VerifySelfReferencingRecords()
{
// Arrange
var source = @"
namespace Tests.Data.Models
{
using System.Collections.Generic;
public record Employee(int Id, string EmployeeCode, Manager Manager);
public record Manager(int Id, string EmployeeCode, Manager Manager, int Level, List<Employee> Employees) : Employee(Id, EmployeeCode, Manager);
}
namespace Tests.Data.ViewModels
{
using System.Collections.Generic;
using Tests.Data.Models;
using MapTo;
[MapFrom(typeof(Employee))]
public partial record EmployeeViewModel(int Id, string EmployeeCode, ManagerViewModel Manager);
[MapFrom(typeof(Manager))]
public partial record ManagerViewModel(int Id, string EmployeeCode, ManagerViewModel Manager, int Level, List<EmployeeViewModel> Employees) : EmployeeViewModel(Id, EmployeeCode, Manager);
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: LanguageVersion.CSharp9);
// Assert
diagnostics.ShouldBeSuccessful();
_output.WriteLine(compilation.PrintSyntaxTree());
}
[Fact]
public void VerifySystemNamespaceConflict()
{
// Arrange
var source = @"
namespace Test
{
public record SomeRecord(int Id);
}
namespace Test.Models
{
using MapTo;
[MapFrom(typeof(Test.SomeRecord))]
public partial record SomeRecordModel(int Id);
}
namespace Test.System
{
public interface IMyInterface { }
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, languageVersion: LanguageVersion.CSharp9);
// Assert
diagnostics.ShouldBeSuccessful();
_output.WriteLine(compilation.PrintSyntaxTree());
}
private static string MainSourceClass => @"
using System;
namespace Test.Data.Models
{
public class User
{
public int Id { get; set; }
public DateTimeOffset RegisteredAt { get; set; }
public Profile Profile { get; set; }
}
}
".Trim();
private static string NestedSourceClass => @"
namespace Test.Data.Models
{
public class Profile
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName => $""{FirstName} {LastName}"";
}
}
".Trim();
private static string MainDestinationClass => @"
using System;
using MapTo;
using Test.Data.Models;
namespace Test.ViewModels
{
[MapFrom(typeof(User))]
public partial class UserViewModel
{
[MapProperty(SourcePropertyName = nameof(User.Id))]
[MapTypeConverter(typeof(IdConverter))]
public string Key { get; }
public DateTimeOffset RegisteredAt { get; set; }
// [IgnoreProperty]
public ProfileViewModel Profile { get; set; }
private class IdConverter : ITypeConverter<int, string>
{
public string Convert(int source, object[] converterParameters) => $""{source:X}"";
}
}
}
".Trim();
private static string NestedDestinationClass => @"
using MapTo;
using Test.Data.Models;
namespace Test.ViewModels
{
[MapFrom(typeof(Profile))]
public partial class ProfileViewModel
{
public string FirstName { get; }
public string LastName { get; }
}
}
".Trim();
private static string MainSourceRecord => BuildSourceRecord("public record User(int Id, DateTimeOffset RegisteredAt, Profile Profile);");
private static string MainDestinationRecord => BuildDestinationRecord(@"
[MapFrom(typeof(User))]
public partial record UserViewModel(
[MapProperty(SourcePropertyName = nameof(User.Id))]
[MapTypeConverter(typeof(UserViewModel.IdConverter))]
string Key,
DateTimeOffset RegisteredAt,
Profile Profile)
{
private class IdConverter : ITypeConverter<int, string>
{
public string Convert(int source, object[] converterParameters) => $""{source:X}"";
}
}");
private static string NestedSourceRecord => BuildSourceRecord("public record Profile(string FirstName, string LastName) { public string FullName => $\"{FirstName} {LastName}\"; }");
private static string NestedDestinationRecord => BuildDestinationRecord("[MapFrom(typeof(Profile))] public partial record ProfileViewModel(string FirstName, string LastName);");
private static string BuildSourceRecord(string record)
{
return $@"
using System;
namespace RecordTest.Data.Models
{{
{record}
}}
".Trim();
}
private static string BuildDestinationRecord(string record)
{
return $@"
using System;
using MapTo;
using RecordTest.Data.Models;
namespace RecordTest.ViewModels
{{
{record}
}}
".Trim();
}
}
}

View File

@ -1,102 +0,0 @@
using MapTo.Sources;
using MapTo.Tests.Extensions;
using MapTo.Tests.Infrastructure;
using Xunit;
using static MapTo.Tests.Common;
namespace MapTo.Tests
{
public class MappingContextTests
{
[Fact]
public void VerifyMappingContextSource()
{
// Arrange
const string source = "";
var expected = @"
// <auto-generated />
using System;
using System.Collections.Generic;
using System.Reflection;
namespace MapTo
{
internal sealed class MappingContext
{
private readonly Dictionary<object, object> _cache;
internal MappingContext()
{
_cache = new Dictionary<object, object>(1);
}
internal static TMapped Create<TOriginal, TMapped>(TOriginal original)
{
if (original == null) throw new ArgumentNullException(nameof(original));
var context = new MappingContext();
var mapped = context.MapFromWithContext<TOriginal, TMapped>(original);
if (mapped == null)
{
throw new InvalidOperationException();
}
return mapped;
}
internal TMapped MapFromWithContext<TOriginal, TMapped>(TOriginal original)
{
if (original == null)
{
return default(TMapped);
}
if (!TryGetValue<TOriginal, TMapped>(original, out var mapped))
{
var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null);
if (instance != null)
{
mapped = (TMapped)instance;
}
}
return mapped;
}
internal void Register<TOriginal, TMapped>(TOriginal original, TMapped mapped)
{
if (original == null) throw new ArgumentNullException(nameof(original));
if (mapped == null) throw new ArgumentNullException(nameof(mapped));
if (!_cache.ContainsKey(original))
{
_cache.Add(original, mapped);
}
}
private bool TryGetValue<TOriginal, TMapped>(TOriginal original, out TMapped mapped)
{
if (original != null && _cache.TryGetValue(original, out var value))
{
mapped = (TMapped)value;
return true;
}
mapped = default(TMapped);
return false;
}
}
}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MappingContextSource.ClassName, expected);
}
}
}

View File

@ -1,49 +0,0 @@
using System;
using MapTo;
namespace BlueWest.Data
{
public enum FinanceSymbol
{
BTC_EUR,
BTC_BUSD,
BTC_USD,
BTC_USDT,
LTC_EUR,
LTC_BUSD,
LTC_USDT
}
public enum FinanceTransactionType
{
Buy,
Sell
}
[MapFrom(typeof(FinanceTransactionInsertDto))]
public partial struct FinanceTransaction
{
public int Id { get; }
public int UserId { get; set; }
public FinanceTransactionType FinanceTransactionType { get; }
public FinanceSymbol FinanceSymbol { get; }
public double Amount { get; } // To Buy
public double Quantity { get; } // Bought
public double Fee { get; }
public DateTime DateTime { get; }
public FinanceTransaction(int id, int userId, FinanceTransactionType financeTransactionType,
FinanceSymbol financeSymbol, double amount, double quantity, double fee, DateTime dateTime)
{
Id = id;
UserId = userId;
FinanceTransactionType = financeTransactionType;
FinanceSymbol = financeSymbol;
Amount = amount;
Quantity = quantity;
Fee = fee;
DateTime = dateTime;
}
}
}

View File

@ -1,16 +0,0 @@
using System;
namespace BlueWest.Data
{
public partial struct FinanceTransactionInsertDto
{
public int UserId { get; set; }
public FinanceTransactionType FinanceTransactionType { get; }
public FinanceSymbol FinanceSymbol { get; }
public double Amount { get; } // To Buy
public double Quantity { get; } // Bought
public double Fee { get; }
public DateTime DateTime { get; }
}
}

View File

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using MapTo;
namespace BlueWest.Data
{
[MapFrom(typeof(FinanceTransaction))]
partial struct FinanceTransactionReadDto
{
public int UserId { get; set; }
public FinanceTransactionType FinanceTransactionType { get; }
public FinanceSymbol FinanceSymbol { get; }
public double Amount { get; } // To Buy
public double Quantity { get; } // Bought
public double Fee { get; }
public DateTime DateTime { get; }
public string ReadData { get; }
}
}

View File

@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MapTo;
using TestConsoleApp.ViewModels;
namespace TestConsoleApp.Data.Models
{
[MapFrom(typeof(CarReadDto))]
[UseUpdate]
partial class Car
{
public int Size { get; }
public int Id { get; }
public string Brand { get; }
public Car(int size, int id, string brand)
{
Size = size;
Id = id;
Brand = brand;
}
}
}

View File

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace TestConsoleApp.Data.Models
{
public class Employee
{
public int Id { get; }
public string EmployeeCode { get; }
public Employee(int id, string employeeCode)
{
Id = id;
EmployeeCode = employeeCode;
}
}
}

View File

@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TestConsoleApp.ViewModels;
using MapTo;
namespace TestConsoleApp.Data.Models
{
[MapFrom(typeof(MyStructViewModel))]
[UseUpdate]
public partial struct MyStruct
{
public int SomeInt { get; set; }
public string ReadOnlyString { get; }
public MyStruct(int someInt, string readOnlyString)
{
SomeInt = someInt;
ReadOnlyString = readOnlyString;
}
}
}

View File

@ -1,43 +0,0 @@

using System.Collections.Generic;
using MapTo;
namespace BlueWest.Data
{
[MapFrom(typeof(UserUpdateDto))]
public partial class User
{
public int Id { get; }
public string Name { get; set; }
public string Address { get; set; }
public string BTCAddress { get; set; }
public string LTCAddress { get; set; }
public double BTCAmount { get; set; }
public double LTCAmount { get; set; }
public List<FinanceTransaction> FinanceTransactions { get; }
public User(int id, string name, string address, string btcAddress, string ltcAddress, double btcAmount, double ltcAmount, List<FinanceTransaction> financeTransactions)
{
Id = id;
Name = name;
Address = address;
BTCAddress = btcAddress;
LTCAddress = ltcAddress;
BTCAmount = btcAmount;
LTCAmount = ltcAmount;
FinanceTransactions = financeTransactions;
}
public void AddTransaction(FinanceTransaction financeTransaction)
{
FinanceTransactions.Add(financeTransaction);
}
}
}

View File

@ -1,16 +0,0 @@
using System.Collections.Generic;
namespace BlueWest.Data
{
public class UserList
{
public List<User> Users;
public UserList(List<User> users)
{
Users = users;
}
public int Length => Users.Count;
}
}

View File

@ -1,19 +0,0 @@
using MapTo;
namespace BlueWest.Data
{
[MapFrom(typeof(User))]
public partial struct UserUpdateDto
{
public string Name { get; set; }
public string Address { get; set; }
public string BTCAddress { get; set; }
public string LTCAddress { get; set; }
public double BTCAmount { get; set; }
public double LTCAmount { get; set; }
}
}

View File

@ -1,32 +0,0 @@
using System;
using MapTo;
using TestConsoleApp.Data.Models;
using TestConsoleApp.ViewModels;
namespace TestConsoleApp
{
internal class Program
{
private static void Main(string[] args)
{
//UserTest();
// EmployeeManagerTest();
Console.WriteLine("done");
}
private static void EmployeeManagerTest()
{
var employee = new Employee(1, "hello");
}
}
}

View File

@ -1,18 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net471</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MapTo\MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<Import Project="..\..\src\MapTo\MapTo.props" />
<PropertyGroup>
<MapTo_ConstructorAccessModifier>Internal</MapTo_ConstructorAccessModifier>
</PropertyGroup>
</Project>

View File

@ -1,23 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MapTo;
using TestConsoleApp.Data.Models;
namespace TestConsoleApp.ViewModels
{
[MapFrom(typeof(Car))]
partial class CarReadDto
{
public int Size { get; }
public string Brand { get; }
public CarReadDto(int size, string brand)
{
Size = size;
Brand = brand;
}
}
}

View File

@ -1,13 +0,0 @@
using MapTo;
using TestConsoleApp.Data.Models;
namespace TestConsoleApp.ViewModels
{
[MapFrom(typeof(Employee))]
public partial class EmployeeViewModel
{
public int Id { get; }
}
}

View File

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TestConsoleApp.Data.Models;
using MapTo;
namespace TestConsoleApp.ViewModels
{
[MapFrom(typeof(MyStruct))]
public partial struct MyStructViewModel
{
public int SomeInt { get; set; }
public MyStructViewModel(int someInt)
{
SomeInt = someInt;
}
}
}