From 9a719a0226d4d787af640859c8f2c5e18c26c182 Mon Sep 17 00:00:00 2001 From: Wvader <34067397+wvader@users.noreply.github.com> Date: Tue, 7 Dec 2021 22:02:59 +0000 Subject: [PATCH] Struct handling with readonly fields (wip) --- src/MapTo/ClassMappingContext.cs | 15 ++- src/MapTo/Extensions/CommonExtensions.cs | 31 ++++++ src/MapTo/MappingContext.cs | 47 +++++++- src/MapTo/Models.cs | 3 +- src/MapTo/RecordMappingContext.cs | 16 ++- src/MapTo/Sources/MapClassSource.cs | 67 +++--------- src/MapTo/Sources/MapRecordSource.cs | 6 +- src/MapTo/Sources/MapStructSource.cs | 102 ++++++++++++++---- .../ReadOnlyPropertyAttributeSource.cs | 36 +++++++ src/MapTo/StructMappingContext.cs | 15 ++- test/TestConsoleApp/Data/Models/Employee.cs | 1 - test/TestConsoleApp/Data/Models/Manager.cs | 13 --- test/TestConsoleApp/Data/Models/MyStruct.cs | 25 +++++ test/TestConsoleApp/Data/Models/Profile.cs | 11 -- test/TestConsoleApp/Data/Models/User.cs | 6 +- test/TestConsoleApp/Program.cs | 57 +--------- .../ViewModels/EmployeeViewModel.cs | 5 +- .../ViewModels/ManagerViewModel.cs | 16 --- .../ViewModels/MyStructViewModel.cs | 22 ++++ .../ViewModels/ProfileViewModel.cs | 13 --- .../ViewModels/UserViewModel.cs | 14 +-- 21 files changed, 311 insertions(+), 210 deletions(-) create mode 100644 src/MapTo/Extensions/CommonExtensions.cs create mode 100644 src/MapTo/Sources/ReadOnlyPropertyAttributeSource.cs delete mode 100644 test/TestConsoleApp/Data/Models/Manager.cs create mode 100644 test/TestConsoleApp/Data/Models/MyStruct.cs delete mode 100644 test/TestConsoleApp/Data/Models/Profile.cs delete mode 100644 test/TestConsoleApp/ViewModels/ManagerViewModel.cs create mode 100644 test/TestConsoleApp/ViewModels/MyStructViewModel.cs delete mode 100644 test/TestConsoleApp/ViewModels/ProfileViewModel.cs diff --git a/src/MapTo/ClassMappingContext.cs b/src/MapTo/ClassMappingContext.cs index b248402..3abc4fc 100644 --- a/src/MapTo/ClassMappingContext.cs +++ b/src/MapTo/ClassMappingContext.cs @@ -11,7 +11,7 @@ namespace MapTo internal ClassMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) : base(compilation, sourceGenerationOptions, typeSyntax) { } - protected override ImmutableArray GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) + protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) { var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); @@ -23,5 +23,18 @@ namespace MapTo .Where(mappedProperty => mappedProperty is not null) .ToImmutableArray()!; } + + protected override ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + + return typeSymbol + .GetAllMembers() + .OfType() + .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) + .Select(property => MapProperty(typeSymbol, sourceProperties, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } } } \ No newline at end of file diff --git a/src/MapTo/Extensions/CommonExtensions.cs b/src/MapTo/Extensions/CommonExtensions.cs new file mode 100644 index 0000000..0458d3f --- /dev/null +++ b/src/MapTo/Extensions/CommonExtensions.cs @@ -0,0 +1,31 @@ +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 WriteMappedProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray mappedProperties) + { + foreach (var item in mappedProperties) + { + builder.WriteComment($"Name: {item.Name}"); + builder.WriteComment($"Type: {item.Type}"); + builder.WriteComment($"MappedSourcePropertyTypeName: {item.MappedSourcePropertyTypeName}"); + builder.WriteComment($"IsEnumerable: {item.IsEnumerable}"); + builder.WriteComment($"SourcePropertyName: {item.SourcePropertyName}"); + + } + + return builder; + } + + } +} diff --git a/src/MapTo/MappingContext.cs b/src/MapTo/MappingContext.cs index f162c42..096b92f 100644 --- a/src/MapTo/MappingContext.cs +++ b/src/MapTo/MappingContext.cs @@ -9,6 +9,12 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; namespace MapTo { + internal static class MappingContextExtensions + { + internal static ImmutableArray GetReadOnlyMappedProperties(this ImmutableArray mappedProperties) => mappedProperties.Where(p => p.isReadOnly).ToImmutableArray()!; + internal static ImmutableArray GetWritableMappedProperties(this ImmutableArray mappedProperties) => mappedProperties.Where(p => !p.isReadOnly).ToImmutableArray()!; + } + internal abstract class MappingContext { private readonly List _ignoredNamespaces; @@ -101,7 +107,8 @@ namespace MapTo return sourceProperties.SingleOrDefault(p => p.Name == propertyName); } - protected abstract ImmutableArray GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); + protected abstract ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); + protected abstract ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); protected INamedTypeSymbol? GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null) => GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel); @@ -138,7 +145,6 @@ namespace MapTo } - string? converterFullyQualifiedName = null; var converterParameters = ImmutableArray.Empty; ITypeSymbol? mappedSourcePropertyType = null; @@ -165,10 +171,38 @@ namespace MapTo sourceProperty.Name, ToQualifiedDisplayName(mappedSourcePropertyType), ToQualifiedDisplayName(enumerableTypeArgumentType), - (sourceProperty as IPropertySymbol).IsReadOnly); + (property as IPropertySymbol).IsReadOnly); ; } + protected virtual MappedProperty? MapPropertySimple(ISymbol sourceTypeSymbol, ISymbol property) + { + if (!property.TryGetTypeSymbol(out var propertyType)) + { + return null; + } + + string? converterFullyQualifiedName = null; + var converterParameters = ImmutableArray.Empty; + ITypeSymbol? mappedSourcePropertyType = null; + ITypeSymbol? enumerableTypeArgumentType = null; + + + AddUsingIfRequired(propertyType); + AddUsingIfRequired(enumerableTypeArgumentType); + AddUsingIfRequired(mappedSourcePropertyType); + + return new MappedProperty( + property.Name, + ToQualifiedDisplayName(propertyType) ?? propertyType.Name, + converterFullyQualifiedName, + converterParameters.ToImmutableArray(), + property.Name, + ToQualifiedDisplayName(mappedSourcePropertyType), + ToQualifiedDisplayName(enumerableTypeArgumentType), + (property as IPropertySymbol).IsReadOnly); + ; + } protected bool TryGetMapTypeConverter(ISymbol property, IPropertySymbol sourceProperty, out string? converterFullyQualifiedName, out ImmutableArray converterParameters) { @@ -265,7 +299,7 @@ namespace MapTo var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel); var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol); - var mappedProperties = GetMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass); + var mappedProperties = GetSourceMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass); if (!mappedProperties.Any()) { AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol)); @@ -274,6 +308,8 @@ namespace MapTo AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq"); + var allProperties = GetTypeMappedProperties(sourceTypeSymbol, typeSymbol , isTypeInheritFromMappedBaseClass); + return new MappingModel( SourceGenerationOptions, TypeSyntax.GetNamespace(), @@ -284,11 +320,14 @@ namespace MapTo sourceTypeIdentifierName, sourceTypeSymbol.ToDisplayString(), mappedProperties, + allProperties, isTypeInheritFromMappedBaseClass, Usings, shouldGenerateSecondaryConstructor); } + + private INamedTypeSymbol? GetTypeConverterBaseInterface(ITypeSymbol converterTypeSymbol, ISymbol property, IPropertySymbol sourceProperty) { if (!property.TryGetTypeSymbol(out var propertyType)) diff --git a/src/MapTo/Models.cs b/src/MapTo/Models.cs index 4fb766c..c8708d8 100644 --- a/src/MapTo/Models.cs +++ b/src/MapTo/Models.cs @@ -44,7 +44,8 @@ namespace MapTo string SourceNamespace, string SourceTypeIdentifierName, string SourceTypeFullName, - ImmutableArray MappedProperties, + ImmutableArray SourceProperties, + ImmutableArray TypeProperties, bool HasMappedBaseClass, ImmutableArray Usings, bool GenerateSecondaryConstructor diff --git a/src/MapTo/RecordMappingContext.cs b/src/MapTo/RecordMappingContext.cs index ba87b2d..5aab2de 100644 --- a/src/MapTo/RecordMappingContext.cs +++ b/src/MapTo/RecordMappingContext.cs @@ -11,7 +11,7 @@ namespace MapTo internal RecordMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) : base(compilation, sourceGenerationOptions, typeSyntax) { } - protected override ImmutableArray GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) + protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) { var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); return typeSymbol.GetMembers() @@ -24,5 +24,19 @@ namespace MapTo .Where(mappedProperty => mappedProperty is not null) .ToImmutableArray()!; } + + protected override ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + return typeSymbol.GetMembers() + .OfType() + .OrderByDescending(s => s.Parameters.Length) + .First(s => s.Name == ".ctor") + .Parameters + .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) + .Select(property => MapProperty(typeSymbol, sourceProperties, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } } } \ No newline at end of file diff --git a/src/MapTo/Sources/MapClassSource.cs b/src/MapTo/Sources/MapClassSource.cs index 73b87a1..9518b24 100644 --- a/src/MapTo/Sources/MapClassSource.cs +++ b/src/MapTo/Sources/MapClassSource.cs @@ -21,35 +21,19 @@ namespace MapTo.Sources .WriteLine($"partial class {model.TypeIdentifierName}") .WriteOpeningBracket(); - // Class body - /*if (model.GenerateSecondaryConstructor) - { - builder - .GenerateSecondaryConstructor(model) - .WriteLine(); - }*/ - builder - .GeneratePrivateConstructor(model) + .GeneratePublicConstructor(model) .WriteLine(); - if(!PropertiesAreReadOnly(model)) + if(!AllPropertiesReadOnly(model)) { builder.GenerateUpdateMethod(model); } builder - //.GenerateFactoryMethod(model) - .GenerateUpdateMethod(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"); @@ -73,8 +57,9 @@ namespace MapTo.Sources .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) + + + private static SourceBuilder GeneratePublicConstructor(this SourceBuilder builder, MappingModel model) { var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); const string mappingContextParameterName = "context"; @@ -82,25 +67,27 @@ namespace MapTo.Sources var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty; builder - .WriteLine($"public {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName}){baseConstructor}") + .WriteLine($"public {model.TypeIdentifierName}({model.SourceType} {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(). - WriteProperties( model, sourceClassParameterName, mappingContextParameterName); + .WriteProperties( model.SourceProperties, sourceClassParameterName, mappingContextParameterName, false); // End constructor declaration return builder.WriteClosingBracket(); } - private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, - string? sourceClassParameterName, string mappingContextParameterName) + private static SourceBuilder WriteProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray properties, + string? sourceClassParameterName, string mappingContextParameterName, bool fromUpdate) { - foreach (var property in model.MappedProperties) + + foreach (var property in properties) { + if (property.isReadOnly && fromUpdate) continue; + if (property.TypeConverter is null) { if (property.IsEnumerable) @@ -130,9 +117,9 @@ namespace MapTo.Sources } - private static bool PropertiesAreReadOnly(MappingModel model) + private static bool AllPropertiesReadOnly(MappingModel model) { - foreach (var property in model.MappedProperties) + foreach (var property in model.SourceProperties) { if (!property.isReadOnly) return false; } @@ -140,19 +127,6 @@ namespace MapTo.Sources } - 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 GenerateUpdateMethod(this SourceBuilder builder, MappingModel model) { var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); @@ -161,7 +135,7 @@ namespace MapTo.Sources .GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName) .WriteLine($"public void Update({model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})") .WriteOpeningBracket() - .WriteProperties( model, sourceClassParameterName,"context" ) + .WriteProperties( model.SourceProperties.GetWritableMappedProperties(), sourceClassParameterName,"context", true ) .WriteClosingBracket(); return builder; @@ -198,15 +172,6 @@ namespace MapTo.Sources .WriteLine($"/// The instance of to use as source."); } - 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(); diff --git a/src/MapTo/Sources/MapRecordSource.cs b/src/MapTo/Sources/MapRecordSource.cs index 3605000..8107acb 100644 --- a/src/MapTo/Sources/MapRecordSource.cs +++ b/src/MapTo/Sources/MapRecordSource.cs @@ -96,9 +96,9 @@ namespace MapTo.Sources private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, string sourceClassParameterName, string mappingContextParameterName) { - for (var i = 0; i < model.MappedProperties.Length; i++) + for (var i = 0; i < model.SourceProperties.Length; i++) { - var property = model.MappedProperties[i]; + var property = model.SourceProperties[i]; if (property.TypeConverter is null) { if (property.IsEnumerable) @@ -123,7 +123,7 @@ namespace MapTo.Sources $"{property.Name}: new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters})"); } - if (i < model.MappedProperties.Length - 1) + if (i < model.SourceProperties.Length - 1) { builder.Write(", "); } diff --git a/src/MapTo/Sources/MapStructSource.cs b/src/MapTo/Sources/MapStructSource.cs index 12be73a..125a3f6 100644 --- a/src/MapTo/Sources/MapStructSource.cs +++ b/src/MapTo/Sources/MapStructSource.cs @@ -1,5 +1,6 @@ using MapTo.Extensions; using static MapTo.Sources.Constants; +using System.Collections.Generic; namespace MapTo.Sources { @@ -19,12 +20,18 @@ namespace MapTo.Sources // Class declaration .WriteLine($"partial struct {model.TypeIdentifierName}") - .WriteOpeningBracket(); + .WriteOpeningBracket() + .WriteLine() - // Class body - - builder - .GeneratePrivateConstructor(model) + // Class body + .GeneratePublicConstructor(model); + + if (!AllPropertiesAreReadOnly(model)) + { + builder.GenerateUpdateMethod(model); + } + + builder .WriteLine() // End class declaration .WriteClosingBracket() @@ -32,9 +39,19 @@ namespace MapTo.Sources // End namespace declaration .WriteClosingBracket(); - + return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs"); } + private static bool AllPropertiesAreReadOnly(MappingModel model) + { + foreach (var property in model.SourceProperties) + { + if (!property.isReadOnly) return false; + } + return true; + + } + private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model) { @@ -54,32 +71,48 @@ namespace MapTo.Sources .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) + + + 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 baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty; + + var readOnlyProperties = model.TypeProperties.GetReadOnlyMappedProperties(); + + var readOnlyFields = ""; + + for (int i = 0; i < readOnlyProperties.Length; i++) + { + var property = readOnlyProperties[i]; + readOnlyFields += $"{property.Type} {property.SourcePropertyName.ToCamelCase()}"; + if (i != readOnlyProperties.Length - 1) readOnlyFields += " ,"; + } + builder - .WriteLine($"public {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceType} {sourceClassParameterName}){baseConstructor}") + .WriteLine($"public {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName}{(string.IsNullOrEmpty(readOnlyFields) ? "" : $", {readOnlyFields}")}){baseConstructor}") .WriteOpeningBracket() - .WriteLine() - .WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);") - .WriteLine(). - - WriteProperties( model, sourceClassParameterName, mappingContextParameterName); + .TryWriteProperties(model.SourceProperties, readOnlyProperties, sourceClassParameterName, mappingContextParameterName, false); // End constructor declaration return builder.WriteClosingBracket(); } - private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, - string? sourceClassParameterName, string mappingContextParameterName) + private static SourceBuilder TryWriteProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray properties, System.Collections.Immutable.ImmutableArray? otherProperties, + string? sourceClassParameterName, string mappingContextParameterName, bool fromUpdate) { - foreach (var property in model.MappedProperties) + if (fromUpdate) { + properties = properties.GetWritableMappedProperties(); + } + + foreach (var property in properties) + { + if (property.isReadOnly && fromUpdate) continue; + if (property.TypeConverter is null) { if (property.IsEnumerable) @@ -91,7 +124,7 @@ namespace MapTo.Sources { builder.WriteLine(property.MappedSourcePropertyTypeName is null ? $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};" - : $"{property.Name} = {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName});"); + : ""); } } else @@ -105,12 +138,39 @@ namespace MapTo.Sources } } + + + 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) diff --git a/src/MapTo/Sources/ReadOnlyPropertyAttributeSource.cs b/src/MapTo/Sources/ReadOnlyPropertyAttributeSource.cs new file mode 100644 index 0000000..3f0b2c5 --- /dev/null +++ b/src/MapTo/Sources/ReadOnlyPropertyAttributeSource.cs @@ -0,0 +1,36 @@ +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class ReadOnlyPropertyAttributeSource + { + internal const string AttributeName = "ReadOnlyProperty"; + internal const string AttributeClassName = AttributeName + "Attribute"; + internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteLine("using System;") + .WriteLine() + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Specifies that the annotated property should be excluded.") + .WriteLine("/// "); + } + + builder + .WriteLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]") + .WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}") + .WriteClosingBracket(); + + return new(builder.ToString(), $"{AttributeClassName}.g.cs"); + } + } +} \ No newline at end of file diff --git a/src/MapTo/StructMappingContext.cs b/src/MapTo/StructMappingContext.cs index 25d0173..9347568 100644 --- a/src/MapTo/StructMappingContext.cs +++ b/src/MapTo/StructMappingContext.cs @@ -11,7 +11,7 @@ namespace MapTo internal StructMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) : base(compilation, sourceGenerationOptions, typeSyntax) { } - protected override ImmutableArray GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) + protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) { var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); @@ -23,5 +23,18 @@ namespace MapTo .Where(mappedProperty => mappedProperty is not null) .ToImmutableArray()!; } + protected override ImmutableArray GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + + return sourceTypeSymbol + .GetAllMembers() + .OfType() + .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) + .Select(property => MapPropertySimple(typeSymbol, property)) + .Where(mappedProperty => mappedProperty is not null) + .ToImmutableArray()!; + } + } } \ No newline at end of file diff --git a/test/TestConsoleApp/Data/Models/Employee.cs b/test/TestConsoleApp/Data/Models/Employee.cs index 9ab3564..7dfdd52 100644 --- a/test/TestConsoleApp/Data/Models/Employee.cs +++ b/test/TestConsoleApp/Data/Models/Employee.cs @@ -10,6 +10,5 @@ namespace TestConsoleApp.Data.Models public string EmployeeCode { get; set; } - public Manager Manager { get; set; } } } diff --git a/test/TestConsoleApp/Data/Models/Manager.cs b/test/TestConsoleApp/Data/Models/Manager.cs deleted file mode 100644 index e41364d..0000000 --- a/test/TestConsoleApp/Data/Models/Manager.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace TestConsoleApp.Data.Models -{ - public class Manager: Employee - { - public int Level { get; set; } - - public IEnumerable Employees { get; set; } = Array.Empty(); - } -} diff --git a/test/TestConsoleApp/Data/Models/MyStruct.cs b/test/TestConsoleApp/Data/Models/MyStruct.cs new file mode 100644 index 0000000..eee7b47 --- /dev/null +++ b/test/TestConsoleApp/Data/Models/MyStruct.cs @@ -0,0 +1,25 @@ +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))] + public partial struct MyStruct + { + public int SomeInt { get; set; } + + public string ReadOnlyString { get; } + + public MyStruct(int someInt, string readOnlyString) + { + SomeInt = someInt; + ReadOnlyString = readOnlyString; + } + + } +} diff --git a/test/TestConsoleApp/Data/Models/Profile.cs b/test/TestConsoleApp/Data/Models/Profile.cs deleted file mode 100644 index 898c13a..0000000 --- a/test/TestConsoleApp/Data/Models/Profile.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace TestConsoleApp.Data.Models -{ - public class Profile - { - public string FirstName { get; set; } - - public string LastName { get; set; } - - public string FullName => $"{FirstName} {LastName}"; - } -} \ No newline at end of file diff --git a/test/TestConsoleApp/Data/Models/User.cs b/test/TestConsoleApp/Data/Models/User.cs index 550b840..4934f52 100644 --- a/test/TestConsoleApp/Data/Models/User.cs +++ b/test/TestConsoleApp/Data/Models/User.cs @@ -1,13 +1,15 @@ using System; +using TestConsoleApp.ViewModels; +using MapTo; namespace TestConsoleApp.Data.Models { - public class User + [MapFrom(typeof(UserViewModel))] + public partial class User { public int Id { get; set; } public DateTimeOffset RegisteredAt { get; set; } - public Profile Profile { get; set; } } } \ No newline at end of file diff --git a/test/TestConsoleApp/Program.cs b/test/TestConsoleApp/Program.cs index be8a84d..dcdaee5 100644 --- a/test/TestConsoleApp/Program.cs +++ b/test/TestConsoleApp/Program.cs @@ -2,7 +2,6 @@ using MapTo; using TestConsoleApp.Data.Models; using TestConsoleApp.ViewModels; -using TestConsoleApp.ViewModels2; namespace TestConsoleApp { @@ -11,7 +10,6 @@ namespace TestConsoleApp private static void Main(string[] args) { //UserTest(); - CyclicReferenceTest(); // EmployeeManagerTest(); Console.WriteLine("done"); @@ -19,74 +17,23 @@ namespace TestConsoleApp private static void EmployeeManagerTest() { - var manager1 = new Manager - { - Id = 1, - EmployeeCode = "M001", - Level = 100 - }; - - var manager2 = new Manager - { - Id = 2, - EmployeeCode = "M002", - Level = 100, - Manager = manager1 - }; - + var employee1 = new Employee { Id = 101, EmployeeCode = "E101", - Manager = manager1 }; var employee2 = new Employee { Id = 102, EmployeeCode = "E102", - Manager = manager2 }; - manager1.Employees = new[] { employee1, manager2 }; - manager2.Employees = new[] { employee2 }; - manager1.ToManagerViewModel(); - 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 - { - Id = 1234, - RegisteredAt = DateTimeOffset.Now, - Profile = new Profile - { - FirstName = "John", - LastName = "Doe" - } - }; - - var vm = user.ToUserViewModel(); - - Console.WriteLine("Key: {0}", vm.Key); - Console.WriteLine("RegisteredAt: {0}", vm.RegisteredAt); - Console.WriteLine("FirstName: {0}", vm.Profile.FirstName); - Console.WriteLine("LastName: {0}", vm.Profile.LastName); - } + } } \ No newline at end of file diff --git a/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs b/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs index 06eeccd..6c3adf4 100644 --- a/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs +++ b/test/TestConsoleApp/ViewModels/EmployeeViewModel.cs @@ -1,16 +1,13 @@ using MapTo; using TestConsoleApp.Data.Models; -using TestConsoleApp.ViewModels2; namespace TestConsoleApp.ViewModels { [MapFrom(typeof(Employee))] public partial class EmployeeViewModel { - public int Id { get; set; } + public int Id { get; } - public string EmployeeCode { get; set; } - public ManagerViewModel Manager { get; set; } } } diff --git a/test/TestConsoleApp/ViewModels/ManagerViewModel.cs b/test/TestConsoleApp/ViewModels/ManagerViewModel.cs deleted file mode 100644 index dc5e660..0000000 --- a/test/TestConsoleApp/ViewModels/ManagerViewModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using MapTo; -using TestConsoleApp.Data.Models; -using TestConsoleApp.ViewModels; - -namespace TestConsoleApp.ViewModels2 -{ - [MapFrom(typeof(Manager))] - public partial class ManagerViewModel : EmployeeViewModel - { - public int Level { get; set; } - - public IEnumerable Employees { get; set; } = Array.Empty(); - } -} \ No newline at end of file diff --git a/test/TestConsoleApp/ViewModels/MyStructViewModel.cs b/test/TestConsoleApp/ViewModels/MyStructViewModel.cs new file mode 100644 index 0000000..46577ea --- /dev/null +++ b/test/TestConsoleApp/ViewModels/MyStructViewModel.cs @@ -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; + } + } +} diff --git a/test/TestConsoleApp/ViewModels/ProfileViewModel.cs b/test/TestConsoleApp/ViewModels/ProfileViewModel.cs deleted file mode 100644 index 67d4b84..0000000 --- a/test/TestConsoleApp/ViewModels/ProfileViewModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using MapTo; -using TestConsoleApp.Data.Models; - -namespace TestConsoleApp.ViewModels -{ - [MapFrom(typeof(Profile))] - public partial class ProfileViewModel - { - public string FirstName { get; } - - public string LastName { get; } - } -} \ No newline at end of file diff --git a/test/TestConsoleApp/ViewModels/UserViewModel.cs b/test/TestConsoleApp/ViewModels/UserViewModel.cs index 91b2f77..7070fe4 100644 --- a/test/TestConsoleApp/ViewModels/UserViewModel.cs +++ b/test/TestConsoleApp/ViewModels/UserViewModel.cs @@ -7,18 +7,8 @@ namespace TestConsoleApp.ViewModels [MapFrom(typeof(User))] public partial class UserViewModel { - [MapProperty(SourcePropertyName = nameof(User.Id))] - [MapTypeConverter(typeof(IdConverter))] - public string Key { get; } + public int Id { get; set; } - public DateTimeOffset RegisteredAt { get; set; } - - // [IgnoreProperty] - public ProfileViewModel Profile { get; set; } - - private class IdConverter : ITypeConverter - { - public string Convert(int source, object[]? converterParameters) => $"{source:X}"; - } + public DateTimeOffset RegisteredAt { get; } } } \ No newline at end of file