Compare commits

..

33 Commits

Author SHA1 Message Date
CodeLiturgy 4e80156708 Remove EfGenerator, migrated to separate project 2022-09-05 23:18:15 +01:00
CodeLiturgy b8637c94ab Using GetOne and GetMany templates 2022-09-05 22:42:51 +01:00
CodeLiturgy 32bf670b9e Get one template working 2022-09-05 03:58:28 +01:00
CodeLiturgy 9743ff3740 Post-refactor 2022-09-05 02:16:32 +01:00
CodeLiturgy 8a664db1f7 pre-refacotring 2022-09-05 02:16:08 +01:00
CodeLiturgy 1de6350c7d Prepare getone template 2022-09-05 02:01:17 +01:00
CodeLiturgy fa3dfc8e86 Repair done 2022-09-05 01:42:08 +01:00
CodeLiturgy 4c4413da41 Before trying to replace with type syntax 2022-09-05 00:33:19 +01:00
CodeLiturgy 9211cde83c Use new model for ef method 2022-09-04 22:11:04 +01:00
CodeLiturgy 3917d231a2 Add generator attribute source 2022-09-04 19:07:59 +01:00
CodeLiturgy 9257d389ba add EfMethods directory 2022-09-04 16:36:46 +01:00
CodeLiturgy 07c8fc4853 Template ok 2022-09-02 00:07:56 +01:00
Wvader e23d932d13 update template using nameof to specify id 2022-08-29 17:30:32 +01:00
CodeLiturgy 654a54d5ef Update template working 2022-08-29 01:41:05 +01:00
CodeLiturgy f419c0f8ea ok state before introducing update 2022-08-28 19:39:47 +01:00
CodeLiturgy ca95c87839 Add model refactored 2022-08-28 18:14:08 +01:00
CodeLiturgy 12d5839f16 ok 2022-08-28 18:00:04 +01:00
CodeLiturgy fabfa562cf ok 2022-08-28 17:59:55 +01:00
CodeLiturgy 4d9cc4b029 End merge Add with template and Update 2022-08-27 21:01:52 +01:00
CodeLiturgy c34da98ad8 ef update wip 2022-08-27 20:56:19 +01:00
Wvader e232fa6e76 Add template working 2022-08-27 04:19:43 +01:00
Wvader 340a89bbd2 Support Ef Add method 2022-08-24 17:56:44 +01:00
Wvader 1400bd08e0 Prepare for Add/Update generators 2022-08-23 17:48:47 +01:00
Wvader 7de9b48c69 Inject empty constructor 2022-08-22 03:14:14 +01:00
Wvader a6f5116656 Supporting 'friendly constructors' 2022-08-21 22:15:00 +01:00
Wvader aa5b01cdc4 Fix UseUpdate usage 2022-08-20 03:47:55 +01:00
Wvader f7963d2d7e Addapt MapTo to support multiple mappings 2022-08-18 16:48:44 +01:00
Wvader ca82e6fb17 Fix docker errors 2022-08-13 06:33:39 +01:00
Wvader 77603643b4 Bump Nerdbank.GitVersioning 2022-08-13 01:15:25 +01:00
Wvader c241fd4cb5 summer changes 2022-08-02 22:02:26 +01:00
Wvader ffc3dc7729 stuff 2021-12-24 17:06:06 +00:00
Wvader 60ccbd53ca k 2021-12-12 00:10:13 +00:00
Wvader 5faebd9141 work with field 2021-12-11 23:54:21 +00:00
85 changed files with 6775 additions and 3139 deletions

View File

@ -29,7 +29,7 @@ jobs:
- name: Publish MapTo - name: Publish MapTo
uses: brandedoutcast/publish-nuget@v2.5.5 uses: brandedoutcast/publish-nuget@v2.5.5
with: with:
PROJECT_FILE_PATH: src/MapTo/MapTo.csproj PROJECT_FILE_PATH: src/BlueWest.MapTo/BlueWest.MapTo.csproj
NUGET_KEY: ${{secrets.NUGET_API_KEY}} NUGET_KEY: ${{secrets.NUGET_API_KEY}}
NUGET_SOURCE: https://api.nuget.org NUGET_SOURCE: https://api.nuget.org
TAG_COMMIT: false TAG_COMMIT: false

View File

@ -1,6 +1,12 @@
 
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\BlueWest.MapTo\BlueWest.MapTo.csproj", "{4DB371AC-48D0-4F01-8EF3-7707D06EF0A7}"
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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution

View File

@ -4,23 +4,21 @@
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<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> <PackageProjectUrl>https://github.com/mrtaikandi/mapto</PackageProjectUrl>
<PackageProjectUrl>https://git.codeliturgy.com/P0/MapTo</PackageProjectUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageVersion>$(Version)</PackageVersion> <PackageVersion>$(Version)</PackageVersion>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<RepositoryUrl>https://git.codeliturgy.com/P0/MapTo</RepositoryUrl> <RepositoryUrl>https://github.com/mrtaikandi/mapto</RepositoryUrl>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<RootNamespace>MapTo</RootNamespace> <RootNamespace>MapTo</RootNamespace>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\MapTo.xml</DocumentationFile> <DocumentationFile>bin\Release\MapTo.xml</DocumentationFile>
<Optimize>false</Optimize>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -30,18 +28,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<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"> <PackageReference Update="Nerdbank.GitVersioning">
<Version>3.6.65-alpha</Version> <Version>3.5.109</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="..\..\LICENSE" Pack="true" PackagePath="" Visible="false" />
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="MapTo.props" Pack="true" PackagePath="build" Visible="false" /> <None Include="MapTo.props" Pack="true" PackagePath="build" Visible="false" />
</ItemGroup> </ItemGroup>

View File

