EndpointParameter.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  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. using System;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Globalization;
  6. using System.Text;
  7. using Microsoft.AspNetCore.Analyzers.Infrastructure;
  8. using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
  9. using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
  10. using Microsoft.CodeAnalysis;
  11. using Microsoft.CodeAnalysis.CSharp;
  12. using WellKnownType = Microsoft.AspNetCore.App.Analyzers.Infrastructure.WellKnownTypeData.WellKnownType;
  13. namespace Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel;
  14. internal class EndpointParameter
  15. {
  16. public EndpointParameter(Endpoint endpoint, IParameterSymbol parameter, WellKnownTypes wellKnownTypes)
  17. {
  18. Type = parameter.Type;
  19. SymbolName = parameter.Name;
  20. LookupName = parameter.Name; // Default lookup name is same as parameter name (which is a valid C# identifier).
  21. Ordinal = parameter.Ordinal;
  22. Source = EndpointParameterSource.Unknown;
  23. IsOptional = parameter.IsOptional();
  24. DefaultValue = parameter.GetDefaultValueString();
  25. IsArray = TryGetArrayElementType(parameter, out var elementType);
  26. ElementType = elementType;
  27. if (parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata), out var fromRouteAttribute))
  28. {
  29. Source = EndpointParameterSource.Route;
  30. LookupName = GetEscapedParameterName(fromRouteAttribute, parameter.Name);
  31. IsParsable = TryGetParsability(parameter, wellKnownTypes, out var parsingBlockEmitter);
  32. ParsingBlockEmitter = parsingBlockEmitter;
  33. }
  34. else if (parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata), out var fromQueryAttribute))
  35. {
  36. Source = EndpointParameterSource.Query;
  37. LookupName = GetEscapedParameterName(fromQueryAttribute, parameter.Name);
  38. IsParsable = TryGetParsability(parameter, wellKnownTypes, out var parsingBlockEmitter);
  39. ParsingBlockEmitter = parsingBlockEmitter;
  40. }
  41. else if (parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromHeaderMetadata), out var fromHeaderAttribute))
  42. {
  43. Source = EndpointParameterSource.Header;
  44. LookupName = GetEscapedParameterName(fromHeaderAttribute, parameter.Name);
  45. IsParsable = TryGetParsability(parameter, wellKnownTypes, out var parsingBlockEmitter);
  46. ParsingBlockEmitter = parsingBlockEmitter;
  47. }
  48. else if (parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromFormMetadata), out _))
  49. {
  50. Source = EndpointParameterSource.Unknown;
  51. }
  52. else if (TryGetExplicitFromJsonBody(parameter, wellKnownTypes, out var isOptional))
  53. {
  54. if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.System_IO_Stream)))
  55. {
  56. Source = EndpointParameterSource.SpecialType;
  57. AssigningCode = "httpContext.Request.Body";
  58. }
  59. else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.System_IO_Pipelines_PipeReader)))
  60. {
  61. Source = EndpointParameterSource.SpecialType;
  62. AssigningCode = "httpContext.Request.BodyReader";
  63. }
  64. else
  65. {
  66. Source = EndpointParameterSource.JsonBody;
  67. }
  68. IsOptional = isOptional;
  69. }
  70. else if (parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata)))
  71. {
  72. Source = EndpointParameterSource.Service;
  73. }
  74. else if (parameter.HasAttribute(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_AsParametersAttribute)))
  75. {
  76. Source = EndpointParameterSource.Unknown;
  77. }
  78. else if (TryGetSpecialTypeAssigningCode(Type, wellKnownTypes, out var specialTypeAssigningCode))
  79. {
  80. Source = EndpointParameterSource.SpecialType;
  81. AssigningCode = specialTypeAssigningCode;
  82. }
  83. else if (SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFile)) ||
  84. SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormFileCollection)) ||
  85. SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IFormCollection)))
  86. {
  87. Source = EndpointParameterSource.Unknown;
  88. }
  89. else if (HasBindAsync(parameter, wellKnownTypes, out var bindMethod))
  90. {
  91. Source = EndpointParameterSource.BindAsync;
  92. BindMethod = bindMethod;
  93. }
  94. else if (parameter.Type.SpecialType == SpecialType.System_String)
  95. {
  96. Source = EndpointParameterSource.RouteOrQuery;
  97. }
  98. else if (ShouldDisableInferredBodyParameters(endpoint.HttpMethod) && IsArray && elementType.SpecialType == SpecialType.System_String)
  99. {
  100. Source = EndpointParameterSource.Query;
  101. }
  102. else if (ShouldDisableInferredBodyParameters(endpoint.HttpMethod) && SymbolEqualityComparer.Default.Equals(parameter.Type, wellKnownTypes.Get(WellKnownType.Microsoft_Extensions_Primitives_StringValues)))
  103. {
  104. Source = EndpointParameterSource.Query;
  105. IsStringValues = true;
  106. }
  107. else if (TryGetParsability(parameter, wellKnownTypes, out var parsingBlockEmitter))
  108. {
  109. Source = EndpointParameterSource.RouteOrQuery;
  110. IsParsable = true;
  111. ParsingBlockEmitter = parsingBlockEmitter;
  112. }
  113. else
  114. {
  115. Source = EndpointParameterSource.JsonBodyOrService;
  116. }
  117. }
  118. private static bool ShouldDisableInferredBodyParameters(string httpMethod)
  119. {
  120. switch (httpMethod)
  121. {
  122. case "MapPut" or "MapPatch" or "MapPost":
  123. return false;
  124. default:
  125. return true;
  126. }
  127. }
  128. public ITypeSymbol Type { get; }
  129. public ITypeSymbol ElementType { get; }
  130. public string SymbolName { get; }
  131. public string LookupName { get; }
  132. public int Ordinal { get; }
  133. public bool IsOptional { get; }
  134. public bool IsArray { get; set; }
  135. public string DefaultValue { get; set; }
  136. public EndpointParameterSource Source { get; }
  137. // Only used for SpecialType parameters that need
  138. // to be resolved by a specific WellKnownType
  139. public string? AssigningCode { get; }
  140. [MemberNotNullWhen(true, nameof(ParsingBlockEmitter))]
  141. public bool IsParsable { get; }
  142. public Action<CodeWriter, string, string>? ParsingBlockEmitter { get; }
  143. public bool IsStringValues { get; }
  144. public BindabilityMethod? BindMethod { get; }
  145. private static bool HasBindAsync(IParameterSymbol parameter, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out BindabilityMethod? bindMethod)
  146. {
  147. var parameterType = parameter.Type.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: true);
  148. return ParsabilityHelper.GetBindability(parameterType, wellKnownTypes, out bindMethod) == Bindability.Bindable;
  149. }
  150. private static bool TryGetArrayElementType(IParameterSymbol parameter, [NotNullWhen(true)]out ITypeSymbol elementType)
  151. {
  152. if (parameter.Type.TypeKind == TypeKind.Array)
  153. {
  154. elementType = parameter.Type.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: false);
  155. return true;
  156. }
  157. else
  158. {
  159. elementType = null!;
  160. return false;
  161. }
  162. }
  163. private bool TryGetParsability(IParameterSymbol parameter, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out Action<CodeWriter, string, string>? parsingBlockEmitter)
  164. {
  165. var parameterType = parameter.Type.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: true);
  166. // ParsabilityHelper returns a single enumeration with a Parsable/NonParsable enumeration result. We use this already
  167. // in the analyzers to determine whether we need to warn on whether a type needs to implement TryParse/IParsable<T>. To
  168. // support usage in the code generator an optional out parameter has been added to hint at what variant of the various
  169. // TryParse methods should be used (this implies that the preferences are baked into ParsabilityHelper). If we aren't
  170. // parsable at all we bail.
  171. if (ParsabilityHelper.GetParsability(parameterType, wellKnownTypes, out var parsabilityMethod) != Parsability.Parsable)
  172. {
  173. parsingBlockEmitter = null;
  174. return false;
  175. }
  176. // If we are parsable we need to emit code based on the enumeration ParsabilityMethod which has a bunch of members
  177. // which spell out the preferred TryParse usage. This switch statement makes slight variations to them based on
  178. // which method was encountered.
  179. Func<string, string, string>? preferredTryParseInvocation = parsabilityMethod switch
  180. {
  181. ParsabilityMethod.IParsable => (string inputArgument, string outputArgument) => $$"""GeneratedRouteBuilderExtensionsCore.TryParseExplicit<{{parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}}>({{inputArgument}}!, CultureInfo.InvariantCulture, out var {{outputArgument}})""",
  182. ParsabilityMethod.TryParseWithFormatProvider => (string inputArgument, string outputArgument) => $$"""{{parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}}.TryParse({{inputArgument}}!, CultureInfo.InvariantCulture, out var {{outputArgument}})""",
  183. ParsabilityMethod.TryParse => (string inputArgument, string outputArgument) => $$"""{{parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}}.TryParse({{inputArgument}}!, out var {{outputArgument}})""",
  184. ParsabilityMethod.Enum => (string inputArgument, string outputArgument) => $$"""Enum.TryParse<{{parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}}>({{inputArgument}}!, out var {{outputArgument}})""",
  185. ParsabilityMethod.Uri => (string inputArgument, string outputArgument) => $$"""Uri.TryCreate({{inputArgument}}!, UriKind.RelativeOrAbsolute, out var {{outputArgument}})""",
  186. ParsabilityMethod.String => null, // string parameters don't require parsing
  187. _ => throw new Exception("Unreachable!"),
  188. };
  189. // Special case handling for specific types
  190. if (parameterType.SpecialType == SpecialType.System_Char)
  191. {
  192. preferredTryParseInvocation = (string inputArgument, string outputArgument) => $$"""{{parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}}.TryParse({{inputArgument}}!, out var {{outputArgument}})""";
  193. }
  194. else if (parameterType.SpecialType == SpecialType.System_DateTime)
  195. {
  196. preferredTryParseInvocation = (string inputArgument, string outputArgument) => $$"""{{parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}}.TryParse({{inputArgument}}!, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces, out var {{outputArgument}})""";
  197. }
  198. else if (SymbolEqualityComparer.Default.Equals(parameterType, wellKnownTypes.Get(WellKnownType.System_DateTimeOffset)))
  199. {
  200. preferredTryParseInvocation = (string inputArgument, string outputArgument) => $$"""{{parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}}.TryParse({{inputArgument}}!, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowWhiteSpaces, out var {{outputArgument}})""";
  201. }
  202. else if (SymbolEqualityComparer.Default.Equals(parameterType, wellKnownTypes.Get(WellKnownType.System_DateOnly)))
  203. {
  204. preferredTryParseInvocation = (string inputArgument, string outputArgument) => $$"""{{parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}}.TryParse({{inputArgument}}!, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var {{outputArgument}})""";
  205. }
  206. // ... so for strings (null) we bail.
  207. if (preferredTryParseInvocation == null)
  208. {
  209. parsingBlockEmitter = null;
  210. return false;
  211. }
  212. if (IsOptional)
  213. {
  214. parsingBlockEmitter = (writer, inputArgument, outputArgument) =>
  215. {
  216. writer.WriteLine($"""{parameter.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {outputArgument} = default;""");
  217. writer.WriteLine($$"""if ({{preferredTryParseInvocation(inputArgument, $"{inputArgument}_parsed_non_nullable")}})""");
  218. writer.StartBlock();
  219. writer.WriteLine($$"""{{outputArgument}} = {{$"{inputArgument}_parsed_non_nullable"}};""");
  220. writer.EndBlock();
  221. writer.WriteLine($$"""else if (string.IsNullOrEmpty({{inputArgument}}))""");
  222. writer.StartBlock();
  223. writer.WriteLine($$"""{{outputArgument}} = {{DefaultValue}};""");
  224. writer.EndBlock();
  225. writer.WriteLine("else");
  226. writer.StartBlock();
  227. writer.WriteLine("wasParamCheckFailure = true;");
  228. writer.EndBlock();
  229. };
  230. }
  231. else
  232. {
  233. parsingBlockEmitter = (writer, inputArgument, outputArgument) =>
  234. {
  235. if (IsArray && ElementType.NullableAnnotation == NullableAnnotation.Annotated)
  236. {
  237. writer.WriteLine($$"""if (!{{preferredTryParseInvocation(inputArgument, outputArgument)}})""");
  238. writer.StartBlock();
  239. writer.WriteLine($$"""if (!string.IsNullOrEmpty({{inputArgument}}))""");
  240. writer.StartBlock();
  241. writer.WriteLine("wasParamCheckFailure = true;");
  242. writer.WriteLine($@"logOrThrowExceptionHelper.RequiredParameterNotProvided({SymbolDisplay.FormatLiteral(Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(SymbolName, true)}, {SymbolDisplay.FormatLiteral(this.ToMessageString(), true)});");
  243. writer.EndBlock();
  244. writer.EndBlock();
  245. }
  246. else
  247. {
  248. writer.WriteLine($$"""if (!{{preferredTryParseInvocation(inputArgument, outputArgument)}})""");
  249. writer.StartBlock();
  250. writer.WriteLine($"if (!string.IsNullOrEmpty({inputArgument}))");
  251. writer.StartBlock();
  252. writer.WriteLine($@"logOrThrowExceptionHelper.ParameterBindingFailed({SymbolDisplay.FormatLiteral(Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat), true)}, {SymbolDisplay.FormatLiteral(SymbolName, true)}, {inputArgument});");
  253. writer.WriteLine("wasParamCheckFailure = true;");
  254. writer.EndBlock();
  255. writer.EndBlock();
  256. }
  257. };
  258. }
  259. // Wrap the TryParse method call in an if-block and if it doesn't work set param check failure.
  260. return true;
  261. }
  262. // TODO: Handle special form types like IFormFileCollection that need special body-reading logic.
  263. private static bool TryGetSpecialTypeAssigningCode(ITypeSymbol type, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out string? callingCode)
  264. {
  265. callingCode = null;
  266. if (SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_HttpContext)))
  267. {
  268. callingCode = "httpContext";
  269. return true;
  270. }
  271. if (SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_HttpRequest)))
  272. {
  273. callingCode = "httpContext.Request";
  274. return true;
  275. }
  276. if (SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_HttpResponse)))
  277. {
  278. callingCode = "httpContext.Response";
  279. return true;
  280. }
  281. if (SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownType.System_IO_Pipelines_PipeReader)))
  282. {
  283. callingCode = "httpContext.Request.BodyReader";
  284. return true;
  285. }
  286. if (SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownType.System_IO_Stream)))
  287. {
  288. callingCode = "httpContext.Request.Body";
  289. return true;
  290. }
  291. if (SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownType.System_Security_Claims_ClaimsPrincipal)))
  292. {
  293. callingCode = "httpContext.User";
  294. return true;
  295. }
  296. if (SymbolEqualityComparer.Default.Equals(type, wellKnownTypes.Get(WellKnownType.System_Threading_CancellationToken)))
  297. {
  298. callingCode = "httpContext.RequestAborted";
  299. return true;
  300. }
  301. return false;
  302. }
  303. private static bool TryGetExplicitFromJsonBody(IParameterSymbol parameter,
  304. WellKnownTypes wellKnownTypes,
  305. out bool isOptional)
  306. {
  307. isOptional = false;
  308. if (!parameter.HasAttributeImplementingInterface(wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromBodyMetadata), out var fromBodyAttribute))
  309. {
  310. return false;
  311. }
  312. isOptional |= fromBodyAttribute.TryGetNamedArgumentValue<int>("EmptyBodyBehavior", out var emptyBodyBehaviorValue) && emptyBodyBehaviorValue == 1;
  313. isOptional |= fromBodyAttribute.TryGetNamedArgumentValue<bool>("AllowEmpty", out var allowEmptyValue) && allowEmptyValue;
  314. isOptional |= (parameter.NullableAnnotation == NullableAnnotation.Annotated || parameter.HasExplicitDefaultValue);
  315. return true;
  316. }
  317. private static string GetEscapedParameterName(AttributeData attribute, string parameterName)
  318. {
  319. if (attribute.TryGetNamedArgumentValue<string>("Name", out var fromSourceName) && fromSourceName is not null)
  320. {
  321. return ConvertEndOfLineAndQuotationCharactersToEscapeForm(fromSourceName);
  322. }
  323. else
  324. {
  325. return parameterName;
  326. }
  327. }
  328. // Lifted from:
  329. // https://github.com/dotnet/runtime/blob/dc5a6c8be1644915c14c4a464447b0d54e223a46/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs#L562
  330. private static string ConvertEndOfLineAndQuotationCharactersToEscapeForm(string s)
  331. {
  332. var index = 0;
  333. while (index < s.Length)
  334. {
  335. if (s[index] is '\n' or '\r' or '"' or '\\')
  336. {
  337. break;
  338. }
  339. index++;
  340. }
  341. if (index >= s.Length)
  342. {
  343. return s;
  344. }
  345. var sb = new StringBuilder(s.Length);
  346. sb.Append(s, 0, index);
  347. while (index < s.Length)
  348. {
  349. switch (s[index])
  350. {
  351. case '\n':
  352. sb.Append('\\');
  353. sb.Append('n');
  354. break;
  355. case '\r':
  356. sb.Append('\\');
  357. sb.Append('r');
  358. break;
  359. case '"':
  360. sb.Append('\\');
  361. sb.Append('"');
  362. break;
  363. case '\\':
  364. sb.Append('\\');
  365. sb.Append('\\');
  366. break;
  367. default:
  368. sb.Append(s[index]);
  369. break;
  370. }
  371. index++;
  372. }
  373. return sb.ToString();
  374. }
  375. public override bool Equals(object obj) =>
  376. obj is EndpointParameter other &&
  377. other.Source == Source &&
  378. other.SymbolName == SymbolName &&
  379. other.Ordinal == Ordinal &&
  380. other.IsOptional == IsOptional &&
  381. SymbolEqualityComparer.Default.Equals(other.Type, Type);
  382. public bool SignatureEquals(object obj) =>
  383. obj is EndpointParameter other &&
  384. SymbolEqualityComparer.Default.Equals(other.Type, Type);
  385. public override int GetHashCode()
  386. {
  387. var hashCode = new HashCode();
  388. hashCode.Add(SymbolName);
  389. hashCode.Add(Type, SymbolEqualityComparer.Default);
  390. return hashCode.ToHashCode();
  391. }
  392. }