Compare commits

...

11 Commits
summer ... Blue

Author SHA1 Message Date
Wvader 9a719a0226 Struct handling with readonly fields (wip) 2021-12-07 22:02:59 +00:00
Wvader b44f5b2fe5 Update method when properties are not readonly 2021-12-07 12:49:31 +00:00
Wvader 1e266bfd3f Update method when properties are not readonly 2021-12-07 12:47:01 +00:00
Wvader 55f735006e Remove mapping context from constructor 2021-12-07 11:58:58 +00:00
Wvader 771adf22ef stf 2021-12-07 11:55:44 +00:00
Wvader 2bcc521bfa stuff 2021-12-07 11:51:50 +00:00
Wvader 808f353cac Try enforce struct usage 2021-12-07 11:46:45 +00:00
Wvader 9d1e064c3a Try use struct source for all 2021-12-06 16:47:16 +00:00
Wvader eb32ef8e94 Struct tweaks 2021-12-06 16:39:48 +00:00
Wvader e314387772 Struct context 2021-12-06 16:26:14 +00:00
Wvader 4d73897b4e Add struct to mapfrom 2021-12-06 16:02:10 +00:00
22 changed files with 361 additions and 252 deletions

View File

@ -11,7 +11,7 @@ namespace MapTo
internal ClassMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) internal ClassMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { } : base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) protected override ImmutableArray<MappedProperty> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{ {
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray(); var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
@ -23,5 +23,18 @@ namespace MapTo
.Where(mappedProperty => mappedProperty is not null) .Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!; .ToImmutableArray()!;
} }
protected override ImmutableArray<MappedProperty> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapProperty(typeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
} }
} }

View File

@ -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<MappedProperty> 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;
}
}
}

View File