@ -0,0 +1,67 @@
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal class ClassMappingContext : MappingContext
{
internal ClassMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedMember> GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IFieldSymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IFieldSymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapField(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IFieldSymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IFieldSymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapFieldSimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapPropertySimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -25,14 +25,10 @@ namespace MapTo
Create($"{ErrorId}030", location, $"No matching properties found between '{classType.ToDisplayString()}' and '{sourceType.ToDisplayString()}' types."); Create($"{ErrorId}030", location, $"No matching properties found between '{classType.ToDisplayString()}' and '{sourceType.ToDisplayString()}' types.");
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 '{IgnoreMemberAttributeSource.FullyQualifiedName}'.");
internal static Diagnostic InvalidTypeConverterGenericTypesError(ISymbol property, ISymbol memberSymbol) internal static Diagnostic InvalidTypeConverterGenericTypesError(ISymbol property, IPropertySymbol sourceProperty) =>
{ 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

@ -0,0 +1,86 @@
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Text;
namespace MapTo.Extensions
{
internal static class CommonExtensions
{
internal static SourceBuilder WriteComment(this SourceBuilder builder, string comment = "")
{
return builder.WriteLine($"// {comment}");
}
internal static SourceBuilder WriteCommentArray(this SourceBuilder builder, IEnumerable<object> enumerable, string name = "")
{
builder.WriteComment($"Printing Array: {name}");
foreach (var o in enumerable)
{
if (o != null)
{
builder.WriteComment($" {o.ToString()}");
}
}
builder.WriteComment($"End printing Array: {name}");
return builder;
}
internal static SourceBuilder WriteModelInfo(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
builder
.WriteLine()
.WriteComment($" IsTypeUpdatable {model.IsTypeUpdatable}")
.WriteComment($" HasMappedBaseClass {model.HasMappedBaseClass.ToString()}")
.WriteComment($" Namespace {model.Namespace}")
.WriteComment($" Options {model.Options.ToString()}")
.WriteComment($" Type {model.Type}")
.WriteComment($" TypeIdentifierName {model.TypeIdentifierName}")
.WriteComment($" SourceNamespace {targetSourceType.SourceNamespace}")
.WriteComment($" SourceTypeFullName {targetSourceType.SourceTypeFullName}")
.WriteComment($" SourceTypeIdentifierName {targetSourceType.SourceTypeIdentifierName}");
}
return builder;
}
internal static SourceBuilder WriteMappedProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray<MappedMember> mappedProperties)
{
foreach (var item in mappedProperties)
{
string str = "";
if (item.NamedTypeSymbol != null)
foreach (var named in item.NamedTypeSymbol?.TypeArguments)
{
str += $"typeToString: {named.ToString()} ";
bool? containedTypeIsJsonEXtension = named?.HasAttribute(MappingContext.JsonExtensionAttributeSymbol);
str += $"typeArgumentTypeIsJsonExtensioN: {containedTypeIsJsonEXtension.ToString()}";
}
builder .WriteComment($" Name {item.Name}")
.WriteComment($" Type {item.Type}")
.WriteComment($" MappedSourcePropertyTypeName {item.MappedSourcePropertyTypeName}")
.WriteComment($" IsEnumerable {item.IsEnumerable}")
.WriteComment($" FullyQualifiedType {item.FullyQualifiedType}")
.WriteComment($" EnumerableTypeArgument {item.EnumerableTypeArgument}")
.WriteComment($" SourcePropertyName {item.SourcePropertyName}")
.WriteComment($" TypeSymbol {item.FullyQualifiedType.ToString()}")
.WriteComment($" isReadOnly {item.isReadOnly.ToString()}")
.WriteComment($" isEnumerable {item.isEnumerable.ToString()}")
.WriteComment($" INamedTypeSymbol {item.NamedTypeSymbol?.ToString()}")
.WriteComment($" INamedTypeSymbolTypeArguments {str}")
.WriteLine();
}
return builder;
}
}
}

View File

@ -0,0 +1,379 @@
using MapTo.Sources;
using static MapTo.Sources.Constants;
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
#pragma warning disable CS8602
namespace MapTo.Extensions
{
internal static class CommonSource
{
internal static SourceCode GenerateStructOrClass(this MappingModel model, string structOrClass)
{
const bool writeDebugInfo = false;
List<string> constructorHeaders = new List<string>();
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
.WriteUsings(model.Usings)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {model.Namespace}")
.WriteOpeningBracket();
builder
// Class declaration
.WriteLine($"partial {structOrClass} {model.TypeIdentifierName}")
.WriteOpeningBracket()
.WriteLine()
// Class body
.GeneratePublicEmptyConstructor(model, ref constructorHeaders, true)
.GeneratePublicConstructor(model, ref constructorHeaders)
.GeneratePublicConstructor(model, ref constructorHeaders, true);
if (model.IsTypeUpdatable) builder.GenerateUpdateMethod(model);
if (model.IsJsonExtension) builder.WriteToJsonMethod(model);
builder
.WriteLine()
// End class declaration
.WriteClosingBracket()
.WriteLine()
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs");
}
private static SourceBuilder GeneratePublicConstructor(this SourceBuilder builder, MappingModel model, ref List<string> constructorHeaders, bool filterNonMapped = false)
{
const string mappingContextParameterName = "context";
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty;
var stringBuilder = new StringBuilder();
var otherProperties = new List<MappedMember>();
foreach (var property in targetSourceType.TypeProperties)
{
if (!targetSourceType.SourceProperties.IsMappedProperty(property) && !filterNonMapped)
{
stringBuilder.Append(", ");
stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}");
otherProperties.Add(property);
}
}
foreach (var property in targetSourceType.TypeFields)
{
if (!targetSourceType.SourceFields.IsMappedProperty(property) && !filterNonMapped)
{
stringBuilder.Append(", ");
stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}");
otherProperties.Add(property);
}
}
var readOnlyPropertiesArguments = stringBuilder.ToString();
var constructorHeader =
$"public {model.TypeIdentifierName}({targetSourceType.SourceType} {sourceClassParameterName}{readOnlyPropertiesArguments}){baseConstructor}";
bool hasAlreadyConstructor = false;
foreach (var header in constructorHeaders)
{
if(constructorHeader.Contains(header)) hasAlreadyConstructor = true;
}
if (hasAlreadyConstructor) continue;
constructorHeaders.Add(constructorHeader);
builder
.WriteLine(constructorHeader)
.WriteOpeningBracket()
.WriteAssignmentMethod(model, filterNonMapped ? null : otherProperties.ToArray().ToImmutableArray(), sourceClassParameterName, mappingContextParameterName, filterNonMapped);
builder.WriteClosingBracket()
.WriteLine();
}
// End constructor declaration
return builder;
}
private static SourceBuilder GeneratePublicEmptyConstructor(this SourceBuilder builder, MappingModel model, ref List<string> constructorHeaders, bool filterNonMapped = false)
{
const string mappingContextParameterName = "context";
foreach (var targetSourceType in model.MappedSourceTypes)
{
var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty;
var constructorHeader =
$"public {model.TypeIdentifierName}(){baseConstructor}";
bool hasAlreadyConstructor = false;
foreach (var header in constructorHeaders)
{
if(constructorHeader.Contains(header)) hasAlreadyConstructor = true;
}
if (hasAlreadyConstructor) continue;
constructorHeaders.Add(constructorHeader);
builder
.WriteLine(constructorHeader)
.WriteOpeningBracket()
.WriteClosingBracket()
.WriteLine();
}
// End constructor declaration
return builder;
}
private static bool IsMappedProperty(this System.Collections.Immutable.ImmutableArray<MappedMember> properties, MappedMember property)
{
foreach (var prop in properties)
{
if (prop.Name == property.Name) return true;
}
return false;
}
private static SourceBuilder WriteToJsonMethod(this SourceBuilder builder, MappingModel model)
{
builder
.WriteLine($"public string ToJson()")
.WriteOpeningBracket()
.WriteLine("var stringBuilder = new System.Text.StringBuilder();")
.WriteLine(GetStringBuilderAppendNoInterpolation("{"));
foreach (var targetSourceType in model.MappedSourceTypes)
{
foreach (var property in targetSourceType.TypeProperties)
{
if (!property.isEnumerable)
HandlePropertyEnumerable(builder, property);
else
{
builder = WriteJsonField(builder, property);
}
}
foreach (var property in targetSourceType.TypeFields)
{
if (!property.isEnumerable)
HandleFieldEnumerable(builder, property);
else
{
builder.WriteLine(GetStringBuilderAppend($"\\\"{property.Name.ToCamelCase()}\\\" : [{GetJsonArrayValue(property, ref builder)}],"));
}
}
builder.WriteLine(GetStringBuilderAppendNoInterpolation("}"));
builder.WriteLine("return stringBuilder.ToString();");
builder.WriteClosingBracket();
}
return builder;
}
private static SourceBuilder WriteJsonField(SourceBuilder builder, MappedMember property)
{
builder.WriteLine(
GetStringBuilderAppend(
$"\\\"{property.Name.ToCamelCase()}\\\" : [{GetJsonArrayValue(property, ref builder)}],"));
return builder;
}
private static void HandleEnumerable(SourceBuilder builder, MappedMember property)
{
var symbol = property.ActualSymbol as IPropertySymbol;
#pragma warning disable CS8602
builder.WriteCommentArray(symbol.Parameters, nameof(symbol.Parameters));
#pragma warning restore CS8602
builder.WriteCommentArray(symbol.TypeCustomModifiers, nameof(symbol.TypeCustomModifiers));
builder.WriteComment($"Is enumerable {(property.ActualSymbol as IPropertySymbol).Parameters}");
builder.WriteLine(
GetStringBuilderAppend($"\\\"{property.Name.ToCamelCase()}\\\" : {GetJsonValue(property, builder)},"));
}
private static void HandleFieldEnumerable(SourceBuilder builder, MappedMember property)
{
HandleEnumerable(builder, property);
}
private static void HandlePropertyEnumerable(SourceBuilder builder, MappedMember property)
{
HandleEnumerable(builder, property);
}
private static string GetJsonArrayValue(MappedMember member, ref SourceBuilder builder)
{
if (member.isEnumerable)
{
// get underlying type (check if is a json extension)
builder.WriteLine("var arrStrBuilder = new StringBuilder();");
foreach (var named in member.NamedTypeSymbol?.TypeArguments!)
{
bool? containedTypeIsJsonEXtension = named?.HasAttribute(MappingContext.JsonExtensionAttributeSymbol);
if (!containedTypeIsJsonEXtension.HasValue) continue;
builder.WriteLine($"foreach (var v in {member.SourcePropertyName.ToString()})");
builder.WriteOpeningBracket();
builder.WriteLine("arrStrBuilder.Append(v.ToJson());");
builder.WriteLine("arrStrBuilder.Append(\", \");");
builder.WriteClosingBracket();
}
builder.WriteLine("arrStrBuilder.Remove(arrStrBuilder.Length -1, 1);");
}
return "{arrStrBuilder.ToString()}";
}
private static string GetJsonValue(MappedMember member, SourceBuilder builder)
{
if (member.FullyQualifiedType == "string") return $"\\\"{{{member.SourcePropertyName}}}\\\"";
if (member.FullyQualifiedType is "int" or "double" or "float" or "long") return $"{{{member.SourcePropertyName}}}";
return "";
}
private static string GetStringBuilderAppend(string stringToAppend)
{
return $"stringBuilder.Append($\"{stringToAppend}\");";
}
private static string GetStringBuilderAppendNoInterpolation(string stringToAppend)
{
return $"stringBuilder.Append(\"{stringToAppend}\");";
}
private static SourceBuilder WriteAssignmentMethod(this SourceBuilder builder, MappingModel model, System.Collections.Immutable.ImmutableArray<MappedMember>? otherProperties,
string? sourceClassParameterName, string mappingContextParameterName, bool fromUpdate)
{
List<MappedMember> _addedMembers = new List<MappedMember>();
foreach (var targetSourceType in model.MappedSourceTypes)
{
foreach (var property in targetSourceType.SourceProperties)
{
if (property.isReadOnly && fromUpdate) continue;
if(_addedMembers.Contains(property)) continue;
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};");
_addedMembers.Add(property);
}
foreach (var property in targetSourceType.SourceFields)
{
if (property.isReadOnly && fromUpdate) continue;
if(_addedMembers.Contains(property)) continue;
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};");
_addedMembers.Add(property);
}
if (otherProperties == null) return builder;
foreach (var property in otherProperties)
{
if(_addedMembers.Contains(property)) continue;
builder.WriteLine(property.MappedSourcePropertyTypeName is null
? $"{property.Name} = {property.SourcePropertyName.ToCamelCase()};"
: "");
_addedMembers.Add(property);
}
}
return builder;
}
private static SourceBuilder GenerateUpdateMethod(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName, targetSourceType)
.WriteLine($"public void Update({targetSourceType.SourceType} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteAssignmentMethod(model, null, sourceClassParameterName, "context", true)
.WriteClosingBracket()
.WriteLine();
}
return builder;
}
private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName,
MappedSourceType mappedSourceType)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Updates <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{mappedSourceType.SourceType}\"/> to use as source.</param>");
return builder;
}
private static SourceBuilder GenerateEnumerableJsonSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static string ToJson(this IEnumerable<{targetSourceType.SourceType}{model.Options.NullableReferenceSyntax}> {sourceClassParameterName}List)")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
.WriteClosingBracket();
}
return builder;
}
}
}

View File

