Skip to content

Commit cddec67

Browse files
Improve error messages for implicit and explicit FromBody attribute in Minimal API #35086 (#35188)
* Improve error messages for implicit and explicit FromBody attribute in Minimal API * Add error message for mapget when user forget to register service * add suggested changes * replace null with typeof(Tvalue) * multiple frombody * resolve more merge issues * address pr suggestions * address more PR comments * clean up * reset HttpRequestJsonExtensions.cs
1 parent 8556362 commit cddec67

File tree

1 file changed

+63
-3
lines changed

1 file changed

+63
-3
lines changed

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq.Expressions;
66
using System.Reflection;
77
using System.Security.Claims;
8+
using System.Text;
89
using Microsoft.AspNetCore.Http.Features;
910
using Microsoft.AspNetCore.Http.Metadata;
1011
using Microsoft.Extensions.DependencyInjection;
@@ -189,6 +190,13 @@ private static Expression[] CreateArguments(ParameterInfo[]? parameters, Factory
189190
args[i] = CreateArgument(parameters[i], factoryContext);
190191
}
191192

193+
if (factoryContext.HasMultipleBodyParameters)
194+
{
195+
var errorMessage = BuildErrorMessageForMultipleBodyParameters(factoryContext);
196+
throw new InvalidOperationException(errorMessage);
197+
198+
}
199+
192200
return args;
193201
}
194202

@@ -203,6 +211,7 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
203211

204212
if (parameterCustomAttributes.OfType<IFromRouteMetadata>().FirstOrDefault() is { } routeAttribute)
205213
{
214+
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.RouteAttribue);
206215
if (factoryContext.RouteParameters is { } routeParams && !routeParams.Contains(parameter.Name, StringComparer.OrdinalIgnoreCase))
207216
{
208217
throw new InvalidOperationException($"{parameter.Name} is not a route paramter.");
@@ -212,18 +221,22 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
212221
}
213222
else if (parameterCustomAttributes.OfType<IFromQueryMetadata>().FirstOrDefault() is { } queryAttribute)
214223
{
224+
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.QueryAttribue);
215225
return BindParameterFromProperty(parameter, QueryExpr, queryAttribute.Name ?? parameter.Name, factoryContext);
216226
}
217227
else if (parameterCustomAttributes.OfType<IFromHeaderMetadata>().FirstOrDefault() is { } headerAttribute)
218228
{
229+
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.HeaderAttribue);
219230
return BindParameterFromProperty(parameter, HeadersExpr, headerAttribute.Name ?? parameter.Name, factoryContext);
220231
}
221232
else if (parameterCustomAttributes.OfType<IFromBodyMetadata>().FirstOrDefault() is { } bodyAttribute)
222233
{
234+
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.BodyAttribue);
223235
return BindParameterFromBody(parameter, bodyAttribute.AllowEmpty, factoryContext);
224236
}
225237
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
226238
{
239+
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.ServiceAttribue);
227240
return BindParameterFromService(parameter);
228241
}
229242
else if (parameter.ParameterType == typeof(HttpContext))
@@ -254,18 +267,22 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
254267
// when RDF.Create is manually invoked.
255268
if (factoryContext.RouteParameters is { } routeParams)
256269
{
270+
257271
if (routeParams.Contains(parameter.Name, StringComparer.OrdinalIgnoreCase))
258272
{
259273
// We're in the fallback case and we have a parameter and route parameter match so don't fallback
260274
// to query string in this case
275+
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.RouteParameter);
261276
return BindParameterFromProperty(parameter, RouteValuesExpr, parameter.Name, factoryContext);
262277
}
263278
else
264279
{
280+
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.QueryStringParameter);
265281
return BindParameterFromProperty(parameter, QueryExpr, parameter.Name, factoryContext);
266282
}
267283
}
268284

285+
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.RouteOrQueryStringParameter);
269286
return BindParameterFromRouteValueOrQueryString(parameter, parameter.Name, factoryContext);
270287
}
271288
else
@@ -274,10 +291,12 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
274291
{
275292
if (serviceProviderIsService.IsService(parameter.ParameterType))
276293
{
294+
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.ServiceParameter);
277295
return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr);
278296
}
279297
}
280298