@ -9,6 +9,12 @@ using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MapTo namespace MapTo
{ {
internal static class MappingContextExtensions
{
internal static ImmutableArray<MappedProperty> GetReadOnlyMappedProperties(this ImmutableArray<MappedProperty> mappedProperties) => mappedProperties.Where(p => p.isReadOnly).ToImmutableArray()!;
internal static ImmutableArray<MappedProperty> GetWritableMappedProperties(this ImmutableArray<MappedProperty> mappedProperties) => mappedProperties.Where(p => !p.isReadOnly).ToImmutableArray()!;
}
internal abstract class MappingContext internal abstract class MappingContext
{ {
private readonly List<SymbolDisplayPart> _ignoredNamespaces; private readonly List<SymbolDisplayPart> _ignoredNamespaces;
@ -60,6 +66,7 @@ namespace MapTo
{ {
MappingContext context = typeSyntax switch MappingContext context = typeSyntax switch
{ {
StructDeclarationSyntax => new StructMappingContext(compilation, sourceGenerationOptions, typeSyntax),
ClassDeclarationSyntax => new ClassMappingContext(compilation, sourceGenerationOptions, typeSyntax), ClassDeclarationSyntax => new ClassMappingContext(compilation, sourceGenerationOptions, typeSyntax),
RecordDeclarationSyntax => new RecordMappingContext(compilation, sourceGenerationOptions, typeSyntax), RecordDeclarationSyntax => new RecordMappingContext(compilation, sourceGenerationOptions, typeSyntax),
_ => throw new ArgumentOutOfRangeException() _ => throw new ArgumentOutOfRangeException()
@ -100,7 +107,8 @@ namespace MapTo
return sourceProperties.SingleOrDefault(p => p.Name == propertyName); return sourceProperties.SingleOrDefault(p => p.Name == propertyName);
} }
protected abstract ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass); protected abstract ImmutableArray<MappedProperty> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected abstract ImmutableArray<MappedProperty> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass);
protected INamedTypeSymbol? GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null) => protected INamedTypeSymbol? GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null) =>
GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel); GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel);
@ -136,6 +144,7 @@ namespace MapTo
return null; return null;
} }
string? converterFullyQualifiedName = null; string? converterFullyQualifiedName = null;
var converterParameters = ImmutableArray<string>.Empty; var converterParameters = ImmutableArray<string>.Empty;
ITypeSymbol? mappedSourcePropertyType = null; ITypeSymbol? mappedSourcePropertyType = null;
@ -161,9 +170,39 @@ namespace MapTo
converterParameters.ToImmutableArray(), converterParameters.ToImmutableArray(),
sourceProperty.Name, sourceProperty.Name,
ToQualifiedDisplayName(mappedSourcePropertyType), 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<string>.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, protected bool TryGetMapTypeConverter(ISymbol property, IPropertySymbol sourceProperty, out string? converterFullyQualifiedName,
out ImmutableArray<string> converterParameters) out ImmutableArray<string> converterParameters)
{ {
@ -260,7 +299,7 @@ namespace MapTo
var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel); var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol); var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
var mappedProperties = GetMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass); var mappedProperties = GetSourceMappedProperties(typeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
if (!mappedProperties.Any()) if (!mappedProperties.Any())
{ {
AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol)); AddDiagnostic(DiagnosticsFactory.NoMatchingPropertyFoundError(TypeSyntax.GetLocation(), typeSymbol, sourceTypeSymbol));
@ -269,6 +308,8 @@ namespace MapTo
AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq"); AddUsingIfRequired(mappedProperties.Any(p => p.IsEnumerable), "System.Linq");
var allProperties = GetTypeMappedProperties(sourceTypeSymbol, typeSymbol , isTypeInheritFromMappedBaseClass);
return new MappingModel( return new MappingModel(
SourceGenerationOptions, SourceGenerationOptions,
TypeSyntax.GetNamespace(), TypeSyntax.GetNamespace(),
@ -279,11 +320,14 @@ namespace MapTo
sourceTypeIdentifierName, sourceTypeIdentifierName,
sourceTypeSymbol.ToDisplayString(), sourceTypeSymbol.ToDisplayString(),
mappedProperties, mappedProperties,
allProperties,
isTypeInheritFromMappedBaseClass, isTypeInheritFromMappedBaseClass,
Usings, Usings,
shouldGenerateSecondaryConstructor); shouldGenerateSecondaryConstructor);
} }
private INamedTypeSymbol? GetTypeConverterBaseInterface(ITypeSymbol converterTypeSymbol, ISymbol property, IPropertySymbol sourceProperty) private INamedTypeSymbol? GetTypeConverterBaseInterface(ITypeSymbol converterTypeSymbol, ISymbol property, IPropertySymbol sourceProperty)
{ {
if (!property.TryGetTypeSymbol(out var propertyType)) if (!property.TryGetTypeSymbol(out var propertyType))

View File

@ -29,7 +29,8 @@ namespace MapTo
ImmutableArray<string> TypeConverterParameters, ImmutableArray<string> TypeConverterParameters,
string SourcePropertyName, string SourcePropertyName,
string? MappedSourcePropertyTypeName, string? MappedSourcePropertyTypeName,
string? EnumerableTypeArgument) string? EnumerableTypeArgument,
bool isReadOnly)
{ {
public bool IsEnumerable => EnumerableTypeArgument is not null; public bool IsEnumerable => EnumerableTypeArgument is not null;
} }
@ -43,7 +44,8 @@ namespace MapTo
string SourceNamespace, string SourceNamespace,
string SourceTypeIdentifierName, string SourceTypeIdentifierName,
string SourceTypeFullName, string SourceTypeFullName,
ImmutableArray<MappedProperty> MappedProperties, ImmutableArray<MappedProperty> SourceProperties,
ImmutableArray<MappedProperty> TypeProperties,
bool HasMappedBaseClass, bool HasMappedBaseClass,
ImmutableArray<string> Usings, ImmutableArray<string> Usings,
bool GenerateSecondaryConstructor bool GenerateSecondaryConstructor

View File

@ -11,7 +11,7 @@ namespace MapTo
internal RecordMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax) internal RecordMappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
: base(compilation, sourceGenerationOptions, typeSyntax) { } : base(compilation, sourceGenerationOptions, typeSyntax) { }
protected override ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass) protected override ImmutableArray<MappedProperty> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{ {
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray(); var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol.GetMembers() return typeSymbol.GetMembers()
@ -24,5 +24,19 @@ namespace MapTo
.Where(mappedProperty => mappedProperty is not null) .Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!; .ToImmutableArray()!;
} }
protected override ImmutableArray<MappedProperty> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isInheritFromMappedBaseClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.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()!;
}
} }
} }

View File