@ -31,13 +31,14 @@ namespace MapTo.Extensions
public static string GetIdentifierName(this TypeDeclarationSyntax typeSyntax) => typeSyntax.Identifier.Text; public static string GetIdentifierName(this TypeDeclarationSyntax typeSyntax) => typeSyntax.Identifier.Text;
public static AttributeSyntax? GetAttribute(this TypeDeclarationSyntax typeDeclarationSyntax, string attributeName) public static AttributeSyntax? GetAttribute(this MemberDeclarationSyntax typeDeclarationSyntax, string attributeName)
{ {
return typeDeclarationSyntax.AttributeLists var attributeLists = typeDeclarationSyntax.AttributeLists;
var selection = attributeLists
.SelectMany(al => al.Attributes) .SelectMany(al => al.Attributes)
.SingleOrDefault(a => .FirstOrDefault(x => x.Name.ToString().Contains(attributeName));
(a.Name as IdentifierNameSyntax)?.Identifier.ValueText == attributeName ||
((a.Name as QualifiedNameSyntax)?.Right as IdentifierNameSyntax)?.Identifier.ValueText == attributeName); return selection;
} }
public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) => public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
@ -49,7 +50,7 @@ namespace MapTo.Extensions
public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) => public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
symbol.GetAttributes(attributeSymbol).FirstOrDefault(); symbol.GetAttributes(attributeSymbol).FirstOrDefault();
public static string? GetNamespace(this TypeDeclarationSyntax typeDeclarationSyntax) => typeDeclarationSyntax public static string? GetNamespace(this MemberDeclarationSyntax typeDeclarationSyntax) => typeDeclarationSyntax
.Ancestors() .Ancestors()
.OfType<NamespaceDeclarationSyntax>() .OfType<NamespaceDeclarationSyntax>()
.FirstOrDefault() .FirstOrDefault()
@ -67,6 +68,7 @@ namespace MapTo.Extensions
case IPropertySymbol propertySymbol: case IPropertySymbol propertySymbol:
typeSymbol = propertySymbol.Type; typeSymbol = propertySymbol.Type;
return true; return true;
case IFieldSymbol fieldSymbol: case IFieldSymbol fieldSymbol:
typeSymbol = fieldSymbol.Type; typeSymbol = fieldSymbol.Type;
return true; return true;
@ -85,7 +87,7 @@ namespace MapTo.Extensions
public static IPropertySymbol? FindProperty(this IEnumerable<IPropertySymbol> properties, IPropertySymbol targetProperty) public static IPropertySymbol? FindProperty(this IEnumerable<IPropertySymbol> properties, IPropertySymbol targetProperty)
{ {
return properties.SingleOrDefault(p => return properties.FirstOrDefault(p =>
p.Name == targetProperty.Name && p.Name == targetProperty.Name &&
(p.NullableAnnotation != NullableAnnotation.Annotated || (p.NullableAnnotation != NullableAnnotation.Annotated ||
p.NullableAnnotation == NullableAnnotation.Annotated && p.NullableAnnotation == NullableAnnotation.Annotated &&

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading;
using MapTo.Extensions; using MapTo.Extensions;
using MapTo.Sources; using MapTo.Sources;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
@ -17,7 +19,9 @@ namespace MapTo
/// <inheritdoc /> /// <inheritdoc />
public void Initialize(GeneratorInitializationContext context) public void Initialize(GeneratorInitializationContext context)
{ {
context.RegisterForSyntaxNotifications(() => new MapToSyntaxReceiver()); context.RegisterForSyntaxNotifications(() => new MapToSyntaxReceiver());
} }
/// <inheritdoc /> /// <inheritdoc />
@ -29,8 +33,9 @@ namespace MapTo
var compilation = context.Compilation var compilation = context.Compilation
.AddSource(ref context, UseUpdateAttributeSource.Generate(options)) .AddSource(ref context, UseUpdateAttributeSource.Generate(options))
.AddSource(ref context, JsonExtensionAttributeSource.Generate(options))
.AddSource(ref context, MapFromAttributeSource.Generate(options)) .AddSource(ref context, MapFromAttributeSource.Generate(options))
.AddSource(ref context, IgnorePropertyAttributeSource.Generate(options)) .AddSource(ref context, IgnoreMemberAttributeSource.Generate(options))
.AddSource(ref context, ITypeConverterSource.Generate(options)) .AddSource(ref context, ITypeConverterSource.Generate(options))
.AddSource(ref context, MapTypeConverterAttributeSource.Generate(options)) .AddSource(ref context, MapTypeConverterAttributeSource.Generate(options))
.AddSource(ref context, MapPropertyAttributeSource.Generate(options)) .AddSource(ref context, MapPropertyAttributeSource.Generate(options))
@ -53,6 +58,7 @@ namespace MapTo
foreach (var typeDeclarationSyntax in candidateTypes) foreach (var typeDeclarationSyntax in candidateTypes)
{ {
var mappingContext = MappingContext.Create(compilation, options, typeDeclarationSyntax); var mappingContext = MappingContext.Create(compilation, options, typeDeclarationSyntax);
mappingContext.Diagnostics.ForEach(context.ReportDiagnostic); mappingContext.Diagnostics.ForEach(context.ReportDiagnostic);
if (mappingContext.Model is null) if (mappingContext.Model is null)

View File

@ -20,7 +20,7 @@ namespace MapTo
var attributeSyntax = attributes var attributeSyntax = attributes
.SelectMany(a => a.Attributes) .SelectMany(a => a.Attributes)
.SingleOrDefault(a => a.Name is .FirstOrDefault(a => a.Name is
IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } // For: [MapFrom] IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } // For: [MapFrom]
or or
QualifiedNameSyntax // For: [MapTo.MapFrom] QualifiedNameSyntax // For: [MapTo.MapFrom]

View File

@ -0,0 +1,630 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
#pragma warning disable CS8602
namespace MapTo
{
internal static class MappingContextExtensions
{
internal static ImmutableArray<MappedMember> GetReadOnlyMappedProperties(this ImmutableArray<MappedMember> mappedProperties) => mappedProperties.Where(p => p.isReadOnly).ToImmutableArray()!;
internal static ImmutableArray<MappedMember> GetWritableMappedProperties(this ImmutableArray<MappedMember> mappedProperties) => mappedProperties.Where(p => !p.isReadOnly).ToImmutableArray()!;
}
internal abstract class MappingContext
{
private readonly List<SymbolDisplayPart> _ignoredNamespaces;
protected MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
_ignoredNamespaces = new();
Diagnostics = ImmutableArray<Diagnostic>.Empty;
Usings = ImmutableArray.Create("System", Constants.RootNamespace);
SourceGenerationOptions = sourceGenerationOptions;
TypeSyntax = typeSyntax;
Compilation = compilation;
IgnoreMemberAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(IgnoreMemberAttributeSource.FullyQualifiedName);
MapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapTypeConverterAttributeSource.FullyQualifiedName);
TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName);
MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName);
MapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName);
UseUpdateAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(UseUpdateAttributeSource.FullyQualifiedName);
JsonExtensionAttributeSymbol = compilation.GetTypeByMetadataNameOrThrow(JsonExtensionAttributeSource.FullyQualifiedName);
MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
}
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
public MappingModel? Model { get; private set; }
protected Compilation Compilation { get; }
protected INamedTypeSymbol IgnoreMemberAttributeTypeSymbol { get; }
protected INamedTypeSymbol MapFromAttributeTypeSymbol { get; }
protected INamedTypeSymbol UseUpdateAttributeTypeSymbol { get; }
public static INamedTypeSymbol JsonExtensionAttributeSymbol { get; set; } = null!;
protected INamedTypeSymbol MappingContextTypeSymbol { get; }
protected INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; }
protected INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; }
protected SourceGenerationOptions SourceGenerationOptions { get; }
protected INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; }
protected TypeDeclarationSyntax TypeSyntax { get; }
protected ImmutableArray<string> Usings { get; private set; }
public static MappingContext Create(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
MappingContext context = typeSyntax switch
{
StructDeclarationSyntax => new StructMappingContext(compilation, sourceGenerationOptions, typeSyntax),
ClassDeclarationSyntax => new ClassMappingContext(compilation, sourceGenerationOptions, typeSyntax),
RecordDeclarationSyntax => new RecordMappingContext(compilation, sourceGenerationOptions, typeSyntax),
_ => throw new ArgumentOutOfRangeException()
};
context.Model = context.CreateMappingModel();
return context;
}
protected void AddDiagnostic(Diagnostic diagnostic)
{
Diagnostics = Diagnostics.Add(diagnostic);
}
protected void AddUsingIfRequired(ISymbol? namedTypeSymbol) =>
AddUsingIfRequired(namedTypeSymbol?.ContainingNamespace.IsGlobalNamespace == false, namedTypeSymbol?.ContainingNamespace);
protected void AddUsingIfRequired(bool condition, INamespaceSymbol? ns) =>
AddUsingIfRequired(condition && ns is not null && !_ignoredNamespaces.Contains(ns.ToDisplayParts().First()), ns?.ToDisplayString());
protected void AddUsingIfRequired(bool condition, string? ns)
{
if (ns is not null && condition && ns != TypeSyntax.GetNamespace() && !Usings.Contains(ns))
{
Usings = Usings.Add(ns);
}
}
protected IPropertySymbol? FindSourceProperty(IEnumerable<IPropertySymbol> sourceProperties, ISymbol property)
{
var propertyName = property
.GetAttribute(MapPropertyAttributeTypeSymbol)
?.NamedArguments
.FirstOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
.Value.Value as string ?? property.Name;
return sourceProperties.FirstOrDefault(p => p.Name == propertyName);
}
protected IFieldSymbol? FindSourceField(IEnumerable<IFieldSymbol> sourceProperties, ISymbol property)
{
var propertyName = property
.GetAttribute(MapPropertyAttributeTypeSymbol)
?.NamedArguments
.FirstOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
.Value.Value as string ?? property.Name;
return sourceProperties.FirstOrDefault(p => p.Name == propertyName);
}
protected abstract ImmutableArray<MappedMember> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected abstract ImmutableArray<MappedMember> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected abstract ImmutableArray<MappedMember> GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected abstract ImmutableArray<MappedMember> GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected ImmutableArray<INamedTypeSymbol> GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null)
{
var attributeData = typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName);
var sourceSymbol = GetSourceTypeSymbol(attributeData, semanticModel);
return sourceSymbol;
}
// we need two possible InamedTypeSymbol
protected ImmutableArray<INamedTypeSymbol> GetSourceTypeSymbol(SyntaxNode? attributeSyntax, SemanticModel? semanticModel = null)
{
if (attributeSyntax is null)
{
return new ImmutableArray<INamedTypeSymbol>(){};
}
semanticModel ??= Compilation.GetSemanticModel(attributeSyntax.SyntaxTree);
var descendentNodes = attributeSyntax
.DescendantNodes();
var sourceTypeExpressionSyntax = descendentNodes
.OfType<TypeOfExpressionSyntax>()
.ToImmutableArray();
// cast
var resultList = new List<INamedTypeSymbol>();
for (int i = 0; i < sourceTypeExpressionSyntax.Length; i++)
{
var sourceTypeExpression = sourceTypeExpressionSyntax[i];
if (semanticModel.GetTypeInfo(sourceTypeExpression.Type).Type is INamedTypeSymbol namedTypeSymbol)
{
resultList.Add(namedTypeSymbol);
}
}
return resultList.ToImmutableArray();
}
protected bool IsTypeInheritFromMappedBaseClass(SemanticModel semanticModel)
{
return TypeSyntax.BaseList is not null && TypeSyntax.BaseList.Types
.Select(t => semanticModel.GetTypeInfo(t.Type).Type)
.Any(t => t?.GetAttribute(MapFromAttributeTypeSymbol) != null);
}
protected bool IsTypeUpdatable()
{
return TypeSyntax.GetAttribute("UseUpdate") != null;
}
protected bool HasJsonExtension()
{
return TypeSyntax.GetAttribute("JsonExtension") != null;
}
protected virtual MappedMember? MapProperty(ISymbol sourceTypeSymbol, IReadOnlyCollection<IPropertySymbol> sourceProperties, ISymbol property)
{
var sourceProperty = FindSourceProperty(sourceProperties, property);
if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
string? converterFullyQualifiedName = null;
var converterParameters = ImmutableArray<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null;
ITypeSymbol? enumerableTypeArgumentType = null;
if (!Compilation.HasCompatibleTypes(sourceProperty, property))
{
if (!TryGetMapTypeConverterForProperty(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) &&
!TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType))
{
return null;
}
}
AddUsingIfRequired(propertyType);
AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType);
INamedTypeSymbol? namedType;
var isEnumerable = IsEnumerable(property, out namedType);
return new MappedMember(
property.Name,
property.GetTypeSymbol().ToString(),
ToQualifiedDisplayName(propertyType) ?? propertyType.Name,
converterFullyQualifiedName,
converterParameters.ToImmutableArray(),
sourceProperty.Name,
ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType),
property,
namedType,
isEnumerable,
(property as IPropertySymbol).IsReadOnly);
;
}
protected virtual MappedMember? MapField(ISymbol sourceTypeSymbol, IReadOnlyCollection<IFieldSymbol> sourceProperties, ISymbol property)
{
var sourceProperty = FindSourceField(sourceProperties, property);
if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
if (property is IFieldSymbol symbol)
{
if (symbol.AssociatedSymbol != null) return null;
}
string? converterFullyQualifiedName = null;
var converterParameters = ImmutableArray<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null;
ITypeSymbol? enumerableTypeArgumentType = null;
if (!Compilation.HasCompatibleTypes(sourceProperty, property))
{
if (!TryGetMapTypeConverterForField(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) &&
!TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType))
{
return null;
}
}
AddUsingIfRequired(propertyType);
AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType);
INamedTypeSymbol? namedType;
var isEnumerable = IsEnumerable(property, out namedType);
return new MappedMember(
property.Name,
property.GetTypeSymbol()?.ToString() ?? throw new InvalidOperationException(),
ToQualifiedDisplayName(propertyType) ?? propertyType.Name,
converterFullyQualifiedName,
converterParameters.ToImmutableArray(),
sourceProperty.Name,
ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType),
property,
namedType,
isEnumerable,
(property as IFieldSymbol).IsReadOnly);
;
}
protected virtual MappedMember? MapPropertySimple(ISymbol sourceTypeSymbol, ISymbol property)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
string? converterFullyQualifiedName = null;
var converterParameters = ImmutableArray<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null;
ITypeSymbol? enumerableTypeArgumentType = null;
AddUsingIfRequired(propertyType);
AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType);
INamedTypeSymbol? namedType;
var isEnumerable = IsEnumerable(property, out namedType);
return new MappedMember(
property.Name,
property.GetTypeSymbol().ToString(),
ToQualifiedDisplayName(propertyType) ?? propertyType.Name,
converterFullyQualifiedName,
converterParameters.ToImmutableArray(),
property.Name,
ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType),
property,
namedType,
isEnumerable,
#pragma warning disable CS8602
(property as IPropertySymbol).IsReadOnly);
#pragma warning restore CS8602
;
}
protected virtual MappedMember? MapFieldSimple(ISymbol sourceTypeSymbol, ISymbol property)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
if(property is IFieldSymbol symbol)
{
if (symbol.AssociatedSymbol != null) return null;
}
string? converterFullyQualifiedName = null;
var converterParameters = ImmutableArray<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null;
ITypeSymbol? enumerableTypeArgumentType = null;
AddUsingIfRequired(propertyType);
AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType);
INamedTypeSymbol? namedType;
var isEnumerable = IsEnumerable(property, out namedType);
return new MappedMember(
property.Name,
property.GetTypeSymbol().ToString(),
ToQualifiedDisplayName(propertyType) ?? propertyType.Name,
converterFullyQualifiedName,
converterParameters.ToImmutableArray(),
property.Name,
ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType),
property,
namedType,
isEnumerable,
(property as IFieldSymbol).IsReadOnly);
;
}
protected bool TryGetMapTypeConverterForProperty(ISymbol property, IPropertySymbol sourceProperty, out string? converterFullyQualifiedName,
out ImmutableArray<string> converterParameters)
{
converterFullyQualifiedName = null;
converterParameters = ImmutableArray<string>.Empty;
if (!Diagnostics.IsEmpty())
{
return false;
}
var typeConverterAttribute = property.GetAttribute(MapTypeConverterAttributeTypeSymbol);
if (typeConverterAttribute?.ConstructorArguments.First().Value is not INamedTypeSymbol converterTypeSymbol)
{
return false;
}
var baseInterface = GetTypeConverterBaseInterfaceForProperty(converterTypeSymbol, property, sourceProperty);
if (baseInterface is null)
{
AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, sourceProperty));
return false;
}
converterFullyQualifiedName = converterTypeSymbol.ToDisplayString();
converterParameters = GetTypeConverterParameters(typeConverterAttribute);
return true;
}
protected bool TryGetMapTypeConverterForField(ISymbol property, IFieldSymbol sourceProperty, out string? converterFullyQualifiedName,
out ImmutableArray<string> converterParameters)
{
converterFullyQualifiedName = null;
converterParameters = ImmutableArray<string>.Empty;
if (!Diagnostics.IsEmpty())
{
return false;
}
var typeConverterAttribute = property.GetAttribute(MapTypeConverterAttributeTypeSymbol);
if (typeConverterAttribute?.ConstructorArguments.First().Value is not INamedTypeSymbol converterTypeSymbol)
{
return false;
}
var baseInterface = GetTypeConverterBaseInterfaceForField(converterTypeSymbol, property, sourceProperty);
if (baseInterface is null)
{
//AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, null));
return false;
}
converterFullyQualifiedName = converterTypeSymbol.ToDisplayString();
converterParameters = GetTypeConverterParameters(typeConverterAttribute);
return true;
}
protected bool TryGetNestedObjectMappings(ISymbol property, out ITypeSymbol? mappedSourcePropertyType, out ITypeSymbol? enumerableTypeArgument)
{
mappedSourcePropertyType = null;
enumerableTypeArgument = null;
if (!Diagnostics.IsEmpty())
{
return false;
}
if (!property.TryGetTypeSymbol(out var propertyType))
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
return false;
}
var mapFromAttribute = propertyType.GetAttribute(MapFromAttributeTypeSymbol);
if (mapFromAttribute is null &&
propertyType is INamedTypeSymbol namedTypeSymbol &&
!propertyType.IsPrimitiveType() &&
(Compilation.IsGenericEnumerable(propertyType) || propertyType.AllInterfaces.Any(i => Compilation.IsGenericEnumerable(i))))
{
enumerableTypeArgument = namedTypeSymbol.TypeArguments.First();
mapFromAttribute = enumerableTypeArgument.GetAttribute(MapFromAttributeTypeSymbol);
}
mappedSourcePropertyType = mapFromAttribute?.ConstructorArguments.First().Value as INamedTypeSymbol;
if (mappedSourcePropertyType is null && enumerableTypeArgument is null)
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
}
return Diagnostics.IsEmpty();
}
protected bool IsEnumerable(ISymbol property, out INamedTypeSymbol? namedTypeSymbolResult)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
namedTypeSymbolResult = null;
return false;
}
if (
propertyType is INamedTypeSymbol namedTypeSymbol &&
!propertyType.IsPrimitiveType() &&
(Compilation.IsGenericEnumerable(propertyType) || propertyType.AllInterfaces.Any(i => Compilation.IsGenericEnumerable(i))))
{
namedTypeSymbolResult = namedTypeSymbol;
return true;
}
namedTypeSymbolResult = null;
return false;
}
private static ImmutableArray<string> GetTypeConverterParameters(AttributeData typeConverterAttribute)
{
var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault();
return converterParameter.IsNull
? ImmutableArray<string>.Empty
: converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()).ToImmutableArray();
}
private MappingModel? CreateMappingModel()
{
var semanticModel = Compilation.GetSemanticModel(TypeSyntax.SyntaxTree);
if (semanticModel.GetDeclaredSymbol(TypeSyntax) is not INamedTypeSymbol typeSymbol)
{
AddDiagnostic(DiagnosticsFactory.TypeNotFoundError(TypeSyntax.GetLocation(), TypeSyntax.Identifier.ValueText));
return null;
}
// We can have 2 sources...
var sourceTypeSymbols = GetSourceTypeSymbol(TypeSyntax, semanticModel);
// lets pick one for now, and then think what to do with the second one
if (sourceTypeSymbols.IsDefaultOrEmpty)
{
AddDiagnostic(DiagnosticsFactory.MapFromAttributeNotFoundError(TypeSyntax.GetLocation()));
return null;
}
var typeIdentifierName = TypeSyntax.GetIdentifierName();
var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
var isTypeUpdatable = true; //IsTypeUpdatable();
var hasJsonExtension = false; // HasJsonExtension();
List<MappedSourceType> mappedSourceTypes = new List<MappedSourceType>();
foreach (var sourceTypeSymbol in sourceTypeSymbols)
{
_ignoredNamespaces.Add(sourceTypeSymbol.ContainingNamespace.ToDisplayParts().First());
var sourceTypeIdentifierName = sourceTypeSymbol.Name;
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
var mappedProperties = GetSourceMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
var mappedFields = GetSourceMappedFields(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq");
var allProperties = GetTypeMappedProperties(sourceTypeSymbol, typeSymbol , isTypeInheritFromMappedBaseClass);
var allFields = GetTypeMappedFields(sourceTypeSymbol, typeSymbol, isTypeInheritFromMappedBaseClass);
mappedSourceTypes.Add(new MappedSourceType(
sourceTypeSymbol.ContainingNamespace.ToDisplayString(),
sourceTypeIdentifierName,
sourceTypeSymbol.ToDisplayString(),
mappedProperties, mappedFields, allProperties, allFields, shouldGenerateSecondaryConstructor));
}
//var sourceTypeSymbol = sourceTypeSymbols[0];
// Pick first one to avoid errors. TODO: Make possible to use different source types
/*if (!mappedProperties.Any())
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol));
return null;
}*/
return new MappingModel(
SourceGenerationOptions,
TypeSyntax.GetNamespace(),
TypeSyntax.Modifiers,
TypeSyntax.Keyword.Text,
typeIdentifierName,
isTypeUpdatable,
hasJsonExtension,
mappedSourceTypes.ToImmutableArray(),
isTypeInheritFromMappedBaseClass,
Usings);
}
private INamedTypeSymbol? GetTypeConverterBaseInterfaceForProperty(ITypeSymbol converterTypeSymbol, ISymbol property, IPropertySymbol sourceProperty)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
return converterTypeSymbol.AllInterfaces
.FirstOrDefault(i =>
i.TypeArguments.Length == 2 &&
SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) &&
SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) &&
SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1]));
}
private INamedTypeSymbol? GetTypeConverterBaseInterfaceForField(ITypeSymbol converterTypeSymbol, ISymbol property, IFieldSymbol sourceProperty)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
return converterTypeSymbol.AllInterfaces
.FirstOrDefault(i =>
i.TypeArguments.Length == 2 &&
SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) &&
SymbolEqualityComparer.Default.Equals(sourceProperty.Type, i.TypeArguments[0]) &&
SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1]));
}
private bool ShouldGenerateSecondaryConstructor(SemanticModel semanticModel, ISymbol sourceTypeSymbol)
{
var constructorSyntax = TypeSyntax.DescendantNodes()
.OfType<ConstructorDeclarationSyntax>()
.FirstOrDefault(c =>
c.ParameterList.Parameters.Count == 1 &&
SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(c.ParameterList.Parameters.Single().Type!).ConvertedType, sourceTypeSymbol));
if (constructorSyntax is null)
{
// Secondary constructor is not defined.
return true;
}
if (constructorSyntax.Initializer?.ArgumentList.Arguments is not { Count: 2 } arguments ||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[0].Expression).ConvertedType, MappingContextTypeSymbol) ||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[1].Expression).ConvertedType, sourceTypeSymbol))
{
AddDiagnostic(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
}
return false;
}
private string? ToQualifiedDisplayName(ISymbol? symbol)
{
if (symbol is null)
{
return null;
}
var containingNamespace = TypeSyntax.GetNamespace();
var symbolNamespace = symbol.ContainingNamespace.ToDisplayString();
return containingNamespace != symbolNamespace && _ignoredNamespaces.Contains(symbol.ContainingNamespace.ToDisplayParts().First())
? symbol.ToDisplayString()
: symbol.Name;
}
}
}

View File

@ -0,0 +1,222 @@
using System;
using System.Collections.Immutable;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace MapTo
{
internal enum AccessModifier
{
Public,
Internal,
Private
}
internal enum NullStaticAnalysisState
{
Default,
Enabled,
Disabled
}
internal record SourceCode(string Text, string HintName);
internal record MappedMember(
string Name,
string FullyQualifiedType,
string Type,
string? TypeConverter,
ImmutableArray<string> TypeConverterParameters,
string SourcePropertyName,
string? MappedSourcePropertyTypeName,
string? EnumerableTypeArgument,
ISymbol ActualSymbol,
INamedTypeSymbol? NamedTypeSymbol,
bool isEnumerable,
bool isReadOnly)
{
public bool IsEnumerable => EnumerableTypeArgument is not null;
}
internal record MappedSourceType
(
string SourceNamespace,
string SourceTypeIdentifierName,
string SourceTypeFullName,
ImmutableArray<MappedMember> SourceProperties,
ImmutableArray<MappedMember> SourceFields,
ImmutableArray<MappedMember> TypeProperties,
ImmutableArray<MappedMember> TypeFields,
bool GenerateSecondaryConstructor
)
{
public string SourceType => SourceTypeFullName;
}
internal record MappingModel(
SourceGenerationOptions Options,
string? Namespace,
SyntaxTokenList Modifiers,
string Type,
string TypeIdentifierName,
bool IsTypeUpdatable,
bool IsJsonExtension,
ImmutableArray<MappedSourceType> MappedSourceTypes,
bool HasMappedBaseClass,
ImmutableArray<string> Usings
);
internal interface IEfMethodsModel { }
internal record EfMethodsModel(
SourceGenerationOptions Options,
string Namespace,
string ContextTypeName,
string ContextFullType,
ImmutableArray<EfEntityDataModel> MethodsModels,
ImmutableArray<string> Usings
);
internal class EfEntityDataModel
{
public string PropertyName { get; set; }
public string EntityTypeFullName { get; set; }
public string EntityTypeIdentifierName { get; set; }
public EfEntityDataModel(string propertyName, string entityTypeFullName, string entityTypeIdentifierName)
{
PropertyName = propertyName;
EntityTypeFullName = entityTypeFullName;
EntityTypeIdentifierName = entityTypeIdentifierName;
}
}
internal class EfAddMethodsModel : EfEntityDataModel
{
public string CreateTypeFullName { get; set; }
public string CreateTypeIdentifierName { get; set; }
public string ReturnTypeFullName { get; set; }
public string ReturnTypeIdentifierName { get; set; }
public EfAddMethodsModel(EfEntityDataModel entity, string createTypeFullName,
string createTypeIdentifierName,
string returnTypeFullName,
string returnTypeIdentifierName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
CreateTypeFullName = createTypeFullName;
CreateTypeIdentifierName = createTypeIdentifierName;
ReturnTypeIdentifierName = returnTypeIdentifierName;
ReturnTypeFullName = returnTypeFullName;
}
}
internal class EfGetOneByModel : EfEntityDataModel
{
public string ByParamPropertyName { get; set; }
public string ByParamFullTypeName { get; set; }
public string ByParamTypeName { get; set; }
public string ReturnTypeIdentifierName { get; set; }
public string ReturnTypeFullName { get; set; }
public EfGetOneByModel(EfEntityDataModel entity, string byParamPropertyName,
string byParamFullTypeName,
string returnTypeFullName,
string returnTypeIdentifierName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
ByParamPropertyName = byParamPropertyName;
ByParamFullTypeName = byParamFullTypeName;
ReturnTypeIdentifierName = returnTypeIdentifierName;
ReturnTypeFullName = returnTypeFullName;
ByParamTypeName = returnTypeFullName;
}
}
internal class EfGetOneWithModel : EfEntityDataModel
{
public string ReturnTypeIdentifierName { get; set; }
public string ReturnTypeFullName { get; set; }
public EfGetOneWithModel(EfEntityDataModel entity, string returnTypeFullName,
string returnTypeIdentifierName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
ReturnTypeIdentifierName = returnTypeIdentifierName;
ReturnTypeFullName = returnTypeFullName;
}
}
internal class EfGetManyModel : EfEntityDataModel
{
public string ReturnTypeIdentifierName { get; set; }
public string ReturnTypeFullName { get; set; }
public EfGetManyModel(EfEntityDataModel entity, string returnTypeFullName,
string returnTypeIdentifierName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
ReturnTypeIdentifierName = returnTypeIdentifierName;
ReturnTypeFullName = returnTypeFullName;
}
}
internal class EfUpdateMethodsModel : EfEntityDataModel
{
public string UpdateTypeFullName;
public string UpdateTypeIdentifierName;
public string ReturnTypeFullName;
public string ReturnTypeIdentifierName;
public string KeyPropertyName;
public string KeyFullTypeName;
public EfUpdateMethodsModel(EfEntityDataModel entity,
string updateTypeFullName,
string updateTypeIdentifierName,
string returnTypeFullName,
string returnTypeIdentifierName,
string keyPropertyName,
string keyFullTypeName) : base(entity.PropertyName, entity.EntityTypeFullName, entity.EntityTypeIdentifierName)
{
UpdateTypeFullName = updateTypeFullName;
UpdateTypeIdentifierName = updateTypeIdentifierName;
ReturnTypeFullName= returnTypeFullName;
ReturnTypeIdentifierName = returnTypeIdentifierName;
KeyPropertyName = keyPropertyName;
KeyFullTypeName = keyFullTypeName;
}
}
internal record SourceGenerationOptions(
AccessModifier ConstructorAccessModifier,
AccessModifier GeneratedMethodsAccessModifier,
bool GenerateXmlDocument,
bool SupportNullableReferenceTypes,
bool SupportNullableStaticAnalysis)
{
internal static SourceGenerationOptions From(GeneratorExecutionContext context)
{
const string allowNullAttributeName = "System.Diagnostics.CodeAnalysis.AllowNullAttribute";
var supportNullableStaticAnalysis = context.GetBuildGlobalOption(propertyName: nameof(SupportNullableStaticAnalysis), NullStaticAnalysisState.Default);
var supportNullableReferenceTypes = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Warnings or NullableContextOptions.Enable;
return new(
ConstructorAccessModifier: context.GetBuildGlobalOption(propertyName: nameof(ConstructorAccessModifier), AccessModifier.Public),
GeneratedMethodsAccessModifier: context.GetBuildGlobalOption(propertyName: nameof(GeneratedMethodsAccessModifier), AccessModifier.Public),
GenerateXmlDocument: context.GetBuildGlobalOption(propertyName: nameof(GenerateXmlDocument), true),
SupportNullableReferenceTypes: supportNullableReferenceTypes,
SupportNullableStaticAnalysis: supportNullableStaticAnalysis switch
{
NullStaticAnalysisState.Enabled => true,
NullStaticAnalysisState.Disabled => false,
_ => context.Compilation is CSharpCompilation { LanguageVersion: >= LanguageVersion.CSharp8 } cs && cs.TypeByMetadataNameExists(allowNullAttributeName)
}
);
}
public string NullableReferenceSyntax => SupportNullableReferenceTypes ? "?" : string.Empty;
}
}

