From 76ec39d2db343566086e29247970ca7f59017eb7 Mon Sep 17 00:00:00 2001
From: stb-co <93922970+stb-co@users.noreply.github.com>
Date: Thu, 16 May 2024 14:47:23 +0200
Subject: [PATCH] Fix schema generation for C# 9 positional record with no
 example (#2901)

* Add schema filter test for when example tag is not present

* Add datetime to schema filter example tag positive test

* Fix missing example property on record xmldoc param tag causing unexpected empty example string in generated schema
---
 .../XmlComments/XmlCommentsSchemaFilter.cs    |  5 +-
 .../Fixtures/XmlAnnotatedRecord.cs            |  4 +-
 .../XmlAnnotatedRecordWithoutExample.cs       | 35 +++++++++
 .../Fixtures/XmlAnnotatedType.cs              |  6 ++
 .../XmlAnnotatedTypeWithoutExample.cs         | 71 +++++++++++++++++++
 .../XmlCommentsSchemaFilterTests.cs           | 42 +++++++++++
 6 files changed, 161 insertions(+), 2 deletions(-)
 create mode 100644 test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecordWithoutExample.cs
 create mode 100644 test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedTypeWithoutExample.cs

diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs
index 4075034e27..a246657d2c 100644
--- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs
+++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs
@@ -50,7 +50,10 @@ private void ApplyMemberTags(OpenApiSchema schema, SchemaFilterContext context)
                     schema.Description = XmlCommentsTextHelper.Humanize(summaryNode);
 
                 var example = recordDefaultConstructorProperty.GetAttribute("example", string.Empty);
-                TrySetExample(schema, context, example);
+                if (!string.IsNullOrEmpty(example))
+                {
+                    TrySetExample(schema, context, example);
+                }
             }
 
             if (fieldOrPropertyNode != null)
diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecord.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecord.cs
index 7b35dbc335..c337c379ed 100644
--- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecord.cs
+++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecord.cs
@@ -4,13 +4,14 @@
 namespace Swashbuckle.AspNetCore.SwaggerGen.Test
 {
     /// <summary>
-    /// Summary for XmlAnnotatedType
+    /// Summary for XmlAnnotatedRecord
     /// </summary>
     /// <param name="BoolProperty" example="true">Summary for BoolProperty</param>
     /// <param name="IntProperty" example="10">Summary for IntProperty</param>
     /// <param name="LongProperty" example="4294967295">Summary for LongProperty</param>
     /// <param name="FloatProperty" example="1.2">Summary for FloatProperty</param>
     /// <param name="DoubleProperty" example="1.25">Summary for DoubleProperty</param>
+    /// <param name="DateTimeProperty" example="6/22/2022 12:00:00 AM">Summary for DateTimeProperty</param>
     /// <param name="EnumProperty" example="2">Summary for EnumProperty</param>
     /// <param name="GuidProperty" example="d3966535-2637-48fa-b911-e3c27405ee09">Summary for GuidProperty</param>
     /// <param name="StringPropertyWithNullExample" example="null">Summary for Nullable StringPropertyWithNullExample</param>
@@ -23,6 +24,7 @@ public record XmlAnnotatedRecord(
         long LongProperty,
         float FloatProperty,
         double DoubleProperty,
+        DateTime DateTimeProperty,
         IntEnum EnumProperty,
         Guid GuidProperty,
         string StringPropertyWithNullExample,
diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecordWithoutExample.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecordWithoutExample.cs
new file mode 100644
index 0000000000..2eea51841e
--- /dev/null
+++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecordWithoutExample.cs
@@ -0,0 +1,35 @@
+using System;
+using Swashbuckle.AspNetCore.TestSupport;
+
+namespace Swashbuckle.AspNetCore.SwaggerGen.Test
+{
+    /// <summary>
+    /// Summary for XmlAnnotatedRecordWithoutExample
+    /// </summary>
+    /// <param name="BoolProperty">Summary for BoolProperty</param>
+    /// <param name="IntProperty">Summary for IntProperty</param>
+    /// <param name="LongProperty">Summary for LongProperty</param>
+    /// <param name="FloatProperty">Summary for FloatProperty</param>
+    /// <param name="DoubleProperty">Summary for DoubleProperty</param>
+    /// <param name="DateTimeProperty">Summary for DateTimeProperty</param>
+    /// <param name="EnumProperty">Summary for EnumProperty</param>
+    /// <param name="GuidProperty">Summary for GuidProperty</param>
+    /// <param name="StringPropertyWithNullExample">Summary for Nullable StringPropertyWithNullExample</param>
+    /// <param name="StringProperty">Summary for StringProperty</param>
+    /// <param name="StringPropertyWithUri">Summary for StringPropertyWithUri</param>
+    /// <param name="ObjectProperty">Summary for ObjectProperty</param>
+    public record XmlAnnotatedRecordWithoutExample(
+        bool BoolProperty,
+        int IntProperty,
+        long LongProperty,
+        float FloatProperty,
+        double DoubleProperty,
+        DateTime DateTimeProperty,
+        IntEnum EnumProperty,
+        Guid GuidProperty,
+        string StringPropertyWithNullExample,
+        string StringProperty,
+        string StringPropertyWithUri,
+        object ObjectProperty
+    );
+}
\ No newline at end of file
diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedType.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedType.cs
index a61b54bd72..4227d35ffd 100644
--- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedType.cs
+++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedType.cs
@@ -45,6 +45,12 @@ public class XmlAnnotatedType
         /// <example>1.25</example>
         public double DoubleProperty { get; set; }
 
+        /// <summary>
+        /// Summary for DateTimeProperty
+        /// </summary>
+        /// <example>6/22/2022 12:00:00 AM</example>
+        public DateTime DateTimeProperty { get; set; }
+
         /// <summary>
         /// Summary for EnumProperty
         /// </summary>
diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedTypeWithoutExample.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedTypeWithoutExample.cs
new file mode 100644
index 0000000000..f06f01e840
--- /dev/null
+++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedTypeWithoutExample.cs
@@ -0,0 +1,71 @@
+using System;
+using Swashbuckle.AspNetCore.TestSupport;
+
+namespace Swashbuckle.AspNetCore.SwaggerGen.Test
+{
+    /// <summary>
+    /// Summary for XmlAnnotatedTypeWithoutExample
+    /// </summary>
+    public class XmlAnnotatedTypeWithoutExample
+    {
+        /// <summary>
+        /// Summary for BoolProperty
+        /// </summary>
+        public bool BoolProperty { get; set; }
+
+        /// <summary>
+        /// Summary for IntProperty
+        /// </summary>
+        public int IntProperty { get; set; }
+
+        /// <summary>
+        /// Summary for LongProperty
+        /// </summary>
+        public long LongProperty { get; set; }
+
+        /// <summary>
+        /// Summary for FloatProperty
+        /// </summary>
+        public float FloatProperty { get; set; }
+
+        /// <summary>
+        /// Summary for DoubleProperty
+        /// </summary>
+        public double DoubleProperty { get; set; }
+
+        /// <summary>
+        /// Summary for DateTimeProperty
+        /// </summary>
+        public DateTime DateTimeProperty { get; set; }
+
+        /// <summary>
+        /// Summary for EnumProperty
+        /// </summary>
+        public IntEnum EnumProperty { get; set; }
+
+        /// <summary>
+        /// Summary for GuidProperty
+        /// </summary>
+        public Guid GuidProperty { get; set; }
+
+        /// <summary>
+        /// Summary for Nullable StringPropertyWithNullExample
+        /// </summary>
+        public string StringPropertyWithNullExample { get; set; }
+
+        /// <summary>
+        /// Summary for StringProperty
+        /// </summary>
+        public string StringProperty { get; set; }
+
+        /// <summary>
+        /// Summary for StringPropertyWithUri
+        /// </summary>
+        public string StringPropertyWithUri { get; set; }
+
+        /// <summary>
+        /// Summary for ObjectProperty
+        /// </summary>
+        public object ObjectProperty { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsSchemaFilterTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsSchemaFilterTests.cs
index ecfba3e080..36c32db543 100644
--- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsSchemaFilterTests.cs
+++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsSchemaFilterTests.cs
@@ -62,6 +62,7 @@ public void Apply_SetsDescription_FromPropertySummaryTag(
         [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.LongProperty), "integer", "4294967295")]
         [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.FloatProperty), "number", "1.2")]
         [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.DoubleProperty), "number", "1.25")]
