Skip to content

Commit 86e3a4b

Browse files
authored
Greater than 1 FromBody analyzer. (#46494)
Report an error daignostic when a minimal API ```Map...``` method contains multiple `[FromBody]` attributes or a type referenced with an `[AsPraameters]` attribute with multiple `[FromBody]` members is present.
1 parent 0134f61 commit 86e3a4b

File tree

6 files changed

+258
-6
lines changed

6 files changed

+258
-6
lines changed

src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,13 @@ internal static class DiagnosticDescriptors
196196
DiagnosticSeverity.Warning,
197197
isEnabledByDefault: true,
198198
helpLinkUri: "https://aka.ms/aspnet/analyzers");
199+
200+
internal static readonly DiagnosticDescriptor AtMostOneFromBodyAttribute = new(
201+
"ASP0024",
202+
new LocalizableResourceString(nameof(Resources.Analyzer_MultipleFromBody_Title), Resources.ResourceManager, typeof(Resources)),
203+
new LocalizableResourceString(nameof(Resources.Analyzer_MultipleFromBody_Message), Resources.ResourceManager, typeof(Resources)),
204+
"Usage",
205+
DiagnosticSeverity.Error,
206+
isEnabledByDefault: true,
207+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
199208
}

src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,10 @@
207207
<data name="Analyzer_HeaderDictionaryAdd_Title" xml:space="preserve">
208208
<value>Suggest using IHeaderDictionary.Append or the indexer</value>
209209
</data>
210-
</root>
210+
<data name="Analyzer_MultipleFromBody_Message" xml:space="preserve">
211+
<value>Route handler has multiple parameters with the [FromBody] attribute or a parameter with an [AsParameters] attribute where the parameter type contains multiple members with [FromBody] attributes. Only one parameter can have a [FromBody] attribute.</value>
212+
</data>
213+
<data name="Analyzer_MultipleFromBody_Title" xml:space="preserve">
214+
<value>Route handler has multiple parameters with the [FromBody] attribute.</value>
215+
</data>
216+
</root>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
7+
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.Diagnostics;
10+
11+
namespace Microsoft.AspNetCore.Analyzers.RouteHandlers;
12+
13+
using WellKnownType = WellKnownTypeData.WellKnownType;
14+
15+
public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer
16+
{
17+
private static void AtMostOneFromBodyAttribute(
18+
in OperationAnalysisContext context,
19+
WellKnownTypes wellKnownTypes,
20+
IMethodSymbol methodSymbol)
21+
{
22+
var fromBodyMetadataInterfaceType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromBodyMetadata);
23+
var asParametersAttributeType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_AsParametersAttribute);
24+
25+
var asParametersDecoratedParameters = methodSymbol.Parameters.Where(p => p.HasAttribute(asParametersAttributeType));
26+
27+
foreach (var asParameterDecoratedParameter in asParametersDecoratedParameters)
28+
{
29+
var fromBodyMetadataInterfaceMembers = asParameterDecoratedParameter.Type.GetMembers().Where(
30+
m => m.HasAttributeImplementingInterface(fromBodyMetadataInterfaceType)
31+
);
32+
33+
if (fromBodyMetadataInterfaceMembers.Count() >= 2)
34+
{
35+
ReportDiagnostics(context, fromBodyMetadataInterfaceMembers);
36+
}
37+
}
38+
39+
var fromBodyMetadataInterfaceParameters = methodSymbol.Parameters.Where(p => p.HasAttributeImplementingInterface(fromBodyMetadataInterfaceType));
40+
41+
if (fromBodyMetadataInterfaceParameters.Count() >= 2)
42+
{
43+
ReportDiagnostics(context, fromBodyMetadataInterfaceParameters);
44+
}
45+
46+
static void ReportDiagnostics(OperationAnalysisContext context, IEnumerable<ISymbol> symbols)
47+
{
48+
foreach (var symbol in symbols)
49+
{
50+
if (symbol.DeclaringSyntaxReferences.Length > 0)
51+
{
52+
var syntax = symbol.DeclaringSyntaxReferences[0].GetSyntax(context.CancellationToken);
53+
var location = syntax.GetLocation();
54+
context.ReportDiagnostic(Diagnostic.Create(
55+
DiagnosticDescriptors.AtMostOneFromBodyAttribute,
56+
location
57+
));
58+
}
59+
}
60+
}
61+
}
62+
}