View File

@ -11,7 +11,12 @@ 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> GetSourceMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) protected override ImmutableArray<MappedMember> GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
throw new System.NotImplementedException();
}
protected override ImmutableArray<MappedMember> GetSourceMappedProperties(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()
@ -19,13 +24,18 @@ namespace MapTo
.OrderByDescending(s => s.Parameters.Length) .OrderByDescending(s => s.Parameters.Length)
.First(s => s.Name == ".ctor") .First(s => s.Name == ".ctor")
.Parameters .Parameters
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.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> GetTypeMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) protected override ImmutableArray<MappedMember> GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
throw new System.NotImplementedException();
}
protected override ImmutableArray<MappedMember> GetTypeMappedProperties(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()
@ -33,7 +43,7 @@ namespace MapTo
.OrderByDescending(s => s.Parameters.Length) .OrderByDescending(s => s.Parameters.Length)
.First(s => s.Name == ".ctor") .First(s => s.Name == ".ctor")
.Parameters .Parameters
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) .Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapProperty(typeSymbol, sourceProperties, property)) .Select(property => MapProperty(typeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null) .Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!; .ToImmutableArray()!;

View File

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

View File

@ -2,9 +2,9 @@
namespace MapTo.Sources namespace MapTo.Sources
{ {
internal static class IgnorePropertyAttributeSource internal static class IgnoreMemberAttributeSource
{ {
internal const string AttributeName = "IgnoreProperty"; internal const string AttributeName = "IgnoreMemberMapTo";
internal const string AttributeClassName = AttributeName + "Attribute"; internal const string AttributeClassName = AttributeName + "Attribute";
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
@ -26,7 +26,7 @@ namespace MapTo.Sources
} }
builder builder
.WriteLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]") .WriteLine("[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = false, AllowMultiple = false)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}") .WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}")
.WriteClosingBracket(); .WriteClosingBracket();

View File

@ -21,7 +21,7 @@ namespace MapTo.Sources
{ {
builder builder
.WriteLine("/// <summary>") .WriteLine("/// <summary>")
.WriteLine("/// Specifies that the annotated needs ToJson method") .WriteLine("/// Specifies that the annotated class has a json extension.")
.WriteLine("/// </summary>"); .WriteLine("/// </summary>");
} }

View File

@ -42,6 +42,13 @@ namespace MapTo.Sources
builder builder
.WriteLine($"public {AttributeName}Attribute(Type sourceType)") .WriteLine($"public {AttributeName}Attribute(Type sourceType)")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine("SourceType = new [] { sourceType };")
.WriteClosingBracket()
.WriteLine();
builder
.WriteLine($"public {AttributeName}Attribute(Type[] sourceType)")
.WriteOpeningBracket()
.WriteLine("SourceType = sourceType;") .WriteLine("SourceType = sourceType;")
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine(); .WriteLine();
@ -55,7 +62,7 @@ namespace MapTo.Sources
} }
builder builder
.WriteLine("public Type SourceType { get; }") .WriteLine("public Type[] SourceType { get; }")
.WriteClosingBracket() // class .WriteClosingBracket() // class
.WriteClosingBracket(); // namespace .WriteClosingBracket(); // namespace

View File

