Skip to content

Commit 922926b

Browse files
committed
Refactor(ProjectR): transition to cache-less DI registration and source-generated cross-assembly discovery
1 parent 183656a commit 922926b

11 files changed

Lines changed: 199 additions & 305 deletions

File tree

ProjectR.Generator/Mapping/MapperGenerator.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Immutable;
77
using System.Diagnostics;
88
using System.Linq;
9+
using System.Text;
910
using System.Threading;
1011

1112
namespace ProjectR
@@ -134,6 +135,103 @@ public partial class {mapperName} : global::ProjectR.Mapper<{entityFullName}, {d
134135
context.ReportDiagnostic(Diagnostic.Create(Diagnostics.UnexpectedError, mapperClass.GetLocation(), ex.Message, ex.StackTrace));
135136
}
136137
}
138+
139+
GenerateDiRegistration(compilation, allMappers, context);
140+
}
141+
142+
private static void GenerateDiRegistration(Compilation compilation, List<INamedTypeSymbol> mappers, SourceProductionContext context)
143+
{
144+
var sb = new StringBuilder();
145+
var assemblyName = compilation.AssemblyName!;
146+
var safeAssemblyName = assemblyName.Replace(".", "_");
147+
var generatedNamespace = $"{assemblyName}.Generated";
148+
var extensionsClassName = $"GeneratedMapperRegistrations_{safeAssemblyName}";
149+
150+
var discoverMappersAttributeSymbol = compilation.GetTypeByMetadataName("ProjectR.Attributes.DiscoverMappersAttribute");
151+
bool shouldDiscover = compilation.Assembly.GetAttributes().Any(ad =>
152+
discoverMappersAttributeSymbol != null &&
153+
ad.AttributeClass?.Equals(discoverMappersAttributeSymbol, SymbolEqualityComparer.Default) == true);
154+
155+
sb.AppendLine("// <auto-generated/>");
156+
sb.AppendLine("#nullable enable");
157+
sb.AppendLine();
158+
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
159+
sb.AppendLine("using Microsoft.Extensions.DependencyInjection.Extensions;");
160+
sb.AppendLine("using ProjectR.Services;");
161+
sb.AppendLine();
162+
163+
var namespaces = mappers
164+
.Select(m => m.ContainingNamespace.ToDisplayString())
165+
.Distinct()
166+
.OrderBy(ns => ns);
167+
168+
foreach (var ns in namespaces)
169+
{
170+
sb.AppendLine($"using {ns};");
171+
}
172+
173+
sb.AppendLine();
174+
sb.AppendLine($"namespace {generatedNamespace};");
175+
sb.AppendLine();
176+
sb.AppendLine("/// <summary>");
177+
sb.AppendLine($"/// Generated mapper registration for {assemblyName}.");
178+
sb.AppendLine("/// Generated by MapperGenerator.");
179+
sb.AppendLine("/// </summary>");
180+
sb.AppendLine($"public static class {extensionsClassName}");
181+
sb.AppendLine("{");
182+
sb.AppendLine(" /// <summary>");
183+
sb.AppendLine(" /// Registers all generated mappers in the DI container.");
184+
sb.AppendLine(" /// </summary>");
185+
sb.AppendLine(" public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddGeneratedMappers(");
186+
sb.AppendLine(" this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services)");
187+
sb.AppendLine(" {");
188+
189+
// Register individual mappers
190+
sb.AppendLine(" // Register individual mappers from this assembly");
191+
foreach (var mapper in mappers.OrderBy(m => m.Name))
192+
{
193+
var fullyQualifiedName = mapper.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
194+
var baseType = mapper.BaseType;
195+
if (baseType != null && baseType.TypeArguments.Length == 2)
196+
{
197+
var sourceType = baseType.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
198+
var destinationType = baseType.TypeArguments[1].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
199+
200+
sb.AppendLine($" services.TryAddSingleton<{fullyQualifiedName}>();");
201+
sb.AppendLine($" services.TryAddSingleton<global::ProjectR.Mapper<{sourceType}, {destinationType}>>(sp => sp.GetRequiredService<{fullyQualifiedName}>());");
202+
}
203+
}
204+
sb.AppendLine();
205+
206+
if (shouldDiscover)
207+
{
208+
sb.AppendLine(" // Discover and register mappers from referenced assemblies");
209+
foreach (var referencedAssembly in compilation.SourceModule.ReferencedAssemblySymbols)
210+
{
211+
var refSafeName = referencedAssembly.Name.Replace(".", "_");
212+
var refNamespace = $"{referencedAssembly.Name}.Generated";
213+
var refClassName = $"GeneratedMapperRegistrations_{refSafeName}";
214+
215+
// Check if the class exists in the referenced assembly
216+
var refType = compilation.GetTypeByMetadataName($"{refNamespace}.{refClassName}");
217+
if (refType != null)
218+
{
219+
sb.AppendLine($" global::{refNamespace}.{refClassName}.AddGeneratedMappers(services);");
220+
}
221+
}
222+
sb.AppendLine();
223+
}
224+
225+
// Register global services (resolver)
226+
sb.AppendLine(" // Register global services (only once)");
227+
sb.AppendLine(" services.TryAddSingleton<global::ProjectR.Services.IMapperResolver, global::ProjectR.Services.MapperResolver>();");
228+
sb.AppendLine();
229+
230+
sb.AppendLine(" return services;");
231+
sb.AppendLine(" }");
232+
sb.AppendLine("}");
233+
234+
context.AddSource("MapperRegistration.g.cs", sb.ToString());
137235
}
138236