src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowNonParsableComplexTypesOnParameters.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@ public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer
1717
{
1818
private static void DisallowNonParsableComplexTypesOnParameters(
1919
in OperationAnalysisContext context,
20+
WellKnownTypes wellKnownTypes,
2021
RouteUsageModel routeUsage,
2122
IMethodSymbol methodSymbol)
2223
{
23-
var wellKnownTypes = WellKnownTypes.GetOrCreate(context.Compilation);
24-
2524
foreach (var handlerDelegateParameter in methodSymbol.Parameters)
2625
{
2726
// If the parameter is decorated with a FromServices attribute then we can skip it.

src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer
2727
DiagnosticDescriptors.DetectMismatchedParameterOptionality,
2828
DiagnosticDescriptors.RouteParameterComplexTypeIsNotParsableOrBindable,
2929
DiagnosticDescriptors.BindAsyncSignatureMustReturnValueTaskOfT,
30-
DiagnosticDescriptors.AmbiguousRouteHandlerRoute
30+
DiagnosticDescriptors.AmbiguousRouteHandlerRoute,
31+
DiagnosticDescriptors.AtMostOneFromBodyAttribute
3132
);
3233

3334
public override void Initialize(AnalysisContext context)
@@ -105,17 +106,19 @@ void DoOperationAnalysis(OperationAnalysisContext context, ConcurrentDictionary<
105106
{
106107
var lambda = (IAnonymousFunctionOperation)delegateCreation.Target;
107108
DisallowMvcBindArgumentsOnParameters(in context, wellKnownTypes, invocation, lambda.Symbol);
108-
DisallowNonParsableComplexTypesOnParameters(in context, routeUsage, lambda.Symbol);
109+
DisallowNonParsableComplexTypesOnParameters(in context, wellKnownTypes, routeUsage, lambda.Symbol);
109110
DisallowReturningActionResultFromMapMethods(in context, wellKnownTypes, invocation, lambda, delegateCreation.Syntax);
110111
DetectMisplacedLambdaAttribute(context, lambda);
111112
DetectMismatchedParameterOptionality(in context, routeUsage, lambda.Symbol);
113+
AtMostOneFromBodyAttribute(in context, wellKnownTypes, lambda.Symbol);
112114
}
113115
else if (delegateCreation.Target.Kind == OperationKind.MethodReference)
114116
{
115117
var methodReference = (IMethodReferenceOperation)delegateCreation.Target;
116118
DisallowMvcBindArgumentsOnParameters(in context, wellKnownTypes, invocation, methodReference.Method);
117-
DisallowNonParsableComplexTypesOnParameters(in context, routeUsage, methodReference.Method);
119+
DisallowNonParsableComplexTypesOnParameters(in context, wellKnownTypes, routeUsage, methodReference.Method);
118120
DetectMismatchedParameterOptionality(in context, routeUsage, methodReference.Method);
121+
AtMostOneFromBodyAttribute(in context, wellKnownTypes, methodReference.Method);
119122

120123
var foundMethodReferenceBody = false;
121124
if (!methodReference.Method.DeclaringSyntaxReferences.IsEmpty)
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Security.Policy;
5+
using Microsoft.CodeAnalysis.Testing;
6+
using VerifyCS = Microsoft.AspNetCore.Analyzers.Verifiers.CSharpAnalyzerVerifier<Microsoft.AspNetCore.Analyzers.RouteHandlers.RouteHandlerAnalyzer>;
7+
8+
namespace Microsoft.AspNetCore.Analyzers.RouteHandlers;
9+
10+
public partial class AtMostOneFromBodyAttributeTest
11+
{
12+
private TestDiagnosticAnalyzerRunner Runner { get; } = new(new RouteHandlerAnalyzer());
13+
14+
[Fact]
15+
public async Task Handler_With_No_FromBody_Attributes_Works()
16+
{
17+
// Arrange
18+
var source = @"
19+
using Microsoft.AspNetCore.Builder;
20+
var webApp = WebApplication.Create();
21+
webApp.MapPost(""/products/{productId}"", (string productId, Product product) => {});
22+
23+
public class Product
24+
{
25+
}
26+
";
27+
28+
// Act
29+
await VerifyCS.VerifyAnalyzerAsync(source);
30+
}
31+
32+
[Fact]
33+
public async Task Handler_With_One_FromBody_Attributes_Works()
34+
{
35+
// Arrange
36+
var source = @"
37+
using Microsoft.AspNetCore.Mvc;
38+
using Microsoft.AspNetCore.Builder;
39+
var webApp = WebApplication.Create();
40+
webApp.MapPost(""/products/{productId}"", (string productId, [FromBody]Product product) => {});
41+
42+
public class Product
43+
{
44+
}
45+
";
46+
47+
// Act
48+
await VerifyCS.VerifyAnalyzerAsync(source);
49+
}
50+
51+
[Fact]
52+
public async Task Handler_With_Two_FromBody_Attributes_Fails()
53+
{
54+
// Arrange
55+
var source = @"
56+
using Microsoft.AspNetCore.Mvc;
57+
using Microsoft.AspNetCore.Builder;
58+
var webApp = WebApplication.Create();
59+
webApp.MapPost(""/products/{productId}"", (string productId, {|#0:[FromBody]Product product1|}, {|#1:[FromBody]Product product2|}) => {});
60+
61+
public class Product
62+
{
63+
}
64+
";
65+
66+
var expectedDiagnostic1 = new DiagnosticResult(DiagnosticDescriptors.AtMostOneFromBodyAttribute).WithLocation(0);
67+
var expectedDiagnostic2 = new DiagnosticResult(DiagnosticDescriptors.AtMostOneFromBodyAttribute).WithLocation(1);
68+
69+
// Act
70+
await VerifyCS.VerifyAnalyzerAsync(
71+
source,
72+
expectedDiagnostic1,
73+
expectedDiagnostic2
74+
);
75+
}
76+
77+
[Fact]
78+
public async Task MethodGroup_Handler_With_Two_FromBody_Attributes_Fails()
79+
{
80+
// Arrange
81+
var source = @"
82+
using Microsoft.AspNetCore.Mvc;
83+
using Microsoft.AspNetCore.Builder;
84+
var webApp = WebApplication.Create();
85+
webApp.MapPost(""/products/{productId}"", MyHandlers.ProcessRequest);
86+
87+
public static class MyHandlers
88+
{
89+
public static void ProcessRequest(string productId, {|#0:[FromBody]Product product1|}, {|#1:[FromBody]Product product2|})
90+
{
91+
}
92+
}
93+
94+
public class Product
95+
{
96+
}
97+
";
98+
99+
var expectedDiagnostic1 = new DiagnosticResult(DiagnosticDescriptors.AtMostOneFromBodyAttribute).WithLocation(0);
100+
var expectedDiagnostic2 = new DiagnosticResult(DiagnosticDescriptors.AtMostOneFromBodyAttribute).WithLocation(1);
101+
102+
// Act
103+
await VerifyCS.VerifyAnalyzerAsync(
104+
source,
105+
expectedDiagnostic1,
106+
expectedDiagnostic2
107+
);
108+
}
109+
110+
[Fact]
111+
public async Task Handler_Handler_With_AsParameters_Argument_With_TwoFromBody_Attributes_Fails()
112+
{
113+
// Arrange
114+
var source = @"
115+
using Microsoft.AspNetCore.Http;
116+
using Microsoft.AspNetCore.Mvc;
117+
using Microsoft.AspNetCore.Builder;
118+
var webApp = WebApplication.Create();
119+
webApp.MapPost(""/products/{productId}"", ([AsParameters]GetProductRequest request) => {});
120+
121+
public class GetProductRequest
122+
{
123+
{|#0:[FromBody]
124+
public Product Product1 { get; set; }|}
125+
126+
{|#1:[FromBody]
127+
public Product Product2 { get; set; }|}
128+
}
129+
130+
public class Product
131+
{
132+
}
133+
";
134+
135+
var expectedDiagnostic1 = new DiagnosticResult(DiagnosticDescriptors.AtMostOneFromBodyAttribute).WithLocation(0);
136+
var expectedDiagnostic2 = new DiagnosticResult(DiagnosticDescriptors.AtMostOneFromBodyAttribute).WithLocation(1);
137+
138+
// Act
139+
await VerifyCS.VerifyAnalyzerAsync(
140+
source,
141+
expectedDiagnostic1,
142+
expectedDiagnostic2
143+
);
144+
}
145+
146+
[Fact]
147+
public async Task Handler_Handler_With_AsParameters_Argument_With_OneFromBody_Attributes_Works()
148+
{
149+
// Arrange
150+
var source = @"
151+
using Microsoft.AspNetCore.Http;
152+
using Microsoft.AspNetCore.Mvc;
153+
using Microsoft.AspNetCore.Builder;
154+
var webApp = WebApplication.Create();
155+
webApp.MapPost(""/products/{productId}"", ([AsParameters]GetProductRequest request) => {});
156+
157+
public class GetProductRequest
158+
{
159+
{|#0:[FromBody]
160+
public Product Product1 { get; set; }|}
161+
162+
public Product Product2 { get; set; }
163+
}
164+
165+
public class Product
166+
{
167+
}
168+
";
169+
170+
// Act
171+
await VerifyCS.VerifyAnalyzerAsync(source);
172+
}
173+
}

0 commit comments

Comments
 (0)