@ -23,6 +23,7 @@ namespace MapTo.Sources
if (options.GenerateXmlDocument) if (options.GenerateXmlDocument)
{ {
builder builder
.WriteLine()
.WriteLine("/// <summary>") .WriteLine("/// <summary>")
.WriteLine("/// Specifies the mapping behavior of the annotated property.") .WriteLine("/// Specifies the mapping behavior of the annotated property.")
.WriteLine("/// </summary>") .WriteLine("/// </summary>")

View File

@ -0,0 +1,235 @@
using System;
using MapTo.Extensions;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapRecordSource
{
internal static SourceCode Generate(MappingModel model)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
.WriteUsings(model.Usings)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {model.Namespace}")
.WriteOpeningBracket()
// Class declaration
.WriteLine($"partial record {model.TypeIdentifierName}")
.WriteOpeningBracket();
foreach (var targetSourceType in model.MappedSourceTypes)
{
if (targetSourceType.GenerateSecondaryConstructor)
{
builder
.GenerateSecondaryConstructor(model)
.WriteLine();
}
}
// Class body
builder
.GeneratePrivateConstructor(model)
.WriteLine()
.GenerateFactoryMethod(model)
// End class declaration
.WriteClosingBracket()
.WriteLine()
// Extension class declaration
.GenerateSourceTypeExtensionClass(model)
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs");
}
private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model)
{
// grab first data from array
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
if (model.Options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
}
builder .WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({targetSourceType.SourceType} {sourceClassParameterName})")
.WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}");
}
return builder;
}
private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model)
{
const string mappingContextParameterName = "context";
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.WriteLine(
$"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {targetSourceType.SourceType} {sourceClassParameterName})")
.Indent()
.Write(": this(").WriteProperties(model, sourceClassParameterName, mappingContextParameterName)
.WriteLine(")")
.Unindent()
.WriteOpeningBracket()
.WriteLine($"if ({mappingContextParameterName} == null) throw new ArgumentNullException(nameof({mappingContextParameterName}));")
.WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));")
.WriteLine()
.WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);")
.WriteClosingBracket();
}
// End constructor declaration
return builder;
}
private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, string sourceClassParameterName,
string mappingContextParameterName)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
for (var i = 0; i < targetSourceType.SourceProperties.Length; i++)
{
var property = targetSourceType.SourceProperties[i];
if (property.TypeConverter is null)
{
if (property.IsEnumerable)
{
builder.Write(
$"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList()");
}
else
{
builder.Write(property.MappedSourcePropertyTypeName is null
? $"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}"
: $"{property.Name}: {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName})");
}
}
else
{
var parameters = property.TypeConverterParameters.IsEmpty
? "null"
: $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}";
builder.Write(
$"{property.Name}: new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters})");
}
if (i < targetSourceType.SourceProperties.Length - 1)
{
builder.Write(", ");
}
}
}
return builder;
}
private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} From({targetSourceType.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine(
$"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{targetSourceType.SourceType}, {model.TypeIdentifierName}>({sourceClassParameterName});")
.WriteClosingBracket();
}
return builder;
}
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
foreach (var targetSourceType in model.MappedSourceTypes)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{targetSourceType.SourceType}\"/> to use as source.</param>")
.WriteLine(
$"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
return builder;
}
private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
builder
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {targetSourceType.SourceTypeIdentifierName}To{model.TypeIdentifierName}Extensions")
.WriteOpeningBracket()
.GenerateSourceTypeExtensionMethod(model)
.WriteClosingBracket();
}
return builder;
}
private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{
foreach (var targetSourceType in model.MappedSourceTypes)
{
var sourceClassParameterName = targetSourceType.SourceTypeIdentifierName.ToCamelCase();
builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} To{model.TypeIdentifierName}(this {targetSourceType.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
.WriteClosingBracket();
}
return builder;
}
}
}

View File

@ -0,0 +1,67 @@
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal class StructMappingContext : MappingContext
{
internal StructMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedMember> GetSourceMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IFieldSymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IFieldSymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapField(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedFields(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IFieldSymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IFieldSymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapFieldSimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedMember> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnoreMemberAttributeTypeSymbol))
.Select(property => MapPropertySimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -1,39 +0,0 @@
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal class ClassMappingContext : MappingContext
{
internal ClassMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedProperty> GetSourceMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().ToArray();
return typeSymbol
.GetAllMembers(!isInheritFromMappedBaseClass)
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedProperty> GetTypeMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().ToArray();
return typeSymbol
.GetAllMembers()
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapProperty(typeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -1,51 +0,0 @@
using MapTo.Sources;
using System;
using System.Collections.Generic;
using System.Text;
namespace MapTo.Extensions
{
internal static class CommonExtensions
{
internal static SourceBuilder WriteComment(this SourceBuilder builder, string comment = "")
{
return builder.WriteLine($"// {comment}");
}
internal static SourceBuilder WriteModelInfo(this SourceBuilder builder, MappingModel model)
{
return builder
.WriteLine()
.WriteComment($" IsTypeUpdatable {model.IsTypeUpdatable}")
.WriteComment($" HasMappedBaseClass {model.HasMappedBaseClass.ToString()}")
.WriteComment($" Namespace {model.Namespace}")
.WriteComment($" Options {model.Options.ToString()}")
.WriteComment($" Type {model.Type}")
.WriteComment($" TypeIdentifierName {model.TypeIdentifierName}")
.WriteComment($" SourceNamespace {model.SourceNamespace}")
.WriteComment($" SourceTypeFullName {model.SourceTypeFullName}")
.WriteComment($" SourceTypeIdentifierName {model.SourceTypeIdentifierName}");
}
internal static SourceBuilder WriteMappedProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray<MappedProperty> mappedProperties)
{
foreach (var item in mappedProperties)
{
builder .WriteComment($" Name {item.Name}")
.WriteComment($" Type {item.Type}")
.WriteComment($" MappedSourcePropertyTypeName {item.MappedSourcePropertyTypeName}")
.WriteComment($" IsEnumerable {item.IsEnumerable}")
.WriteComment($" FullyQualifiedType {item.FullyQualifiedType}")
.WriteComment($" EnumerableTypeArgument {item.EnumerableTypeArgument}")
.WriteComment($" SourcePropertyName {item.SourcePropertyName}")
.WriteComment($" TypeSymbol {item.FullyQualifiedType.ToString()}")
.WriteComment($" isReadOnly {item.isReadOnly.ToString()}")
.WriteLine();
}
return builder;
}
}
}

View File

@ -1,184 +0,0 @@
using MapTo.Sources;
using static MapTo.Sources.Constants;
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Immutable;
namespace MapTo.Extensions
{
internal static class CommonSource
{
internal static SourceCode GenerateStructOrClass(this MappingModel model, string structOrClass)
{
const bool writeDebugInfo = true;
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
.WriteUsings(model.Usings)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {model.Namespace}")
.WriteOpeningBracket();
if(writeDebugInfo)
builder
.WriteModelInfo(model)
.WriteLine()
.WriteComment("Type properties")
.WriteComment()
.WriteMappedProperties(model.TypeProperties)
.WriteLine()
.WriteComment("Source properties")
.WriteMappedProperties(model.SourceProperties)
.WriteLine();
builder
// Class declaration
.WriteLine($"partial {structOrClass} {model.TypeIdentifierName}")
.WriteOpeningBracket()
.WriteLine()
// Class body
.GeneratePublicConstructor(model);
if (model.IsTypeUpdatable && model.TypeProperties.GetWritableMappedProperties().Length > 0) builder.GenerateUpdateMethod(model);
builder
.WriteLine()
// End class declaration
.WriteClosingBracket()
.WriteLine()
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs");
}
private static SourceBuilder GeneratePublicConstructor(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
const string mappingContextParameterName = "context";
var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty;
var stringBuilder = new StringBuilder();
var otherProperties = new List<MappedProperty>();
foreach (var property in model.TypeProperties)
{
if (!model.SourceProperties.IsMappedProperty(property))
{
stringBuilder.Append(", ");
stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}");
otherProperties.Add(property);
}
}
var readOnlyPropertiesArguments = stringBuilder.ToString();
builder
.WriteLine($"public {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName}{readOnlyPropertiesArguments}){baseConstructor}")
.WriteOpeningBracket()
.TryWriteProperties(model.SourceProperties, otherProperties.ToArray().ToImmutableArray(), sourceClassParameterName, mappingContextParameterName, false);
// End constructor declaration
return builder.WriteClosingBracket();
}
private static bool IsMappedProperty(this ImmutableArray<MappedProperty> properties, MappedProperty property) {
foreach(var prop in properties)
{
if (prop.Name == property.Name) return true;
}
return false;
}
private static SourceBuilder TryWriteProperties(this SourceBuilder builder, ImmutableArray<MappedProperty> properties, System.Collections.Immutable.ImmutableArray<MappedProperty>? otherProperties,
string? sourceClassParameterName, string mappingContextParameterName, bool fromUpdate)
{
if (fromUpdate)
{
properties = properties.GetWritableMappedProperties();
}
foreach (var property in properties)
{
if (property.isReadOnly && fromUpdate) continue;
if (property.TypeConverter is null)
{
if (property.IsEnumerable)
{
builder.WriteLine(
$"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList();");
}
else
{
builder.WriteLine(property.MappedSourcePropertyTypeName is null
? $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};"
: "");
}
}
else
{
var parameters = property.TypeConverterParameters.IsEmpty
? "null"
: $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}";
builder.WriteLine(
$"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters});");
}
}
if (otherProperties == null) return builder;
foreach (var property in otherProperties)
{
builder.WriteLine(property.MappedSourcePropertyTypeName is null
? $"{property.Name} = {property.SourcePropertyName.ToCamelCase()};"
: "");
}
return builder;
}
private static SourceBuilder GenerateUpdateMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
builder
.GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName)
.WriteLine($"public void Update({model.SourceType} {sourceClassParameterName})")
.WriteOpeningBracket()
.TryWriteProperties(model.SourceProperties, null, sourceClassParameterName, "context", true)
.WriteClosingBracket();
return builder;
}
private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Updates <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>");
}
}
}

View File