139237
private static void CollectUsings(ITypeSymbol typeSymbol, HashSet<string> usings)

ProjectR.Sample/Program.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
using Microsoft.OpenApi;
2-
using ProjectR.DI;
2+
using ProjectR.Sample.Generated;
33
using ProjectR.Sample.Application.DTOs;
44
using System.Reflection;
55

6+
[assembly: ProjectR.Attributes.DiscoverMappers]
7+
68
var builder = WebApplication.CreateBuilder(args);
79

810
// Add services to the container.
911
builder.Services.AddControllers();
1012

11-
// Aggiungi la scansione e registrazione dei mapper
12-
builder.Services.AddMappers(typeof(ProductDto).Assembly);
13+
// Source Generator registration (AOT-compatible)
14+
builder.Services.AddGeneratedMappers();
1315

1416
// *** 1. Configurazione avanzata di Swagger/OpenAPI ***
1517
builder.Services.AddEndpointsApiExplorer();

ProjectR.Tests/DiagnosticsTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public void NoValidCreationMethod_HasCorrectProperties()
4545
// Assert
4646
diagnostic.Id.Should().Be("PR0003");
4747
diagnostic.Title.ToString().Should().Be("No valid creation method found");
48-
diagnostic.MessageFormat.ToString().Should().Be("ProjectR could not find a valid constructor or static factory method to create an instance of '{0}' based on the source type.");
48+
diagnostic.MessageFormat.ToString().Should().Be("ProjectR could not find a valid constructor or static factory method to create an instance of '{0}' based on the source type");
4949
diagnostic.Category.Should().Be("ProjectR.Policies");
5050
diagnostic.DefaultSeverity.Should().Be(DiagnosticSeverity.Error);
5151
diagnostic.IsEnabledByDefault.Should().BeTrue();
@@ -60,7 +60,7 @@ public void UnmappableConstructorParameter_HasCorrectProperties()
6060
// Assert
6161
diagnostic.Id.Should().Be("PR0004");
6262
diagnostic.Title.ToString().Should().Be("Unmappable constructor parameter");
63-
diagnostic.MessageFormat.ToString().Should().Be("The parameter '{0}' of the constructor for '{1}' could not be mapped from any source property.");
63+
diagnostic.MessageFormat.ToString().Should().Be("The parameter '{0}' of the constructor for '{1}' could not be mapped from any source property");
6464
diagnostic.Category.Should().Be("ProjectR.Policies");
6565
diagnostic.DefaultSeverity.Should().Be(DiagnosticSeverity.Error);
6666
diagnostic.IsEnabledByDefault.Should().BeTrue();
@@ -75,7 +75,7 @@ public void UnmappedDestinationProperty_HasCorrectProperties()
7575
// Assert
7676
diagnostic.Id.Should().Be("PR0005");
7777
diagnostic.Title.ToString().Should().Be("Unmapped destination property");
78-
diagnostic.MessageFormat.ToString().Should().Be("The property '{0}' on destination type '{1}' was not mapped.");
78+
diagnostic.MessageFormat.ToString().Should().Be("The property '{0}' on destination type '{1}' was not mapped");
7979
diagnostic.Category.Should().Be("ProjectR.Policies");
8080
diagnostic.DefaultSeverity.Should().Be(DiagnosticSeverity.Warning);
8181
diagnostic.IsEnabledByDefault.Should().BeTrue();

