diff --git a/.gitignore b/.gitignore
index fadc911..6bc23ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,4 +55,10 @@ coverage/
# NUnit
*.VisualState.xml
TestResult.xml
-nunit-*.xml
\ No newline at end of file
+nunit-*.xml
+
+# Verify snapshots (received files)
+*.received.*
+
+# User-specific files
+*.user
\ No newline at end of file
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 8dc8ef3..5dfb3df 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -4,6 +4,7 @@
+
@@ -19,5 +20,8 @@
+
+
+
\ No newline at end of file
diff --git a/src/Endpointer.Generator.Tests/Endpointer.Generator.Tests.csproj b/src/Endpointer.Generator.Tests/Endpointer.Generator.Tests.csproj
index 76027d8..17cf5ea 100644
--- a/src/Endpointer.Generator.Tests/Endpointer.Generator.Tests.csproj
+++ b/src/Endpointer.Generator.Tests/Endpointer.Generator.Tests.csproj
@@ -9,6 +9,9 @@
+
+
+
diff --git a/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.EndpointerRegistration_WithMultipleEndpoints_MatchesSnapshot.verified.txt b/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.EndpointerRegistration_WithMultipleEndpoints_MatchesSnapshot.verified.txt
new file mode 100644
index 0000000..e4c12ac
--- /dev/null
+++ b/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.EndpointerRegistration_WithMultipleEndpoints_MatchesSnapshot.verified.txt
@@ -0,0 +1,36 @@
+[
+//
+#nullable enable
+
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Endpointer;
+
+public static class EndpointerExtensions
+{
+ public static IServiceCollection AddEndpointer(this IServiceCollection services)
+ {
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ return services;
+ }
+
+ public static IEndpointRouteBuilder MapEndpointer(this IEndpointRouteBuilder endpoints)
+ {
+ // App.Users.GetUserEndpoint
+ new App.Users.GetUserEndpoint.Endpoint().MapEndpoint(endpoints);
+
+ // App.Users.CreateUserEndpoint
+ new App.Users.CreateUserEndpoint.Endpoint().MapEndpoint(endpoints);
+
+ // App.Orders.GetOrderEndpoint
+ new App.Orders.GetOrderEndpoint.Endpoint().MapEndpoint(endpoints);
+
+ return endpoints;
+ }
+}
+
+]
\ No newline at end of file
diff --git a/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.EndpointerRegistration_WithNoEndpoints_MatchesSnapshot.verified.txt b/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.EndpointerRegistration_WithNoEndpoints_MatchesSnapshot.verified.txt
new file mode 100644
index 0000000..9834d95
--- /dev/null
+++ b/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.EndpointerRegistration_WithNoEndpoints_MatchesSnapshot.verified.txt
@@ -0,0 +1,23 @@
+[
+//
+#nullable enable
+
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Endpointer;
+
+public static class EndpointerExtensions
+{
+ public static IServiceCollection AddEndpointer(this IServiceCollection services)
+ {
+ return services;
+ }
+
+ public static IEndpointRouteBuilder MapEndpointer(this IEndpointRouteBuilder endpoints)
+ {
+ return endpoints;
+ }
+}
+
+]
\ No newline at end of file
diff --git a/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.EndpointerRegistration_WithSingleEndpoint_MatchesSnapshot.verified.txt b/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.EndpointerRegistration_WithSingleEndpoint_MatchesSnapshot.verified.txt
new file mode 100644
index 0000000..368af2a
--- /dev/null
+++ b/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.EndpointerRegistration_WithSingleEndpoint_MatchesSnapshot.verified.txt
@@ -0,0 +1,28 @@
+[
+//
+#nullable enable
+
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Endpointer;
+
+public static class EndpointerExtensions
+{
+ public static IServiceCollection AddEndpointer(this IServiceCollection services)
+ {
+ services.AddScoped();
+
+ return services;
+ }
+
+ public static IEndpointRouteBuilder MapEndpointer(this IEndpointRouteBuilder endpoints)
+ {
+ // TestApp.GetTimeEndpoint
+ new TestApp.GetTimeEndpoint.Endpoint().MapEndpoint(endpoints);
+
+ return endpoints;
+ }
+}
+
+]
\ No newline at end of file
diff --git a/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.IEndpointInterface_MatchesSnapshot.verified.txt b/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.IEndpointInterface_MatchesSnapshot.verified.txt
new file mode 100644
index 0000000..5b265b6
--- /dev/null
+++ b/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.IEndpointInterface_MatchesSnapshot.verified.txt
@@ -0,0 +1,16 @@
+[
+//
+#nullable enable
+
+using Microsoft.AspNetCore.Routing;
+
+namespace Endpointer;
+
+///
+/// Marker interface for endpoint classes to be discovered by the source generator.
+///
+public interface IEndpoint
+{
+ void MapEndpoint(IEndpointRouteBuilder endpoints);
+}
+]
\ No newline at end of file
diff --git a/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.cs b/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.cs
new file mode 100644
index 0000000..775ba75
--- /dev/null
+++ b/src/Endpointer.Generator.Tests/EndpointerGeneratorSnapshotTests.cs
@@ -0,0 +1,149 @@
+using Basic.Reference.Assemblies;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Endpointer.Generator.Tests;
+
+public class EndpointerGeneratorSnapshotTests
+{
+ // Stub for ASP.NET Core types needed in test compilation
+ private const string AspNetCoreStubs = """
+ namespace Microsoft.AspNetCore.Routing
+ {
+ public interface IEndpointRouteBuilder { }
+ }
+ namespace Microsoft.Extensions.DependencyInjection
+ {
+ public interface IServiceCollection { }
+
+ public static class ServiceCollectionExtensions
+ {
+ public static IServiceCollection AddScoped(this IServiceCollection services) => services;
+ }
+ }
+ """;
+
+ [Test]
+ public Task IEndpointInterface_MatchesSnapshot()
+ {
+ var (result, _) = RunGenerator("");
+
+ var generatedSources = result.Results[0].GeneratedSources
+ .Where(s => string.Equals(s.HintName, "IEndpoint.g.cs", StringComparison.Ordinal))
+ .Select(s => s.SourceText.ToString());
+
+ return Verify(generatedSources);
+ }
+
+ [Test]
+ public Task EndpointerRegistration_WithNoEndpoints_MatchesSnapshot()
+ {
+ var (result, _) = RunGenerator("");
+
+ var generatedSources = result.Results[0].GeneratedSources
+ .Where(s => string.Equals(s.HintName, "EndpointerRegistration.g.cs", StringComparison.Ordinal))
+ .Select(s => s.SourceText.ToString());
+
+ return Verify(generatedSources);
+ }
+
+ [Test]
+ public Task EndpointerRegistration_WithSingleEndpoint_MatchesSnapshot()
+ {
+ const string source = """
+ using Endpointer;
+ using Microsoft.AspNetCore.Routing;
+
+ namespace TestApp;
+
+ public class GetTimeEndpoint
+ {
+ public class Endpoint : IEndpoint
+ {
+ public void MapEndpoint(IEndpointRouteBuilder endpoints) { }
+ }
+ }
+ """;
+
+ var (result, _) = RunGenerator(source);
+
+ var generatedSources = result.Results[0].GeneratedSources
+ .Where(s => string.Equals(s.HintName, "EndpointerRegistration.g.cs", StringComparison.Ordinal))
+ .Select(s => s.SourceText.ToString());
+
+ return Verify(generatedSources);
+ }
+
+ [Test]
+ public Task EndpointerRegistration_WithMultipleEndpoints_MatchesSnapshot()
+ {
+ const string source = """
+ using Endpointer;
+ using Microsoft.AspNetCore.Routing;
+
+ namespace App.Users
+ {
+ public class GetUserEndpoint
+ {
+ public class Endpoint : IEndpoint
+ {
+ public void MapEndpoint(IEndpointRouteBuilder endpoints) { }
+ }
+ }
+
+ public class CreateUserEndpoint
+ {
+ public class Endpoint : IEndpoint
+ {
+ public void MapEndpoint(IEndpointRouteBuilder endpoints) { }
+ }
+ }
+ }
+
+ namespace App.Orders
+ {
+ public class GetOrderEndpoint
+ {
+ public class Endpoint : IEndpoint
+ {
+ public void MapEndpoint(IEndpointRouteBuilder endpoints) { }
+ }
+ }
+ }
+ """;
+
+ var (result, _) = RunGenerator(source);
+
+ var generatedSources = result.Results[0].GeneratedSources
+ .Where(s => string.Equals(s.HintName, "EndpointerRegistration.g.cs", StringComparison.Ordinal))
+ .Select(s => s.SourceText.ToString());
+
+ return Verify(generatedSources);
+ }
+
+ private static (GeneratorDriverRunResult Result, Compilation OutputCompilation) RunGenerator(string source)
+ {
+ var syntaxTrees = new List
+ {
+ CSharpSyntaxTree.ParseText(AspNetCoreStubs),
+ };
+
+ if (!string.IsNullOrEmpty(source))
+ {
+ syntaxTrees.Add(CSharpSyntaxTree.ParseText(source));
+ }
+
+ var compilation = CSharpCompilation.Create(
+ assemblyName: "TestAssembly",
+ syntaxTrees: syntaxTrees,
+ references: Net80.References.All,
+ options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ var generator = new EndpointerGenerator();
+
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
+ driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _);
+
+ return (driver.GetRunResult(), outputCompilation);
+ }
+}
diff --git a/src/Endpointer.Generator.Tests/EndpointerGeneratorTests.cs b/src/Endpointer.Generator.Tests/EndpointerGeneratorTests.cs
index b69ec60..d95fdc4 100644
--- a/src/Endpointer.Generator.Tests/EndpointerGeneratorTests.cs
+++ b/src/Endpointer.Generator.Tests/EndpointerGeneratorTests.cs
@@ -1,3 +1,4 @@
+using Basic.Reference.Assemblies;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@@ -5,12 +6,27 @@ namespace Endpointer.Generator.Tests;
public class EndpointerGeneratorTests
{
+ // Stub for ASP.NET Core types needed in test compilation
+ private const string AspNetCoreStubs = """
+ namespace Microsoft.AspNetCore.Routing
+ {
+ public interface IEndpointRouteBuilder { }
+ }
+ namespace Microsoft.Extensions.DependencyInjection
+ {
+ public interface IServiceCollection { }
+
+ public static class ServiceCollectionExtensions
+ {
+ public static IServiceCollection AddScoped(this IServiceCollection services) => services;
+ }
+ }
+ """;
+
[Test]
public async Task Generator_EmitsIEndpointInterface()
{
- const string source = "";
-
- var result = RunGenerator(source);
+ var (result, _) = RunGenerator("");
var iEndpointSource = result.Results[0].GeneratedSources
.First(s => string.Equals(s.HintName, "IEndpoint.g.cs", StringComparison.Ordinal));
@@ -21,9 +37,7 @@ public async Task Generator_EmitsIEndpointInterface()
[Test]
public async Task Generator_EmitsMapEndpointerExtension()
{
- const string source = "";
-
- var result = RunGenerator(source);
+ var (result, _) = RunGenerator("");
var registrationSource = result.Results[0].GeneratedSources
.First(s => string.Equals(s.HintName, "EndpointerRegistration.g.cs", StringComparison.Ordinal));
@@ -34,9 +48,7 @@ public async Task Generator_EmitsMapEndpointerExtension()
[Test]
public async Task Generator_EmitsAddEndpointerExtension()
{
- const string source = "";
-
- var result = RunGenerator(source);
+ var (result, _) = RunGenerator("");
var registrationSource = result.Results[0].GeneratedSources
.First(s => string.Equals(s.HintName, "EndpointerRegistration.g.cs", StringComparison.Ordinal));
@@ -57,21 +69,24 @@ public class GetTimeEndpoint
{
public class Endpoint : IEndpoint
{
- public void MapEndpoint(IEndpointRouteBuilder endpoints)
- {
- endpoints.MapGet("/time", () => "ok");
- }
+ public void MapEndpoint(IEndpointRouteBuilder endpoints) { }
}
}
""";
- var result = RunGenerator(source);
+ var (result, outputCompilation) = RunGenerator(source);
var registrationSource = result.Results[0].GeneratedSources
.First(s => string.Equals(s.HintName, "EndpointerRegistration.g.cs", StringComparison.Ordinal));
string generatedCode = registrationSource.SourceText.ToString();
await Assert.That(generatedCode).Contains("services.AddScoped();");
+
+ // Verify no compilation errors
+ var errors = outputCompilation.GetDiagnostics()
+ .Where(d => d.Severity == DiagnosticSeverity.Error)
+ .ToList();
+ await Assert.That(errors).IsEmpty();
}
[Test]
@@ -87,15 +102,12 @@ public class GetTimeEndpoint
{
public class Endpoint : IEndpoint
{
- public void MapEndpoint(IEndpointRouteBuilder endpoints)
- {
- endpoints.MapGet("/time", () => "ok");
- }
+ public void MapEndpoint(IEndpointRouteBuilder endpoints) { }
}
}
""";
- var result = RunGenerator(source);
+ var (result, _) = RunGenerator(source);
var registrationSource = result.Results[0].GeneratedSources
.First(s => string.Equals(s.HintName, "EndpointerRegistration.g.cs", StringComparison.Ordinal));
@@ -115,46 +127,176 @@ namespace TestApp;
public class NotNestedEndpoint : IEndpoint
{
- public void MapEndpoint(IEndpointRouteBuilder endpoints)
+ public void MapEndpoint(IEndpointRouteBuilder endpoints) { }
+ }
+ """;
+
+ var (result, _) = RunGenerator(source);
+
+ var registrationSource = result.Results[0].GeneratedSources
+ .First(s => string.Equals(s.HintName, "EndpointerRegistration.g.cs", StringComparison.Ordinal));
+
+ string generatedCode = registrationSource.SourceText.ToString();
+ await Assert.That(generatedCode).DoesNotContain("NotNestedEndpoint");
+ }
+
+ [Test]
+ public async Task Generator_RegistersMultipleEndpoints()
+ {
+ const string source = """
+ using Endpointer;
+ using Microsoft.AspNetCore.Routing;
+
+ namespace TestApp;
+
+ public class GetTimeEndpoint
+ {
+ public class Endpoint : IEndpoint
+ {
+ public void MapEndpoint(IEndpointRouteBuilder endpoints) { }
+ }
+ }
+
+ public class CreateUserEndpoint
+ {
+ public class Endpoint : IEndpoint
{
- endpoints.MapGet("/test", () => "ok");
+ public void MapEndpoint(IEndpointRouteBuilder endpoints) { }
}
}
""";
- var result = RunGenerator(source);
+ var (result, outputCompilation) = RunGenerator(source);
var registrationSource = result.Results[0].GeneratedSources
.First(s => string.Equals(s.HintName, "EndpointerRegistration.g.cs", StringComparison.Ordinal));
string generatedCode = registrationSource.SourceText.ToString();
- await Assert.That(generatedCode).DoesNotContain("NotNestedEndpoint");
+ await Assert.That(generatedCode).Contains("services.AddScoped();");
+ await Assert.That(generatedCode).Contains("services.AddScoped();");
+
+ // Verify no compilation errors
+ var errors = outputCompilation.GetDiagnostics()
+ .Where(d => d.Severity == DiagnosticSeverity.Error)
+ .ToList();
+ await Assert.That(errors).IsEmpty();
}
- private static GeneratorDriverRunResult RunGenerator(string source)
+ [Test]
+ public async Task Generator_HandlesEndpointsInDifferentNamespaces()
{
- var syntaxTree = CSharpSyntaxTree.ParseText(source);
+ const string source = """
+ using Endpointer;
+ using Microsoft.AspNetCore.Routing;
- var references = new List
+ namespace App.Users
+ {
+ public class GetUserEndpoint
+ {
+ public class Endpoint : IEndpoint
+ {
+ public void MapEndpoint(IEndpointRouteBuilder endpoints) { }
+ }
+ }
+ }
+
+ namespace App.Orders
+ {
+ public class GetOrderEndpoint
+ {
+ public class Endpoint : IEndpoint
+ {
+ public void MapEndpoint(IEndpointRouteBuilder endpoints) { }
+ }
+ }
+ }
+ """;
+
+ var (result, outputCompilation) = RunGenerator(source);
+
+ var registrationSource = result.Results[0].GeneratedSources
+ .First(s => string.Equals(s.HintName, "EndpointerRegistration.g.cs", StringComparison.Ordinal));
+
+ string generatedCode = registrationSource.SourceText.ToString();
+ await Assert.That(generatedCode).Contains("services.AddScoped();");
+ await Assert.That(generatedCode).Contains("services.AddScoped();");
+
+ // Verify no compilation errors
+ var errors = outputCompilation.GetDiagnostics()
+ .Where(d => d.Severity == DiagnosticSeverity.Error)
+ .ToList();
+ await Assert.That(errors).IsEmpty();
+ }
+
+ [Test]
+ public async Task Generator_SupportsNonDefaultNestedClassName()
+ {
+ const string source = """
+ using Endpointer;
+ using Microsoft.AspNetCore.Routing;
+
+ namespace TestApp;
+
+ public class GetTimeEndpoint
+ {
+ public class Handler : IEndpoint
+ {
+ public void MapEndpoint(IEndpointRouteBuilder endpoints) { }
+ }
+ }
+ """;
+
+ var (result, outputCompilation) = RunGenerator(source);
+
+ var registrationSource = result.Results[0].GeneratedSources
+ .First(s => string.Equals(s.HintName, "EndpointerRegistration.g.cs", StringComparison.Ordinal));
+
+ string generatedCode = registrationSource.SourceText.ToString();
+ await Assert.That(generatedCode).Contains("services.AddScoped();");
+ await Assert.That(generatedCode).Contains("new TestApp.GetTimeEndpoint.Handler()");
+
+ // Verify no compilation errors
+ var errors = outputCompilation.GetDiagnostics()
+ .Where(d => d.Severity == DiagnosticSeverity.Error)
+ .ToList();
+ await Assert.That(errors).IsEmpty();
+ }
+
+ [Test]
+ public async Task Generator_GeneratesCompilableCode_WithNoEndpoints()
+ {
+ var (_, outputCompilation) = RunGenerator("");
+
+ var errors = outputCompilation.GetDiagnostics()
+ .Where(d => d.Severity == DiagnosticSeverity.Error)
+ .ToList();
+
+ await Assert.That(errors).IsEmpty();
+ }
+
+ private static (GeneratorDriverRunResult Result, Compilation OutputCompilation) RunGenerator(string source)
+ {
+ var syntaxTrees = new List
{
- MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
+ CSharpSyntaxTree.ParseText(AspNetCoreStubs),
};
- // Add runtime assemblies
- string runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
- references.Add(MetadataReference.CreateFromFile(Path.Combine(runtimeDir, "System.Runtime.dll")));
+ if (!string.IsNullOrEmpty(source))
+ {
+ syntaxTrees.Add(CSharpSyntaxTree.ParseText(source));
+ }
var compilation = CSharpCompilation.Create(
assemblyName: "TestAssembly",
- syntaxTrees: [syntaxTree],
- references: references,
+ syntaxTrees: syntaxTrees,
+ references: Net80.References.All,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var generator = new EndpointerGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
- driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _);
+ driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _);
- return driver.GetRunResult();
+ return (driver.GetRunResult(), outputCompilation);
}
}
diff --git a/src/Endpointer.Generator.Tests/ModuleInitializer.cs b/src/Endpointer.Generator.Tests/ModuleInitializer.cs
new file mode 100644
index 0000000..55567ee
--- /dev/null
+++ b/src/Endpointer.Generator.Tests/ModuleInitializer.cs
@@ -0,0 +1,12 @@
+using System.Runtime.CompilerServices;
+
+namespace Endpointer.Generator.Tests;
+
+internal static class ModuleInitializer
+{
+ [ModuleInitializer]
+ public static void Init()
+ {
+ VerifySourceGenerators.Initialize();
+ }
+}
diff --git a/src/Endpointer/SourceGenerationHelper.cs b/src/Endpointer/SourceGenerationHelper.cs
index c24192b..1b478ea 100644
--- a/src/Endpointer/SourceGenerationHelper.cs
+++ b/src/Endpointer/SourceGenerationHelper.cs
@@ -58,7 +58,11 @@ private static void GenerateAddEndpointerMethod(StringBuilder sb, ImmutableArray
sb.AppendLine($" services.AddScoped<{endpoint.FullyQualifiedOuterName}>();");
}
- sb.AppendLine();
+ if (endpoints.Length > 0)
+ {
+ sb.AppendLine();
+ }
+
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();