• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ImmediatePlatform / Immediate.Apis / 29608434317

17 Jul 2026 07:41PM UTC coverage: 91.759% (-0.6%) from 92.407%
29608434317

Pull #314

github

web-flow
Merge 25493f2e4 into 7e12df6cd
Pull Request #314: Support tagged registrations of endpoints

1325 of 1444 relevant lines covered (91.76%)

3.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.36
/src/Immediate.Apis.Generators/ImmediateApisGenerator.Transform.cs
1
using System.Diagnostics.CodeAnalysis;
2
using Microsoft.CodeAnalysis;
3
using Microsoft.CodeAnalysis.CSharp;
4

5
namespace Immediate.Apis.Generators;
6

7
public sealed partial class ImmediateApisGenerator
8
{
9
        private static Method? TransformEndpoint(
10
                GeneratorAttributeSyntaxContext context,
11
                CancellationToken token
12
        )
13
        {
14
                token.ThrowIfCancellationRequested();
4✔
15

16
                var symbol = (INamedTypeSymbol)context.TargetSymbol;
4✔
17
                var attributes = symbol.GetAttributes();
4✔
18

19
                token.ThrowIfCancellationRequested();
4✔
20

21
                if (attributes.GetMethodAttribute() is not { } attribute)
4✔
22
                        return null;
×
23

24
                if (attribute.GetRoutes() is not { Count: > 0 } routes)
4✔
25
                        return null;
×
26

27
                token.ThrowIfCancellationRequested();
4✔
28

29
                if (symbol.GetValidHandleMethod() is not { } handleMethod)
4✔
30
                        return null;
4✔
31

32
                token.ThrowIfCancellationRequested();
4✔
33

34
                if (!TryGetMapGoup(symbol, out var routeGroupFullClassName))
4✔
35
                        return null;
×
36

37
                token.ThrowIfCancellationRequested();
4✔
38

39
                var allowAnonymous = attributes.Any(a => a.AttributeClass.IsAllowAnonymousAttribute);
4✔
40

41
                var authorizeAttribute = attributes.FirstOrDefault(a => a.AttributeClass.IsAuthorizeAttribute);
4✔
42
                var authorize = authorizeAttribute != null;
4✔
43
                var authorizePolicy = string.Empty;
4✔
44

45
                switch (authorizeAttribute)
46
                {
47
                        case { ConstructorArguments.Length: > 0 }:
48
                                authorizePolicy = (string)authorizeAttribute.ConstructorArguments[0].Value!;
4✔
49
                                break;
4✔
50

51
                        case { NamedArguments.Length: > 0 }:
52
                        {
53
                                foreach (var argument in authorizeAttribute.NamedArguments)
4✔
54
                                {
55
                                        if (argument is not { Key: "Policy", Value.Value: string ap })
4✔
56
                                                return null;
4✔
57

58
                                        authorizePolicy = ap;
4✔
59
                                }
60

61
                                break;
62
                        }
63

64
                        default:
65
                                break;
66
                }
67

68
                token.ThrowIfCancellationRequested();
4✔
69

70
                var @namespace = symbol.ContainingNamespace.ToDisplayString().NullIf("<global namespace>");
4✔
71
                var @class = GetClass(symbol);
4✔
72

73
                token.ThrowIfCancellationRequested();
4✔
74

75
                var className = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
4✔
76
                var classAsMethodName = symbol.ToDisplayString().Replace('.', '_');
4✔
77

78
                var parameterType = handleMethod.Parameters[0].Type;
4✔
79

80
                token.ThrowIfCancellationRequested();
4✔
81

82
                var mapMethod = attribute.GetMapMethodName();
4✔
83
                var httpMethod = attribute.GetMapMethodMethod();
4✔
84
                var parameterAttribute = GetParameterAttribute(handleMethod.Parameters[0], mapMethod);
4✔
85
                var handleMethodAttributes = GetHandleMethodAttributes(handleMethod);
4✔
86
                var useCustomization = HasCustomizeEndpointMethod(symbol);
4✔
87
                var useTransformMethod = HasTransformResultMethod(symbol, handleMethod.ReturnType);
4✔
88
                var tags = attribute.NamedArguments.GetStringArray("Tags");
4✔
89

90
                token.ThrowIfCancellationRequested();
4✔
91

92
                return new()
4✔
93
                {
4✔
94
                        MapMethod = mapMethod,
4✔
95
                        HttpMethod = httpMethod,
4✔
96
                        Attributes = handleMethodAttributes,
4✔
97
                        ParameterAttribute = parameterAttribute,
4✔
98
                        Routes = new(routes),
4✔
99

4✔
100
                        Namespace = @namespace,
4✔
101
                        Class = @class,
4✔
102
                        ClassFullName = className,
4✔
103

4✔
104
                        ParameterType = parameterType.ToDisplayString(DisplayNameFormatters.FullyQualifiedWithNullableFormat),
4✔
105
                        ParameterTypeDoc = parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
4✔
106

4✔
107
                        AllowAnonymous = allowAnonymous,
4✔
108
                        Authorize = authorize,
4✔
109
                        AuthorizePolicy = authorizePolicy,
4✔
110

4✔
111
                        UseCustomization = useCustomization,
4✔
112
                        UseTransformMethod = useTransformMethod,
4✔
113
                        HasReturn = handleMethod.ReturnType.IsValueTask1,
4✔
114

4✔
115
                        Tags = tags,
4✔
116

4✔
117
                        RouteGroupClassFullName = routeGroupFullClassName,
4✔
118
                };
4✔
119
        }
120

121
        private static RouteGroupDefinition? TransformRouteGroup(
122
                GeneratorAttributeSyntaxContext context,
123
                CancellationToken token
124
        )
125
        {
126
                token.ThrowIfCancellationRequested();
4✔
127

128
                if (context.Attributes[0].ConstructorArguments is not [{ Value: string route }])
4✔
129
                        return null;
×
130

131
                var symbol = (INamedTypeSymbol)context.TargetSymbol;
4✔
132

133
                if (symbol.ContainingType is { } && symbol.ContainingType.GetAttributes().GetRouteGroupAttribute() is null)
4✔
134
                        return null;
×
135

136
                var @namespace = symbol.ContainingNamespace.ToDisplayString().NullIf("<global namespace>");
4✔
137
                var outerClasses = GetOuterClasses(symbol);
4✔
138
                var @class = GetClass(symbol);
4✔
139
                var customization = HasCustomizeGroupMethod(symbol);
4✔
140
                var tags = context.Attributes[0].NamedArguments.GetStringArray("Tags");
4✔
141

142
                return new()
4✔
143
                {
4✔
144
                        Namespace = @namespace,
4✔
145
                        OuterClasses = outerClasses,
4✔
146
                        Class = @class,
4✔
147
                        ClassFullName = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
4✔
148
                        RouteGroupClassFullName = symbol.ContainingType?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
4✔
149
                        Route = route,
4✔
150
                        UseCustomization = customization,
4✔
151
                        Tags = tags,
4✔
152
                };
4✔
153
        }
154

155
        private static bool TryGetMapGoup(INamedTypeSymbol symbol, out string? routeGroupFullClassName)
156
        {
157
                if (symbol.GetAttributes().GetMapGroupAttribute() is not { AttributeClass.TypeArguments: [{ } groupTypeSymbol] })
4✔
158
                {
159
                        routeGroupFullClassName = null;
4✔
160
                        return true;
4✔
161
                }
162

163
                if (groupTypeSymbol.GetAttributes().GetRouteGroupAttribute() is null)
4✔
164
                {
165
                        routeGroupFullClassName = null;
×
166
                        return false;
×
167
                }
168

169
                routeGroupFullClassName = groupTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
4✔
170
                return true;
4✔
171
        }
172

173
        private static bool HasCustomizeEndpointMethod(INamedTypeSymbol symbol)
174
                => symbol
4✔
175
                        .GetMembers()
4✔
176
                        .OfType<IMethodSymbol>()
4✔
177
                        .Any(m =>
4✔
178
                                m is
4✔
179
                                {
4✔
180
                                        Name: "CustomizeEndpoint",
4✔
181
                                        IsStatic: true,
4✔
182
                                        DeclaredAccessibility: Accessibility.Internal or Accessibility.Private,
4✔
183
                                        ReturnsVoid: true,
4✔
184
                                        Parameters: [{ Type.IsIEndpointConventionBuilderOrRouteHandlerBuilder: true }],
4✔
185
                                }
4✔
186
                        );
4✔
187

188
        private static bool HasCustomizeGroupMethod(INamedTypeSymbol symbol)
189
                => symbol
4✔
190
                        .GetMembers()
4✔
191
                        .OfType<IMethodSymbol>()
4✔
192
                        .Any(m =>
4✔
193
                                m is
4✔
194
                                {
4✔
195
                                        Name: "CustomizeGroup",
4✔
196
                                        IsStatic: true,
4✔
197
                                        DeclaredAccessibility: Accessibility.Private,
4✔
198
                                        ReturnsVoid: true,
4✔
199
                                        Parameters: [{ Type.IsRouteGroupBuilder: true }],
4✔
200
                                }
4✔
201
                        );
4✔
202

203
        private static bool HasTransformResultMethod(INamedTypeSymbol symbol, ITypeSymbol returnType)
204
        {
205
                return (
4✔
206
                        returnType is INamedTypeSymbol { IsValueTask1: true, TypeArguments: [{ } returnInnerType] }
4✔
207
                        && symbol
4✔
208
                                .GetMembers()
4✔
209
                                .OfType<IMethodSymbol>()
4✔
210
                                .Any(m =>
4✔
211
                                        m is
4✔
212
                                        {
4✔
213
                                                Name: "TransformResult",
4✔
214
                                                IsStatic: true,
4✔
215
                                                DeclaredAccessibility: Accessibility.Internal,
4✔
216
                                                ReturnsVoid: false,
4✔
217
                                                Parameters: [{ Type: { } paramType }],
4✔
218
                                        }
4✔
219
                                        && SymbolEqualityComparer.IncludeNullability.Equals(returnInnerType, paramType)
4✔
220
                                )
4✔
221
                )
4✔
222
                || (
4✔
223
                        returnType is INamedTypeSymbol { IsValueTask: true }
4✔
224
                        && symbol
4✔
225
                                .GetMembers()
4✔
226
                                .OfType<IMethodSymbol>()
4✔
227
                                .Any(m =>
4✔
228
                                        m is
4✔
229
                                        {
4✔
230
                                                Name: "TransformResult",
4✔
231
                                                IsStatic: true,
4✔
232
                                                DeclaredAccessibility: Accessibility.Internal,
4✔
233
                                                ReturnsVoid: false,
4✔
234
                                                Parameters: [],
4✔
235
                                        }
4✔
236
                                )
4✔
237
                                );
4✔
238
        }
239

240
        private static EquatableReadOnlyList<Class> GetOuterClasses(INamedTypeSymbol symbol)
241
        {
242
                List<Class>? outerClasses = null;
4✔
243
                var outerSymbol = symbol.ContainingType;
4✔
244
                while (outerSymbol is not null)
4✔
245
                {
246
                        (outerClasses ??= []).Add(GetClass(outerSymbol));
4✔
247
                        outerSymbol = outerSymbol.ContainingType;
4✔
248
                }
249

250
                if (outerClasses is null)
4✔
251
                        return default;
4✔
252

253
                outerClasses.Reverse();
4✔
254

255
                return outerClasses.ToEquatableReadOnlyList();
4✔
256
        }
257

258
        private static Class GetClass(INamedTypeSymbol symbol) =>
259
                new()
4✔
260
                {
4✔
261
                        Name = symbol.Name,
4✔
262
                        Type = symbol switch
4✔
263
                        {
4✔
264
                                { TypeKind: TypeKind.Interface } => "interface",
×
265
                                { IsRecord: true, TypeKind: TypeKind.Struct, } => "record struct",
×
266
                                { IsRecord: true, } => "record",
×
267
                                { TypeKind: TypeKind.Struct, } => "struct",
×
268
                                _ => "class",
4✔
269
                        },
4✔
270
                };
4✔
271

272
        private static string GetParameterAttribute(IParameterSymbol parameterSymbol, string httpMethod)
273
        {
274
                foreach (var a in parameterSymbol.GetAttributes())
4✔
275
                {
276
                        if (a.AttributeClass.IsBindingParameterAttribute)
4✔
277
                                return a.AttributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
4✔
278
                }
279

280
                if (parameterSymbol.Type is INamedTypeSymbol typeSymbol)
4✔
281
                {
282
                        foreach (var p in typeSymbol.GetMembers().OfType<IPropertySymbol>())
4✔
283
                        {
284
                                if (p.Type.IsIFormFile)
4✔
285
                                        return "global::Microsoft.AspNetCore.Mvc.FromFormAttribute";
4✔
286
                        }
287

288
                        foreach (var p in typeSymbol.GetMembers().OfType<IPropertySymbol>())
4✔
289
                        {
290
                                if (p.GetAttributes().Any(a => a.AttributeClass.IsFromXxxAttribute))
4✔
291
                                        return "global::Microsoft.AspNetCore.Http.AsParametersAttribute";
4✔
292
                        }
293
                }
294

295
                return httpMethod is "MapPatch" or "MapPost" or "MapPut"
4✔
296
                        ? "global::Microsoft.AspNetCore.Mvc.FromBodyAttribute"
4✔
297
                        : "global::Microsoft.AspNetCore.Http.AsParametersAttribute";
4✔
298
        }
4✔
299

300
        private static EquatableReadOnlyList<string> GetHandleMethodAttributes(IMethodSymbol methodSymbol) =>
301
                methodSymbol.GetAttributes()
4✔
302
                        .Select(GetAttributeString)
4✔
303
                        .ToEquatableReadOnlyList();
4✔
304

305
        private static string GetAttributeString(AttributeData attributeData)
306
        {
307
                var @class = attributeData.AttributeClass!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
4✔
308

309
                var parameters = new List<string>();
4✔
310

311
                foreach (var tc in attributeData.ConstructorArguments)
4✔
312
                {
313
                        if (GetTypedConstantString(tc) is { } str)
4✔
314
                                parameters.Add(str);
4✔
315
                }
316

317
                foreach (var na in attributeData.NamedArguments)
4✔
318
                {
319
                        if (GetTypedConstantString(na.Value) is { } str)
×
320
                                parameters.Add($"{na.Key} = {str}");
×
321
                }
322

323
                return parameters.Count == 0
4✔
324
                        ? @class
4✔
325
                        : $"{@class}({string.Join(", ", parameters)})";
4✔
326
        }
327

328
        [SuppressMessage("Style", "IDE0072:Add missing cases")]
329
        private static string? GetTypedConstantString(TypedConstant tc) =>
330
                tc.Kind switch
4✔
331
                {
4✔
332
                        TypedConstantKind.Array => $"[{string.Join(", ", tc.Values.Select(GetTypedConstantString))}]",
×
333
                        _ => tc.ToCSharpString(),
4✔
334
                };
4✔
335
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc