Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3b122ee
Move nested generator types to Common (#61)
jscarle Nov 16, 2025
c6cafe8
Cleanup.
jscarle Nov 16, 2025
c61c976
Further refactor.
jscarle Nov 16, 2025
72548fb
More refactoring.
jscarle Nov 17, 2025
7ab0473
Refactor.
jscarle Nov 17, 2025
de7d86a
Refactor.
jscarle Nov 17, 2025
4eff3a8
Refactored.
jscarle Nov 17, 2025
605f93e
Refactor.
jscarle Nov 17, 2025
7fb06b6
Refactored.
jscarle Nov 17, 2025
ce11fd6
Refactor.
jscarle Nov 17, 2025
4803be2
Add MapGroup individual test (#62)
jscarle Nov 17, 2025
ab6c288
Refactored.
jscarle Nov 17, 2025
e5b3e37
Refactor.
jscarle Nov 17, 2025
f656ab8
Optimize MinimalApiGenerator hot paths (#63)
jscarle Nov 17, 2025
a79c489
Optimize handler collision tracking (#64)
jscarle Nov 17, 2025
d4f96e2
Update MinimalApiGenerator.cs
jscarle Nov 17, 2025
57a200d
Refactor.
jscarle Nov 17, 2025
866c7e5
Refactor.
jscarle Nov 17, 2025
4eb5967
Add endpoint name collision coverage (#65)
jscarle Nov 17, 2025
8f7ca52
Removed compilation cache.
jscarle Nov 17, 2025
48ec71b
Optimize endpoint name collision handling (#66)
jscarle Nov 17, 2025
911bdec
Refactor.
jscarle Nov 17, 2025
4d48252
Fix.
jscarle Nov 17, 2025
5f61074
Simplify request handler normalization (#67)
jscarle Nov 17, 2025
d66c941
Use EquatableImmutableArray for request handlers (#68)
jscarle Nov 17, 2025
7821816
Refactored.
jscarle Nov 17, 2025
2fe6fb6
Refactored.
jscarle Nov 17, 2025
9541c72
Streamline endpoint name collision resolution (#69)
jscarle Nov 17, 2025
b623d5d
Refactor.
jscarle Nov 17, 2025
0adff99
Optimize endpoint name collision handling (#70)
jscarle Nov 17, 2025
5ebcee9
Cleanup.
jscarle Nov 17, 2025
c1c89c1
Reformat.
jscarle Nov 17, 2025
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
76 changes: 38 additions & 38 deletions README.md

Large diffs are not rendered by default.

105 changes: 105 additions & 0 deletions src/GeneratedEndpoints/AddEndpointHandlersGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Text;
using GeneratedEndpoints.Common;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using static GeneratedEndpoints.Common.Constants;

namespace GeneratedEndpoints;

// ReSharper disable ForCanBeConvertedToForeach
// ReSharper disable LoopCanBeConvertedToQuery
// Do not refactor, use for loop to avoid allocations.

internal static class AddEndpointHandlersGenerator
{
public static void GenerateSource(SourceProductionContext context, EquatableImmutableArray<RequestHandler> requestHandlers)
{
context.CancellationToken.ThrowIfCancellationRequested();

var nonStaticClassNames = GetDistinctNonStaticClassNames(requestHandlers);
var source = GetAddEndpointHandlersStringBuilder(nonStaticClassNames);
source.AppendLine(FileHeader);

source.AppendLine();

source.AppendLine("using Microsoft.Extensions.DependencyInjection;");
source.AppendLine("using Microsoft.Extensions.DependencyInjection.Extensions;");
source.AppendLine();

source.Append("namespace ");
source.Append(RoutingNamespace);
source.AppendLine(";");

source.AppendLine();

source.Append("internal static class ");
source.Append(AddEndpointHandlersClassName);
source.AppendLine();

source.AppendLine("{");

source.Append(" internal static void ");
source.Append(AddEndpointHandlersMethodName);
source.AppendLine("(this IServiceCollection services)");

source.AppendLine(" {");

foreach (var className in nonStaticClassNames)
{
source.Append(" services.TryAddScoped<");
source.Append(className);
source.Append(">();");
source.AppendLine();
}

source.AppendLine("""
}
}
"""
);

var sourceText = StringBuilderPool.ToStringAndReturn(source);
context.AddSource(AddEndpointHandlersMethodHint, SourceText.From(sourceText, Encoding.UTF8));
}

private static List<string> GetDistinctNonStaticClassNames(EquatableImmutableArray<RequestHandler> requestHandlers)
{
var classNames = new List<string>();
if (requestHandlers.Count == 0)
return classNames;

var seen = new HashSet<string>(StringComparer.Ordinal);
for (var index = 0; index < requestHandlers.Count; index++)
{
var requestHandler = requestHandlers[index];
if (requestHandler.Class.IsStatic)
continue;

var className = requestHandler.Class.Name;
if (seen.Add(className))
classNames.Add(className);
}

return classNames;
}

private static StringBuilder GetAddEndpointHandlersStringBuilder(List<string> nonStaticClassNames)
{
var estimate = 512L;
for (var index = 0; index < nonStaticClassNames.Count; index++)
{
var className = nonStaticClassNames[index];
estimate += 36 + className.Length;
}

estimate += Math.Max(256, nonStaticClassNames.Count * 12);
estimate = (long)(estimate * 1.10);

if (estimate < 512)
estimate = 512;
else if (estimate > int.MaxValue)
estimate = int.MaxValue;

return StringBuilderPool.Get((int)estimate);
}
}
8 changes: 8 additions & 0 deletions src/GeneratedEndpoints/Common/AcceptsMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace GeneratedEndpoints.Common;

internal readonly record struct AcceptsMetadata(
string RequestType,
string ContentType,
EquatableImmutableArray<string>? AdditionalContentTypes,
bool IsOptional
);
91 changes: 91 additions & 0 deletions src/GeneratedEndpoints/Common/AttributeDataExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Microsoft.CodeAnalysis;

namespace GeneratedEndpoints.Common;

internal static class AttributeDataExtensions
{
public static string? GetConstructorStringValue(this AttributeData attribute, int position = 0)
{
if (attribute.ConstructorArguments.Length > position)
return (attribute.ConstructorArguments[position].Value as string).NormalizeOptionalString();

return null;
}

public static EquatableImmutableArray<string>? GetConstructorStringArray(this AttributeData attribute, int position = 0)
{
if (attribute.ConstructorArguments.Length <= position)
return null;

var arg = attribute.ConstructorArguments[position];
if (arg.Kind == TypedConstantKind.Array)
{
if (arg.Values.Length == 0)
return null;

List<string>? normalized = null;
foreach (var value in arg.Values)
{
if (value.Value is not string stringValue)
continue;

var trimmed = stringValue.NormalizeOptionalString();
if (trimmed is not { Length: > 0 })
continue;

normalized ??= new List<string>(arg.Values.Length);
normalized.Add(trimmed);
}

if (normalized is { Count: > 0 })
return normalized.ToEquatableImmutableArray();
}
else if (arg.Value is string singleHost && !string.IsNullOrWhiteSpace(singleHost))
{
return new[] { singleHost.Trim() }.ToEquatableImmutableArray();
}

return null;
}

public static int? GetConstructorIntValue(this AttributeData attribute, int position = 0)
{
if (attribute.ConstructorArguments.Length > position && attribute.ConstructorArguments[position].Value is int value)
return value;

return null;
}

public static ITypeSymbol? GetNamedTypeSymbol(this AttributeData attribute, string namedParameter)
{
foreach (var namedArg in attribute.NamedArguments)
{
if (namedArg.Key == namedParameter && namedArg.Value.Value is ITypeSymbol typeSymbol)
return typeSymbol;
}

return null;
}

public static bool GetNamedBoolValue(this AttributeData attribute, string namedParameter, bool defaultValue = false)
{
foreach (var namedArg in attribute.NamedArguments)
{
if (namedArg.Key == namedParameter && namedArg.Value.Value is bool boolValue)
return boolValue;
}

return defaultValue;
}

public static string? GetNamedStringValue(this AttributeData attribute, string namedParameter)
{
foreach (var namedArg in attribute.NamedArguments)
{
if (namedArg.Key == namedParameter && namedArg.Value.Value is string stringValue)
return stringValue.NormalizeOptionalString();
}

return null;
}
}
25 changes: 25 additions & 0 deletions src/GeneratedEndpoints/Common/AttributeSymbolMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.CodeAnalysis;

namespace GeneratedEndpoints.Common;

internal static class AttributeSymbolMatcher
{
public static bool IsAttribute(INamedTypeSymbol attributeClass, string attributeName, string[] namespaceParts)
{
var definition = attributeClass.OriginalDefinition;
return definition.Name == attributeName && IsInNamespace(definition.ContainingNamespace, namespaceParts);
}

public static bool IsInNamespace(INamespaceSymbol? namespaceSymbol, string[] namespaceParts)
{
for (var i = namespaceParts.Length - 1; i >= 0; i--)
{
if (namespaceSymbol is null || namespaceSymbol.Name != namespaceParts[i])
return false;

namespaceSymbol = namespaceSymbol.ContainingNamespace;
}

return namespaceSymbol is null || namespaceSymbol.IsGlobalNamespace;
}
}
14 changes: 14 additions & 0 deletions src/GeneratedEndpoints/Common/BindingSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace GeneratedEndpoints.Common;

internal enum BindingSource
{
None = 0,
FromRoute = 1,
FromQuery = 2,
FromHeader = 3,
FromBody = 4,
FromForm = 5,
FromServices = 6,
FromKeyedServices = 7,
AsParameters = 8,
}
3 changes: 3 additions & 0 deletions src/GeneratedEndpoints/Common/ConfigureMethodDetails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace GeneratedEndpoints.Common;

internal readonly record struct ConfigureMethodDetails(bool HasConfigureMethod, bool ConfigureMethodAcceptsServiceProvider);
Loading
Loading