@ -21,28 +21,19 @@ namespace MapTo.Sources
.WriteLine($"partial class {model.TypeIdentifierName}") .WriteLine($"partial class {model.TypeIdentifierName}")
.WriteOpeningBracket(); .WriteOpeningBracket();
// Class body builder
if (model.GenerateSecondaryConstructor) .GeneratePublicConstructor(model)
{ .WriteLine();
builder
.GenerateSecondaryConstructor(model)
.WriteLine();
}
builder if(!AllPropertiesReadOnly(model))
.GeneratePrivateConstructor(model) {
.WriteLine() builder.GenerateUpdateMethod(model);
.GenerateFactoryMethod(model) }
.GenerateUpdateMethod(model)
// End class declaration builder
.WriteClosingBracket() .WriteClosingBracket()
.WriteLine() .WriteLine()
// Extension class declaration
.GenerateSourceTypeExtensionClass(model)
// End namespace declaration
.WriteClosingBracket(); .WriteClosingBracket();
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs"); return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs");
@ -67,33 +58,36 @@ namespace MapTo.Sources
.WriteLine($" : this(new {MappingContextSource.ClassName}(), {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(); var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
const string mappingContextParameterName = "context"; const string mappingContextParameterName = "context";
var baseConstructor = model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" : string.Empty; var baseConstructor = /*model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" :*/ string.Empty;
builder builder
.WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceType} {sourceClassParameterName}){baseConstructor}") .WriteLine($"public {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName}){baseConstructor}")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine($"if ({mappingContextParameterName} == null) throw new ArgumentNullException(nameof({mappingContextParameterName}));") //.WriteLine($"if ({mappingContextParameterName} == null) throw new ArgumentNullException(nameof({mappingContextParameterName}));")
.WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));") //.WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));")
.WriteLine() //.WriteLine()
.WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);") //.WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);")
.WriteLine().
WriteProperties( model, sourceClassParameterName, mappingContextParameterName); .WriteProperties( model.SourceProperties, sourceClassParameterName, mappingContextParameterName, false);
// End constructor declaration // End constructor declaration
return builder.WriteClosingBracket(); return builder.WriteClosingBracket();
} }
private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, private static SourceBuilder WriteProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray<MappedProperty> properties,
string? sourceClassParameterName, string mappingContextParameterName) 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.TypeConverter is null)
{ {
if (property.IsEnumerable) if (property.IsEnumerable)
@ -105,7 +99,7 @@ namespace MapTo.Sources
{ {
builder.WriteLine(property.MappedSourcePropertyTypeName is null builder.WriteLine(property.MappedSourcePropertyTypeName is null
? $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};" ? $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};"
: $"{property.Name} = {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName});"); : "");
} }
} }
else else
@ -123,17 +117,14 @@ namespace MapTo.Sources
} }
private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model) private static bool AllPropertiesReadOnly(MappingModel model)
{ {
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); foreach (var property in model.SourceProperties)
{
if (!property.isReadOnly) return false;
}
return true ;
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) private static SourceBuilder GenerateUpdateMethod(this SourceBuilder builder, MappingModel model)
@ -142,9 +133,9 @@ namespace MapTo.Sources
builder builder
.GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName) .GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName)
.WriteLine($"public void {model.Options.NullableReferenceSyntax}Update({model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})") .WriteLine($"public void Update({model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteProperties( model, sourceClassParameterName,"context" ) .WriteProperties( model.SourceProperties.GetWritableMappedProperties(), sourceClassParameterName,"context", true )
.WriteClosingBracket(); .WriteClosingBracket();
return builder; return builder;
@ -181,15 +172,6 @@ namespace MapTo.Sources
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>"); .WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>");
} }
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) private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{ {
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase(); var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();

View File

@ -26,7 +26,7 @@ namespace MapTo.Sources
} }
builder builder
.WriteLine("[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]") .WriteLine("[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]")
.WriteLine($"public sealed class {AttributeName}Attribute : Attribute") .WriteLine($"public sealed class {AttributeName}Attribute : Attribute")
.WriteOpeningBracket(); .WriteOpeningBracket();

View File

@ -96,9 +96,9 @@ namespace MapTo.Sources
private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, string sourceClassParameterName, private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, string sourceClassParameterName,
string mappingContextParameterName) 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.TypeConverter is null)
{ {
if (property.IsEnumerable) if (property.IsEnumerable)
@ -123,7 +123,7 @@ namespace MapTo.Sources
$"{property.Name}: new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters})"); $"{property.Name}: new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.SourcePropertyName}, {parameters})");
} }
if (i < model.MappedProperties.Length - 1) if (i < model.SourceProperties.Length - 1)
{ {
builder.Write(", "); builder.Write(", ");
} }

View File