ProjectR.Tests/GeneratorTests.cs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.CodeAnalysis.CSharp.Testing;
66
using Microsoft.CodeAnalysis.Testing;
77
using Microsoft.CodeAnalysis.Testing.Verifiers;
8+
using Microsoft.Extensions.DependencyInjection;
89
using Xunit;
910
using ProjectR;
1011

@@ -17,7 +18,9 @@ public class Test : CSharpSourceGeneratorTest<TSourceGenerator, XUnitVerifier>
1718
{
1819
public Test()
1920
{
20-
ReferenceAssemblies = ReferenceAssemblies.Net.Net80;
21+
ReferenceAssemblies = ReferenceAssemblies.Net.Net80.AddPackages(ImmutableArray.Create(
22+
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "8.0.0"),
23+
new PackageIdentity("Microsoft.Extensions.DependencyInjection.Abstractions", "8.0.0")));
2124
}
2225

2326
protected override CompilationOptions CreateCompilationOptions()
@@ -110,6 +113,41 @@ public override TestNamespace.Source BuildGenerated(TestNamespace.Destination so
110113
111114
}
112115
}
116+
";
117+
118+
var expectedRegistration = @"// <auto-generated/>
119+
#nullable enable
120+
121+
using Microsoft.Extensions.DependencyInjection;
122+
using Microsoft.Extensions.DependencyInjection.Extensions;
123+
using ProjectR.Services;
124+
125+
using TestNamespace;
126+
127+
namespace TestProject.Generated;
128+
129+
/// <summary>
130+
/// Generated mapper registration for TestProject.
131+
/// Generated by MapperGenerator.
132+
/// </summary>
133+
public static class GeneratedMapperRegistrations_TestProject
134+
{
135+
/// <summary>
136+
/// Registers all generated mappers in the DI container.
137+
/// </summary>
138+
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddGeneratedMappers(
139+
this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services)
140+
{
141+
// Register individual mappers from this assembly
142+
services.TryAddSingleton<global::TestNamespace.TestMapper>();
143+
services.TryAddSingleton<global::ProjectR.Mapper<global::TestNamespace.Source, global::TestNamespace.Destination>>(sp => sp.GetRequiredService<global::TestNamespace.TestMapper>());
144+
145+
// Register global services (only once)
146+
services.TryAddSingleton<global::ProjectR.Services.IMapperResolver, global::ProjectR.Services.MapperResolver>();
147+
148+
return services;
149+
}
150+
}
113151
";
114152

115153
var test = new CSharpSourceGeneratorVerifier<MapperGenerator>.Test
@@ -119,9 +157,13 @@ public override TestNamespace.Source BuildGenerated(TestNamespace.Destination so
119157
Sources = { source },
120158
GeneratedSources =
121159
{
122-
(typeof(MapperGenerator), "TestMapper.g.cs", expected)
160+
(typeof(MapperGenerator), "TestMapper.g.cs", expected),
161+
(typeof(MapperGenerator), "MapperRegistration.g.cs", expectedRegistration)
123162
},
124-
AdditionalReferences = { MetadataReference.CreateFromFile(typeof(ProjectR.Mapper<,>).Assembly.Location) }
163+
AdditionalReferences =
164+
{
165+
MetadataReference.CreateFromFile(typeof(ProjectR.Mapper<,>).Assembly.Location)
166+
}
125167
},
126168
};
127169

