Fix cyclic reference issue.
This commit is contained in:
parent
9f9fb2d158
commit
3d0f9e5bbb
|
@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapToTests", "test\MapTo.Te
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsoleApp", "test\TestConsoleApp\TestConsoleApp.csproj", "{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapTo.Integration.Tests", "test\MapTo.Integration.Tests\MapTo.Integration.Tests.csproj", "{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -24,5 +26,9 @@ Global
|
|||
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5BE2551A-9EF9-42FA-B6D1-5B5E6A90CC85}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{23B46FDF-6A1E-4287-88C9-C8C5D7EECB8C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
A convention based object to object mapper using [Roslyn source generator](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md).
|
||||
|
||||
MapTo is a library to programmatically generate the necessary code to map one object to another during compile-time. It creates mappings during compile-time, eliminating the need to use reflection to map one object to another and make it simpler to use faster than other libraries at runtime.
|
||||
MapTo is a library to programmatically generate the necessary code to map one object to another during compile-time, eliminating the need to use reflection to map objects and make it much faster in runtime. It provides compile-time safety checks and ease of use by leveraging extension methods.
|
||||
|
||||
|
||||
## Installation
|
||||
|
|
|
@ -75,5 +75,21 @@ namespace MapTo.Extensions
|
|||
compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T).Equals(typeSymbol.OriginalDefinition, SymbolEqualityComparer.Default);
|
||||
|
||||
public static bool IsArray(this Compilation compilation, ITypeSymbol typeSymbol) => typeSymbol is IArrayTypeSymbol;
|
||||
|
||||
public static bool IsPrimitiveType(this ITypeSymbol type) => type.SpecialType is
|
||||
SpecialType.System_String or
|
||||
SpecialType.System_Boolean or
|
||||
SpecialType.System_SByte or
|
||||
SpecialType.System_Int16 or
|
||||
SpecialType.System_Int32 or
|
||||
SpecialType.System_Int64 or
|
||||
SpecialType.System_Byte or
|
||||
SpecialType.System_UInt16 or
|
||||
SpecialType.System_UInt32 or
|
||||
SpecialType.System_UInt64 or
|
||||
SpecialType.System_Single or
|
||||
SpecialType.System_Double or
|
||||
SpecialType.System_Char or
|
||||
SpecialType.System_Object;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MapTo.Extensions;
|
||||
using MapTo.Sources;
|
||||
|
@ -22,18 +23,27 @@ namespace MapTo
|
|||
/// <inheritdoc />
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
{
|
||||
var options = SourceGenerationOptions.From(context);
|
||||
|
||||
var compilation = context.Compilation
|
||||
.AddSource(ref context, MapFromAttributeSource.Generate(options))
|
||||
.AddSource(ref context, IgnorePropertyAttributeSource.Generate(options))
|
||||
.AddSource(ref context, ITypeConverterSource.Generate(options))
|
||||
.AddSource(ref context, MapTypeConverterAttributeSource.Generate(options))
|
||||
.AddSource(ref context, MapPropertyAttributeSource.Generate(options));
|
||||
|
||||
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any())
|
||||
try
|
||||
{
|
||||
AddGeneratedMappingsClasses(context, compilation, receiver.CandidateClasses, options);
|
||||
var options = SourceGenerationOptions.From(context);
|
||||
|
||||
var compilation = context.Compilation
|
||||
.AddSource(ref context, MapFromAttributeSource.Generate(options))
|
||||
.AddSource(ref context, IgnorePropertyAttributeSource.Generate(options))
|
||||
.AddSource(ref context, ITypeConverterSource.Generate(options))
|
||||
.AddSource(ref context, MapTypeConverterAttributeSource.Generate(options))
|
||||
.AddSource(ref context, MapPropertyAttributeSource.Generate(options))
|
||||
.AddSource(ref context, MappingContextSource.Generate(options));
|
||||
|
||||
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any())
|
||||
{
|
||||
AddGeneratedMappingsClasses(context, compilation, receiver.CandidateClasses, options);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace MapTo
|
|||
internal MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, ClassDeclarationSyntax classSyntax)
|
||||
{
|
||||
_diagnostics = new List<Diagnostic>();
|
||||
_usings = new List<string> { "System" };
|
||||
_usings = new List<string> { "System", Constants.RootNamespace };
|
||||
_sourceGenerationOptions = sourceGenerationOptions;
|
||||
_classSyntax = classSyntax;
|
||||
_compilation = compilation;
|
||||
|
@ -35,7 +35,7 @@ namespace MapTo
|
|||
_mapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName);
|
||||
_mapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName);
|
||||
|
||||
AddUsingIfRequired(sourceGenerationOptions.SupportNullableReferenceTypes, "System.Diagnostics.CodeAnalysis");
|
||||
AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis");
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
@ -154,7 +154,10 @@ namespace MapTo
|
|||
}
|
||||
|
||||
var mapFromAttribute = property.Type.GetAttribute(_mapFromAttributeTypeSymbol);
|
||||
if (mapFromAttribute is null && property.Type is INamedTypeSymbol namedTypeSymbol && _compilation.IsGenericEnumerable(property.Type))
|
||||
if (mapFromAttribute is null &&
|
||||
property.Type is INamedTypeSymbol namedTypeSymbol &&
|
||||
!property.Type.IsPrimitiveType() &&
|
||||
(_compilation.IsGenericEnumerable(property.Type) || property.Type.AllInterfaces.Any(i => _compilation.IsGenericEnumerable(i))))
|
||||
{
|
||||
enumerableTypeArgument = namedTypeSymbol.TypeArguments.First();
|
||||
mapFromAttribute = enumerableTypeArgument.GetAttribute(_mapFromAttributeTypeSymbol);
|
||||
|
|
|
@ -36,17 +36,30 @@ namespace MapTo
|
|||
AccessModifier ConstructorAccessModifier,
|
||||
AccessModifier GeneratedMethodsAccessModifier,
|
||||
bool GenerateXmlDocument,
|
||||
bool SupportNullableReferenceTypes)
|
||||
bool SupportNullableReferenceTypes,
|
||||
bool SupportNullableStaticAnalysis,
|
||||
LanguageVersion LanguageVersion)
|
||||
{
|
||||
internal static SourceGenerationOptions From(GeneratorExecutionContext context)
|
||||
{
|
||||
var compilationOptions = (context.Compilation as CSharpCompilation)?.Options;
|
||||
var compilation = context.Compilation as CSharpCompilation;
|
||||
var supportNullableReferenceTypes = false;
|
||||
var supportNullableStaticAnalysis = false;
|
||||
|
||||
if (compilation is not null)
|
||||
{
|
||||
supportNullableStaticAnalysis = compilation.LanguageVersion >= LanguageVersion.CSharp8;
|
||||
supportNullableReferenceTypes = compilation.Options.NullableContextOptions == NullableContextOptions.Warnings ||
|
||||
compilation.Options.NullableContextOptions == NullableContextOptions.Enable;
|
||||
}
|
||||
|
||||
return new(
|
||||
context.GetBuildGlobalOption(nameof(ConstructorAccessModifier), AccessModifier.Public),
|
||||
context.GetBuildGlobalOption(nameof(GeneratedMethodsAccessModifier), AccessModifier.Public),
|
||||
context.GetBuildGlobalOption(nameof(GenerateXmlDocument), true),
|
||||
compilationOptions is not null && (compilationOptions.NullableContextOptions == NullableContextOptions.Warnings || compilationOptions.NullableContextOptions == NullableContextOptions.Enable)
|
||||
supportNullableReferenceTypes,
|
||||
supportNullableStaticAnalysis,
|
||||
compilation?.LanguageVersion ?? LanguageVersion.Default
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System.Linq;
|
||||
using MapTo.Extensions;
|
||||
using MapTo.Extensions;
|
||||
using static MapTo.Sources.Constants;
|
||||
|
||||
namespace MapTo.Sources
|
||||
|
@ -12,7 +11,7 @@ namespace MapTo.Sources
|
|||
.WriteLine(GeneratedFilesHeader)
|
||||
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
|
||||
.WriteLine()
|
||||
.WriteUsings(model)
|
||||
.WriteUsings(model.Usings)
|
||||
.WriteLine()
|
||||
|
||||
// Namespace declaration
|
||||
|
@ -24,7 +23,9 @@ namespace MapTo.Sources
|
|||
.WriteOpeningBracket()
|
||||
|
||||
// Class body
|
||||
.GenerateConstructor(model)
|
||||
.GenerateSecondaryConstructor(model)
|
||||
.WriteLine()
|
||||
.GeneratePrivateConstructor(model)
|
||||
.WriteLine()
|
||||
.GenerateFactoryMethod(model)
|
||||
|
||||
|
@ -41,13 +42,7 @@ namespace MapTo.Sources
|
|||
return new(builder.ToString(), $"{model.ClassName}.g.cs");
|
||||
}
|
||||
|
||||
private static SourceBuilder WriteUsings(this SourceBuilder builder, MappingModel model)
|
||||
{
|
||||
model.Usings.Sort().ForEach(u => builder.WriteLine($"using {u};"));
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static SourceBuilder GenerateConstructor(this SourceBuilder builder, MappingModel model)
|
||||
private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model)
|
||||
{
|
||||
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
|
||||
|
||||
|
@ -61,12 +56,25 @@ namespace MapTo.Sources
|
|||
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
|
||||
}
|
||||
|
||||
var baseConstructor = model.HasMappedBaseClass ? $" : base({sourceClassParameterName})" : string.Empty;
|
||||
|
||||
return builder
|
||||
.WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.ClassName}({model.SourceClassName} {sourceClassParameterName})")
|
||||
.WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}");
|
||||
}
|
||||
|
||||
private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model)
|
||||
{
|
||||
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
|
||||
const string mappingContextParameterName = "context";
|
||||
|
||||
var baseConstructor = model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" : string.Empty;
|
||||
|
||||
builder
|
||||
.WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.ClassName}({model.SourceClassName} {sourceClassParameterName}){baseConstructor}")
|
||||
.WriteLine($"private protected {model.ClassName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceClassName} {sourceClassParameterName}){baseConstructor}")
|
||||
.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);")
|
||||
.WriteLine();
|
||||
|
||||
foreach (var property in model.MappedProperties)
|
||||
|
@ -75,13 +83,13 @@ namespace MapTo.Sources
|
|||
{
|
||||
if (property.IsEnumerable)
|
||||
{
|
||||
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName}.Select({property.MappedSourcePropertyTypeName}To{property.EnumerableTypeArgument}Extensions.To{property.EnumerableTypeArgument}).ToList();");
|
||||
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};"
|
||||
: $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName}.To{property.Type}();");
|
||||
: $"{property.Name} = {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName});");
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -104,10 +112,10 @@ namespace MapTo.Sources
|
|||
|
||||
return builder
|
||||
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
|
||||
.WriteLineIf(model.Options.SupportNullableReferenceTypes, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
|
||||
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
|
||||
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName}{model.Options.NullableReferenceSyntax} From({model.SourceClassName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});")
|
||||
.WriteLine($"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{model.SourceClassName}, {model.ClassName}>({sourceClassParameterName});")
|
||||
.WriteClosingBracket();
|
||||
}
|
||||
|
||||
|
@ -142,7 +150,7 @@ namespace MapTo.Sources
|
|||
|
||||
return builder
|
||||
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
|
||||
.WriteLineIf(model.Options.SupportNullableReferenceTypes, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
|
||||
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
|
||||
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName}{model.Options.NullableReferenceSyntax} To{model.ClassName}(this {model.SourceClassName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});")
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
using System.Collections.Generic;
|
||||
using static MapTo.Sources.Constants;
|
||||
|
||||
namespace MapTo.Sources
|
||||
{
|
||||
internal static class MappingContextSource
|
||||
{
|
||||
internal const string ClassName = "MappingContext";
|
||||
internal const string FullyQualifiedName = RootNamespace + "." + ClassName;
|
||||
internal const string FactoryMethodName = "Create";
|
||||
internal const string RegisterMethodName = "Register";
|
||||
internal const string MapMethodName = "MapFromWithContext";
|
||||
|
||||
internal static SourceCode Generate(SourceGenerationOptions options)
|
||||
{
|
||||
var usings = new List<string> { "System", "System.Collections.Generic", "System.Reflection" };
|
||||
|
||||
using var builder = new SourceBuilder()
|
||||
.WriteLine(GeneratedFilesHeader)
|
||||
.WriteLine()
|
||||
.WriteUsings(usings)
|
||||
.WriteLine()
|
||||
|
||||
// Namespace declaration
|
||||
.WriteLine($"namespace {RootNamespace}")
|
||||
.WriteOpeningBracket()
|
||||
|
||||
// Class declaration
|
||||
.WriteLine($"internal sealed class {ClassName}")
|
||||
.WriteOpeningBracket()
|
||||
|
||||
.WriteLine("private readonly Dictionary<object, object> _cache;")
|
||||
.WriteLine()
|
||||
|
||||
// Constructor
|
||||
.WriteLine($"internal {ClassName}()")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine("_cache = new Dictionary<object, object>(1);")
|
||||
.WriteClosingBracket()
|
||||
.WriteLine()
|
||||
|
||||
// Factory
|
||||
.WriteLine($"internal static TMapped {FactoryMethodName}<TOriginal, TMapped>(TOriginal original)")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));")
|
||||
.WriteLine()
|
||||
.WriteLine("var context = new MappingContext();")
|
||||
.WriteLine("var mapped = context.MapFromWithContext<TOriginal, TMapped>(original);")
|
||||
.WriteLine()
|
||||
.WriteLine("if (mapped == null)")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine("throw new InvalidOperationException();")
|
||||
.WriteClosingBracket()
|
||||
.WriteLine()
|
||||
.WriteLine("return mapped;")
|
||||
.WriteClosingBracket()
|
||||
.WriteLine()
|
||||
|
||||
// MapFromWithContext method
|
||||
.WriteLine($"internal TMapped MapFromWithContext<TOriginal, TMapped>(TOriginal original)")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine("if (original == null)")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine("return default(TMapped);")
|
||||
.WriteClosingBracket()
|
||||
.WriteLine()
|
||||
.WriteLine("if (!TryGetValue<TOriginal, TMapped>(original, out var mapped))")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine("var instance = Activator.CreateInstance(typeof(TMapped), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { this, original }, null);")
|
||||
.WriteLine("if (instance != null)")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine("mapped = (TMapped)instance;")
|
||||
.WriteClosingBracket()
|
||||
.WriteClosingBracket()
|
||||
.WriteLine()
|
||||
.WriteLine("return mapped;")
|
||||
.WriteClosingBracket()
|
||||
.WriteLine()
|
||||
|
||||
// Register method
|
||||
.WriteLine("internal void Register<TOriginal, TMapped>(TOriginal original, TMapped mapped)")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine("if (original == null) throw new ArgumentNullException(nameof(original));")
|
||||
.WriteLine("if (mapped == null) throw new ArgumentNullException(nameof(mapped));")
|
||||
.WriteLine()
|
||||
.WriteLine("if (!_cache.ContainsKey(original))")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine("_cache.Add(original, mapped);")
|
||||
.WriteClosingBracket()
|
||||
.WriteClosingBracket()
|
||||
.WriteLine()
|
||||
|
||||
// TryGetValue method
|
||||
.WriteLine("private bool TryGetValue<TOriginal, TMapped>(TOriginal original, out TMapped mapped)")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine("if (original != null && _cache.TryGetValue(original, out var value))")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine("mapped = (TMapped)value;")
|
||||
.WriteLine("return true;")
|
||||
.WriteClosingBracket()
|
||||
.WriteLine()
|
||||
.WriteLine("mapped = default(TMapped);")
|
||||
.WriteLine("return false;")
|
||||
.WriteClosingBracket()
|
||||
|
||||
// End class declaration
|
||||
.WriteClosingBracket()
|
||||
|
||||
// End namespace declaration
|
||||
.WriteClosingBracket();
|
||||
|
||||
return new(builder.ToString(), $"{ClassName}.g.cs");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.CodeDom.Compiler;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace MapTo.Sources
|
||||
{
|
||||
|
@ -8,7 +10,7 @@ namespace MapTo.Sources
|
|||
{
|
||||
private readonly StringWriter _writer;
|
||||
private readonly IndentedTextWriter _indentedWriter;
|
||||
|
||||
|
||||
public SourceBuilder()
|
||||
{
|
||||
_writer = new StringWriter();
|
||||
|
@ -21,12 +23,12 @@ namespace MapTo.Sources
|
|||
_writer.Dispose();
|
||||
_indentedWriter.Dispose();
|
||||
}
|
||||
|
||||
|
||||
public SourceBuilder WriteLine(string? value = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
_indentedWriter.WriteLineNoTabs(string.Empty);
|
||||
_indentedWriter.WriteLineNoTabs(string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -64,6 +66,23 @@ namespace MapTo.Sources
|
|||
return this;
|
||||
}
|
||||
|
||||
public SourceBuilder WriteUsings(IEnumerable<string> usings)
|
||||
{
|
||||
foreach (var u in usings.OrderBy(s => s))
|
||||
{
|
||||
WriteUsing(u);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public SourceBuilder WriteUsing(string u)
|
||||
{
|
||||
WriteLine($"using {u};");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => _writer.ToString();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MapTo\MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||
<PackageReference Include="Shouldly" Version="4.0.1" />
|
||||
<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="1.3.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -21,6 +21,16 @@ namespace MapTo.Tests.Extensions
|
|||
syntax.ShouldBe(expectedSource, customMessage);
|
||||
}
|
||||
|
||||
internal static void ShouldContainPartialSource(this IEnumerable<SyntaxTree> syntaxTree, string typeName, string expectedSource, string customMessage = null)
|
||||
{
|
||||
var syntax = syntaxTree
|
||||
.Select(s => s.ToString().Trim())
|
||||
.SingleOrDefault(s => s.Contains(typeName));
|
||||
|
||||
syntax.ShouldNotBeNullOrWhiteSpace();
|
||||
syntax.ShouldContainWithoutWhitespace(expectedSource, customMessage);
|
||||
}
|
||||
|
||||
internal static void ShouldContainPartialSource(this SyntaxTree syntaxTree, string expectedSource, string customMessage = null)
|
||||
{
|
||||
var syntax = syntaxTree.ToString();
|
||||
|
|
|
@ -53,9 +53,15 @@ namespace MapTo
|
|||
partial class Foo
|
||||
{
|
||||
public Foo(Baz baz)
|
||||
: this(new MappingContext(), baz) { }
|
||||
|
||||
private protected Foo(MappingContext context, 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;
|
||||
|
|
|
@ -11,10 +11,25 @@ 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) =>
|
||||
GetOutputCompilation(new[] { source }, assertCompilation, analyzerConfigOptions, nullableContextOptions);
|
||||
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)
|
||||
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))
|
||||
|
@ -23,10 +38,10 @@ namespace MapTo.Tests.Infrastructure
|
|||
|
||||
var compilation = CSharpCompilation.Create(
|
||||
$"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic",
|
||||
sources.Select((source, index) => CSharpSyntaxTree.ParseText(source, path: $"Test{index:00}.g.cs")),
|
||||
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
|
||||
|
@ -35,7 +50,8 @@ namespace MapTo.Tests.Infrastructure
|
|||
|
||||
var driver = CSharpGeneratorDriver.Create(
|
||||
new[] { new MapToGenerator() },
|
||||
optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions)
|
||||
optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions),
|
||||
parseOptions: new CSharpParseOptions(languageVersion)
|
||||
);
|
||||
|
||||
driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics);
|
||||
|
|
|
@ -4,7 +4,7 @@ using MapTo.Sources;
|
|||
using MapTo.Tests.Extensions;
|
||||
using MapTo.Tests.Infrastructure;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Shouldly;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Xunit;
|
||||
using static MapTo.Tests.Common;
|
||||
|
||||
|
@ -20,6 +20,7 @@ namespace MapTo.Tests
|
|||
// 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)}
|
||||
|
@ -36,8 +37,9 @@ namespace MapTo
|
|||
".Trim();
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: nullableContextOptions);
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: nullableContextOptions, languageVersion: languageVersion);
|
||||
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface);
|
||||
|
@ -61,9 +63,15 @@ namespace MapTo
|
|||
partial class Foo
|
||||
{
|
||||
public Foo(Baz baz)
|
||||
: this(new MappingContext(), baz) { }
|
||||
|
||||
private protected Foo(MappingContext context, 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;
|
||||
|
|
|
@ -40,7 +40,7 @@ namespace MapTo
|
|||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.ShouldContainSource(MapFromAttributeSource.AttributeName, ExpectedAttribute);
|
||||
compilation.SyntaxTrees.ShouldContainSource(MapFromAttributeSource.AttributeClassName, ExpectedAttribute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -84,7 +84,7 @@ namespace MapTo
|
|||
var expectedFactory = @"
|
||||
internal static Foo From(Baz baz)
|
||||
{
|
||||
return baz == null ? null : new Foo(baz);
|
||||
return baz == null ? null : MappingContext.Create<Baz, Foo>(baz);
|
||||
}".Trim();
|
||||
|
||||
// Act
|
||||
|
@ -123,6 +123,7 @@ namespace Test
|
|||
const string expectedResult = @"
|
||||
// <auto-generated />
|
||||
|
||||
using MapTo;
|
||||
using System;
|
||||
|
||||
namespace Test
|
||||
|
@ -130,9 +131,15 @@ namespace Test
|
|||
partial class Foo
|
||||
{
|
||||
public Foo(Baz baz)
|
||||
: this(new MappingContext(), baz) { }
|
||||
|
||||
private protected Foo(MappingContext context, 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;
|
||||
}
|
||||
";
|
||||
|
@ -193,6 +200,7 @@ namespace Test
|
|||
const string expectedResult = @"
|
||||
// <auto-generated />
|
||||
|
||||
using MapTo;
|
||||
using System;
|
||||
|
||||
namespace Test
|
||||
|
@ -200,9 +208,15 @@ namespace Test
|
|||
partial class Foo
|
||||
{
|
||||
public Foo(Baz baz)
|
||||
: this(new MappingContext(), baz) { }
|
||||
|
||||
private protected Foo(MappingContext context, 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;
|
||||
}
|
||||
";
|
||||
|
@ -250,6 +264,7 @@ namespace Test
|
|||
// <auto-generated />
|
||||
|
||||
using Bazaar;
|
||||
using MapTo;
|
||||
using System;
|
||||
|
||||
namespace Test
|
||||
|
@ -273,9 +288,15 @@ namespace Test
|
|||
partial class Foo
|
||||
{
|
||||
public Foo(Baz baz)
|
||||
: this(new MappingContext(), baz) { }
|
||||
|
||||
private protected Foo(MappingContext context, 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;
|
||||
|
@ -299,7 +320,7 @@ namespace Test
|
|||
const string expectedResult = @"
|
||||
public static Foo From(Baz baz)
|
||||
{
|
||||
return baz == null ? null : new Foo(baz);
|
||||
return baz == null ? null : MappingContext.Create<Baz, Foo>(baz);
|
||||
}
|
||||
";
|
||||
|
||||
|
@ -358,13 +379,19 @@ namespace Test
|
|||
partial class Foo
|
||||
{
|
||||
public Foo(Baz baz)
|
||||
: this(new MappingContext(), baz) { }
|
||||
|
||||
private protected Foo(MappingContext context, 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 = baz.InnerProp1.ToB();
|
||||
InnerProp1 = context.MapFromWithContext<A, B>(baz.InnerProp1);
|
||||
}
|
||||
".Trim();
|
||||
|
||||
|
@ -444,9 +471,15 @@ namespace Test
|
|||
partial class Foo
|
||||
{
|
||||
public Foo(Baz baz)
|
||||
: this(new MappingContext(), baz) { }
|
||||
|
||||
private protected Foo(MappingContext context, 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;
|
||||
|
@ -469,11 +502,16 @@ namespace Test
|
|||
var sources = GetEmployeeManagerSourceText();
|
||||
|
||||
const string expectedResult = @"
|
||||
public ManagerViewModel(Manager manager) : base(manager)
|
||||
private protected ManagerViewModel(MappingContext context, 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<Employee, EmployeeViewModel>).ToList();
|
||||
}
|
||||
";
|
||||
|
||||
// Act
|
||||
|
@ -500,12 +538,19 @@ namespace Test.ViewModels
|
|||
{
|
||||
partial class ManagerViewModel
|
||||
{
|
||||
public ManagerViewModel(Manager manager) : base(manager)
|
||||
public ManagerViewModel(Manager manager)
|
||||
: this(new MappingContext(), manager) { }
|
||||
|
||||
private protected ManagerViewModel(MappingContext context, 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(EmployeeToEmployeeViewModelExtensions.ToEmployeeViewModel).ToList();
|
||||
Employees = manager.Employees.Select(context.MapFromWithContext<Employee, EmployeeViewModel>).ToList();
|
||||
}
|
||||
";
|
||||
|
||||
// Act
|
||||
|
@ -533,12 +578,19 @@ namespace Test.ViewModels2
|
|||
{
|
||||
partial class ManagerViewModel
|
||||
{
|
||||
public ManagerViewModel(Manager manager) : base(manager)
|
||||
public ManagerViewModel(Manager manager)
|
||||
: this(new MappingContext(), manager) { }
|
||||
|
||||
private protected ManagerViewModel(MappingContext context, 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(EmployeeToEmployeeViewModelExtensions.ToEmployeeViewModel).ToList();
|
||||
Employees = manager.Employees.Select(context.MapFromWithContext<Employee, EmployeeViewModel>).ToList();
|
||||
}
|
||||
";
|
||||
|
||||
// Act
|
||||
|
|
|
@ -3,7 +3,7 @@ using MapTo.Sources;
|
|||
using MapTo.Tests.Extensions;
|
||||
using MapTo.Tests.Infrastructure;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Shouldly;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Xunit;
|
||||
using static MapTo.Tests.Common;
|
||||
|
||||
|
@ -77,7 +77,7 @@ namespace MapTo
|
|||
".Trim();
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable);
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8);
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
|
@ -128,7 +128,7 @@ namespace MapTo
|
|||
".Trim();
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable);
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable, languageVersion: LanguageVersion.CSharp8);
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
|
@ -220,9 +220,15 @@ namespace Test
|
|||
partial class Foo
|
||||
{
|
||||
public Foo(Baz baz)
|
||||
: this(new MappingContext(), baz) { }
|
||||
|
||||
private protected Foo(MappingContext context, 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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using MapTo;
|
||||
using TestConsoleApp.Data.Models;
|
||||
using TestConsoleApp.ViewModels;
|
||||
using TestConsoleApp.ViewModels2;
|
||||
|
@ -10,6 +11,14 @@ namespace TestConsoleApp
|
|||
private static void Main(string[] args)
|
||||
{
|
||||
//UserTest();
|
||||
CyclicReferenceTest();
|
||||
|
||||
// EmployeeManagerTest();
|
||||
Console.WriteLine("done");
|
||||
}
|
||||
|
||||
private static void EmployeeManagerTest()
|
||||
{
|
||||
var manager1 = new Manager
|
||||
{
|
||||
Id = 1,
|
||||
|
@ -46,6 +55,19 @@ namespace TestConsoleApp
|
|||
employee1.ToEmployeeViewModel();
|
||||
}
|
||||
|
||||
private static ManagerViewModel CyclicReferenceTest()
|
||||
{
|
||||
var manager1 = new Manager
|
||||
{
|
||||
Id = 1,
|
||||
EmployeeCode = "M001",
|
||||
Level = 100
|
||||
};
|
||||
|
||||
manager1.Manager = manager1;
|
||||
return manager1.ToManagerViewModel();
|
||||
}
|
||||
|
||||
private static void UserTest()
|
||||
{
|
||||
var user = new User
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
|
||||
"version": "0.6",
|
||||
"version": "0.7",
|
||||
"semVer1NumericIdentifierPadding": 1,
|
||||
"publicReleaseRefSpec": [
|
||||
"^refs/heads/master$",
|
||||
|
|
Loading…
Reference in New Issue