+        [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.DateTimeProperty), "string", "\"6/22/2022 12:00:00 AM\"")]
         [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.EnumProperty), "integer", "2")]
         [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.GuidProperty), "string", "\"d3966535-2637-48fa-b911-e3c27405ee09\"")]
         [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.StringProperty), "string", "\"Example for StringProperty\"")]
@@ -73,6 +74,7 @@ public void Apply_SetsDescription_FromPropertySummaryTag(
         [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.LongProperty), "integer", "4294967295")]
         [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.FloatProperty), "number", "1.2")]
         [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.DoubleProperty), "number", "1.25")]
+        [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.DateTimeProperty), "string", "\"6/22/2022 12:00:00 AM\"")]
         [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.EnumProperty), "integer", "2")]
         [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.GuidProperty), "string", "\"d3966535-2637-48fa-b911-e3c27405ee09\"")]
         [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.StringProperty), "string", "\"Example for StringProperty\"")]
@@ -96,6 +98,46 @@ public void Apply_SetsExample_FromPropertyExampleTag(
             Assert.Equal(expectedExampleAsJson, schema.Example.ToJson());
         }
 
+        [Theory]
+        [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.BoolProperty), "boolean")]
+        [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.IntProperty), "integer")]
+        [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.LongProperty), "integer")]
+        [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.FloatProperty), "number")]
+        [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.DoubleProperty), "number")]
+        [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.DateTimeProperty), "string")]
+        [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.EnumProperty), "integer")]
+        [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.GuidProperty), "string")]
+        [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.StringProperty), "string")]
+        [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.ObjectProperty), "object")]
+        [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.StringPropertyWithNullExample), "string")]
+        [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.StringPropertyWithUri), "string")]
+        [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.BoolProperty), "boolean")]
+        [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.IntProperty), "integer")]
+        [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.LongProperty), "integer")]
+        [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.FloatProperty), "number")]
+        [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.DoubleProperty), "number")]
+        [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.DateTimeProperty), "string")]
+        [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.EnumProperty), "integer")]
+        [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.GuidProperty), "string")]
+        [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.StringProperty), "string")]
+        [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.ObjectProperty), "object")]
+        [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.StringPropertyWithNullExample), "string")]
+        [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.StringPropertyWithUri), "string")]
+        [UseInvariantCulture]
+        public void Apply_DoesNotSetExample_WhenPropertyExampleTagIsNotProvided(
+            Type declaringType,
+            string propertyName,
+            string schemaType)
+        {
+            var propertyInfo = declaringType.GetProperty(propertyName);
+            var schema = new OpenApiSchema { Type = schemaType };
+            var filterContext = new SchemaFilterContext(propertyInfo.PropertyType, null, null, memberInfo: propertyInfo);
+
+            Subject().Apply(schema, filterContext);
+
+            Assert.Null(schema.Example);
+        }
+
         [Theory]
         [InlineData("en-US", 1.2F)]
         [InlineData("sv-SE", 1.2F)]