From ebccf4686c4f86b2dde28b9d27b981983569156c Mon Sep 17 00:00:00 2001 From: James Date: Thu, 22 Jan 2026 21:01:15 +0100 Subject: [PATCH 1/3] fix: add fully qualified type names and type conversion in MapTo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes two major bugs in the source generator: 1. **Missing fully qualified type names**: The generator now uses fully qualified type names (including namespace) for all type references in generated code. This prevents compilation errors when source and target classes are in different namespaces. Changes in MappingOptions: - Added TargetNamespace property to store target class namespace - Added TargetFullName property to get fully qualified target name - Updated Create method to extract and store target namespace Changes in MapperGenerator: - Use TargetFullName instead of just TargetName for all type references - Add using directive for target namespace when it differs from current - Apply to MapFrom method signature, MapTo return type, and all instantiations 2. **Missing type conversion in MapTo**: The MapTo method now properly handles properties with different types between source and target classes by using mapper.Map() for type conversion, matching the behavior of MapFrom. Changes in MapperGenerator: - Added type checking in MapTo property mapping loop - Use mapper.Map() when property types don't match - Direct assignment when property types match All existing tests pass. The example project builds successfully with the fixes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../TenJames.CompMap/MapperGenerator.cs | 30 +++++++++++++++---- .../Properties/MappingOptions.cs | 25 ++++++++++++---- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs b/TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs index a53f586..3850f0f 100644 --- a/TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs +++ b/TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs @@ -57,6 +57,13 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati var sourceText = new SourceBuilder(); sourceText.AppendLine($"using {Consts.MapperNamespace};"); + + // Add using for target namespace if different from current namespace + if (ma.TargetNamespace != ma.Namespace && ma.TargetNamespace != "GlobalNamespace") + { + sourceText.AppendLine($"using {ma.TargetNamespace};"); + } + sourceText.AppendLine(); sourceText.AppendLine($"namespace {ma.Namespace};"); sourceText.AppendLine(); @@ -102,7 +109,7 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati } } sourceText.AppendLine(); - sourceText.AppendLine($"private static partial {ma.TargetName}UnmappedProperties Get{ma.TargetName}UnmappedProperties(IMapper mapper, {ma.Target.Identifier.Text} source);"); + sourceText.AppendLine($"private static partial {ma.TargetName}UnmappedProperties Get{ma.TargetName}UnmappedProperties(IMapper mapper, {ma.TargetFullName} source);"); } sourceText.AppendLine(); @@ -111,7 +118,7 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati sourceText.AppendLine("/// "); sourceText.AppendLine("/// Mapping method generated by TenJames.CompMap"); sourceText.AppendLine("/// "); - using var mapFromBlock = sourceText.BeginBlock($"public static {className} MapFrom(IMapper mapper, {ma.Target?.Identifier.Text} source)"); + using var mapFromBlock = sourceText.BeginBlock($"public static {className} MapFrom(IMapper mapper, {ma.TargetFullName} source)"); if (isMissing) { @@ -185,17 +192,30 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati sourceText.AppendLine("/// Mapping method generated by TenJames.CompMap"); sourceText.AppendLine("/// "); using var mapToBlock = sourceText.BeginBlock( - $"public {ma.TargetName} MapTo(IMapper mapper)" + $"public {ma.TargetFullName} MapTo(IMapper mapper)" ); if (isMissing) { sourceText.AppendLine("var unmapped = Get" + ma.TargetName + "UnmappedProperties(mapper, this);"); } - sourceText.AppendLine($"var target = new {ma.TargetName}() {{"); + sourceText.AppendLine($"var target = new {ma.TargetFullName}() {{"); sourceText.IncreaseIndent(); foreach (var prop in matchingFields) { - sourceText.AppendLine($" {prop.Identifier.Text} = this.{prop.Identifier.Text},"); + // Get the corresponding target property to check type + var targetProp = ma.Target.Members + .OfType() + .FirstOrDefault(p => p.Identifier.Text == prop.Identifier.Text); + + if (targetProp != null && prop.Type.ToFullString() != targetProp.Type.ToFullString()) + { + // Type mismatch, use mapper + sourceText.AppendLine($" {prop.Identifier.Text} = mapper.Map<{targetProp.Type.ToFullString()}>(this.{prop.Identifier.Text}),"); + } + else + { + sourceText.AppendLine($" {prop.Identifier.Text} = this.{prop.Identifier.Text},"); + } } if (isMissing) { diff --git a/TenJames.CompMap/TenJames.CompMap/Properties/MappingOptions.cs b/TenJames.CompMap/TenJames.CompMap/Properties/MappingOptions.cs index d501436..01ffeca 100644 --- a/TenJames.CompMap/TenJames.CompMap/Properties/MappingOptions.cs +++ b/TenJames.CompMap/TenJames.CompMap/Properties/MappingOptions.cs @@ -12,6 +12,8 @@ public class MappingOptions { public string Namespace { get; set; } public ClassDeclarationSyntax Target { get; set; } public string TargetName => Target.Identifier.Text; + public string TargetNamespace { get; set; } + public string TargetFullName => string.IsNullOrEmpty(TargetNamespace) ? TargetName : $"{TargetNamespace}.{TargetName}"; public static MappingOptions? Create( @@ -33,15 +35,28 @@ public class MappingOptions { var attributeName = attributeSyntax.Name.ToString(); if (AttributeDefinitions.GetAllAttributes().Select(x => x.Name).Any(x => attributeName.Contains(x))) { + var targetClass = attributeSyntax.ArgumentList?.Arguments.First().Expression switch { + TypeOfExpressionSyntax typeOfExpression => context.SemanticModel.GetSymbolInfo(typeOfExpression.Type).Symbol + ?.DeclaringSyntaxReferences.First().GetSyntax() as ClassDeclarationSyntax, + _ => null + } ?? throw new InvalidOperationException("Target type could not be determined."); + + // Get target namespace + var targetNs = targetClass.FirstAncestorOrSelf(); + var targetFileScoped = targetClass.FirstAncestorOrSelf(); + + var targetNamespace = targetNs != null + ? targetNs.Name.ToString() + : targetFileScoped != null + ? targetFileScoped.Name.ToString() + : "GlobalNamespace"; + return new MappingOptions { ClassDeclarationSyntax = classDeclarationSyntax, AttributeName = attributeName, Namespace = namespaceName, - Target = attributeSyntax.ArgumentList?.Arguments.First().Expression switch { - TypeOfExpressionSyntax typeOfExpression => context.SemanticModel.GetSymbolInfo(typeOfExpression.Type).Symbol - ?.DeclaringSyntaxReferences.First().GetSyntax() as ClassDeclarationSyntax, - _ => null - } ?? throw new InvalidOperationException("Target type could not be determined.") + Target = targetClass, + TargetNamespace = targetNamespace }; } } From e7ad6894fdacb6883afceaaa938696db17db2762 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 22 Jan 2026 21:09:42 +0100 Subject: [PATCH 2/3] feat: add integration tests and prepare for inheritance support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds comprehensive integration tests for derived fields and begins work on supporting property inheritance from base classes. Changes: - Added Vehicle entity and DTOs with inheritance for testing - Added integration tests for base class property mapping - Refactored property handling to use PropertyInfo model - Updated all property access to work with new model Current Status: - Namespace and type conversion fixes are working correctly - Integration tests compile and demonstrate expected behavior - Base class property mapping is partially implemented Known Issue: The inheritance feature encounters "Syntax node is not within syntax tree" errors when accessing properties from base classes in different files. This requires additional work to properly resolve properties across compilation units using the semantic model. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../MappingIntegrationTests.cs | 68 ++++++++++++++ .../TestDtos.cs | 34 +++++++ .../TestEntities.cs | 32 +++++++ .../TenJames.CompMap/MapperGenerator.cs | 94 ++++++++++--------- .../Properties/MappingOptions.cs | 41 +++++++- 5 files changed, 220 insertions(+), 49 deletions(-) diff --git a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/MappingIntegrationTests.cs b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/MappingIntegrationTests.cs index d38ed57..17bcadf 100644 --- a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/MappingIntegrationTests.cs +++ b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/MappingIntegrationTests.cs @@ -397,4 +397,72 @@ public void ProductReadDto_WithNoReviews_ShouldSetAverageRatingToZero() Assert.Equal(0, dto.ReviewCount); Assert.Equal(0.0, dto.AverageRating); } + + [Fact] + public void VehicleReadDto_MapFrom_ShouldMapPropertiesFromBaseClass() + { + // Arrange + var vehicle = new Vehicle + { + // Properties from BaseEntity + Id = 1, + CreatedAt = new DateTime(2024, 1, 1), + UpdatedAt = new DateTime(2024, 1, 15), + IsDeleted = false, + // Properties from Vehicle + Make = "Toyota", + Model = "Camry", + Year = 2024, + Color = "Blue" + }; + + // Act + var dto = VehicleReadDto.MapFrom(_mapper, vehicle); + + // Assert - Properties from base class BaseDto + Assert.Equal(vehicle.Id, dto.Id); + Assert.Equal(vehicle.CreatedAt, dto.CreatedAt); + Assert.Equal(vehicle.UpdatedAt, dto.UpdatedAt); + + // Assert - Properties from VehicleReadDto + Assert.Equal(vehicle.Make, dto.Make); + Assert.Equal(vehicle.Model, dto.Model); + Assert.Equal(vehicle.Year, dto.Year); + Assert.Equal(vehicle.Color, dto.Color); + } + + [Fact] + public void VehicleCreateDto_MapTo_ShouldMapPropertiesFromBaseClass() + { + // Arrange + var dto = new VehicleCreateDto + { + // Properties from BaseDto + Id = 2, + CreatedAt = new DateTime(2024, 2, 1), + UpdatedAt = new DateTime(2024, 2, 10), + // Properties from VehicleCreateDto + Make = "Honda", + Model = "Accord", + Year = 2023, + Color = "Red" + }; + + // Act + var vehicle = dto.MapTo(_mapper); + + // Assert - Properties from base class BaseEntity + Assert.Equal(dto.Id, vehicle.Id); + Assert.Equal(dto.CreatedAt, vehicle.CreatedAt); + Assert.Equal(dto.UpdatedAt, vehicle.UpdatedAt); + + // Assert - Properties from Vehicle + Assert.Equal(dto.Make, vehicle.Make); + Assert.Equal(dto.Model, vehicle.Model); + Assert.Equal(dto.Year, vehicle.Year); + Assert.Equal(dto.Color, vehicle.Color); + + // Assert - Unmapped property + Assert.False(vehicle.IsDeleted); + } } diff --git a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestDtos.cs b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestDtos.cs index 9830b6b..f749ba8 100644 --- a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestDtos.cs +++ b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestDtos.cs @@ -179,3 +179,37 @@ private static partial ProductUnmappedProperties GetProductUnmappedProperties(IM }; } } + +/// +/// DTO for Vehicle that inherits from BaseDto +/// Tests that properties from base classes are properly mapped +/// +[MapFrom(typeof(Vehicle))] +public partial class VehicleReadDto : BaseDto +{ + public string Make { get; set; } = string.Empty; + public string Model { get; set; } = string.Empty; + public int Year { get; set; } + public string Color { get; set; } = string.Empty; +} + +/// +/// DTO for creating Vehicle that inherits from BaseDto +/// Tests MapTo with inheritance +/// +[MapTo(typeof(Vehicle))] +public partial class VehicleCreateDto : BaseDto +{ + public string Make { get; set; } = string.Empty; + public string Model { get; set; } = string.Empty; + public int Year { get; set; } + public string Color { get; set; } = string.Empty; + + private static partial VehicleUnmappedProperties GetVehicleUnmappedProperties(IMapper mapper, VehicleCreateDto source) + { + return new VehicleUnmappedProperties + { + IsDeleted = false + }; + } +} diff --git a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestEntities.cs b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestEntities.cs index 904a10d..9144f0f 100644 --- a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestEntities.cs +++ b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestEntities.cs @@ -74,3 +74,35 @@ public class Order public decimal TotalAmount { get; set; } public DateTime CreatedAt { get; set; } } + +/// +/// Base entity class with common properties +/// +public abstract class BaseEntity +{ + public int Id { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public bool IsDeleted { get; set; } +} + +/// +/// Vehicle entity that inherits from BaseEntity +/// +public class Vehicle : BaseEntity +{ + public string Make { get; set; } = string.Empty; + public string Model { get; set; } = string.Empty; + public int Year { get; set; } + public string Color { get; set; } = string.Empty; +} + +/// +/// Base DTO class with common properties +/// +public abstract class BaseDto +{ + public int Id { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs b/TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs index 3850f0f..873127a 100644 --- a/TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs +++ b/TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs @@ -70,27 +70,27 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati sourceText.AppendLine($"partial class {className}"); sourceText.AppendLine("{"); sourceText.IncreaseIndent(); - var matchingFields = ma.ClassDeclarationSyntax.Members - .OfType() - .Where(prop => ma.Target != null - && ma.Target.Members - .OfType() - .Any(targetProp => targetProp.Identifier.Text == prop.Identifier.Text)) + + // Get all properties including inherited ones + var allSourceProperties = MappingOptions.GetAllProperties(ma.SemanticModel, ma.ClassDeclarationSyntax); + var allTargetProperties = MappingOptions.GetAllProperties(ma.SemanticModel, ma.Target); + + var matchingFields = allSourceProperties + .Where(prop => allTargetProperties + .Any(targetProp => targetProp.Name == prop.Name)) .ToList(); - + if (ma.AttributeName.Contains("MapFrom")) { - var missingFields = ma.ClassDeclarationSyntax.Members - .OfType() - .Where(prop => ma.Target != null - && ma.Target.Members.OfType().All(targetProp => targetProp.Identifier.Text != prop.Identifier.Text)) + var missingFields = allSourceProperties + .Where(prop => allTargetProperties.All(targetProp => targetProp.Name != prop.Name)) .ToList(); var isMissing = missingFields.Any(); if (isMissing) { - // create a subclass inside + // create a subclass inside sourceText.AppendLine(); sourceText.AppendLine("///"); sourceText.AppendLine("/// The following properties were not mapped because they do not exist in the target class"); @@ -99,13 +99,15 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati using var block = sourceText.BeginBlock($"internal class {ma.TargetName}UnmappedProperties"); foreach (var prop in missingFields) { - var location = prop.GetLocation().GetMappedLineSpan(); - sourceText.AppendLine($"/// "); - sourceText.AppendLine($"/// Found at {location.Path.Substring(location.Path.LastIndexOf('/'))} at {location.StartLinePosition.Line + 1}"); - sourceText.AppendLine($"/// "); - sourceText.AppendLine($"public { - string.Join("",prop.Modifiers.Where(x => !x.ToFullString().Contains("public")).Select(x => x.ToFullString())) - }{prop.Type.ToFullString().Trim()} {prop.Identifier.Text} {{ get; set; }}"); + var location = prop.PropertySymbol.Locations.FirstOrDefault(); + if (location != null && location.IsInSource) + { + var lineSpan = location.GetMappedLineSpan(); + sourceText.AppendLine($"/// "); + sourceText.AppendLine($"/// Found at {lineSpan.Path.Substring(lineSpan.Path.LastIndexOf('/') + 1)} at {lineSpan.StartLinePosition.Line + 1}"); + sourceText.AppendLine($"/// "); + } + sourceText.AppendLine($"public {prop.TypeFullName.Replace("global::", "")} {prop.Name} {{ get; set; }}"); } } sourceText.AppendLine(); @@ -131,23 +133,23 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati sourceText.IncreaseIndent(); foreach (var prop in matchingFields) { - if (prop.Type.ToFullString() != ma.Target?.Members - .OfType() - .FirstOrDefault(p => p.Identifier.Text == prop.Identifier.Text)? - .Type.ToFullString()) + var targetProp = allTargetProperties + .FirstOrDefault(p => p.Name == prop.Name); + + if (targetProp != null && prop.TypeFullName != targetProp.TypeFullName) { // Type mismatch, use mapper - sourceText.AppendLine($"{prop.Identifier.Text} = mapper.Map<{prop.Type.ToFullString()}>(source.{prop.Identifier.Text}),"); + sourceText.AppendLine($"{prop.Name} = mapper.Map<{prop.TypeFullName.Replace("global::", "")}>(source.{prop.Name}),"); } else { - sourceText.AppendLine($"{prop.Identifier.Text} = source.{prop.Identifier.Text},"); + sourceText.AppendLine($"{prop.Name} = source.{prop.Name},"); } } - + foreach (var prop in missingFields) { - sourceText.AppendLine($"{prop.Identifier.Text} = unmapped.{prop.Identifier.Text},"); + sourceText.AppendLine($"{prop.Name} = unmapped.{prop.Name},"); } sourceText.DecreaseIndent(); sourceText.AppendLine("};"); @@ -156,15 +158,14 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati } else if (ma.AttributeName.Contains("MapTo")) { - var missingFields = ma.Target.Members - .OfType() - .Where(prop => ma.ClassDeclarationSyntax.Members.OfType().All(targetProp => targetProp.Identifier.Text != prop.Identifier.Text)) + var missingFields = allTargetProperties + .Where(prop => allSourceProperties.All(targetProp => targetProp.Name != prop.Name)) .ToList(); - + var isMissing = missingFields.Any(); if (isMissing) { - // create a subclass inside + // create a subclass inside sourceText.AppendLine(); sourceText.AppendLine("///"); sourceText.AppendLine("/// The following properties were not mapped because they do not exist in the target class"); @@ -173,13 +174,15 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati using var block = sourceText.BeginBlock($"internal class {ma.TargetName}UnmappedProperties"); foreach (var prop in missingFields) { - var location = prop.GetLocation().GetMappedLineSpan(); - sourceText.AppendLine($"/// "); - sourceText.AppendLine($"/// Found at {location.Path.Substring(location.Path.LastIndexOf('/'))} at {location.StartLinePosition.Line + 1}"); - sourceText.AppendLine($"/// "); - sourceText.AppendLine($"public { - string.Join("",prop.Modifiers.Where(x => !x.ToFullString().Contains("public")).Select(x => x.ToFullString())) - }{prop.Type.ToFullString().Trim()} {prop.Identifier.Text} {{ get; set; }}"); + var location = prop.PropertySymbol.Locations.FirstOrDefault(); + if (location != null && location.IsInSource) + { + var lineSpan = location.GetMappedLineSpan(); + sourceText.AppendLine($"/// "); + sourceText.AppendLine($"/// Found at {lineSpan.Path.Substring(lineSpan.Path.LastIndexOf('/') + 1)} at {lineSpan.StartLinePosition.Line + 1}"); + sourceText.AppendLine($"/// "); + } + sourceText.AppendLine($"public {prop.TypeFullName.Replace("global::", "")} {prop.Name} {{ get; set; }}"); } } sourceText.AppendLine(); @@ -203,25 +206,24 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati foreach (var prop in matchingFields) { // Get the corresponding target property to check type - var targetProp = ma.Target.Members - .OfType() - .FirstOrDefault(p => p.Identifier.Text == prop.Identifier.Text); + var targetProp = allTargetProperties + .FirstOrDefault(p => p.Name == prop.Name); - if (targetProp != null && prop.Type.ToFullString() != targetProp.Type.ToFullString()) + if (targetProp != null && prop.TypeFullName != targetProp.TypeFullName) { // Type mismatch, use mapper - sourceText.AppendLine($" {prop.Identifier.Text} = mapper.Map<{targetProp.Type.ToFullString()}>(this.{prop.Identifier.Text}),"); + sourceText.AppendLine($" {prop.Name} = mapper.Map<{targetProp.TypeFullName.Replace("global::", "")}>(this.{prop.Name}),"); } else { - sourceText.AppendLine($" {prop.Identifier.Text} = this.{prop.Identifier.Text},"); + sourceText.AppendLine($" {prop.Name} = this.{prop.Name},"); } } if (isMissing) { foreach (var prop in missingFields) { - sourceText.AppendLine($" {prop.Identifier.Text} = unmapped.{prop.Identifier.Text},"); + sourceText.AppendLine($" {prop.Name} = unmapped.{prop.Name},"); } } sourceText.DecreaseIndent(); diff --git a/TenJames.CompMap/TenJames.CompMap/Properties/MappingOptions.cs b/TenJames.CompMap/TenJames.CompMap/Properties/MappingOptions.cs index 01ffeca..81069e1 100644 --- a/TenJames.CompMap/TenJames.CompMap/Properties/MappingOptions.cs +++ b/TenJames.CompMap/TenJames.CompMap/Properties/MappingOptions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -14,8 +15,34 @@ public class MappingOptions { public string TargetName => Target.Identifier.Text; public string TargetNamespace { get; set; } public string TargetFullName => string.IsNullOrEmpty(TargetNamespace) ? TargetName : $"{TargetNamespace}.{TargetName}"; - - + public SemanticModel SemanticModel { get; set; } + + /// + /// Gets all properties including inherited ones from a class declaration + /// + public static List GetAllProperties(SemanticModel semanticModel, ClassDeclarationSyntax classDecl) + { + var properties = new List(); + var symbol = semanticModel.GetDeclaredSymbol(classDecl) as INamedTypeSymbol; + + if (symbol == null) return properties; + + // Get all properties from the class hierarchy + var allMembers = symbol.GetMembers().OfType(); + + foreach (var prop in allMembers) + { + properties.Add(new PropertyInfo + { + Name = prop.Name, + TypeFullName = prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + PropertySymbol = prop + }); + } + + return properties; + } + public static MappingOptions? Create( GeneratorSyntaxContext context, ClassDeclarationSyntax classDeclarationSyntax) @@ -56,11 +83,19 @@ public class MappingOptions { AttributeName = attributeName, Namespace = namespaceName, Target = targetClass, - TargetNamespace = targetNamespace + TargetNamespace = targetNamespace, + SemanticModel = context.SemanticModel }; } } return null; } +} + +public class PropertyInfo +{ + public string Name { get; set; } = string.Empty; + public string TypeFullName { get; set; } = string.Empty; + public IPropertySymbol PropertySymbol { get; set; } = null!; } \ No newline at end of file From 10573d3be3517461d02a3755c36a4872eb99c3c7 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 22 Jan 2026 21:20:48 +0100 Subject: [PATCH 3/3] feat: added namespace and derived fields --- .../MappingIntegrationTests.cs | 2 +- .../TestDtos.cs | 2 ++ .../TenJames.CompMap/MapperGenerator.cs | 29 ++++++++---------- .../Properties/MappingOptions.cs | 30 +++++++++++++------ 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/MappingIntegrationTests.cs b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/MappingIntegrationTests.cs index 17bcadf..ad8b3bd 100644 --- a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/MappingIntegrationTests.cs +++ b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/MappingIntegrationTests.cs @@ -449,7 +449,7 @@ public void VehicleCreateDto_MapTo_ShouldMapPropertiesFromBaseClass() }; // Act - var vehicle = dto.MapTo(_mapper); + var vehicle = _mapper.Map(dto); // Assert - Properties from base class BaseEntity Assert.Equal(dto.Id, vehicle.Id); diff --git a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestDtos.cs b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestDtos.cs index f749ba8..4b03a25 100644 --- a/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestDtos.cs +++ b/TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestDtos.cs @@ -129,6 +129,7 @@ private static partial UserUnmappedProperties GetUserUnmappedProperties(IMapper }; } + private static string MaskEmail(string email) { if (string.IsNullOrEmpty(email)) return string.Empty; @@ -212,4 +213,5 @@ private static partial VehicleUnmappedProperties GetVehicleUnmappedProperties(IM IsDeleted = false }; } + } diff --git a/TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs b/TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs index 873127a..81c266f 100644 --- a/TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs +++ b/TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs @@ -73,7 +73,10 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati // Get all properties including inherited ones var allSourceProperties = MappingOptions.GetAllProperties(ma.SemanticModel, ma.ClassDeclarationSyntax); - var allTargetProperties = MappingOptions.GetAllProperties(ma.SemanticModel, ma.Target); + + // Get semantic model for target (which might be in a different file) + var targetSemanticModel = compilation.GetSemanticModel(ma.Target.SyntaxTree); + var allTargetProperties = MappingOptions.GetAllProperties(targetSemanticModel, ma.Target); var matchingFields = allSourceProperties .Where(prop => allTargetProperties @@ -99,14 +102,10 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati using var block = sourceText.BeginBlock($"internal class {ma.TargetName}UnmappedProperties"); foreach (var prop in missingFields) { - var location = prop.PropertySymbol.Locations.FirstOrDefault(); - if (location != null && location.IsInSource) - { - var lineSpan = location.GetMappedLineSpan(); - sourceText.AppendLine($"/// "); - sourceText.AppendLine($"/// Found at {lineSpan.Path.Substring(lineSpan.Path.LastIndexOf('/') + 1)} at {lineSpan.StartLinePosition.Line + 1}"); - sourceText.AppendLine($"/// "); - } + // Add property documentation + sourceText.AppendLine($"/// "); + sourceText.AppendLine($"/// Property: {prop.Name} of type {prop.TypeFullName.Replace("global::", "")}"); + sourceText.AppendLine($"/// "); sourceText.AppendLine($"public {prop.TypeFullName.Replace("global::", "")} {prop.Name} {{ get; set; }}"); } } @@ -174,14 +173,10 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati using var block = sourceText.BeginBlock($"internal class {ma.TargetName}UnmappedProperties"); foreach (var prop in missingFields) { - var location = prop.PropertySymbol.Locations.FirstOrDefault(); - if (location != null && location.IsInSource) - { - var lineSpan = location.GetMappedLineSpan(); - sourceText.AppendLine($"/// "); - sourceText.AppendLine($"/// Found at {lineSpan.Path.Substring(lineSpan.Path.LastIndexOf('/') + 1)} at {lineSpan.StartLinePosition.Line + 1}"); - sourceText.AppendLine($"/// "); - } + // Add property documentation + sourceText.AppendLine($"/// "); + sourceText.AppendLine($"/// Property: {prop.Name} of type {prop.TypeFullName.Replace("global::", "")}"); + sourceText.AppendLine($"/// "); sourceText.AppendLine($"public {prop.TypeFullName.Replace("global::", "")} {prop.Name} {{ get; set; }}"); } } diff --git a/TenJames.CompMap/TenJames.CompMap/Properties/MappingOptions.cs b/TenJames.CompMap/TenJames.CompMap/Properties/MappingOptions.cs index 81069e1..a01ea30 100644 --- a/TenJames.CompMap/TenJames.CompMap/Properties/MappingOptions.cs +++ b/TenJames.CompMap/TenJames.CompMap/Properties/MappingOptions.cs @@ -27,17 +27,29 @@ public static List GetAllProperties(SemanticModel semanticModel, C if (symbol == null) return properties; - // Get all properties from the class hierarchy - var allMembers = symbol.GetMembers().OfType(); - - foreach (var prop in allMembers) + // Walk up the inheritance chain to get all properties + var currentType = symbol; + while (currentType != null && currentType.SpecialType != SpecialType.System_Object) { - properties.Add(new PropertyInfo + // Get properties declared on this type + var typeProperties = currentType.GetMembers().OfType(); + + foreach (var prop in typeProperties) { - Name = prop.Name, - TypeFullName = prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - PropertySymbol = prop - }); + // Avoid duplicates (overridden properties) + if (!properties.Any(p => p.Name == prop.Name)) + { + properties.Add(new PropertyInfo + { + Name = prop.Name, + TypeFullName = prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + PropertySymbol = prop + }); + } + } + + // Move to base type + currentType = currentType.BaseType; } return properties;