-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEndpointMetadataEmitter.cs
More file actions
222 lines (198 loc) · 9.56 KB
/
EndpointMetadataEmitter.cs
File metadata and controls
222 lines (198 loc) · 9.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
namespace ErrorOr.Generators.Emitters;
/// <summary>
/// Shared metadata emission logic for both grouped and ungrouped endpoints.
/// Ensures consistent AOT-compatible metadata emission across all endpoint types.
/// </summary>
internal static class EndpointMetadataEmitter
{
/// <summary>
/// Emits all endpoint metadata: tags, accepts, produces, and middleware.
/// Call this after emitting WithName().
/// </summary>
/// <param name="code">StringBuilder to append to.</param>
/// <param name="ep">The endpoint descriptor.</param>
/// <param name="indent">Indentation string (e.g., " " for ungrouped, " " for grouped).</param>
/// <param name="maxArity">Maximum arity for union type calculation.</param>
public static void EmitEndpointMetadata(
StringBuilder code,
in EndpointDescriptor ep,
string indent,
int maxArity)
{
// Get tag name from endpoint identity
var (tagName, _) = EndpointNameHelper.GetEndpointIdentity(ep.HandlerContainingTypeFqn, ep.HandlerMethodName);
// 1. Tags for OpenAPI grouping
code.AppendLine($"{indent}.WithTags(\"{tagName}\")");
// 2. Deprecation metadata from [Obsolete] attribute
EmitDeprecationMetadata(code, in ep, indent);
// 3. Accepts metadata (Content-Type for request body)
EmitAcceptsMetadata(code, in ep, indent);
// 4. Produces metadata (OpenAPI response types)
EmitProducesMetadata(code, in ep, indent, maxArity);
// 5. Middleware fluent calls
var middleware = ep.Middleware;
EmitMiddlewareCalls(code, in middleware, indent);
}
/// <summary>
/// Emits AcceptsMetadata for body or form parameters.
/// AOT-safe: Uses WithMetadata instead of .Accepts() which requires RouteHandlerBuilder.
/// </summary>
private static void EmitAcceptsMetadata(StringBuilder code, in EndpointDescriptor ep, string indent)
{
var bodyParam = ep.HandlerParameters.AsImmutableArray()
.FirstOrDefault(static p => p.Source == ParameterSource.Body);
if (bodyParam.Name is not null)
code.AppendLine(
$"{indent}.WithMetadata(new global::Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata(new[] {{ \"{WellKnownTypes.Constants.ContentTypeJson}\" }}, typeof({bodyParam.TypeFqn})))");
else if (ep.HasFormParams)
code.AppendLine(
$"{indent}.WithMetadata(new global::Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata(new[] {{ \"{WellKnownTypes.Constants.ContentTypeFormData}\" }}, typeof(object)))");
}
/// <summary>
/// Emits ProducesResponseTypeMetadata for OpenAPI documentation.
/// AOT-safe: Uses WithMetadata instead of .Produces().
/// </summary>
private static void EmitProducesMetadata(StringBuilder code, in EndpointDescriptor ep, string indent, int maxArity)
{
// SSE endpoints have special content type
if (ep.Sse.IsSse)
{
code.AppendLine(
$"{indent}.WithMetadata(new {WellKnownTypes.Fqn.ProducesResponseTypeMetadata}(200, null, new[] {{ \"text/event-stream\" }}))");
return;
}
var successInfo = ResultsUnionTypeBuilder.GetSuccessResponseInfo(
ep.SuccessTypeFqn,
ep.SuccessKind,
ep.IsAcceptedResponse);
var hasBodyBinding = ep.HasBodyOrFormBinding;
var unionResult = ResultsUnionTypeBuilder.ComputeReturnType(
ep.SuccessTypeFqn,
ep.SuccessKind,
ep.ErrorInference.InferredErrorTypeNames,
ep.ErrorInference.InferredCustomErrors,
ep.ErrorInference.DeclaredProducesErrors,
hasBodyBinding,
maxArity,
ep.IsAcceptedResponse,
ep.Middleware,
ep.HasParameterValidation);
// Always emit explicit Produces metadata for OpenAPI visibility.
// The wrapper uses RequestDelegate signature (returns Task), so ASP.NET Core's
// RequestDelegateFactory never inspects the union return type for metadata.
// Success response
EmitProducesMetadataLine(code, indent, successInfo.StatusCode,
successInfo.HasBody ? ep.SuccessTypeFqn : null,
WellKnownTypes.Constants.ContentTypeJson);
// Error responses
foreach (var statusCode in unionResult.ExplicitProduceCodes.AsImmutableArray().Distinct()
.OrderBy(static x => x))
{
EmitProducesMetadataLine(code, indent, statusCode,
statusCode == 400
? WellKnownTypes.Fqn.HttpValidationProblemDetails
: WellKnownTypes.Fqn.ProblemDetails,
WellKnownTypes.Constants.ContentTypeProblemJson);
}
}
/// <summary>
/// Emits a single ProducesResponseTypeMetadata line.
/// </summary>
private static void EmitProducesMetadataLine(StringBuilder code, string indent, int statusCode, string? typeFqn,
string contentType)
{
code.AppendLine(typeFqn is not null
? $"{indent}.WithMetadata(new {WellKnownTypes.Fqn.ProducesResponseTypeMetadata}({statusCode}, typeof({typeFqn}), new[] {{ \"{contentType}\" }}))"
: $"{indent}.WithMetadata(new {WellKnownTypes.Fqn.ProducesResponseTypeMetadata}({statusCode}))");
}
/// <summary>
/// Emits middleware fluent calls based on BCL attributes detected on the endpoint.
/// Dispatches to per-concern methods for maintainability.
/// </summary>
private static void EmitMiddlewareCalls(StringBuilder code, in MiddlewareInfo middleware, string indent)
{
if (!middleware.HasAny) return;
EmitAuthMiddleware(code, in middleware, indent);
EmitRateLimitingMiddleware(code, in middleware, indent);
EmitOutputCacheMiddleware(code, in middleware, indent);
EmitCorsMiddleware(code, in middleware, indent);
}
/// <summary>
/// Emits authorization middleware: [Authorize] / [Authorize("Policy")] / [AllowAnonymous].
/// </summary>
private static void EmitAuthMiddleware(StringBuilder code, in MiddlewareInfo middleware, string indent)
{
if (middleware.AllowAnonymous)
{
code.AppendLine($"{indent}.AllowAnonymous()");
}
else if (middleware.RequiresAuthorization)
{
var policies = middleware.AuthorizationPolicies.AsImmutableArray();
if (policies.IsDefaultOrEmpty)
code.AppendLine($"{indent}.RequireAuthorization()");
else if (policies.Length == 1)
code.AppendLine($"{indent}.RequireAuthorization(\"{policies[0]}\")");
else
code.AppendLine(
$"{indent}.RequireAuthorization({string.Join(", ", policies.Select(static p => $"\"{p}\""))})");
}
}
/// <summary>
/// Emits rate limiting middleware: [EnableRateLimiting("policy")] / [DisableRateLimiting].
/// </summary>
private static void EmitRateLimitingMiddleware(StringBuilder code, in MiddlewareInfo middleware, string indent)
{
if (middleware.DisableRateLimiting)
code.AppendLine($"{indent}.DisableRateLimiting()");
else if (middleware.EnableRateLimiting)
code.AppendLine(middleware.RateLimitingPolicy is not null
? $"{indent}.RequireRateLimiting(\"{middleware.RateLimitingPolicy}\")"
: $"{indent}.RequireRateLimiting()");
}
/// <summary>
/// Emits output cache middleware: [OutputCache] / [OutputCache(Duration = 60)] / [OutputCache(PolicyName = "x")].
/// </summary>
private static void EmitOutputCacheMiddleware(StringBuilder code, in MiddlewareInfo middleware, string indent)
{
if (!middleware.EnableOutputCache) return;
if (middleware.OutputCachePolicy is not null)
code.AppendLine($"{indent}.CacheOutput(\"{middleware.OutputCachePolicy}\")");
else if (middleware.OutputCacheDuration is { } duration)
code.AppendLine(
$"{indent}.CacheOutput(p => p.Expire(global::System.TimeSpan.FromSeconds({duration})))");
else
code.AppendLine($"{indent}.CacheOutput()");
}
/// <summary>
/// Emits CORS middleware: [EnableCors("policy")] / [EnableCors] / [DisableCors].
/// </summary>
private static void EmitCorsMiddleware(StringBuilder code, in MiddlewareInfo middleware, string indent)
{
if (middleware.DisableCors)
code.AppendLine($"{indent}.DisableCors()");
else if (middleware.EnableCors)
code.AppendLine(middleware.CorsPolicy is not null
? $"{indent}.RequireCors(\"{middleware.CorsPolicy}\")"
: $"{indent}.RequireCors()");
}
/// <summary>
/// Emits deprecation metadata from [Obsolete] attribute.
/// Adds ObsoleteAttribute metadata to the endpoint for OpenAPI documentation.
/// </summary>
private static void EmitDeprecationMetadata(StringBuilder code, in EndpointDescriptor ep, string indent)
{
if (!ep.HasMetadata(MetadataKeys.Deprecated)) return;
var message = ep.GetMetadata(MetadataKeys.DeprecatedMessage);
if (message is not null)
{
// Escape any quotes in the message
var escapedMessage = message.Replace("\"", "\\\"");
code.AppendLine($"{indent}.WithMetadata(new global::System.ObsoleteAttribute(\"{escapedMessage}\"))");
}
else
{
code.AppendLine($"{indent}.WithMetadata(new global::System.ObsoleteAttribute())");
}
}
}