diff --git a/README.md b/README.md
index 99807be..cce2494 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,6 @@ Perfect for developers seeking both flexibility and precision in their API solut
## Example
```cs
-
var collection = new ServiceCollection();
using var services = collection.BuildServiceProvider();
services.AddOpenApi(opt =>
@@ -37,6 +36,53 @@ services.AddOpenApi(opt =>
## Extensions
+### Enrich with XML comments
+XML documentation is often used to make a comprehensive documentation to your API documentation.
+By using XML comments it is easy to describe various aspects of your code, including methods, properties, parameters, return values, and more.
+
+To create a detailed documentation using XML comments, you need to enable it in your project. Like shown below:
+
+```xml
+
+
+
+ net9.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+```
+
+After enabling XML Comments you will maybe see a new warning show up `CS1591`, to suppress the warning add this to your csproj file:
+
+```xml
+...
+
+ $(NoWarn);1591
+
+...
+```
+
+Lastly add it to your Open Api configuration
+
+```cs
+var collection = new ServiceCollection();
+using var services = collection.BuildServiceProvider();
+services.AddOpenApi(opt =>
+ {
+ opt.OpenApiVersion = OpenApiSpecVersion.OpenApi3_0;
+ opt.ConfigureNodaTime();
+ opt.AddXmlComments();
+ });
+```
+
## NodaTime
Allows to configure Asp.Net Core and OpenApi to use NodaTime types.
diff --git a/Samples/OpenApi.Sample/Module.cs b/Samples/OpenApi.Sample/Module.cs
new file mode 100644
index 0000000..f9ce44d
--- /dev/null
+++ b/Samples/OpenApi.Sample/Module.cs
@@ -0,0 +1,3 @@
+namespace OpenApi.Sample;
+
+internal class Module;
diff --git a/Samples/OpenApi.Sample/OpenApi.Sample.csproj b/Samples/OpenApi.Sample/OpenApi.Sample.csproj
index 62af060..aedb11b 100644
--- a/Samples/OpenApi.Sample/OpenApi.Sample.csproj
+++ b/Samples/OpenApi.Sample/OpenApi.Sample.csproj
@@ -3,6 +3,7 @@
net9.0
enable
+ true
diff --git a/Samples/OpenApi.Sample/Program.cs b/Samples/OpenApi.Sample/Program.cs
index 94e1b3b..1a33cf8 100644
--- a/Samples/OpenApi.Sample/Program.cs
+++ b/Samples/OpenApi.Sample/Program.cs
@@ -8,6 +8,7 @@
using NodaTime;
using OpenApi.Extensions.Extensions;
using OpenApi.NodaTime.Extensions;
+using OpenApi.Sample;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
@@ -18,6 +19,7 @@
{
opt.AddDescription("This project contains samples on the extensions library OpenApi.Extensions.");
opt.ConfigureNodaTime();
+ opt.AddXmlComments();
opt.AddResponseType(HttpStatusCode.BadRequest);
});
@@ -54,7 +56,13 @@
app.Run();
+///
+/// Shows the weather forecast
+///
internal record WeatherForecast(LocalDate Date, int TemperatureC, string? Summary)
{
+ ///
+ /// Uses TemperatureC to calculate TemperatureF
+ ///
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
diff --git a/src/OpenApi.Extensions/Extensions/OpenApiOptionsExtensions.cs b/src/OpenApi.Extensions/Extensions/OpenApiOptionsExtensions.cs
index 1ee2dda..ea78da1 100644
--- a/src/OpenApi.Extensions/Extensions/OpenApiOptionsExtensions.cs
+++ b/src/OpenApi.Extensions/Extensions/OpenApiOptionsExtensions.cs
@@ -2,13 +2,15 @@
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
-using OpenApi.Extensions.Transformers;
+using OpenApi.Extensions.Transformers.Documents;
using OpenApi.Extensions.Transformers.Operations;
+using OpenApi.Extensions.Transformers.Schemas;
namespace OpenApi.Extensions.Extensions;
@@ -466,6 +468,31 @@ public static OpenApiOptions AddType(this OpenApiOptions optio
return options;
}
+ ///
+ /// Loads the assembly and adds XML documentation to the type.
+ ///
+ /// The assembly type to add XML documentation for.
+ /// The updated instance.
+ public static OpenApiOptions AddXmlComments(this OpenApiOptions options)
+ where T : class, new()
+ {
+ options.AddSchemaTransformer>();
+ return options;
+ }
+
+ ///
+ /// Loads the assembly and adds XML documentation to the type.
+ ///
+ /// The assembly type to add XML documentation for.
+ /// The updated instance.
+ public static OpenApiOptions AddXmlComments(this OpenApiOptions options,
+ Func> onChangeDescription)
+ where T : class, new()
+ {
+ options.AddSchemaTransformer(new XmlSchemaTransformer(onChangeDescription));
+ return options;
+ }
+
private static string FormatJson(T obj, JsonSerializerOptions? jsonSerializerOptions)
{
var formatToJson = JsonSerializer.Serialize(obj, jsonSerializerOptions);
diff --git a/src/OpenApi.Extensions/Transformers/DescriptionDocumentTransform.cs b/src/OpenApi.Extensions/Transformers/Documents/DescriptionDocumentTransform.cs
similarity index 91%
rename from src/OpenApi.Extensions/Transformers/DescriptionDocumentTransform.cs
rename to src/OpenApi.Extensions/Transformers/Documents/DescriptionDocumentTransform.cs
index d2c3cd3..63b8ec1 100644
--- a/src/OpenApi.Extensions/Transformers/DescriptionDocumentTransform.cs
+++ b/src/OpenApi.Extensions/Transformers/Documents/DescriptionDocumentTransform.cs
@@ -3,7 +3,7 @@
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;
-namespace OpenApi.Extensions.Transformers;
+namespace OpenApi.Extensions.Transformers.Documents;
public class DescriptionDocumentTransform : IOpenApiDocumentTransformer
{
diff --git a/src/OpenApi.Extensions/Transformers/Schemas/XmlSchemaTransformer.cs b/src/OpenApi.Extensions/Transformers/Schemas/XmlSchemaTransformer.cs
new file mode 100644
index 0000000..0e459cf
--- /dev/null
+++ b/src/OpenApi.Extensions/Transformers/Schemas/XmlSchemaTransformer.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using System.Reflection;
+using System.Text.Json.Serialization.Metadata;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.XPath;
+using Microsoft.AspNetCore.OpenApi;
+using Microsoft.OpenApi.Models;
+
+namespace OpenApi.Extensions.Transformers.Schemas;
+
+public class XmlSchemaTransformer : IOpenApiSchemaTransformer
+ where T : class, new()
+{
+ private readonly ConcurrentDictionary _descriptions = [];
+ private readonly Func>? _getDescription;
+ private readonly Lazy _navigator;
+
+ public XmlSchemaTransformer(Func>? getDescription = null)
+ {
+ _getDescription = getDescription;
+ _navigator = new Lazy(CreateNavigator);
+ }
+
+ public async Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
+ {
+ var memberInfo = context.JsonPropertyInfo?.DeclaringType;
+ if (schema.Description is null &&
+ memberInfo is not null &&
+ GetMemberName(context.JsonTypeInfo, context.JsonPropertyInfo) is { Length: > 0 } memberName)
+ {
+ schema.Description = await GetDescription(memberName, context.JsonTypeInfo, context.JsonPropertyInfo);
+ }
+ }
+
+ private async ValueTask GetDescription(string memberName, JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo)
+ {
+ if (_descriptions.TryGetValue(memberName, out var description))
+ {
+ return description;
+ }
+
+ var navigator = _navigator.Value;
+
+ var xpath = $"/doc/members/member[@name='{memberName}']/summary";
+ var summaryNode = navigator?.SelectSingleNode(xpath);
+ description = summaryNode?.Value.Trim();
+ if (_getDescription is not null)
+ {
+ description = await _getDescription(typeInfo, propertyInfo, description);
+ }
+
+ _descriptions[memberName] = description;
+
+ return description;
+ }
+
+ private static XPathNavigator? CreateNavigator()
+ {
+ var assemblyPath = typeof(T).Assembly.Location;
+ var xmlPath = Path.ChangeExtension(assemblyPath, ".xml");
+
+ if (!File.Exists(xmlPath))
+ {
+ return null;
+ }
+
+ using var reader = XmlReader.Create(xmlPath);
+ return new XPathDocument(reader).CreateNavigator();
+ }
+
+ private static string? GetMemberName(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo)
+ {
+ if (propertyInfo is null)
+ {
+ return $"T:{typeInfo.Type.FullName}";
+ }
+
+ var declaringType = propertyInfo.DeclaringType.FullName;
+ if (declaringType is null)
+ {
+ return null;
+ }
+
+ var memberName = propertyInfo.AttributeProvider switch
+ {
+ MemberInfo member => member.Name,
+ _ => $"{char.ToUpperInvariant(propertyInfo.Name[0])}{propertyInfo.Name[1..]}"
+ };
+
+ // Determine member type prefix: Property (P) or Field (F)
+ var memberType = propertyInfo.AttributeProvider switch
+ {
+ PropertyInfo => "P",
+ FieldInfo => "F",
+ _ => null
+ };
+
+ return memberType is null
+ ? null
+ : $"{memberType}:{declaringType}{Type.Delimiter}{memberName}";
+ }
+}