CompletenessTests.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  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.Globalization;
  4. using System.Text.Json.Nodes;
  5. using Microsoft.OpenApi.Models;
  6. namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
  7. // Test scenarios derived from https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/examples
  8. [UsesVerify]
  9. public class CompletenessTests
  10. {
  11. [Fact]
  12. public async Task SupportsAllXmlTagsOnSchemas()
  13. {
  14. var source = """
  15. using System;
  16. using System.Collections.Generic;
  17. using System.Threading.Tasks;
  18. using Microsoft.AspNetCore.Builder;
  19. using Microsoft.Extensions.DependencyInjection;
  20. var builder = WebApplication.CreateBuilder();
  21. builder.Services.AddOpenApi();
  22. var app = builder.Build();
  23. app.MapPost("/example-class", (ExampleClass example) => { });
  24. app.MapPost("/person", (Person person) => { });
  25. app.MapPost("/derived-class", (DerivedClass child) => { });
  26. app.MapPost("/main-class", (MainClass main) => { });
  27. app.MapPost("/test-interface", (ITestInterface test) => { });
  28. app.MapPost("/implementing-class", (ImplementingClass impl) => { });
  29. app.MapPost("/inherit-only-returns", (InheritOnlyReturns returns) => { });
  30. app.MapPost("/inherit-all-but-remarks", (InheritAllButRemarks remarks) => { });
  31. app.MapPost("/generic-class", (GenericClass<string> generic) => { });
  32. app.MapPost("/generic-parent", (GenericParent parent) => { });
  33. app.MapPost("/params-and-param-refs", (ParamsAndParamRefs refs) => { });
  34. app.Run();
  35. /// <summary>
  36. /// Every class and member should have a one sentence
  37. /// summary describing its purpose.
  38. /// </summary>
  39. /// <remarks>
  40. /// You can expand on that one sentence summary to
  41. /// provide more information for readers. In this case,
  42. /// the <c>ExampleClass</c> provides different C#
  43. /// elements to show how you would add documentation
  44. ///comments for most elements in a typical class.
  45. /// <para>
  46. /// The remarks can add multiple paragraphs, so you can
  47. /// write detailed information for developers that use
  48. /// your work. You should add everything needed for
  49. /// readers to be successful. This class contains
  50. /// examples for the following:
  51. /// </para>
  52. /// <list type="table">
  53. /// <item>
  54. /// <term>Summary</term>
  55. /// <description>
  56. /// This should provide a one sentence summary of the class or member.
  57. /// </description>
  58. /// </item>
  59. /// <item>
  60. /// <term>Remarks</term>
  61. /// <description>
  62. /// This is typically a more detailed description of the class or member
  63. /// </description>
  64. /// </item>
  65. /// <item>
  66. /// <term>para</term>
  67. /// <description>
  68. /// The para tag separates a section into multiple paragraphs
  69. /// </description>
  70. /// </item>
  71. /// <item>
  72. /// <term>list</term>
  73. /// <description>
  74. /// Provides a list of terms or elements
  75. /// </description>
  76. /// </item>
  77. /// <item>
  78. /// <term>returns, param</term>
  79. /// <description>
  80. /// Used to describe parameters and return values
  81. /// </description>
  82. /// </item>
  83. /// <item>
  84. /// <term>value</term>
  85. /// <description>Used to describe properties</description>
  86. /// </item>
  87. /// <item>
  88. /// <term>exception</term>
  89. /// <description>
  90. /// Used to describe exceptions that may be thrown
  91. /// </description>
  92. /// </item>
  93. /// <item>
  94. /// <term>c, cref, see, seealso</term>
  95. /// <description>
  96. /// These provide code style and links to other
  97. /// documentation elements
  98. /// </description>
  99. /// </item>
  100. /// <item>
  101. /// <term>example, code</term>
  102. /// <description>
  103. /// These are used for code examples
  104. /// </description>
  105. /// </item>
  106. /// </list>
  107. /// <para>
  108. /// The list above uses the "table" style. You could
  109. /// also use the "bullet" or "number" style. Neither
  110. /// would typically use the "term" element.
  111. /// <br/>
  112. /// Note: paragraphs are double spaced. Use the *br*
  113. /// tag for single spaced lines.
  114. /// </para>
  115. /// </remarks>
  116. public class ExampleClass
  117. {
  118. /// <value>
  119. /// The <c>Label</c> property represents a label
  120. /// for this instance.
  121. /// </value>
  122. /// <remarks>
  123. /// The <see cref="Label"/> is a <see langword="string"/>
  124. /// that you use for a label.
  125. /// <para>
  126. /// Note that there isn't a way to provide a "cref" to
  127. /// each accessor, only to the property itself.
  128. /// </para>
  129. /// </remarks>
  130. public string? Label
  131. {
  132. get;
  133. set;
  134. }
  135. /// <summary>
  136. /// Adds two integers and returns the result.
  137. /// </summary>
  138. /// <returns>
  139. /// The sum of two integers.
  140. /// </returns>
  141. /// <param name="left">
  142. /// The left operand of the addition.
  143. /// </param>
  144. /// <param name="right">
  145. /// The right operand of the addition.
  146. /// </param>
  147. /// <example>
  148. /// <code>
  149. /// int c = Math.Add(4, 5);
  150. /// if (c > 10)
  151. /// {
  152. /// Console.WriteLine(c);
  153. /// }
  154. /// </code>
  155. /// </example>
  156. /// <exception cref="System.OverflowException">
  157. /// Thrown when one parameter is
  158. /// <see cref="Int32.MaxValue">MaxValue</see> and the other is
  159. /// greater than 0.
  160. /// Note that here you can also use
  161. /// <see href="https://learn.microsoft.com/dotnet/api/system.int32.maxvalue"/>
  162. /// to point a web page instead.
  163. /// </exception>
  164. /// <see cref="ExampleClass"/> for a list of all
  165. /// the tags in these examples.
  166. /// <seealso cref="ExampleClass.Label"/>
  167. public static int Add(int left, int right)
  168. {
  169. if ((left == int.MaxValue && right > 0) || (right == int.MaxValue && left > 0))
  170. throw new System.OverflowException();
  171. return left + right;
  172. }
  173. /// <summary>
  174. /// This method is an example of a method that
  175. /// returns an awaitable item.
  176. /// </summary>
  177. public static Task<int> AddAsync(int left, int right)
  178. {
  179. return Task.FromResult(Add(left, right));
  180. }
  181. /// <summary>
  182. /// This method is an example of a method that
  183. /// returns a Task which should map to a void return type.
  184. /// </summary>
  185. public static Task DoNothingAsync()
  186. {
  187. return Task.CompletedTask;
  188. }
  189. /// <summary>
  190. /// This method is an example of a method that consumes
  191. /// an params array.
  192. /// </summary>
  193. public static int AddNumbers(params int[] numbers)
  194. {
  195. var sum = 0;
  196. foreach (var number in numbers)
  197. {
  198. sum += number;
  199. }
  200. return sum;
  201. }
  202. }
  203. /// <summary>
  204. /// This is an example of a positional record.
  205. /// </summary>
  206. /// <remarks>
  207. /// There isn't a way to add XML comments for properties
  208. /// created for positional records, yet. The language
  209. /// design team is still considering what tags should
  210. /// be supported, and where. Currently, you can use
  211. /// the "param" tag to describe the parameters to the
  212. /// primary constructor.
  213. /// </remarks>
  214. /// <param name="FirstName">
  215. /// This tag will apply to the primary constructor parameter.
  216. /// </param>
  217. /// <param name="LastName">
  218. /// This tag will apply to the primary constructor parameter.
  219. /// </param>
  220. public record Person(string FirstName, string LastName);
  221. /// <summary>
  222. /// A summary about this class.
  223. /// </summary>
  224. /// <remarks>
  225. /// These remarks would explain more about this class.
  226. /// In this example, these comments also explain the
  227. /// general information about the derived class.
  228. /// </remarks>
  229. public class MainClass
  230. {
  231. }
  232. ///<inheritdoc/>
  233. public class DerivedClass : MainClass
  234. {
  235. }
  236. /// <summary>
  237. /// This interface would describe all the methods in
  238. /// its contract.
  239. /// </summary>
  240. /// <remarks>
  241. /// While elided for brevity, each method or property
  242. /// in this interface would contain docs that you want
  243. /// to duplicate in each implementing class.
  244. /// </remarks>
  245. public interface ITestInterface
  246. {
  247. /// <summary>
  248. /// This method is part of the test interface.
  249. /// </summary>
  250. /// <remarks>
  251. /// This content would be inherited by classes
  252. /// that implement this interface when the
  253. /// implementing class uses "inheritdoc"
  254. /// </remarks>
  255. /// <returns>The value of <paramref name="arg" /> </returns>
  256. /// <param name="arg">The argument to the method</param>
  257. int Method(int arg);
  258. }
  259. ///<inheritdoc cref="ITestInterface"/>
  260. public class ImplementingClass : ITestInterface
  261. {
  262. // doc comments are inherited here.
  263. public int Method(int arg) => arg;
  264. }
  265. /// <summary>
  266. /// This class shows hows you can "inherit" the doc
  267. /// comments from one method in another method.
  268. /// </summary>
  269. /// <remarks>
  270. /// You can inherit all comments, or only a specific tag,
  271. /// represented by an xpath expression.
  272. /// </remarks>
  273. public class InheritOnlyReturns
  274. {
  275. /// <summary>
  276. /// In this example, this summary is only visible for this method.
  277. /// </summary>
  278. /// <returns>A boolean</returns>
  279. public static bool MyParentMethod(bool x) { return x; }
  280. /// <inheritdoc cref="MyParentMethod" path="/returns"/>
  281. public static bool MyChildMethod() { return false; }
  282. }
  283. /// <summary>
  284. /// This class shows an example of sharing comments across methods.
  285. /// </summary>
  286. public class InheritAllButRemarks
  287. {
  288. /// <summary>
  289. /// In this example, this summary is visible on all the methods.
  290. /// </summary>
  291. /// <remarks>
  292. /// The remarks can be inherited by other methods
  293. /// using the xpath expression.
  294. /// </remarks>
  295. /// <returns>A boolean</returns>
  296. public static bool MyParentMethod(bool x) { return x; }
  297. /// <inheritdoc cref="MyParentMethod" path="//*[not(self::remarks)]"/>
  298. public static bool MyChildMethod() { return false; }
  299. }
  300. /// <summary>
  301. /// This is a generic class.
  302. /// </summary>
  303. /// <remarks>
  304. /// This example shows how to specify the <see cref="GenericClass{T}"/>
  305. /// type as a cref attribute.
  306. /// In generic classes and methods, you'll often want to reference the
  307. /// generic type, or the type parameter.
  308. /// </remarks>
  309. public class GenericClass<T>
  310. {
  311. // Fields and members.
  312. }
  313. /// <summary>
  314. /// This class validates the behavior for mapping
  315. /// generic types to open generics for use in
  316. /// typeof expressions.
  317. /// </summary>
  318. public class GenericParent
  319. {
  320. /// <summary>
  321. /// This property is a nullable value type.
  322. /// </summary>
  323. public int? Id { get; set; }
  324. /// <summary>
  325. /// This property is a nullable reference type.
  326. /// </summary>
  327. public string? Name { get; set; }
  328. /// <summary>
  329. /// This property is a generic type containing a tuple.
  330. /// </summary>
  331. public Task<(int, string)> TaskOfTupleProp { get; set; }
  332. /// <summary>
  333. /// This property is a tuple with a generic type inside.
  334. /// </summary>
  335. public (int, Dictionary<int, string>) TupleWithGenericProp { get; set; }
  336. /// <summary>
  337. /// This property is a tuple with a nested generic type inside.
  338. /// </summary>
  339. public (int, Dictionary<int, Dictionary<string, int>>) TupleWithNestedGenericProp { get; set; }
  340. /// <summary>
  341. /// This method returns a generic type containing a tuple.
  342. /// </summary>
  343. public static Task<(int, string)> GetTaskOfTuple()
  344. {
  345. return Task.FromResult((1, "test"));
  346. }
  347. /// <summary>
  348. /// This method returns a tuple with a generic type inside.
  349. /// </summary>
  350. public static (int, Dictionary<int, string>) GetTupleOfTask()
  351. {
  352. return (1, new Dictionary<int, string>());
  353. }
  354. /// <summary>
  355. /// This method return a tuple with a generic type containing a
  356. /// type parameter inside.
  357. /// </summary>
  358. public static (int, Dictionary<int, T>) GetTupleOfTask1<T>()
  359. {
  360. return (1, new Dictionary<int, T>());
  361. }
  362. /// <summary>
  363. /// This method return a tuple with a generic type containing a
  364. /// type parameter inside.
  365. /// </summary>
  366. public static (T, Dictionary<int, string>) GetTupleOfTask2<T>()
  367. {
  368. return (default, new Dictionary<int, string>());
  369. }
  370. /// <summary>
  371. /// This method returns a nested generic with all types resolved.
  372. /// </summary>
  373. public static Dictionary<int, Dictionary<int, string>> GetNestedGeneric()
  374. {
  375. return new Dictionary<int, Dictionary<int, string>>();
  376. }
  377. /// <summary>
  378. /// This method returns a nested generic with a type parameter.
  379. /// </summary>
  380. public static Dictionary<int, Dictionary<int, T>> GetNestedGeneric1<T>()
  381. {
  382. return new Dictionary<int, Dictionary<int, T>>();
  383. }
  384. }
  385. /// <summary>
  386. /// This shows examples of typeparamref and typeparam tags
  387. /// </summary>
  388. public class ParamsAndParamRefs
  389. {
  390. /// <summary>
  391. /// The GetGenericValue method.
  392. /// </summary>
  393. /// <remarks>
  394. /// This sample shows how to specify the <see cref="GetGenericValue"/>
  395. /// method as a cref attribute.
  396. /// The parameter and return value are both of an arbitrary type,
  397. /// <typeparamref name="T"/>
  398. /// </remarks>
  399. public static T GetGenericValue<T>(T para)
  400. {
  401. return para;
  402. }
  403. }
  404. """;
  405. var generator = new XmlCommentGenerator();
  406. await SnapshotTestHelper.Verify(source, generator, out var compilation);
  407. await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
  408. {
  409. var path = document.Paths["/example-class"].Operations[OperationType.Post];
  410. var exampleClass = path.RequestBody.Content["application/json"].Schema;
  411. Assert.Equal("Every class and member should have a one sentence\nsummary describing its purpose.", exampleClass.Description, ignoreLineEndingDifferences: true);
  412. Assert.Equal("The `Label` property represents a label\nfor this instance.", exampleClass.Properties["label"].Description, ignoreLineEndingDifferences: true);
  413. path = document.Paths["/person"].Operations[OperationType.Post];
  414. var person = path.RequestBody.Content["application/json"].Schema;
  415. Assert.Equal("This is an example of a positional record.", person.Description);
  416. Assert.Equal("This tag will apply to the primary constructor parameter.", person.Properties["firstName"].Description);
  417. Assert.Equal("This tag will apply to the primary constructor parameter.", person.Properties["lastName"].Description);
  418. path = document.Paths["/derived-class"].Operations[OperationType.Post];
  419. var derivedClass = path.RequestBody.Content["application/json"].Schema;
  420. Assert.Equal("A summary about this class.", derivedClass.Description);
  421. path = document.Paths["/main-class"].Operations[OperationType.Post];
  422. var mainClass = path.RequestBody.Content["application/json"].Schema;
  423. Assert.Equal("A summary about this class.", mainClass.Description);
  424. path = document.Paths["/implementing-class"].Operations[OperationType.Post];
  425. var implementingClass = path.RequestBody.Content["application/json"].Schema;
  426. Assert.Equal("This interface would describe all the methods in\nits contract.", implementingClass.Description, ignoreLineEndingDifferences: true);
  427. path = document.Paths["/inherit-only-returns"].Operations[OperationType.Post];
  428. var inheritOnlyReturns = path.RequestBody.Content["application/json"].Schema;
  429. Assert.Equal("This class shows hows you can \"inherit\" the doc\ncomments from one method in another method.", inheritOnlyReturns.Description, ignoreLineEndingDifferences: true);
  430. path = document.Paths["/inherit-all-but-remarks"].Operations[OperationType.Post];
  431. var inheritAllButRemarks = path.RequestBody.Content["application/json"].Schema;
  432. Assert.Equal("This class shows an example of sharing comments across methods.", inheritAllButRemarks.Description);
  433. path = document.Paths["/generic-class"].Operations[OperationType.Post];
  434. var genericClass = path.RequestBody.Content["application/json"].Schema;
  435. Assert.Equal("This is a generic class.", genericClass.Description);
  436. path = document.Paths["/generic-parent"].Operations[OperationType.Post];
  437. var genericParent = path.RequestBody.Content["application/json"].Schema;
  438. Assert.Equal("This class validates the behavior for mapping\ngeneric types to open generics for use in\ntypeof expressions.", genericParent.Description, ignoreLineEndingDifferences: true);
  439. Assert.Equal("This property is a nullable value type.", genericParent.Properties["id"].Description);
  440. Assert.Equal("This property is a nullable reference type.", genericParent.Properties["name"].Description);
  441. Assert.Equal("This property is a generic type containing a tuple.", genericParent.Properties["taskOfTupleProp"].Description);
  442. Assert.Equal("This property is a tuple with a generic type inside.", genericParent.Properties["tupleWithGenericProp"].Description);
  443. Assert.Equal("This property is a tuple with a nested generic type inside.", genericParent.Properties["tupleWithNestedGenericProp"].Description);
  444. path = document.Paths["/params-and-param-refs"].Operations[OperationType.Post];
  445. var paramsAndParamRefs = path.RequestBody.Content["application/json"].Schema;
  446. Assert.Equal("This shows examples of typeparamref and typeparam tags", paramsAndParamRefs.Description);
  447. });
  448. }
  449. }