Skip to content

Commit 7cf2363

Browse files
committed
fix(generator): resolve factory discovery and code generation logic
- Fix captured variable issue in AnalysisHelper.FindBestFactory that prevented finding valid factories in base types. - Update CodeBuilder and Mapper to support partial method generation (BuildGenerated/ProjectGenerated) and correct fallback handling.
1 parent 881df62 commit 7cf2363

11 files changed

Lines changed: 62 additions & 43 deletions

File tree

ProjectR.Sample/DTOs/Dtos.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace ProjectR.Sample.Application.DTOs
1010
public class MoneyDto
1111
{
1212
public decimal Amount { get; set; }
13-
public string Currency { get; set; }
13+
public string Currency { get; set; }
1414
}
1515

1616
// A DTO for the Review entity.

ProjectR.Sample/Mappers/Mappers.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ namespace ProjectR.Sample.Application.Mappers
88
[GeneratorExclude]
99
public partial class UpdateProductMapper : Mapper<Product, UpdateProductDto>
1010
{
11-
public override Product Build(UpdateProductDto dto)
11+
public override Product BuildGenerated(UpdateProductDto dto)
1212
{
1313
throw new NotImplementedException();
1414
}
1515

16-
public override UpdateProductDto Project(Product source)
16+
public override UpdateProductDto ProjectGenerated(Product source)
1717
{
1818
throw new NotImplementedException();
1919
}

ProjectR.Tests/MapperRegistrationExtensionsTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ private class TestDto2 { }
1616

1717
private class TestMapper : Mapper<TestEntity, TestDto>
1818
{
19-
public override TestDto Project(TestEntity source) => new TestDto();
20-
public override TestEntity Build(TestDto dto) => new TestEntity();
19+
public override TestDto ProjectGenerated(TestEntity source) => new TestDto();
20+
public override TestEntity BuildGenerated(TestDto dto) => new TestEntity();
2121
}
2222

2323
private class TestMapper2 : Mapper<TestEntity2, TestDto2>
2424
{
25-
public override TestDto2 Project(TestEntity2 source) => new TestDto2();
26-
public override TestEntity2 Build(TestDto2 dto) => new TestEntity2();
25+
public override TestDto2 ProjectGenerated(TestEntity2 source) => new TestDto2();
26+
public override TestEntity2 BuildGenerated(TestDto2 dto) => new TestEntity2();
2727
}
2828

2929
private abstract class AbstractMapper : Mapper<TestEntity, TestDto>

ProjectR.Tests/MapperResolverTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ private class UnknownDto { }
1515

1616
private class TestMapper : Mapper<TestEntity, TestDto>
1717
{
18-
public override TestDto Project(TestEntity source) => new TestDto();
19-
public override TestEntity Build(TestDto dto) => new TestEntity();
18+
public override TestDto ProjectGenerated(TestEntity source) => new TestDto();
19+
public override TestEntity BuildGenerated(TestDto dto) => new TestEntity();
2020
}
2121

2222
[Fact]

ProjectR.Tests/MapperTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ private class TestDto
1919

2020
private class TestMapper : Mapper<TestEntity, TestDto>
2121
{
22-
public override TestDto Project(TestEntity source)
22+
public override TestDto ProjectGenerated(TestEntity source)
2323
{
2424
return new TestDto
2525
{
@@ -28,7 +28,7 @@ public override TestDto Project(TestEntity source)
2828
};
2929
}
3030

31-
public override TestEntity Build(TestDto dto)
31+
public override TestEntity BuildGenerated(TestDto dto)
3232
{
3333
return new TestEntity
3434
{
@@ -40,7 +40,7 @@ public override TestEntity Build(TestDto dto)
4040

4141
private class TestMapperWithRefinement : Mapper<TestEntity, TestDto>
4242
{
43-
public override TestDto Project(TestEntity source)
43+
public override TestDto ProjectGenerated(TestEntity source)
4444
{
4545
return new TestDto
4646
{
@@ -49,7 +49,7 @@ public override TestDto Project(TestEntity source)
4949
};
5050
}
5151

52-
public override TestEntity Build(TestDto dto)
52+
public override TestEntity BuildGenerated(TestDto dto)
5353
{
5454
return new TestEntity
5555
{
@@ -166,7 +166,7 @@ private class TestDto
166166

167167
private class TestMapper : Mapper<TestEntity, TestDto>
168168
{
169-
public override TestDto Project(TestEntity source)
169+
public override TestDto ProjectGenerated(TestEntity source)
170170
{
171171
return new TestDto
172172
{
@@ -175,7 +175,7 @@ public override TestDto Project(TestEntity source)
175175
};
176176
}
177177

178-
public override TestEntity Build(TestDto dto)
178+
public override TestEntity BuildGenerated(TestDto dto)
179179
{
180180
return new TestEntity
181181
{

ProjectR.Tests/MapperTypeCacheTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ private class TestDto2 { }
1313

1414
private class TestMapper : Mapper<TestEntity, TestDto>
1515
{
16-
public override TestDto Project(TestEntity source) => new TestDto();
17-
public override TestEntity Build(TestDto dto) => new TestEntity();
16+
public override TestDto ProjectGenerated(TestEntity source) => new TestDto();
17+
public override TestEntity BuildGenerated(TestDto dto) => new TestEntity();
1818
}
1919

2020
private class TestMapper2 : Mapper<TestEntity2, TestDto2>
2121
{
22-
public override TestDto2 Project(TestEntity2 source) => new TestDto2();
23-
public override TestEntity2 Build(TestDto2 dto) => new TestEntity2();
22+
public override TestDto2 ProjectGenerated(TestEntity2 source) => new TestDto2();
23+
public override TestEntity2 BuildGenerated(TestDto2 dto) => new TestEntity2();
2424
}
2525

2626
private class NonMapperClass { }

ProjectR/Builders/CodeBuilder.cs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ private void BuildProjectAsMethod(INamedTypeSymbol mapperSymbol, MappingPlan pla
9494
var sourceTypeName = plan.SourceType.ToDisplayString();
9595
var destTypeName = plan.DestinationType.ToDisplayString();
9696
BuildXmlDoc(mapperSymbol, "Project");
97-
Indent(); _sb.AppendLine($"public override {destTypeName} Project({sourceTypeName} source)");
97+
Indent(); _sb.AppendLine($"public override {destTypeName} ProjectGenerated({sourceTypeName} source)");
9898
Indent(); _sb.AppendLine("{");
9999
_indentationLevel++;
100100
GenerateNullCheck("source", destTypeName);
@@ -109,7 +109,7 @@ private void BuildBuildMethod(INamedTypeSymbol mapperSymbol, MappingPlan plan)
109109
var sourceTypeName = plan.SourceType.ToDisplayString();
110110
var destTypeName = plan.DestinationType.ToDisplayString();
111111
BuildXmlDoc(mapperSymbol, "Build");
112-
Indent(); _sb.AppendLine($"public override {destTypeName} Build({sourceTypeName} source)");
112+
Indent(); _sb.AppendLine($"public override {destTypeName} BuildGenerated({sourceTypeName} source)");
113113
Indent(); _sb.AppendLine("{");
114114
_indentationLevel++;
115115
GenerateNullCheck("source", destTypeName);
@@ -122,39 +122,34 @@ private void BuildBuildMethod(INamedTypeSymbol mapperSymbol, MappingPlan plan)
122122
private void GenerateCreationBlock(string variableName, ITypeSymbol variableType, MappingPlan plan, string sourceVarName, bool isBuild = false)
123123
{
124124
Indent();
125-
if(isBuild) _sb.AppendLine("return BuildRefiner(");
126-
else _sb.AppendLine("return ProjectAsRefiner(");
127-
_indentationLevel++;
128-
Indent();
129125

130126
switch (plan.Creation.Method)
131127
{
132128
case CreationMethod.ConstructorWithParameters:
133129
var constructorArgs = GetMethodArguments(plan.Creation.Constructor, plan.Creation, sourceVarName);
134-
_sb.AppendLine($"new {variableType.ToDisplayString()}({constructorArgs})");
130+
_sb.AppendLine($"return new {variableType.ToDisplayString()}({constructorArgs});");
135131
break;
136132
case CreationMethod.FactoryMethod:
137133
var factoryArgs = GetMethodArguments(plan.Creation.FactoryMethod, plan.Creation, sourceVarName);
138-
_sb.AppendLine($"{variableType.ToDisplayString()}.{plan.Creation.FactoryMethod.Name}({factoryArgs})");
134+
_sb.AppendLine($"return {variableType.ToDisplayString()}.{plan.Creation.FactoryMethod.Name}({factoryArgs});");
139135
break;
140136
case CreationMethod.ParameterlessConstructor:
141-
_sb.AppendLine($"new {variableType.ToDisplayString()}");
137+
_sb.AppendLine($"return new {variableType.ToDisplayString()}");
142138
Indent(); _sb.AppendLine("{");
143139
_indentationLevel++;
144140
GenerateMappingInstructions(sourceVarName, null, plan);
145141
_indentationLevel--;
146-
Indent(); _sb.AppendLine("}");
142+
Indent(); _sb.AppendLine("};");
147143
break;
148144
default:
149145
if (isBuild)
150146
{
151-
_sb.AppendLine($"BuildFactoryFallback({variableName})");
147+
_sb.AppendLine($"var fallback = BuildFactoryFallback({sourceVarName});");
148+
_sb.AppendLine("if (fallback != null) return fallback;");
152149
}
153-
_sb.AppendLine($"new {variableType.ToDisplayString()}()");
150+
_sb.AppendLine($"return new {variableType.ToDisplayString()}();");
154151
break;
155152
}
156-
Indent(); _sb.Append($", {sourceVarName});");
157-
_indentationLevel--;
158153
_sb.AppendLine();
159154

160155
}

ProjectR/Mapping/Mapper.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,21 @@ public abstract partial class Mapper<TEntity, TDto>
1414
/// </summary>
1515
/// <param name="source">The entity object instance.</param>
1616
/// <returns>A new dto object instance.</returns>
17-
public abstract TDto Project(TEntity source);
17+
public TDto Project(TEntity source)
18+
{
19+
return ProjectAsRefiner(ProjectGenerated(source), source);
20+
}
1821

1922
/// <summary>
2023
/// Builds a new entity object from a dto object.
2124
/// This implementation will be generated automatically based on creation policies.
2225
/// </summary>
2326
/// <param name="dto">The dto object instance.</param>
2427
/// <returns>A new entity object instance.</returns>
25-
public abstract TEntity Build(TDto dto);
28+
public TEntity Build(TDto dto)
29+
{
30+
return BuildRefiner(BuildGenerated(dto), dto);
31+
}
2632

2733
/// <summary>
2834
/// A factory method used by the entity generator to create a new entity object.
@@ -32,6 +38,22 @@ public abstract partial class Mapper<TEntity, TDto>
3238
/// <returns>A new entity object instance.</returns>
3339
public static TEntity BuildFactoryFallback(TDto dto) => default!;
3440

41+
/// <summary>
42+
/// Projects the specified entity to a data transfer object (DTO) of type <typeparamref name="TDto"/>.
43+
/// </summary>
44+
/// <param name="source">The entity instance to project. Cannot be null.</param>
45+
/// <returns>A DTO of type <typeparamref name="TDto"/> representing the projected data from the source entity.</returns>
46+
public virtual TDto ProjectGenerated(TEntity source) => default!;
47+
48+
/// <summary>
49+
/// Creates a new instance of <typeparamref name="TEntity"/> based on the data provided in the specified
50+
/// <typeparamref name="TDto"/> object.
51+
/// </summary>
52+
/// <param name="dto">The data transfer object containing the information used to construct the <typeparamref name="TEntity"/>
53+
/// instance. Cannot be null.</param>
54+
/// <returns>A new <typeparamref name="TEntity"/> instance populated with values from <paramref name="dto"/>.</returns>
55+
public virtual TEntity BuildGenerated(TDto dto) => default!;
56+
3557
/// <summary>
3658
/// A refinement method used by the entity generator to apply final adjustments to the entity object after building.
3759
/// This method can be overridden to provide custom refinement logic.

ProjectR/Mapping/MapperGenerator.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
5757
explicitMappers[classSymbol.Name] = classNode;
5858
}
5959
}
60-
61-
// Check for DTO attribute
60+
6261
var attribute = classSymbol.GetAttributes().FirstOrDefault(ad =>
6362
dtoAttributeSymbol != null && ad.AttributeClass?.OriginalDefinition.Equals(dtoAttributeSymbol, SymbolEqualityComparer.Default) == true);
6463

@@ -73,7 +72,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
7372
return (compilation, explicitMappers, dtosWithAttribute);
7473
});
7574

76-
7775
context.RegisterSourceOutput(compilationAndClasses,
7876
static (spc, source) => Execute(source.compilation, source.explicitMappers, source.dtosWithAttribute, spc));
7977
}

ProjectR/Policies/AnalysisHelper.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,20 @@ public void FindBestConstructor(ITypeSymbol sourceType, ITypeSymbol destinationT
6161

6262
public void FindBestFactory(ITypeSymbol sourceType, ITypeSymbol destinationType, MappingPlan plan)
6363
{
64-
var factories = destinationType.GetMembers().OfType<IMethodSymbol>().Where(m => m.IsStatic && m.DeclaredAccessibility == Accessibility.Public && SymbolEqualityComparer.Default.Equals(m.ReturnType, destinationType));
64+
var targetType = destinationType;
65+
var factories = destinationType.GetMembers().OfType<IMethodSymbol>().Where(m => m.IsStatic && m.DeclaredAccessibility == Accessibility.Public && SymbolEqualityComparer.Default.Equals(m.ReturnType, targetType));
6566

6667
if (!factories.Any())
6768
{
68-
var destinationBaseType = destinationType.BaseType;
69-
while (destinationType != null)
69+
var currentType = destinationType.BaseType;
70+
while (currentType != null)
7071
{
71-
factories = factories.Concat(destinationType.GetMembers().OfType<IMethodSymbol>().Where(m => m.IsStatic && m.DeclaredAccessibility == Accessibility.Public && SymbolEqualityComparer.Default.Equals(m.ReturnType, destinationType)));
72+
var typeToCheck = currentType;
73+
var extraFactories = currentType.GetMembers().OfType<IMethodSymbol>().Where(m => m.IsStatic && m.DeclaredAccessibility == Accessibility.Public && SymbolEqualityComparer.Default.Equals(m.ReturnType, typeToCheck));
74+
factories = factories.Concat(extraFactories);
75+
7276
if (factories.Any()) break;
73-
destinationType = destinationType.BaseType;
77+
currentType = currentType.BaseType;
7478
}
7579
}
7680

0 commit comments

Comments
 (0)