299+
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.BodyParameter);
281300
return BindParameterFromBody(parameter, allowEmpty: false, factoryContext);
282301
}
283302
}
@@ -500,7 +519,6 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
500519
return async (target, httpContext) =>
501520
{
502521
object? bodyValue = defaultBodyValue;
503-
504522
var feature = httpContext.Features.Get<IHttpRequestBodyDetectionFeature>();
505523
if (feature?.CanHaveBody == true)
506524
{
@@ -515,12 +533,12 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
515533
}
516534
catch (InvalidDataException ex)
517535
{
536+
518537
Log.RequestBodyInvalidDataException(httpContext, ex);
519538
httpContext.Response.StatusCode = 400;
520539
return;
521540
}
522541
}
523-
524542
await invoker(target, httpContext, bodyValue);
525543
};
526544
}
@@ -725,7 +743,14 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al
725743
{
726744
if (factoryContext.JsonRequestBodyType is not null)
727745
{
728-
throw new InvalidOperationException("Action cannot have more than one FromBody attribute.");
746+
factoryContext.HasMultipleBodyParameters = true;
747+
var parameterName = parameter.Name;
748+
if (parameterName is not null && factoryContext.TrackedParameters.ContainsKey(parameterName))
749+
{
750+
factoryContext.TrackedParameters.Remove(parameterName);
751+
factoryContext.TrackedParameters.Add(parameterName, "UNKNOWN");
752+
753+
}
729754
}
730755

731756
var nullability = NullabilityContext.Create(parameter);
@@ -963,6 +988,24 @@ private class FactoryContext
963988
public bool UsingTempSourceString { get; set; }
964989
public List<ParameterExpression> ExtraLocals { get; } = new();
965990
public List<Expression> ParamCheckExpressions { get; } = new();
991+
992+
public Dictionary<string, string> TrackedParameters { get; } = new();
993+
public bool HasMultipleBodyParameters { get; set; }
994+
}
995+
996+
private static class RequestDelegateFactoryConstants
997+
{
998+
public const string RouteAttribue = "Route (Attribute)";
999+
public const string QueryAttribue = "Query (Attribute)";
1000+
public const string HeaderAttribue = "Header (Attribute)";
1001+
public const string BodyAttribue = "Body (Attribute)";
1002+
public const string ServiceAttribue = "Service (Attribute)";
1003+
public const string RouteParameter = "Route (Inferred)";
1004+
public const string QueryStringParameter = "Query String (Inferred)";
1005+
public const string ServiceParameter = "Services (Inferred)";
1006+
public const string BodyParameter = "Body (Inferred)";
1007+
public const string RouteOrQueryStringParameter = "Route or Query String (Inferred)";
1008+
9661009
}
9671010

9681011
private static partial class Log
@@ -1032,5 +1075,22 @@ private static void SetPlaintextContentType(HttpContext httpContext)
10321075
{
10331076
httpContext.Response.ContentType ??= "text/plain; charset=utf-8";
10341077
}
1078+
1079+
private static string BuildErrorMessageForMultipleBodyParameters(FactoryContext factoryContext)
1080+
{
1081+
var errorMessage = new StringBuilder();
1082+
errorMessage.Append($"Failure to infer one or more parameters.\n");
1083+
errorMessage.Append("Below is the list of parameters that we found: \n\n");
1084+
errorMessage.Append($"{"Parameter",-20}|{"Source",-30} \n");
1085+
errorMessage.Append("---------------------------------------------------------------------------------\n");
1086+
1087+
foreach (var kv in factoryContext.TrackedParameters)
1088+
{
1089+
errorMessage.Append($"{kv.Key,-19} | {kv.Value,-15}\n");
1090+
}
1091+
errorMessage.Append("\n\n");
1092+
errorMessage.Append("Did you mean to register the \"UNKNOWN\" parameters as a Service?\n\n");
1093+
return errorMessage.ToString();
1094+
}
10351095
}
10361096
}

0 commit comments

Comments
 (0)