diff --git a/MapTo.sln b/MapTo.sln
index 9b0a09b..0c203d5 100644
--- a/MapTo.sln
+++ b/MapTo.sln
@@ -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
diff --git a/README.md b/README.md
index 0f21dd8..8db4591 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/MapTo/Extensions/RoslynExtensions.cs b/src/MapTo/Extensions/RoslynExtensions.cs
index 6d35e1c..cc2b0d4 100644
--- a/src/MapTo/Extensions/RoslynExtensions.cs
+++ b/src/MapTo/Extensions/RoslynExtensions.cs
@@ -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;
}
}
\ No newline at end of file
diff --git a/src/MapTo/MapToGenerator.cs b/src/MapTo/MapToGenerator.cs
index 6ac4970..2c42e74 100644
--- a/src/MapTo/MapToGenerator.cs
+++ b/src/MapTo/MapToGenerator.cs
@@ -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
///
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;
}
}
diff --git a/src/MapTo/MappingContext.cs b/src/MapTo/MappingContext.cs
index 445fa05..4ca5a24 100644
--- a/src/MapTo/MappingContext.cs
+++ b/src/MapTo/MappingContext.cs
@@ -24,7 +24,7 @@ namespace MapTo
internal MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, ClassDeclarationSyntax classSyntax)
{
_diagnostics = new List();
- _usings = new List { "System" };
+ _usings = new List { "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);
diff --git a/src/MapTo/Models.cs b/src/MapTo/Models.cs
index 409724f..d23d71e 100644
--- a/src/MapTo/Models.cs
+++ b/src/MapTo/Models.cs
@@ -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
);
}
diff --git a/src/MapTo/Sources/MapClassSource.cs b/src/MapTo/Sources/MapClassSource.cs
index 3aa5f9b..72c435e 100644
--- a/src/MapTo/Sources/MapClassSource.cs
+++ b/src/MapTo/Sources/MapClassSource.cs
@@ -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($"/// {sourceClassParameterName} is null");
}
- 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});")
diff --git a/src/MapTo/Sources/MappingContextSource.cs b/src/MapTo/Sources/MappingContextSource.cs
new file mode 100644
index 0000000..76b6286
--- /dev/null
+++ b/src/MapTo/Sources/MappingContextSource.cs
@@ -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 { "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