From 4077dfd692dd250de5ab1623e11317baa614f9f9 Mon Sep 17 00:00:00 2001 From: Wvader <34067397+wvader@users.noreply.github.com> Date: Wed, 8 Dec 2021 17:58:21 +0000 Subject: [PATCH] Handle structs and updates --- src/MapTo/ClassMappingContext.cs | 15 +- src/MapTo/Extensions/CommonExtensions.cs | 49 +++++ src/MapTo/Extensions/CommonSource.cs | 177 ++++++++++++++++ src/MapTo/MapToGenerator.cs | 1 + src/MapTo/MappingContext.cs | 64 +++++- src/MapTo/Models.cs | 8 +- src/MapTo/RecordMappingContext.cs | 16 +- src/MapTo/Sources/MapClassSource.cs | 196 +----------------- src/MapTo/Sources/MapFromAttributeSource.cs | 2 +- src/MapTo/Sources/MapRecordSource.cs | 6 +- src/MapTo/Sources/MapStructSource.cs | 163 +-------------- .../ReadOnlyPropertyAttributeSource.cs | 36 ++++ src/MapTo/Sources/UseUpdateAttributeSource.cs | 40 ++++ src/MapTo/StructMappingContext.cs | 40 ++++ test/TestConsoleApp/Data/Models/Car.cs | 27 +++ test/TestConsoleApp/Data/Models/Employee.cs | 12 +- test/TestConsoleApp/Data/Models/Manager.cs | 13 -- test/TestConsoleApp/Data/Models/MyStruct.cs | 26 +++ test/TestConsoleApp/Data/Models/Profile.cs | 11 - test/TestConsoleApp/Data/Models/User.cs | 20 +- test/TestConsoleApp/Program.cs | 66 +----- test/TestConsoleApp/ViewModels/CarReadDto.cs | 23 ++ .../ViewModels/EmployeeViewModel.cs | 5 +- .../ViewModels/ManagerViewModel.cs | 16 -- .../ViewModels/MyStructViewModel.cs | 22 ++ .../ViewModels/ProfileViewModel.cs | 13 -- .../ViewModels/UserViewModel.cs | 16 +- 27 files changed, 580 insertions(+), 503 deletions(-) create mode 100644 src/MapTo/Extensions/CommonExtensions.cs create mode 100644 src/MapTo/Extensions/CommonSource.cs create mode 100644 src/MapTo/Sources/ReadOnlyPropertyAttributeSource.cs create mode 100644 src/MapTo/Sources/UseUpdateAttributeSource.cs create mode 100644 src/MapTo/StructMappingContext.cs create mode 100644 test/TestConsoleApp/Data/Models/Car.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 create mode 100644 test/TestConsoleApp/ViewModels/CarReadDto.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..3291ac4 --- /dev/null +++ b/src/MapTo/Extensions/CommonExtensions.cs @@ -0,0 +1,49 @@ +using MapTo.Sources; +using System; +using System.Collections.Generic; +using System.Text; + +namespace MapTo.Extensions +{ + internal static class CommonExtensions + { + internal static SourceBuilder WriteComment(this SourceBuilder builder, string comment = "") + { + return builder.WriteLine($"// {comment}"); + } + + internal static SourceBuilder WriteModelInfo(this SourceBuilder builder, MappingModel model) + { + return builder + .WriteLine() + .WriteComment($" IsTypeUpdatable {model.IsTypeUpdatable}") + .WriteComment($" HasMappedBaseClass {model.HasMappedBaseClass.ToString()}") + .WriteComment($" Namespace {model.Namespace}") + .WriteComment($" Options {model.Options.ToString()}") + .WriteComment($" Type {model.Type}") + .WriteComment($" TypeIdentifierName {model.TypeIdentifierName}") + .WriteComment($" SourceNamespace {model.SourceNamespace}") + .WriteComment($" SourceTypeFullName {model.SourceTypeFullName}") + .WriteComment($" SourceTypeIdentifierName {model.SourceTypeIdentifierName}"); + + } + + internal static SourceBuilder WriteMappedProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray mappedProperties) + { + foreach (var item in mappedProperties) + { + builder .WriteComment($" Name {item.Name}") + .WriteComment($" Type {item.Type}") + .WriteComment($" MappedSourcePropertyTypeName {item.MappedSourcePropertyTypeName}") + .WriteComment($" IsEnumerable {item.IsEnumerable}") + .WriteComment($" SourcePropertyName {item.SourcePropertyName}") + .WriteComment($" TypeSymbol {item.FullyQualifiedType.ToString()}") + .WriteComment($" TypeSymbol {item.FullyQualifiedType.ToString()}") + .WriteLine(); + } + + return builder; + } + + } +} diff --git a/src/MapTo/Extensions/CommonSource.cs b/src/MapTo/Extensions/CommonSource.cs new file mode 100644 index 0000000..8e07e8e --- /dev/null +++ b/src/MapTo/Extensions/CommonSource.cs @@ -0,0 +1,177 @@ +using MapTo.Sources; +using static MapTo.Sources.Constants; +using System; +using System.Collections.Generic; +using System.Text; + +namespace MapTo.Extensions +{ + internal static class CommonSource + { + internal static SourceCode GenerateStructOrClass(this MappingModel model, string structOrClass) + { + const bool writeDebugInfo = false; + + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes) + .WriteUsings(model.Usings) + .WriteLine() + + // Namespace declaration + .WriteLine($"namespace {model.Namespace}") + .WriteOpeningBracket(); + + if(writeDebugInfo) + builder + .WriteModelInfo(model) + .WriteLine() + .WriteComment("Type properties") + .WriteComment() + .WriteMappedProperties(model.TypeProperties) + .WriteLine() + .WriteComment("Source properties") + .WriteMappedProperties(model.SourceProperties) + .WriteLine(); + + builder + // Class declaration + .WriteLine($"partial {structOrClass} {model.TypeIdentifierName}") + .WriteOpeningBracket() + .WriteLine() + // Class body + .GeneratePublicConstructor(model); + + if (model.IsTypeUpdatable && model.TypeProperties.GetWritableMappedProperties().Length > 0) builder.GenerateUpdateMethod(model); + + builder + .WriteLine() + // End class declaration + .WriteClosingBracket() + .WriteLine() + // End namespace declaration + .WriteClosingBracket(); + + return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs"); + } + + private static SourceBuilder GeneratePublicConstructor(this SourceBuilder builder, MappingModel model) + { + var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); + const string mappingContextParameterName = "context"; + + var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty; + + var readOnlyProperties = model.TypeProperties.GetReadOnlyMappedProperties(); + + var stringBuilder = new StringBuilder(); + + for (int i = 0; i < readOnlyProperties.Length; i++) + { + var property = readOnlyProperties[i]; + if(!model.SourceProperties.IsMappedProperty(property)) + { + stringBuilder.Append(", "); + stringBuilder.Append($"{property.FullyQualifiedType} {property.SourcePropertyName.ToCamelCase()}"); + } + + } + + var readOnlyPropertiesArguments = stringBuilder.ToString(); + + builder + .WriteLine($"public {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName}{readOnlyPropertiesArguments}){baseConstructor}") + .WriteOpeningBracket() + .TryWriteProperties(model.SourceProperties, readOnlyProperties, sourceClassParameterName, mappingContextParameterName, false); + + // End constructor declaration + return builder.WriteClosingBracket(); + } + + private static bool IsMappedProperty(this System.Collections.Immutable.ImmutableArray properties, MappedProperty property) => properties.Contains(property); + + private static SourceBuilder TryWriteProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray properties, System.Collections.Immutable.ImmutableArray? otherProperties, + string? sourceClassParameterName, string mappingContextParameterName, bool fromUpdate) + { + if (fromUpdate) + { + properties = properties.GetWritableMappedProperties(); + } + + foreach (var property in properties) + { + if (property.isReadOnly && fromUpdate) continue; + + if (property.TypeConverter is null) + { + if (property.IsEnumerable) + { + builder.WriteLine( + $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList();"); + } + else + { + builder.WriteLine(property.MappedSourcePropertyTypeName is null + ? $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};" + : ""); + } + } + else + { + var parameters = property.TypeConverterParameters.IsEmpty + ? "null" + : $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}"; + + builder.WriteLine( + $"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters});"); + } + + } + + + if (otherProperties == null) return builder; + + foreach (var property in otherProperties) + { + if(!properties.IsMappedProperty(property)) + builder.WriteLine(property.MappedSourcePropertyTypeName is null + ? $"{property.Name} = {property.SourcePropertyName.ToCamelCase()};" + : ""); + + } + + return builder; + + } + + + private static SourceBuilder GenerateUpdateMethod(this SourceBuilder builder, MappingModel model) + { + var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); + + builder + .GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName) + .WriteLine($"public void Update({model.SourceType} {sourceClassParameterName})") + .WriteOpeningBracket() + .TryWriteProperties(model.SourceProperties, null, sourceClassParameterName, "context", true) + .WriteClosingBracket(); + + return builder; + } + + private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName) + { + if (!model.Options.GenerateXmlDocument) + { + return builder; + } + + return builder + .WriteLine("/// ") + .WriteLine($"/// Updates and sets its participating properties") + .WriteLine($"/// using the property values from .") + .WriteLine("/// ") + .WriteLine($"/// The instance of to use as source."); + } + } +} diff --git a/src/MapTo/MapToGenerator.cs b/src/MapTo/MapToGenerator.cs index d740394..8a524a7 100644 --- a/src/MapTo/MapToGenerator.cs +++ b/src/MapTo/MapToGenerator.cs @@ -28,6 +28,7 @@ namespace MapTo var options = SourceGenerationOptions.From(context); var compilation = context.Compilation + .AddSource(ref context, UseUpdateAttributeSource.Generate(options)) .AddSource(ref context, MapFromAttributeSource.Generate(options)) .AddSource(ref context, IgnorePropertyAttributeSource.Generate(options)) .AddSource(ref context, ITypeConverterSource.Generate(options)) diff --git a/src/MapTo/MappingContext.cs b/src/MapTo/MappingContext.cs index 59160c4..892e2c5 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; @@ -27,6 +33,7 @@ namespace MapTo TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(ITypeConverterSource.FullyQualifiedName); MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapPropertyAttributeSource.FullyQualifiedName); MapFromAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MapFromAttributeSource.FullyQualifiedName); + UseUpdateAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(UseUpdateAttributeSource.FullyQualifiedName); MappingContextTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(MappingContextSource.FullyQualifiedName); AddUsingIfRequired(sourceGenerationOptions.SupportNullableStaticAnalysis, "System.Diagnostics.CodeAnalysis"); @@ -41,6 +48,8 @@ namespace MapTo protected INamedTypeSymbol IgnorePropertyAttributeTypeSymbol { get; } protected INamedTypeSymbol MapFromAttributeTypeSymbol { get; } + + protected INamedTypeSymbol UseUpdateAttributeTypeSymbol { get; } protected INamedTypeSymbol MappingContextTypeSymbol { get; } @@ -60,6 +69,7 @@ namespace MapTo { MappingContext context = typeSyntax switch { + StructDeclarationSyntax => new StructMappingContext(compilation, sourceGenerationOptions, typeSyntax), ClassDeclarationSyntax => new ClassMappingContext(compilation, sourceGenerationOptions, typeSyntax), RecordDeclarationSyntax => new RecordMappingContext(compilation, sourceGenerationOptions, typeSyntax), _ => throw new ArgumentOutOfRangeException() @@ -100,7 +110,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); @@ -128,6 +139,11 @@ namespace MapTo .Any(t => t?.GetAttribute(MapFromAttributeTypeSymbol) != null); } + protected bool IsTypeUpdatable() + { + return TypeSyntax.GetAttribute("UseUpdate") != null; + } + protected virtual MappedProperty? MapProperty(ISymbol sourceTypeSymbol, IReadOnlyCollection sourceProperties, ISymbol property) { var sourceProperty = FindSourceProperty(sourceProperties, property); @@ -136,6 +152,7 @@ namespace MapTo return null; } + string? converterFullyQualifiedName = null; var converterParameters = ImmutableArray.Empty; ITypeSymbol? mappedSourcePropertyType = null; @@ -154,16 +171,50 @@ namespace MapTo AddUsingIfRequired(enumerableTypeArgumentType); AddUsingIfRequired(mappedSourcePropertyType); + return new MappedProperty( property.Name, + property.GetTypeSymbol().ToString(), ToQualifiedDisplayName(propertyType) ?? propertyType.Name, converterFullyQualifiedName, converterParameters.ToImmutableArray(), sourceProperty.Name, ToQualifiedDisplayName(mappedSourcePropertyType), - ToQualifiedDisplayName(enumerableTypeArgumentType)); + ToQualifiedDisplayName(enumerableTypeArgumentType), + (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, + property.GetTypeSymbol().ToString(), + 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) { @@ -258,9 +309,10 @@ namespace MapTo var typeIdentifierName = TypeSyntax.GetIdentifierName(); var sourceTypeIdentifierName = sourceTypeSymbol.Name; var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel); + var isTypeUpdatable = IsTypeUpdatable(); 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)); @@ -269,6 +321,8 @@ namespace MapTo AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq"); + var allProperties = GetTypeMappedProperties(sourceTypeSymbol, typeSymbol , isTypeInheritFromMappedBaseClass); + return new MappingModel( SourceGenerationOptions, TypeSyntax.GetNamespace(), @@ -278,12 +332,16 @@ namespace MapTo sourceTypeSymbol.ContainingNamespace.ToDisplayString(), sourceTypeIdentifierName, sourceTypeSymbol.ToDisplayString(), + isTypeUpdatable, 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 9bda8be..e8997bd 100644 --- a/src/MapTo/Models.cs +++ b/src/MapTo/Models.cs @@ -24,12 +24,14 @@ namespace MapTo internal record MappedProperty( string Name, + string FullyQualifiedType, string Type, string? TypeConverter, ImmutableArray TypeConverterParameters, string SourcePropertyName, string? MappedSourcePropertyTypeName, - string? EnumerableTypeArgument) + string? EnumerableTypeArgument, + bool isReadOnly) { public bool IsEnumerable => EnumerableTypeArgument is not null; } @@ -43,7 +45,9 @@ namespace MapTo string SourceNamespace, string SourceTypeIdentifierName, string SourceTypeFullName, - ImmutableArray MappedProperties, + bool IsTypeUpdatable, + 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 21c94de..9278fe6 100644 --- a/src/MapTo/Sources/MapClassSource.cs +++ b/src/MapTo/Sources/MapClassSource.cs @@ -1,4 +1,5 @@ using MapTo.Extensions; +using System.Text; using static MapTo.Sources.Constants; namespace MapTo.Sources @@ -7,200 +8,7 @@ namespace MapTo.Sources { internal static SourceCode Generate(MappingModel model) { - using var builder = new SourceBuilder() - .WriteLine(GeneratedFilesHeader) - .WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes) - .WriteUsings(model.Usings) - .WriteLine() - - // Namespace declaration - .WriteLine($"namespace {model.Namespace}") - .WriteOpeningBracket() - - // Class declaration - .WriteLine($"partial class {model.TypeIdentifierName}") - .WriteOpeningBracket(); - - // Class body - if (model.GenerateSecondaryConstructor) - { - builder - .GenerateSecondaryConstructor(model) - .WriteLine(); - } - - builder - .GeneratePrivateConstructor(model) - .WriteLine() - .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"); - } - - private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model) - { - var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); - - if (model.Options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine($"/// Initializes a new instance of the class") - .WriteLine($"/// using the property values from the specified .") - .WriteLine("/// ") - .WriteLine($"/// {sourceClassParameterName} is null"); - } - - return builder - .WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName})") - .WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}"); - } - - private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model) - { - var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); - const string mappingContextParameterName = "context"; - - var baseConstructor = model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" : string.Empty; - - builder - .WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {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); - - // End constructor declaration - return builder.WriteClosingBracket(); - } - - private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, - string? sourceClassParameterName, string mappingContextParameterName) - { - foreach (var property in model.MappedProperties) - { - if (property.TypeConverter is null) - { - if (property.IsEnumerable) - { - builder.WriteLine( - $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList();"); - } - else - { - builder.WriteLine(property.MappedSourcePropertyTypeName is null - ? $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};" - : $"{property.Name} = {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName});"); - } - } - else - { - var parameters = property.TypeConverterParameters.IsEmpty - ? "null" - : $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}"; - - builder.WriteLine( - $"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters});"); - } - - } - return builder; - - } - - private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model) - { - var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); - - return builder - .GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName) - .WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]") - .WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} From({model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})") - .WriteOpeningBracket() - .WriteLine($"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{model.SourceType}, {model.TypeIdentifierName}>({sourceClassParameterName});") - .WriteClosingBracket(); - } - - private static SourceBuilder GenerateUpdateMethod(this SourceBuilder builder, MappingModel model) - { - var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); - - builder - .GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName) - .WriteLine($"public void {model.Options.NullableReferenceSyntax}Update({model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})") - .WriteOpeningBracket() - .WriteProperties( model, sourceClassParameterName,"context" ) - .WriteClosingBracket(); - - return builder; - } - - private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName) - { - if (!model.Options.GenerateXmlDocument) - { - return builder; - } - - return builder - .WriteLine("/// ") - .WriteLine($"/// Creates a new instance of and sets its participating properties") - .WriteLine($"/// using the property values from .") - .WriteLine("/// ") - .WriteLine($"/// The instance of to use as source.") - .WriteLine($"/// A new instance of -or- null if is null."); - } - - private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName) - { - if (!model.Options.GenerateXmlDocument) - { - return builder; - } - - return builder - .WriteLine("/// ") - .WriteLine($"/// Updates and sets its participating properties") - .WriteLine($"/// using the property values from .") - .WriteLine("/// ") - .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(); - - return builder - .GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName) - .WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]") - .WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} To{model.TypeIdentifierName}(this {model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})") - .WriteOpeningBracket() - .WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});") - .WriteClosingBracket(); + return model.GenerateStructOrClass("class"); } } } \ No newline at end of file diff --git a/src/MapTo/Sources/MapFromAttributeSource.cs b/src/MapTo/Sources/MapFromAttributeSource.cs index fc3f52b..6594832 100644 --- a/src/MapTo/Sources/MapFromAttributeSource.cs +++ b/src/MapTo/Sources/MapFromAttributeSource.cs @@ -26,7 +26,7 @@ namespace MapTo.Sources } builder - .WriteLine("[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]") + .WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]") .WriteLine($"public sealed class {AttributeName}Attribute : Attribute") .WriteOpeningBracket(); 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 6177468..32f4960 100644 --- a/src/MapTo/Sources/MapStructSource.cs +++ b/src/MapTo/Sources/MapStructSource.cs @@ -1,5 +1,7 @@ using MapTo.Extensions; using static MapTo.Sources.Constants; +using System.Collections.Generic; +using System.Text; namespace MapTo.Sources { @@ -7,166 +9,7 @@ namespace MapTo.Sources { internal static SourceCode Generate(MappingModel model) { - using var builder = new SourceBuilder() - .WriteLine(GeneratedFilesHeader) - .WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes) - .WriteUsings(model.Usings) - .WriteLine() - - // Namespace declaration - .WriteLine($"namespace {model.Namespace}") - .WriteOpeningBracket() - - // Class declaration - .WriteLine($"partial class {model.TypeIdentifierName}") - .WriteOpeningBracket(); - - // Class body - if (model.GenerateSecondaryConstructor) - { - builder - .GenerateSecondaryConstructor(model) - .WriteLine(); - } - - builder - .GeneratePrivateConstructor(model) - .WriteLine() - // End class declaration - .WriteClosingBracket() - .WriteLine() - - // End namespace declaration - .WriteClosingBracket(); - - return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs"); - } - - private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model) - { - var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); - - if (model.Options.GenerateXmlDocument) - { - builder - .WriteLine("/// ") - .WriteLine($"/// Initializes a new instance of the class") - .WriteLine($"/// using the property values from the specified .") - .WriteLine("/// ") - .WriteLine($"/// {sourceClassParameterName} is null"); - } - - return builder - .WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName})") - .WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}"); - } - - private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model) - { - var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); - const string mappingContextParameterName = "context"; - - var baseConstructor = model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" : string.Empty; - - builder - .WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceType} {sourceClassParameterName}){baseConstructor}") - .WriteOpeningBracket() - .WriteLine() - .WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);") - .WriteLine(). - - WriteProperties( model, sourceClassParameterName, mappingContextParameterName); - - // End constructor declaration - return builder.WriteClosingBracket(); - } - - private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, - string? sourceClassParameterName, string mappingContextParameterName) - { - foreach (var property in model.MappedProperties) - { - if (property.TypeConverter is null) - { - if (property.IsEnumerable) - { - builder.WriteLine( - $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName}.Select({mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.EnumerableTypeArgument}>).ToList();"); - } - else - { - builder.WriteLine(property.MappedSourcePropertyTypeName is null - ? $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};" - : $"{property.Name} = {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName});"); - } - } - else - { - var parameters = property.TypeConverterParameters.IsEmpty - ? "null" - : $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}"; - - builder.WriteLine( - $"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters});"); - } - - } - return builder; - - } - - - private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName) - { - if (!model.Options.GenerateXmlDocument) - { - return builder; - } - - return builder - .WriteLine("/// ") - .WriteLine($"/// Creates a new instance of and sets its participating properties") - .WriteLine($"/// using the property values from .") - .WriteLine("/// ") - .WriteLine($"/// The instance of to use as source.") - .WriteLine($"/// A new instance of -or- null if is null."); - } - - private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName) - { - if (!model.Options.GenerateXmlDocument) - { - return builder; - } - - return builder - .WriteLine("/// ") - .WriteLine($"/// Updates and sets its participating properties") - .WriteLine($"/// using the property values from .") - .WriteLine("/// ") - .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(); - - return builder - .GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName) - .WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]") - .WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} To{model.TypeIdentifierName}(this {model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})") - .WriteOpeningBracket() - .WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});") - .WriteClosingBracket(); + return model.GenerateStructOrClass("struct"); } } } \ No newline at end of file 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/Sources/UseUpdateAttributeSource.cs b/src/MapTo/Sources/UseUpdateAttributeSource.cs new file mode 100644 index 0000000..2d0f49a --- /dev/null +++ b/src/MapTo/Sources/UseUpdateAttributeSource.cs @@ -0,0 +1,40 @@ +using static MapTo.Sources.Constants; + +namespace MapTo.Sources +{ + internal static class UseUpdateAttributeSource + { + internal const string AttributeName = "UseUpdate"; + internal const string AttributeClassName = AttributeName + "Attribute"; + internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName; + + internal static SourceCode Generate(SourceGenerationOptions options) + { + using var builder = new SourceBuilder() + .WriteLine(GeneratedFilesHeader) + .WriteLine("using System;") + .WriteLine() + .WriteLine($"namespace {RootNamespace}") + .WriteOpeningBracket(); + + if (options.GenerateXmlDocument) + { + builder + .WriteLine("/// ") + .WriteLine("/// Specifies that the annotated class can be mapped from the provided .") + .WriteLine("/// "); + } + + builder + .WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]") + .WriteLine($"public sealed class {AttributeName}Attribute : Attribute") + .WriteOpeningBracket(); + + builder + .WriteClosingBracket() // class + .WriteClosingBracket(); // namespace + + return new(builder.ToString(), $"{AttributeName}Attribute.g.cs"); + } + } +} \ No newline at end of file diff --git a/src/MapTo/StructMappingContext.cs b/src/MapTo/StructMappingContext.cs new file mode 100644 index 0000000..9347568 --- /dev/null +++ b/src/MapTo/StructMappingContext.cs @@ -0,0 +1,40 @@ +using System.Collections.Immutable; +using System.Linq; +using MapTo.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace MapTo +{ + internal class StructMappingContext : MappingContext + { + internal StructMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) + : base(compilation, sourceGenerationOptions, typeSyntax) { } + + protected override ImmutableArray GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass) + { + var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType().ToArray(); + + return typeSymbol + .GetAllMembers() + .OfType() + .Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol)) + .Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property)) + .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/Car.cs b/test/TestConsoleApp/Data/Models/Car.cs new file mode 100644 index 0000000..fe6f130 --- /dev/null +++ b/test/TestConsoleApp/Data/Models/Car.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MapTo; +using TestConsoleApp.ViewModels; + +namespace TestConsoleApp.Data.Models +{ + [MapFrom(typeof(CarReadDto))] + [UseUpdate] + partial class Car + { + public int Size { get; } + public int Id { get; } + + public string Brand { get; } + + public Car(int size, int id, string brand) + { + Size = size; + Id = id; + Brand = brand; + } + } +} diff --git a/test/TestConsoleApp/Data/Models/Employee.cs b/test/TestConsoleApp/Data/Models/Employee.cs index 9ab3564..a25a6d9 100644 --- a/test/TestConsoleApp/Data/Models/Employee.cs +++ b/test/TestConsoleApp/Data/Models/Employee.cs @@ -4,12 +4,18 @@ using System.Text; namespace TestConsoleApp.Data.Models { + public class Employee { - public int Id { get; set; } + public int Id { get; } - public string EmployeeCode { get; set; } + public string EmployeeCode { get; } + + public Employee(int id, string employeeCode) + { + Id = id; + EmployeeCode = employeeCode; + } - 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..2be6f38 --- /dev/null +++ b/test/TestConsoleApp/Data/Models/MyStruct.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TestConsoleApp.ViewModels; +using MapTo; + +namespace TestConsoleApp.Data.Models +{ + [MapFrom(typeof(MyStructViewModel))] + [UseUpdate] + public partial struct MyStruct + { + public int SomeInt { get; set; } + + public string ReadOnlyString { get; } + + public MyStruct(int someInt, string readOnlyString) + { + SomeInt = someInt; + ReadOnlyString = readOnlyString; + } + + } +} 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..5f49b57 100644 --- a/test/TestConsoleApp/Data/Models/User.cs +++ b/test/TestConsoleApp/Data/Models/User.cs @@ -1,13 +1,29 @@ using System; +using TestConsoleApp.ViewModels; +using MapTo; +using System.Collections.Generic; namespace TestConsoleApp.Data.Models { - public class User + [MapFrom(typeof(UserViewModel))] + public partial struct User { public int Id { get; set; } + public List> ListOfListOfString {get; } + + public List StringList { get; } public DateTimeOffset RegisteredAt { get; set; } - public Profile Profile { get; set; } + public User( int id, List> listOfListOfString, List stringList, DateTimeOffset registeredAt) + { + this.StringList = stringList; + this.Id = id; + ListOfListOfString = listOfListOfString; + RegisteredAt = registeredAt; + } + + + } } \ No newline at end of file diff --git a/test/TestConsoleApp/Program.cs b/test/TestConsoleApp/Program.cs index be8a84d..06a1bc2 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,16 @@ 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 employee = new Employee(1, "hello"); + - 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/CarReadDto.cs b/test/TestConsoleApp/ViewModels/CarReadDto.cs new file mode 100644 index 0000000..2241eff --- /dev/null +++ b/test/TestConsoleApp/ViewModels/CarReadDto.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MapTo; +using TestConsoleApp.Data.Models; + +namespace TestConsoleApp.ViewModels +{ + [MapFrom(typeof(Car))] + partial class CarReadDto + { + public int Size { get; } + public string Brand { get; } + + public CarReadDto(int size, string brand) + { + Size = size; + Brand = brand; + } + } +} 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..b017d3f 100644 --- a/test/TestConsoleApp/ViewModels/UserViewModel.cs +++ b/test/TestConsoleApp/ViewModels/UserViewModel.cs @@ -5,20 +5,10 @@ using TestConsoleApp.Data.Models; namespace TestConsoleApp.ViewModels { [MapFrom(typeof(User))] - public partial class UserViewModel + public partial struct 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