Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = _mapper.Map<Vehicle>(dto);

// 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);
}
}
36 changes: 36 additions & 0 deletions TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ private static partial UserUnmappedProperties GetUserUnmappedProperties(IMapper
};
}


private static string MaskEmail(string email)
{
if (string.IsNullOrEmpty(email)) return string.Empty;
Expand Down Expand Up @@ -179,3 +180,38 @@ private static partial ProductUnmappedProperties GetProductUnmappedProperties(IM
};
}
}

/// <summary>
/// DTO for Vehicle that inherits from BaseDto
/// Tests that properties from base classes are properly mapped
/// </summary>
[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;
}

/// <summary>
/// DTO for creating Vehicle that inherits from BaseDto
/// Tests MapTo with inheritance
/// </summary>
[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
};
}

}
32 changes: 32 additions & 0 deletions TenJames.CompMap/TenJames.CompMap.IntegrationTests/TestEntities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,35 @@ public class Order
public decimal TotalAmount { get; set; }
public DateTime CreatedAt { get; set; }
}

/// <summary>
/// Base entity class with common properties
/// </summary>
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public bool IsDeleted { get; set; }
}

/// <summary>
/// Vehicle entity that inherits from BaseEntity
/// </summary>
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;
}

/// <summary>
/// Base DTO class with common properties
/// </summary>
public abstract class BaseDto
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
99 changes: 58 additions & 41 deletions TenJames.CompMap/TenJames.CompMap/MapperGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,33 +57,43 @@ 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();
sourceText.AppendLine($"partial class {className}");
sourceText.AppendLine("{");
sourceText.IncreaseIndent();
var matchingFields = ma.ClassDeclarationSyntax.Members
.OfType<PropertyDeclarationSyntax>()
.Where(prop => ma.Target != null
&& ma.Target.Members
.OfType<PropertyDeclarationSyntax>()
.Any(targetProp => targetProp.Identifier.Text == prop.Identifier.Text))

// Get all properties including inherited ones
var allSourceProperties = MappingOptions.GetAllProperties(ma.SemanticModel, ma.ClassDeclarationSyntax);

// 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
.Any(targetProp => targetProp.Name == prop.Name))
.ToList();


if (ma.AttributeName.Contains("MapFrom"))
{
var missingFields = ma.ClassDeclarationSyntax.Members
.OfType<PropertyDeclarationSyntax>()
.Where(prop => ma.Target != null
&& ma.Target.Members.OfType<PropertyDeclarationSyntax>().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("///<summary>");
sourceText.AppendLine("/// The following properties were not mapped because they do not exist in the target class");
Expand All @@ -92,17 +102,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();
// Add property documentation
sourceText.AppendLine($"/// <summary>");
sourceText.AppendLine($"/// Found at {location.Path.Substring(location.Path.LastIndexOf('/'))} at {location.StartLinePosition.Line + 1}");
sourceText.AppendLine($"/// Property: {prop.Name} of type {prop.TypeFullName.Replace("global::", "")}");
sourceText.AppendLine($"/// </summary>");
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; }}");
sourceText.AppendLine($"public {prop.TypeFullName.Replace("global::", "")} {prop.Name} {{ get; set; }}");
}
}
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();
Expand All @@ -111,7 +119,7 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati
sourceText.AppendLine("/// <summary>");
sourceText.AppendLine("/// Mapping method generated by TenJames.CompMap");
sourceText.AppendLine("/// </summary>");
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)
{
Expand All @@ -124,23 +132,23 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati
sourceText.IncreaseIndent();
foreach (var prop in matchingFields)
{
if (prop.Type.ToFullString() != ma.Target?.Members
.OfType<PropertyDeclarationSyntax>()
.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("};");
Expand All @@ -149,15 +157,14 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati
}
else if (ma.AttributeName.Contains("MapTo"))
{
var missingFields = ma.Target.Members
.OfType<PropertyDeclarationSyntax>()
.Where(prop => ma.ClassDeclarationSyntax.Members.OfType<PropertyDeclarationSyntax>().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("///<summary>");
sourceText.AppendLine("/// The following properties were not mapped because they do not exist in the target class");
Expand All @@ -166,13 +173,11 @@ 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();
// Add property documentation
sourceText.AppendLine($"/// <summary>");
sourceText.AppendLine($"/// Found at {location.Path.Substring(location.Path.LastIndexOf('/'))} at {location.StartLinePosition.Line + 1}");
sourceText.AppendLine($"/// Property: {prop.Name} of type {prop.TypeFullName.Replace("global::", "")}");
sourceText.AppendLine($"/// </summary>");
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; }}");
sourceText.AppendLine($"public {prop.TypeFullName.Replace("global::", "")} {prop.Name} {{ get; set; }}");
}
}
sourceText.AppendLine();
Expand All @@ -185,23 +190,35 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati
sourceText.AppendLine("/// Mapping method generated by TenJames.CompMap");
sourceText.AppendLine("/// </summary>");
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 = allTargetProperties
.FirstOrDefault(p => p.Name == prop.Name);

if (targetProp != null && prop.TypeFullName != targetProp.TypeFullName)
{
// Type mismatch, use mapper
sourceText.AppendLine($" {prop.Name} = mapper.Map<{targetProp.TypeFullName.Replace("global::", "")}>(this.{prop.Name}),");
}
else
{
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();
Expand Down
Loading
Loading