@ -1,5 +1,6 @@
using MapTo.Extensions; using MapTo.Extensions;
using static MapTo.Sources.Constants; using static MapTo.Sources.Constants;
using System.Collections.Generic;
namespace MapTo.Sources namespace MapTo.Sources
{ {
@ -18,19 +19,19 @@ namespace MapTo.Sources
.WriteOpeningBracket() .WriteOpeningBracket()
// Class declaration // Class declaration
.WriteLine($"partial class {model.TypeIdentifierName}") .WriteLine($"partial struct {model.TypeIdentifierName}")
.WriteOpeningBracket(); .WriteOpeningBracket()
.WriteLine()
// Class body // Class body
if (model.GenerateSecondaryConstructor) .GeneratePublicConstructor(model);
{
builder
.GenerateSecondaryConstructor(model)
.WriteLine();
}
builder if (!AllPropertiesAreReadOnly(model))
.GeneratePrivateConstructor(model) {
builder.GenerateUpdateMethod(model);
}
builder
.WriteLine() .WriteLine()
// End class declaration // End class declaration
.WriteClosingBracket() .WriteClosingBracket()
@ -41,6 +42,16 @@ namespace MapTo.Sources
return new(builder.ToString(), $"{model.Namespace}.{model.TypeIdentifierName}.g.cs"); 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) private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model)
{ {
@ -50,7 +61,7 @@ namespace MapTo.Sources
{ {
builder builder
.WriteLine("/// <summary>") .WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class") .WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> struct")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.") .WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>") .WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>"); .WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
@ -61,31 +72,47 @@ namespace MapTo.Sources
.WriteLine($" : this(new {MappingContextSource.ClassName}(), {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(); var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
const string mappingContextParameterName = "context"; 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 builder
.WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceType} {sourceClassParameterName}){baseConstructor}") .WriteLine($"public {model.TypeIdentifierName}({model.SourceType} {sourceClassParameterName}{(string.IsNullOrEmpty(readOnlyFields) ? "" : $", {readOnlyFields}")}){baseConstructor}")
.WriteOpeningBracket() .WriteOpeningBracket()
.WriteLine() .TryWriteProperties(model.SourceProperties, readOnlyProperties, sourceClassParameterName, mappingContextParameterName, false);
.WriteLine($"{mappingContextParameterName}.{MappingContextSource.RegisterMethodName}({sourceClassParameterName}, this);")
.WriteLine().
WriteProperties( model, sourceClassParameterName, mappingContextParameterName);
// End constructor declaration // End constructor declaration
return builder.WriteClosingBracket(); return builder.WriteClosingBracket();
} }
private static SourceBuilder WriteProperties(this SourceBuilder builder, MappingModel model, private static SourceBuilder TryWriteProperties(this SourceBuilder builder, System.Collections.Immutable.ImmutableArray<MappedProperty> properties, System.Collections.Immutable.ImmutableArray<MappedProperty>? otherProperties,
string? sourceClassParameterName, string mappingContextParameterName) 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.TypeConverter is null)
{ {
if (property.IsEnumerable) if (property.IsEnumerable)
@ -97,7 +124,7 @@ namespace MapTo.Sources
{ {
builder.WriteLine(property.MappedSourcePropertyTypeName is null builder.WriteLine(property.MappedSourcePropertyTypeName is null
? $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};" ? $"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};"
: $"{property.Name} = {mappingContextParameterName}.{MappingContextSource.MapMethodName}<{property.MappedSourcePropertyTypeName}, {property.Type}>({sourceClassParameterName}.{property.SourcePropertyName});"); : "");
} }
} }
else else
@ -111,25 +138,37 @@ 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; return builder;
} }
private static SourceBuilder GenerateConvertorMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
{
if (!model.Options.GenerateXmlDocument)
{
return builder;
}
return builder private static SourceBuilder GenerateUpdateMethod(this SourceBuilder builder, MappingModel model)
.WriteLine("/// <summary>") {
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties") var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>") builder
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>") .GenerateUpdaterMethodsXmlDocs(model, sourceClassParameterName)
.WriteLine($"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>"); .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) private static SourceBuilder GenerateUpdaterMethodsXmlDocs(this SourceBuilder builder, MappingModel model, string sourceClassParameterName)
@ -147,26 +186,5 @@ namespace MapTo.Sources
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>"); .WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceType}\"/> to use as source.</param>");
} }
private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MappingModel model)
{
return builder
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {model.SourceTypeIdentifierName}To{model.TypeIdentifierName}Extensions")
.WriteOpeningBracket()
.GenerateSourceTypeExtensionMethod(model)
.WriteClosingBracket();
}
private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} To{model.TypeIdentifierName}(this {model.SourceType}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
.WriteClosingBracket();
}
} }
} }

View File

