diff --git a/SourceGenerators/Mapper/MintPlayer.Mapper.Attributes/MapAsDictionaryAttribute.cs b/SourceGenerators/Mapper/MintPlayer.Mapper.Attributes/MapAsDictionaryAttribute.cs
new file mode 100644
index 00000000..8131ed28
--- /dev/null
+++ b/SourceGenerators/Mapper/MintPlayer.Mapper.Attributes/MapAsDictionaryAttribute.cs
@@ -0,0 +1,9 @@
+namespace MintPlayer.Mapper.Attributes;
+
+///
+/// Indicates that this type should be treated as a dictionary when mapping.
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
+public sealed class MapAsDictionaryAttribute : Attribute
+{
+}
diff --git a/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.Producer.cs b/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.Producer.cs
index 6a9fb3ca..598c61c0 100644
--- a/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.Producer.cs
+++ b/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.Producer.cs
@@ -112,7 +112,7 @@ protected override void ProduceSource(IndentedTextWriter writer, CancellationTok
foreach (var (source, destination) in type.MappedProperties)
{
- HandleProperty(writer, source, destination, EWriteType.Assignment);
+ HandleProperty(writer, source, destination, type.TypeToMap.SourceTypeHasIndexer, type.TypeToMap.DestinationTypeHasIndexer, EWriteType.Assignment);
}
writer.Indent--;
@@ -134,7 +134,7 @@ protected override void ProduceSource(IndentedTextWriter writer, CancellationTok
foreach (var (source, destination) in type.MappedProperties)
{
- HandleProperty(writer, source, destination, EWriteType.Initializer);
+ HandleProperty(writer, source, destination, type.TypeToMap.SourceTypeHasIndexer, type.TypeToMap.DestinationTypeHasIndexer, EWriteType.Initializer);
}
writer.Indent--;
@@ -155,7 +155,7 @@ protected override void ProduceSource(IndentedTextWriter writer, CancellationTok
foreach (var (source, destination) in type.MappedProperties)
{
- HandleProperty(writer, destination, source, EWriteType.Assignment);
+ HandleProperty(writer, destination, source, type.TypeToMap.DestinationTypeHasIndexer, type.TypeToMap.SourceTypeHasIndexer, EWriteType.Assignment);
}
writer.Indent--;
@@ -178,7 +178,7 @@ protected override void ProduceSource(IndentedTextWriter writer, CancellationTok
// TODO: add properties
foreach (var (source, destination) in type.MappedProperties)
{
- HandleProperty(writer, destination, source, EWriteType.Initializer);
+ HandleProperty(writer, destination, source, type.TypeToMap.DestinationTypeHasIndexer, type.TypeToMap.SourceTypeHasIndexer, EWriteType.Initializer);
}
writer.Indent--;
@@ -218,21 +218,21 @@ protected override void ProduceSource(IndentedTextWriter writer, CancellationTok
}
// Helper methods for type checks
- private static bool IsArrayType(string type)
- => type.EndsWith("[]");
+ private static bool IsArrayType(string? type)
+ => type is not null && type.EndsWith("[]");
- private static bool IsListType(string type)
- => type.StartsWith("global::System.Collections.Generic.List<") || type.StartsWith("List<");
+ private static bool IsListType(string? type)
+ => type is not null && (type.StartsWith("global::System.Collections.Generic.List<") || type.StartsWith("List<"));
- private static bool IsCollectionType(string type)
- => type.StartsWith("global::System.Collections.Generic.ICollection<") || type.StartsWith("ICollection<");
+ private static bool IsCollectionType(string? type)
+ => type is not null && (type.StartsWith("global::System.Collections.Generic.ICollection<") || type.StartsWith("ICollection<"));
- private static bool IsNullableType(string type)
- => type.EndsWith("?") || type.StartsWith("System.Nullable<");
+ private static bool IsNullableType(string? type)
+ => type is not null && (type.EndsWith("?") || type.StartsWith("System.Nullable<"));
- private static bool IsPrimitiveOrString(string type)
+ private static bool IsPrimitiveOrString(string? type)
{
- switch (type.WithoutGlobal())
+ switch (type?.WithoutGlobal())
{
case "string":
case "System.String":
@@ -292,15 +292,27 @@ private static string GetElementType(string collectionType)
return collectionType;
}
- private static void HandleProperty(IndentedTextWriter writer, PropertyDeclaration source, PropertyDeclaration destination, EWriteType writeType)
+ private static void HandleProperty(IndentedTextWriter writer, PropertyDeclaration? source, PropertyDeclaration? destination, bool sourceWithIndexer, bool destinationWithIndexer, EWriteType writeType)
{
- var prefix = writeType switch
+ var destProp = destination is { } ? destination.Alias ?? destination.PropertyName : null;
+ var sourceProp = source is { } ? source.Alias ?? source.PropertyName : null;
+
+ destProp ??= sourceProp;
+ sourceProp ??= destProp;
+
+ var sourceValueAccessor = (sourceWithIndexer, writeType) switch
{
- EWriteType.Initializer => $"{source.PropertyName} = ",
- EWriteType.Assignment => $"output.{source.PropertyName} = ",
+ (true, EWriteType.Initializer) => $"[{destProp}] = ",
+ (true, EWriteType.Assignment) => $"output[{destProp}] = ",
+ (false, EWriteType.Initializer) => $"{destProp} = ",
+ (false, EWriteType.Assignment) => $"output.{destProp} = ",
_ => throw new NotImplementedException(),
};
+ var destinationValueAccessor = destinationWithIndexer
+ ? $""""input["{destProp}"]""""
+ : $"input.{destProp}";
+
var suffix = writeType switch
{
EWriteType.Initializer => ",",
@@ -309,66 +321,69 @@ private static void HandleProperty(IndentedTextWriter writer, PropertyDeclaratio
};
// Handle primitive types
- if (source.IsPrimitive && destination.IsPrimitive)
+ if ((source is { IsPrimitive: true } && destination is { IsPrimitive: true }) ||
+ (source is { HasStringIndexer: true } && destination is { IsPrimitive: true }) ||
+ (source is { IsPrimitive: true } && destination is { HasStringIndexer: true }))
{
if (source.PropertyType != destination.PropertyType)
- writer.WriteLine($"{prefix}ConvertProperty<{destination.PropertyType}, {source.PropertyType}>(input.{destination.PropertyName}){suffix}");
+ writer.WriteLine($"{sourceValueAccessor}ConvertProperty<{destination.PropertyType}, {source.PropertyType}>({destinationValueAccessor}){suffix}");
else if (source.StateName != null && destination.StateName != null)
- writer.WriteLine($"""{prefix}ConvertProperty<{destination.PropertyType}, {source.PropertyType}>(input.{destination.PropertyName}, {source.StateName}, {destination.StateName}){suffix}""");
+ writer.WriteLine($"""{sourceValueAccessor}ConvertProperty<{destination.PropertyType}, {source.PropertyType}>({destinationValueAccessor}, {source.StateName}, {destination.StateName}){suffix}""");
else
- writer.WriteLine($"{prefix}input.{destination.PropertyName}{suffix}");
+ writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor}{suffix}");
}
// Handle arrays
- else if (IsArrayType(source.PropertyType) && IsArrayType(destination.PropertyType))
+ else if (source is not null && IsArrayType(source.PropertyType) && IsArrayType(destination?.PropertyType))
{
var elementType = GetElementType(source.PropertyType);
if (IsPrimitiveOrString(elementType))
{
- writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.ToArray(){suffix}");
+ writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.ToArray(){suffix}");
}
else
{
var shortName = elementType.WithoutGlobal().Split('.').Last();
- writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.Select(x => x.MapTo{shortName}()).ToArray(){suffix}");
+ writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.Select(x => x.MapTo{shortName}()).ToArray(){suffix}");
}
}
// Handle List
- else if (IsListType(source.PropertyType) && IsListType(destination.PropertyType))
+ else if (source is not null && IsListType(source.PropertyType) && IsListType(destination?.PropertyType))
{
var elementType = GetElementType(source.PropertyType);
if (IsPrimitiveOrString(elementType))
{
- writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.ToList(){suffix}");
+ writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.ToList(){suffix}");
}
else
{
var shortName = elementType.WithoutGlobal().Split('.').Last();
- writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.Select(x => x.MapTo{shortName}()).ToList(){suffix}");
+ writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.Select(x => x.MapTo{shortName}()).ToList(){suffix}");
}
}
// Handle ICollection
- else if (IsCollectionType(source.PropertyType) && IsCollectionType(destination.PropertyType))
+ else if (source is not null && IsCollectionType(source.PropertyType) && IsCollectionType(destination?.PropertyType))
{
var elementType = GetElementType(source.PropertyType);
if (IsPrimitiveOrString(elementType))
{
- writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.ToList(){suffix}");
+ writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.ToList(){suffix}");
}
else
{
var shortName = elementType.WithoutGlobal().Split('.').Last();
- writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.Select(x => x.MapTo{shortName}()).ToList(){suffix}");
+ writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.Select(x => x.MapTo{shortName}()).ToList(){suffix}");
}
}
// Handle nullable reference types
- else if (IsNullableType(source.PropertyType) && IsNullableType(destination.PropertyType))
+ else if (source is not null && IsNullableType(source.PropertyType) && IsNullableType(destination?.PropertyType))
{
- writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.MapTo{source.PropertyTypeName}(){suffix}");
+ writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.MapTo{source.PropertyTypeName}(){suffix}");
}
// Handle complex types
else
{
- writer.WriteLine($"{prefix}input.{destination.PropertyName}.MapTo{source.PropertyTypeName}(){suffix}");
+ var type = source?.PropertyTypeName ?? destination?.PropertyTypeName ?? throw new InvalidOperationException("Both source and destination are null");
+ writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor}.MapTo{type}(){suffix}");
}
}
}
diff --git a/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.cs b/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.cs
index b0046455..6c14dbce 100644
--- a/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.cs
+++ b/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.cs
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
+using MintPlayer.Mapper.Models;
using MintPlayer.SourceGenerators.Tools;
using MintPlayer.SourceGenerators.Tools.Extensions;
@@ -31,7 +32,7 @@ public override void Initialize(IncrementalGeneratorInitializationContext contex
//var destTypeMethodName = destAttr?.ConstructorArguments.ElementAtOrDefault(1);
var mappingMethodName = destType1.Name.EnsureStartsWith("MapTo");
var declaredMethodName = sourceType.Name.EnsureStartsWith("MapTo");
- return new Models.TypeToMap
+ var res = new Models.TypeToMap
{
DestinationNamespace = sourceType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)),
DeclaredType = sourceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
@@ -44,10 +45,13 @@ public override void Initialize(IncrementalGeneratorInitializationContext contex
AppliedOn = Models.EAppliedOn.Assembly,
HasError = false,
Location = attr1.ApplicationSyntaxReference?.GetSyntax(ct)?.GetLocation() ?? Location.None,
+ SourceTypeHasIndexer = sourceType.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "MintPlayer.Mapper.Attributes.MapAsDictionaryAttribute"),
+ DestinationTypeHasIndexer = destType1.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "MintPlayer.Mapper.Attributes.MapAsDictionaryAttribute"),
DeclaredProperties = ProcessProperties(sourceType).ToArray(),
MappingProperties = ProcessProperties(destType1).ToArray(),
};
+ return res;
}
else
{
@@ -74,7 +78,7 @@ public override void Initialize(IncrementalGeneratorInitializationContext contex
var destAttr = destType2.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "MintPlayer.Mapper.Attributes.GenerateMapperAttribute");
var destTypeMethodName = destAttr?.ConstructorArguments.ElementAtOrDefault(1);
var mappingMethodName = destTypeMethodName is null ? destType2.Name.EnsureStartsWith("MapTo") : CreateMethodName((TypedConstant)destTypeMethodName, destType2);
- return new Models.TypeToMap
+ var res = new Models.TypeToMap
{
DestinationNamespace = typeSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)),
DeclaredType = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
@@ -87,10 +91,13 @@ public override void Initialize(IncrementalGeneratorInitializationContext contex
AppliedOn = Models.EAppliedOn.Class,
HasError = false,
Location = attr2.ApplicationSyntaxReference?.GetSyntax(ct)?.GetLocation() ?? Location.None,
+ SourceTypeHasIndexer = typeSymbol.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "MintPlayer.Mapper.Attributes.MapAsDictionaryAttribute"),
+ DestinationTypeHasIndexer = destType2.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "MintPlayer.Mapper.Attributes.MapAsDictionaryAttribute"),
DeclaredProperties = ProcessProperties(typeSymbol).ToArray(),
MappingProperties = ProcessProperties(destType2).ToArray(),
};
+ return res;
}
else
{
@@ -155,9 +162,26 @@ public override void Initialize(IncrementalGeneratorInitializationContext contex
.Select(static (i, ct) => new Models.TypeWithMappedProperties
{
TypeToMap = i,
- MappedProperties = i.DeclaredProperties
- .Select(dp => (Source: dp, Destination: i.MappingProperties.FirstOrDefault(mp => mp.Alias == dp.Alias)))
- .Where(p => p.Source is { IsStatic: false } && p.Destination is { IsStatic: false })
+ //MappedProperties = i.DeclaredProperties
+ // .Select(dp => (Source: dp, Destination: i.MappingProperties.FirstOrDefault(mp => mp.Alias == dp.Alias)))
+ // .Where(p => p.Source is { IsStatic: false } && p.Destination is { IsStatic: false })
+
+ MappedProperties = (i.SourceTypeHasIndexer, i.DestinationTypeHasIndexer) switch
+ {
+ (true, true) => [],
+ (true, false) => i.MappingProperties
+ .Where(mp => !mp.IsStatic)
+ .Select(mp => (Source: (PropertyDeclaration?)null, Destination: mp.AsNullable()))
+ .Where(p => p.Destination is { IsStatic: false }),
+ (false, true) => i.DeclaredProperties
+ .Where(dp => !dp.IsStatic)
+ .Select(dp => (Source: dp.AsNullable(), Destination: (PropertyDeclaration?)null))
+ .Where(p => p.Source is { IsStatic: false }),
+ (false, false) => i.DeclaredProperties
+ .Where(dp => !dp.IsStatic)
+ .Select(dp => (Source: dp.AsNullable(), Destination: (PropertyDeclaration?)i.MappingProperties.FirstOrDefault(mp => mp.Alias == dp.Alias)))
+ .Where(p => p.Source is { IsStatic: false } && p.Destination is { IsStatic: false }),
+ }
})
.WithComparer()
.Collect();
@@ -277,6 +301,8 @@ private static string CreateMethodName(TypedConstant preferred, INamedTypeSymbol
//IsAbstract = p.IsAbstract,
//IsOverride = p.IsOverride,
IsPrimitive = p.Type.IsValueType || p.Type.SpecialType == SpecialType.System_String,
+ HasStringIndexer = p.Type is INamedTypeSymbol namedType2 && namedType2.GetMembers().OfType().Any(pi => pi.IsIndexer && pi.Parameters.Length == 1 && pi.Parameters[0].Type.SpecialType == SpecialType.System_String),
+ //ShouldMapAsDictionary = p.Type.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "MintPlayer.Mapper.Attributes.MapAsDictionaryAttribute"),
});
}
diff --git a/SourceGenerators/Mapper/MintPlayer.Mapper/Models/PropertyDeclaration.cs b/SourceGenerators/Mapper/MintPlayer.Mapper/Models/PropertyDeclaration.cs
index bc37b746..14a89963 100644
--- a/SourceGenerators/Mapper/MintPlayer.Mapper/Models/PropertyDeclaration.cs
+++ b/SourceGenerators/Mapper/MintPlayer.Mapper/Models/PropertyDeclaration.cs
@@ -12,6 +12,8 @@ public partial class PropertyDeclaration
public int? StateName { get; set; }
public bool IsStatic { get; set; }
public bool IsPrimitive { get; set; }
+ public bool HasStringIndexer { get; set; }
+ //public bool ShouldMapAsDictionary { get; set; }
public override string ToString() => $"{PropertyName} ({PropertyType})";
}
diff --git a/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeToMap.cs b/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeToMap.cs
index 7c27be72..e24eefc3 100644
--- a/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeToMap.cs
+++ b/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeToMap.cs
@@ -37,6 +37,16 @@ public partial class TypeToMap
///
public bool AreBothDecorated { get; set; }
+ ///
+ /// Indicates that the source type has an indexer
+ ///
+ public bool SourceTypeHasIndexer { get; set; }
+
+ ///
+ /// Indicates that the destination type has an indexer
+ ///
+ public bool DestinationTypeHasIndexer { get; set; }
+
public EAppliedOn AppliedOn { get; set; }
public bool HasError { get; set; }
diff --git a/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeWithMappedProperties.cs b/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeWithMappedProperties.cs
index 6cb9a5c1..7cffeab9 100644
--- a/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeWithMappedProperties.cs
+++ b/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeWithMappedProperties.cs
@@ -6,5 +6,5 @@ namespace MintPlayer.Mapper.Models;
public partial class TypeWithMappedProperties
{
public TypeToMap TypeToMap { get; set; } = null!;
- public IEnumerable<(PropertyDeclaration Source, PropertyDeclaration Destination)> MappedProperties { get; set; } = [];
+ public IEnumerable<(PropertyDeclaration? Source, PropertyDeclaration? Destination)> MappedProperties { get; set; } = [];
}
\ No newline at end of file
diff --git a/SourceGenerators/MintPlayer.SourceGenerators.Tools/Extensions/EnumerableExtensions.cs b/SourceGenerators/MintPlayer.SourceGenerators.Tools/Extensions/EnumerableExtensions.cs
index 79075ad2..315dd20f 100644
--- a/SourceGenerators/MintPlayer.SourceGenerators.Tools/Extensions/EnumerableExtensions.cs
+++ b/SourceGenerators/MintPlayer.SourceGenerators.Tools/Extensions/EnumerableExtensions.cs
@@ -10,4 +10,29 @@ public static IEnumerable NotNull(this IEnumerable source) where T : c
{
return source.Where(item => item is not null).Cast();
}
+
+ ///
+ /// Converts a sequence of value types to a sequence of nullable value types of the same underlying type.
+ ///
+ /// This method enables LINQ queries and other operations that require nullable value types when
+ /// working with sequences of non-nullable value types. The order and number of elements in the returned sequence
+ /// are the same as in the source sequence.
+ /// The value type of the elements in the source sequence.
+ /// The sequence of value type elements to convert. Cannot be null.
+ /// An IEnumerable containing the elements of the source sequence, each cast to a nullable value type.
+ public static IEnumerable AsNullable(this IEnumerable source) where T : struct
+ {
+ return source.Cast();
+ }
+
+ ///
+ /// Converts a reference type value to its nullable equivalent.
+ ///
+ /// The reference type to convert to a nullable type.
+ /// The reference type value to convert. Can be null.
+ /// A nullable value of type T that represents the original value, or null if the source is null.
+ public static T? AsNullable(this T source) where T : class
+ {
+ return (T?)source;
+ }
}
diff --git a/SourceGenerators/TestProjects/MapperDebugging/Program.cs b/SourceGenerators/TestProjects/MapperDebugging/Program.cs
index d9e54ea8..16bc8fcf 100644
--- a/SourceGenerators/TestProjects/MapperDebugging/Program.cs
+++ b/SourceGenerators/TestProjects/MapperDebugging/Program.cs
@@ -128,17 +128,22 @@ public class PersonDto
[GenerateMapper(typeof(AddressDto))]
public class Address
{
+ [MapperAlias("Straatnaam")]
public string? Street { get; set; }
+ [MapperAlias("Stad")]
public string? City { get; set; }
}
+[MapAsDictionary]
public class AddressDto
{
- [MapperAlias(nameof(Address.Street))]
- public string? Straatnaam { get; set; }
+ private readonly Dictionary data = new();
- [MapperAlias(nameof(Address.City))]
- public string? Stad { get; set; }
+ public object? this[string key]
+ {
+ get => data.TryGetValue(key, out object? value) ? value : null;
+ set => data[key] = value;
+ }
}
public class ContactInfo