ProjectR.Tests/MapperRegistrationExtensionsTests.cs

Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public void FoundMappers_IsReadOnlyCollection()
105105
public class AddMappersExtensionTests
106106
{
107107
[Fact]
108-
public void AddMappers_WithValidAssembly_RegistersAllServices()
108+
public void AddMappers_RegistersResolverService()
109109
{
110110
// Arrange
111111
var services = new ServiceCollection();
@@ -116,25 +116,14 @@ public void AddMappers_WithValidAssembly_RegistersAllServices()
116116
var serviceProvider = services.BuildServiceProvider();
117117

118118
// Assert
119-
// Check that MapperTypeCache is registered
120-
var cache = serviceProvider.GetService<MapperTypeCache>();
121-
cache.Should().NotBeNull();
122-
123119
// Check that IMapperResolver is registered
124120
var resolver = serviceProvider.GetService<IMapperResolver>();
125121
resolver.Should().NotBeNull();
126122
resolver.Should().BeOfType<MapperResolver>();
127-
128-
// Check that individual mappers are registered
129-
var mapper1 = serviceProvider.GetService<TestMapper>();
130-
mapper1.Should().NotBeNull();
131-
132-
var mapper2 = serviceProvider.GetService<TestMapper2>();
133-
mapper2.Should().NotBeNull();
134123
}
135124

136125
[Fact]
137-
public void AddMappers_WithNullAssemblies_UsesCallingAssembly()
126+
public void AddMappers_WithNullAssemblies_StillRegistersResolver()
138127
{
139128
// Arrange
140129
var services = new ServiceCollection();
@@ -148,21 +137,6 @@ public void AddMappers_WithNullAssemblies_UsesCallingAssembly()
148137
resolver.Should().NotBeNull();
149138
}
150139

151-
[Fact]
152-
public void AddMappers_WithEmptyAssemblies_UsesCallingAssembly()
153-
{
154-
// Arrange
155-
var services = new ServiceCollection();
156-
157-
// Act
158-
services.AddMappers();
159-
var serviceProvider = services.BuildServiceProvider();
160-
161-
// Assert
162-
var resolver = serviceProvider.GetService<IMapperResolver>();
163-
resolver.Should().NotBeNull();
164-
}
165-
166140
[Fact]
167141
public void AddMappers_ReturnsServiceCollection_ForChaining()
168142
{
@@ -177,40 +151,6 @@ public void AddMappers_ReturnsServiceCollection_ForChaining()
177151
result.Should().BeSameAs(services);
178152
}
179153

180-
[Fact]
181-
public void AddMappers_RegistersMapperTypeCacheAsSingleton()
182-
{
183-
// Arrange
184-
var services = new ServiceCollection();
185-
var assembly = Assembly.GetExecutingAssembly();
186-
187-
// Act
188-
services.AddMappers(assembly);
189-
var serviceProvider = services.BuildServiceProvider();
190-
191-
// Assert
192-
var cache1 = serviceProvider.GetService<MapperTypeCache>();
193-
var cache2 = serviceProvider.GetService<MapperTypeCache>();
194-
cache1.Should().BeSameAs(cache2);
195-
}
196-
197-
[Fact]
198-
public void AddMappers_RegistersIndividualMappersAsSingletons()
199-
{
200-
// Arrange
201-
var services = new ServiceCollection();
202-
var assembly = Assembly.GetExecutingAssembly();
203-
204-
// Act
205-
services.AddMappers(assembly);
206-
var serviceProvider = services.BuildServiceProvider();
207-
208-
// Assert
209-
var mapper1a = serviceProvider.GetService<TestMapper>();
210-
var mapper1b = serviceProvider.GetService<TestMapper>();
211-
mapper1a.Should().BeSameAs(mapper1b);
212-
}
213-
214154
[Fact]
215155
public void AddMappers_RegistersResolverAsSingleton()
216156
{

0 commit comments

Comments
 (0)