@ -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("/// <summary>")
.WriteLine("/// Specifies that the annotated property should be excluded.")
.WriteLine("/// </summary>");
}
builder
.WriteLine("[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]")
.WriteLine($"public sealed class {AttributeClassName} : Attribute {{ }}")
.WriteClosingBracket();
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
}
}
}

View File

@ -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<MappedProperty> GetSourceMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return typeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapProperty(sourceTypeSymbol, sourceProperties, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
protected override ImmutableArray<MappedProperty> GetTypeMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool hasInheritedClass)
{
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
return sourceTypeSymbol
.GetAllMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(IgnorePropertyAttributeTypeSymbol))
.Select(property => MapPropertySimple(typeSymbol, property))
.Where(mappedProperty => mappedProperty is not null)
.ToImmutableArray()!;
}
}
}

View File

@ -10,6 +10,5 @@ namespace TestConsoleApp.Data.Models
public string EmployeeCode { get; set; } public string EmployeeCode { get; set; }
public Manager Manager { get; set; }
} }
} }

View File

@ -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<Employee> Employees { get; set; } = Array.Empty<Employee>();
}
}

View File

@ -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;
}
}
}

View File

@ -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}";
}
}

View File

@ -1,13 +1,15 @@
using System; using System;
using TestConsoleApp.ViewModels;
using MapTo;
namespace TestConsoleApp.Data.Models namespace TestConsoleApp.Data.Models
{ {
public class User [MapFrom(typeof(UserViewModel))]
public partial class User
{ {
public int Id { get; set; } public int Id { get; set; }
public DateTimeOffset RegisteredAt { get; set; } public DateTimeOffset RegisteredAt { get; set; }
public Profile Profile { get; set; }
} }
} }

View File

@ -2,7 +2,6 @@
using MapTo; using MapTo;
using TestConsoleApp.Data.Models; using TestConsoleApp.Data.Models;
using TestConsoleApp.ViewModels; using TestConsoleApp.ViewModels;
using TestConsoleApp.ViewModels2;
namespace TestConsoleApp namespace TestConsoleApp
{ {
@ -11,7 +10,6 @@ namespace TestConsoleApp
private static void Main(string[] args) private static void Main(string[] args)
{ {
//UserTest(); //UserTest();
CyclicReferenceTest();
// EmployeeManagerTest(); // EmployeeManagerTest();
Console.WriteLine("done"); Console.WriteLine("done");
@ -19,74 +17,23 @@ namespace TestConsoleApp
private static void EmployeeManagerTest() 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 var employee1 = new Employee
{ {
Id = 101, Id = 101,
EmployeeCode = "E101", EmployeeCode = "E101",
Manager = manager1
}; };
var employee2 = new Employee var employee2 = new Employee
{ {
Id = 102, Id = 102,
EmployeeCode = "E102", 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);
}
} }
} }

View File

@ -1,16 +1,13 @@
using MapTo; using MapTo;
using TestConsoleApp.Data.Models; using TestConsoleApp.Data.Models;
using TestConsoleApp.ViewModels2;
namespace TestConsoleApp.ViewModels namespace TestConsoleApp.ViewModels
{ {
[MapFrom(typeof(Employee))] [MapFrom(typeof(Employee))]
public partial class EmployeeViewModel public partial class EmployeeViewModel
{ {
public int Id { get; set; } public int Id { get; }
public string EmployeeCode { get; set; }
public ManagerViewModel Manager { get; set; }
} }
} }

View File

@ -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<EmployeeViewModel> Employees { get; set; } = Array.Empty<EmployeeViewModel>();
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TestConsoleApp.Data.Models;
using MapTo;
namespace TestConsoleApp.ViewModels
{
[MapFrom(typeof(MyStruct))]
public partial struct MyStructViewModel
{
public int SomeInt { get; set; }
public MyStructViewModel(int someInt)
{
SomeInt = someInt;
}
}
}

View File

@ -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; }
}
}

View File

@ -7,18 +7,8 @@ namespace TestConsoleApp.ViewModels
[MapFrom(typeof(User))] [MapFrom(typeof(User))]
public partial class UserViewModel public partial class UserViewModel
{ {
[MapProperty(SourcePropertyName = nameof(User.Id))] public int Id { get; set; }
[MapTypeConverter(typeof(IdConverter))]
public string Key { get; }
public DateTimeOffset RegisteredAt { get; set; } public DateTimeOffset RegisteredAt { get; }
// [IgnoreProperty]
public ProfileViewModel Profile { get; set; }
private class IdConverter : ITypeConverter<int, string>
{
public string Convert(int source, object[]? converterParameters) => $"{source:X}";
}
} }
} }