@ -1,453 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using MapTo.Sources;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal static class MappingContextExtensions
{
internal static ImmutableArray<MappedProperty> GetReadOnlyMappedProperties(this ImmutableArray<MappedProperty> mappedProperties) => mappedProperties.Where(p => p.isReadOnly).ToImmutableArray()!;
internal static ImmutableArray<MappedProperty> GetWritableMappedProperties(this ImmutableArray<MappedProperty> mappedProperties) => mappedProperties.Where(p => !p.isReadOnly).ToImmutableArray()!;
}
internal abstract class MappingContext
{
private readonly List<SymbolDisplayPart> _ignoredNamespaces;
protected MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
_ignoredNamespaces = new();
Diagnostics = ImmutableArray<Diagnostic>.Empty;
Usings = ImmutableArray.Create("System", Constants.RootNamespace);
SourceGenerationOptions = sourceGenerationOptions;
TypeSyntax = typeSyntax;
Compilation = compilation;
IgnorePropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(IgnorePropertyAttributeSource.FullyQualifiedName);
MapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapTypeConverterAttributeSource.FullyQualifiedName);
TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName);
MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName);
MapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName);
UseUpdateAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(UseUpdateAttributeSource.FullyQualifiedName);
JsonExtensionAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(UseUpdateAttributeSource.FullyQualifiedName);
MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName);
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
}
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
public MappingModel? Model { get; private set; }
protected Compilation Compilation { get; }
protected INamedTypeSymbol IgnorePropertyAttributeTypeSymbol { get; }
protected INamedTypeSymbol MapFromAttributeTypeSymbol { get; }
protected INamedTypeSymbol UseUpdateAttributeTypeSymbol { get; }
protected INamedTypeSymbol JsonExtensionAttributeTypeSymbol { get; }
protected INamedTypeSymbol MappingContextTypeSymbol { get; }
protected INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; }
protected INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; }
protected SourceGenerationOptions SourceGenerationOptions { get; }
protected INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; }
protected TypeDeclarationSyntax TypeSyntax { get; }
protected ImmutableArray<string> Usings { get; private set; }
public static MappingContext Create(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
MappingContext context = typeSyntax switch
{
StructDeclarationSyntax => new StructMappingContext(compilation, sourceGenerationOptions, typeSyntax),
ClassDeclarationSyntax => new ClassMappingContext(compilation, sourceGenerationOptions, typeSyntax),
RecordDeclarationSyntax => new RecordMappingContext(compilation, sourceGenerationOptions, typeSyntax),
_ => throw new ArgumentOutOfRangeException()
};
context.Model = context.CreateMappingModel();
return context;
}
protected void AddDiagnostic(Diagnostic diagnostic)
{
Diagnostics = Diagnostics.Add(diagnostic);
}
protected void AddUsingIfRequired(ISymbol? namedTypeSymbol) =>
AddUsingIfRequired(namedTypeSymbol?.ContainingNamespace.IsGlobalNamespace == false, namedTypeSymbol?.ContainingNamespace);
protected void AddUsingIfRequired(bool condition, INamespaceSymbol? ns) =>
AddUsingIfRequired(condition && ns is not null && !_ignoredNamespaces.Contains(ns.ToDisplayParts().First()), ns?.ToDisplayString());
protected void AddUsingIfRequired(bool condition, string? ns)
{
if (ns is not null && condition && ns != TypeSyntax.GetNamespace() && !Usings.Contains(ns))
{
Usings = Usings.Add(ns);
}
}
protected ISymbol? FindSourceProperty(IEnumerable<ISymbol> sourceMembers, ISymbol member)
{
var propertyName = member
.GetAttribute(MapPropertyAttributeTypeSymbol)
?.NamedArguments
.SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
.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 sourceMembers.SingleOrDefault(p => p.Name == propertyName);
}
protected abstract ImmutableArray<MappedProperty> GetSourceMappedMembers(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) =>
GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel);
protected INamedTypeSymbol? GetSourceTypeSymbol(SyntaxNode? attributeSyntax, SemanticModel? semanticModel = null)
{
if (attributeSyntax is null)
{
return null;
}
semanticModel ??= Compilation.GetSemanticModel(attributeSyntax.SyntaxTree);
var sourceTypeExpressionSyntax = attributeSyntax
.DescendantNodes()
.OfType<TypeOfExpressionSyntax>()
.SingleOrDefault();
return sourceTypeExpressionSyntax is not null ? semanticModel.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null;
}
protected bool IsTypeInheritFromMappedBaseClass(SemanticModel semanticModel)
{
return TypeSyntax.BaseList is not null && TypeSyntax.BaseList.Types
.Select(t => semanticModel.GetTypeInfo(t.Type).Type)
.Any(t => t?.GetAttribute(MapFromAttributeTypeSymbol) != null);
}
protected bool IsTypeUpdatable()
{
return TypeSyntax.GetAttribute("UseUpdate") != null;
}
protected bool IsJsonExtension()
{
return TypeSyntax.GetAttribute("JsonExtension") != null;
}
protected virtual MappedProperty? MapProperty(ISymbol sourceTypeSymbol, IReadOnlyCollection<ISymbol> sourceProperties, ISymbol property)
{
var sourceProperty = FindSourceProperty(sourceProperties, property);
if (sourceProperty is null || !property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
string? converterFullyQualifiedName = null;
var converterParameters = ImmutableArray<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null;
ITypeSymbol? enumerableTypeArgumentType = null;
if (!Compilation.HasCompatibleTypes(sourceProperty, property))
{
if (!TryGetMapTypeConverter(property, sourceProperty, out converterFullyQualifiedName, out converterParameters) &&
!TryGetNestedObjectMappings(property, out mappedSourcePropertyType, out enumerableTypeArgumentType))
{
return null;
}
}
AddUsingIfRequired(propertyType);
AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType);
bool isReadOnly = false;
if(property is IPropertySymbol pSymbol)
{
isReadOnly = pSymbol.IsReadOnly;
}
if (property is IFieldSymbol fSymbol)
{
isReadOnly = fSymbol.IsReadOnly;
}
return new MappedProperty(
property.Name,
property.GetTypeSymbol().ToString(),
ToQualifiedDisplayName(propertyType) ?? propertyType.Name,
converterFullyQualifiedName,
converterParameters.ToImmutableArray(),
sourceProperty.Name,
ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType),
isReadOnly);
;
}
protected virtual MappedProperty? MapPropertySimple(ISymbol sourceTypeSymbol, ISymbol property)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
return null;
}
string? converterFullyQualifiedName = null;
var converterParameters = ImmutableArray<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null;
ITypeSymbol? enumerableTypeArgumentType = null;
AddUsingIfRequired(propertyType);
AddUsingIfRequired(enumerableTypeArgumentType);
AddUsingIfRequired(mappedSourcePropertyType);
bool isReadOnly = false;
if (property is IPropertySymbol pSymbol)
{
isReadOnly = pSymbol.IsReadOnly;
}
if (property is IFieldSymbol fSymbol)
{
isReadOnly = fSymbol.IsReadOnly;
}
return new MappedProperty(
property.Name,
property.GetTypeSymbol().ToString(),
ToQualifiedDisplayName(propertyType) ?? propertyType.Name,
converterFullyQualifiedName,
converterParameters.ToImmutableArray(),
property.Name,
ToQualifiedDisplayName(mappedSourcePropertyType),
ToQualifiedDisplayName(enumerableTypeArgumentType),
isReadOnly);
;
}
protected bool TryGetMapTypeConverter(ISymbol property, ISymbol sourceMember, out string? converterFullyQualifiedName,
out ImmutableArray<string> converterParameters)
{
converterFullyQualifiedName = null;
converterParameters = ImmutableArray<string>.Empty;
if (!Diagnostics.IsEmpty())
{
return false;
}
var typeConverterAttribute = property.GetAttribute(MapTypeConverterAttributeTypeSymbol);
if (typeConverterAttribute?.ConstructorArguments.First().Value is not INamedTypeSymbol converterTypeSymbol)
{
return false;
}
var baseInterface = GetTypeConverterBaseInterface(converterTypeSymbol, property, sourceMember);
if (baseInterface is null)
{
AddDiagnostic(DiagnosticsFactory.InvalidTypeConverterGenericTypesError(property, sourceMember));
return false;
}
converterFullyQualifiedName = converterTypeSymbol.ToDisplayString();
converterParameters = GetTypeConverterParameters(typeConverterAttribute);
return true;
}
protected bool TryGetNestedObjectMappings(ISymbol property, out ITypeSymbol? mappedSourcePropertyType, out ITypeSymbol? enumerableTypeArgument)
{
mappedSourcePropertyType = null;
enumerableTypeArgument = null;
if (!Diagnostics.IsEmpty())
{
return false;
}
if (!property.TryGetTypeSymbol(out var propertyType))
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
return false;
}
var mapFromAttribute = propertyType.GetAttribute(MapFromAttributeTypeSymbol);
if (mapFromAttribute is null &&
propertyType is INamedTypeSymbol namedTypeSymbol &&
!propertyType.IsPrimitiveType() &&
(Compilation.IsGenericEnumerable(propertyType) || propertyType.AllInterfaces.Any(i => Compilation.IsGenericEnumerable(i))))
{
enumerableTypeArgument = namedTypeSymbol.TypeArguments.First();
mapFromAttribute = enumerableTypeArgument.GetAttribute(MapFromAttributeTypeSymbol);
}
mappedSourcePropertyType = mapFromAttribute?.ConstructorArguments.First().Value as INamedTypeSymbol;
if (mappedSourcePropertyType is null && enumerableTypeArgument is null)
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyTypeFoundError(property));
}
return Diagnostics.IsEmpty();
}
private static ImmutableArray<string> GetTypeConverterParameters(AttributeData typeConverterAttribute)
{
var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault();
return converterParameter.IsNull
? ImmutableArray<string>.Empty
: converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()).ToImmutableArray();
}
private MappingModel? CreateMappingModel()
{
var semanticModel = Compilation.GetSemanticModel(TypeSyntax.SyntaxTree);
if (semanticModel.GetDeclaredSymbol(TypeSyntax) is not INamedTypeSymbol typeSymbol)
{
AddDiagnostic(DiagnosticsFactory.TypeNotFoundError(TypeSyntax.GetLocation(), TypeSyntax.Identifier.ValueText));
return null;
}
var sourceTypeSymbol = GetSourceTypeSymbol(TypeSyntax, semanticModel);
if (sourceTypeSymbol is null)
{
AddDiagnostic(DiagnosticsFactory.MapFromAttributeNotFoundError(TypeSyntax.GetLocation()));
return null;
}
_ignoredNamespaces.Add(sourceTypeSymbol.ContainingNamespace.ToDisplayParts().First());
var typeIdentifierName = TypeSyntax.GetIdentifierName();
var sourceTypeIdentifierName = sourceTypeSymbol.Name;
var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
var isTypeUpdatable = IsTypeUpdatable();
var isJsonExtension = IsJsonExtension();
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
var mappedProperties = GetSourceMappedMembers(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
/* if (!mappedProperties.Any())
{
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol));
return null;
}*/
AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq");
var allProperties = GetTypeMappedMembers(sourceTypeSymbol, typeSymbol , isTypeInheritFromMappedBaseClass);
return new MappingModel(
SourceGenerationOptions,
TypeSyntax.GetNamespace(),
TypeSyntax.Modifiers,
TypeSyntax.Keyword.Text,
typeIdentifierName,
sourceTypeSymbol.ContainingNamespace.ToDisplayString(),
sourceTypeIdentifierName,
sourceTypeSymbol.ToDisplayString(),
isTypeUpdatable,
isJsonExtension,
mappedProperties,
allProperties,
isTypeInheritFromMappedBaseClass,
Usings,
shouldGenerateSecondaryConstructor);
}
private INamedTypeSymbol? GetTypeConverterBaseInterface(ITypeSymbol converterTypeSymbol, ISymbol property, ISymbol sourceMember)
{
if (!property.TryGetTypeSymbol(out var propertyType))
{
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
.SingleOrDefault(i =>
i.TypeArguments.Length == 2 &&
SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, TypeConverterInterfaceTypeSymbol) &&
SymbolEqualityComparer.Default.Equals(type, i.TypeArguments[0]) &&
SymbolEqualityComparer.Default.Equals(propertyType, i.TypeArguments[1]));
}
private bool ShouldGenerateSecondaryConstructor(SemanticModel semanticModel, ISymbol sourceTypeSymbol)
{
var constructorSyntax = TypeSyntax.DescendantNodes()
.OfType<ConstructorDeclarationSyntax>()
.SingleOrDefault(c =>
c.ParameterList.Parameters.Count == 1 &&
SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(c.ParameterList.Parameters.Single().Type!).ConvertedType, sourceTypeSymbol));
if (constructorSyntax is null)
{
// Secondary constructor is not defined.
return true;
}
if (constructorSyntax.Initializer?.ArgumentList.Arguments is not { Count: 2 } arguments ||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[0].Expression).ConvertedType, MappingContextTypeSymbol) ||
!SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(arguments[1].Expression).ConvertedType, sourceTypeSymbol))
{
AddDiagnostic(DiagnosticsFactory.MissingConstructorArgument(constructorSyntax));
}
return false;
}
private string? ToQualifiedDisplayName(ISymbol? symbol)
{
if (symbol is null)
{
return null;
}
var containingNamespace = TypeSyntax.GetNamespace();
var symbolNamespace = symbol.ContainingNamespace.ToDisplayString();
return containingNamespace != symbolNamespace && _ignoredNamespaces.Contains(symbol.ContainingNamespace.ToDisplayParts().First())
? symbol.ToDisplayString()
: symbol.Name;
}
}
}

View File

@ -1,89 +0,0 @@
using System;
using System.Collections.Immutable;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace MapTo
{
internal enum AccessModifier
{
Public,
Internal,
Private
}
internal enum NullStaticAnalysisState
{
Default,
Enabled,
Disabled
}
internal record SourceCode(string Text, string HintName);
internal record MappedProperty(
string Name,
string FullyQualifiedType,
string Type,
string? TypeConverter,
ImmutableArray<string> TypeConverterParameters,
string SourcePropertyName,
string? MappedSourcePropertyTypeName,
string? EnumerableTypeArgument,
bool isReadOnly)
{
public bool IsEnumerable => EnumerableTypeArgument is not null;
}
internal record MappingModel (
SourceGenerationOptions Options,
string? Namespace,
SyntaxTokenList Modifiers,
string Type,
string TypeIdentifierName,
string SourceNamespace,
string SourceTypeIdentifierName,
string SourceTypeFullName,
bool IsTypeUpdatable,
bool IsJsonExtension,
ImmutableArray<MappedProperty> SourceProperties,
ImmutableArray<MappedProperty> TypeProperties,
bool HasMappedBaseClass,
ImmutableArray<string> Usings,
bool GenerateSecondaryConstructor
)
{
public string SourceType => SourceTypeFullName;
}
internal record SourceGenerationOptions(
AccessModifier ConstructorAccessModifier,
AccessModifier GeneratedMethodsAccessModifier,
bool GenerateXmlDocument,
bool SupportNullableReferenceTypes,
bool SupportNullableStaticAnalysis)
{
internal static SourceGenerationOptions From(GeneratorExecutionContext context)
{
const string allowNullAttributeName = "System.Diagnostics.CodeAnalysis.AllowNullAttribute";
var supportNullableStaticAnalysis = context.GetBuildGlobalOption(propertyName: nameof(SupportNullableStaticAnalysis), NullStaticAnalysisState.Default);
var supportNullableReferenceTypes = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Warnings or NullableContextOptions.Enable;
return new(
ConstructorAccessModifier: context.GetBuildGlobalOption(propertyName: nameof(ConstructorAccessModifier), AccessModifier.Public),
GeneratedMethodsAccessModifier: context.GetBuildGlobalOption(propertyName: nameof(GeneratedMethodsAccessModifier), AccessModifier.Public),
GenerateXmlDocument: context.GetBuildGlobalOption(propertyName: nameof(GenerateXmlDocument), true),
SupportNullableReferenceTypes: supportNullableReferenceTypes,
SupportNullableStaticAnalysis: supportNullableStaticAnalysis switch
{
NullStaticAnalysisState.Enabled => true,
NullStaticAnalysisState.Disabled => false,
_ => context.Compilation is CSharpCompilation { LanguageVersion: >= LanguageVersion.CSharp8 } cs && cs.TypeByMetadataNameExists(allowNullAttributeName)
}
);
}
public string NullableReferenceSyntax => SupportNullableReferenceTypes ? "?" : string.Empty;
}
}

View File

@ -1,6 +0,0 @@
<?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

@ -1,191 +0,0 @@
using System;
using MapTo.Extensions;
using static MapTo.Sources.Constants;
namespace MapTo.Sources
{
internal static class MapRecordSource
{
internal static SourceCode Generate(MappingModel model)
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
.WriteUsings(model.Usings)
.WriteLine()
// Namespace declaration
.WriteLine($"namespace {model.Namespace}")
.WriteOpeningBracket()
// Class declaration
.WriteLine($"partial record {model.TypeIdentifierName}")
.WriteOpeningBracket();
// Class body
if (model.GenerateSecondaryConstructor)
{
builder
.GenerateSecondaryConstructor(model)
.WriteLine();
}
builder
.GeneratePrivateConstructor(model)
.WriteLine()
.GenerateFactoryMethod(model)
// End class declaration
.WriteClosingBracket()
.WriteLine()
// Extension class declaration
.GenerateSourceTypeExtensionClass(model)
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs");
}
private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
if (model.Options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
}
return builder
.WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName})")
.WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}");
}
private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
const string mappingContextParameterName = "context";
builder
.WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceType} {sourceClassParameterName})")
.Indent()
.Write(": this(").
WriteProperties(model, sourceClassParameterName, mappingContextParameterName)
.WriteLine(")")
.Unindent()
.WriteOpeningBracket()
.WriteLine($"if ({mappingContextParameterName} == null) throw new ArgumentNullException(nameof({mappingContextParameterName}));")
.WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));")
.WriteLine()
.WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);");
// End constructor declaration
return builder.WriteClosingBracket();
}
private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, string sourceClassParameterName,
string mappingContextParameterName)
{
for (var i = 0; i < model.SourceProperties.Length; i++)
{
var property = model.SourceProperties[i];
if (property.TypeConverter is null)
{
if (property.IsEnumerable)
{
builder.Write(
$"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList()");
}
else
{
builder.Write(property.MappedSourcePropertyTypeName is null
? $"{property.Name}: {sourceClassParameterName}.{property.SourcePropertyName}"
: $"{property.Name}: {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName})");
}
}
else
{
var parameters = property.TypeConverterParameters.IsEmpty
? "null"
: $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}";
builder.Write(
$"{property.Name}: new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters})");
}
if (i < model.SourceProperties.Length - 1)
{
builder.Write(", ");
}
}
return builder;
}
private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} From({model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine(
$"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{model.SourceType}, {model.TypeIdentifierName}>({sourceClassParameterName});")
.WriteClosingBracket();
}
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>")
.WriteLine(
$"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MappingModel model)
{
return builder
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {model.SourceTypeIdentifierName}To{model.TypeIdentifierName}Extensions")
.WriteOpeningBracket()
.GenerateSourceTypeExtensionMethod(model)
.WriteClosingBracket();
}
private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine(
$"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} To{model.TypeIdentifierName}(this {model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
.WriteClosingBracket();
}
}
}

View File

@ -1,38 +0,0 @@
using System.Collections.Immutable;
using System.Linq;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo
{
internal class StructMappingContext : MappingContext
{
internal StructMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedProperty> GetSourceMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedProperty> GetTypeMappedMembers(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapPropertySimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<RootNamespace>MapTo.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.2.0" />
<PackageReference Update="Nerdbank.GitVersioning">
<Version>3.5.109</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BlueWest.MapTo\BlueWest.MapTo.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,252 @@
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

@ -0,0 +1,16 @@
// ReSharper disable UnusedType.Global
// ReSharper disable CheckNamespace
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
namespace System.Runtime.CompilerServices
{
/// <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

@ -0,0 +1,42 @@
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.FirstOrDefault(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

@ -0,0 +1,88 @@
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())
.FirstOrDefault(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())
.FirstOrDefault(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.FirstOrDefault(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

@ -0,0 +1,81 @@
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

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
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 List<ISourceGenerator>() { 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

@ -0,0 +1,19 @@
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

@ -0,0 +1,24 @@
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

@ -0,0 +1,202 @@
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

@ -0,0 +1,601 @@
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

@ -0,0 +1,283 @@
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

@ -0,0 +1,562 @@
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

@ -0,0 +1,102 @@
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

@ -0,0 +1,85 @@
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

@ -0,0 +1,29 @@
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

@ -0,0 +1,11 @@
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

@ -0,0 +1,14 @@
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

@ -0,0 +1,13 @@
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

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BlueWest.MapTo\BlueWest.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

@ -0,0 +1,59 @@
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
}
[JsonExtension]
[MapFrom(typeof(FinanceTransactionInsertDto))]
public partial struct FinanceTransaction
{
public readonly int Id;
public readonly int UserId;
public readonly FinanceTransactionType FinanceTransactionType;
public readonly FinanceSymbol FinanceSymbol;
public readonly double Amount; // To Buy
public readonly double Quantity; // Bought
public readonly double Fee;
public readonly DateTime DateTime;
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

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

View File

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

View File

@ -0,0 +1,27 @@
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

@ -0,0 +1,21 @@
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

@ -0,0 +1,26 @@
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

@ -0,0 +1,44 @@

using System.Collections.Generic;
using MapTo;
namespace BlueWest.Data
{
[MapFrom(typeof(UserUpdateDto))]
[JsonExtension]
public partial class User
{
public readonly int Id;
public string Name;
public string Address;
public string BTCAddress;
public string LTCAddress;
public double BTCAmount;
public double LTCAmount;
public readonly List<FinanceTransaction> FinanceTransactions;
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

@ -0,0 +1,16 @@
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

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

View File

@ -0,0 +1,32 @@
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

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

View File

@ -0,0 +1,23 @@
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

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

View File

@ -0,0 +1,22 @@
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;
}
}
}