Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 60 additions & 9 deletions src/OpenApi/gen/XmlComments/MemberKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,68 @@ internal sealed record MemberKey(

public static MemberKey FromMethodSymbol(IMethodSymbol method)
{
string returnType;
if (method.ReturnsVoid)
{
returnType = "typeof(void)";
}
else
{
// Handle Task/ValueTask for async methods
var actualReturnType = method.ReturnType;
if (method.IsAsync && actualReturnType is INamedTypeSymbol namedType)
{
if (namedType.TypeArguments.Length > 0)
{
actualReturnType = namedType.TypeArguments[0];
}
else
{
returnType = "typeof(void)";
}
}

returnType = actualReturnType.TypeKind == TypeKind.TypeParameter
? "typeof(object)"
: $"typeof({actualReturnType.ToDisplayString(_typeKeyFormat)})";
}

// Handle extension methods by skipping the 'this' parameter
var parameters = method.Parameters
.Where(p => !p.IsThis)
.Select(p =>
{
if (p.Type.TypeKind == TypeKind.TypeParameter)
{
return "typeof(object)";
}

// For params arrays, use the array type
if (p.IsParams && p.Type is IArrayTypeSymbol arrayType)
{
return $"typeof({arrayType.ToDisplayString(_typeKeyFormat)})";
}

return $"typeof({p.Type.ToDisplayString(_typeKeyFormat)})";
})
.ToArray();

// For generic methods, use the containing type with generic parameters
var declaringType = method.ContainingType;
var typeDisplay = declaringType.ToDisplayString(_typeKeyFormat);

// If the method is in a generic type, we need to handle the type parameters
if (declaringType.IsGenericType)
{
typeDisplay = ReplaceGenericArguments(typeDisplay);
}

return new MemberKey(
$"typeof({ReplaceGenericArguments(method.ContainingType.ToDisplayString(_typeKeyFormat))})",
$"typeof({typeDisplay})",
MemberType.Method,
method.MetadataName,
method.ReturnType.TypeKind == TypeKind.TypeParameter
? "typeof(object)"
: $"typeof({method.ReturnType.ToDisplayString(_typeKeyFormat)})",
[.. method.Parameters.Select(p =>
p.Type.TypeKind == TypeKind.TypeParameter
? "typeof(object)"
: $"typeof({p.Type.ToDisplayString(_typeKeyFormat)})")]);
method.MetadataName, // Use MetadataName to match runtime MethodInfo.Name
returnType,
parameters);
}

public static MemberKey FromPropertySymbol(IPropertySymbol property)
Expand Down
2 changes: 1 addition & 1 deletion src/OpenApi/gen/XmlComments/XmlComment.InheritDoc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Xml;

/// <remarks>
/// Source code in this class is derived from Roslyn's Xml documentation comment processing
/// Source code in this class is derived from Roslyn's XML documentation comment processing
/// to support the full resolution of <inheritdoc /> tags.
/// For the original code, see https://github.com/dotnet/roslyn/blob/ef1d7fe925c94e96a93e4c9af50983e0f675a9fd/src/Workspaces/Core/Portable/Shared/Extensions/ISymbolExtensions.cs.
/// </remarks>
Expand Down
2 changes: 1 addition & 1 deletion src/OpenApi/gen/XmlComments/XmlComment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ private static void ResolveCrefLink(Compilation compilation, XNode node, string
var symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(cref, compilation);
if (symbol is not null)
{
var type = symbol.ToDisplayString();
var type = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
item.ReplaceWith(new XText(type));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json.Nodes;
using Microsoft.OpenApi.Models;

namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;

[UsesVerify]
public partial class AdditionalTextsTests
{
[Fact]
public async Task CanHandleXmlForSchemasInAdditionalTexts()
{
var source = """
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ClassLibrary;
var builder = WebApplication.CreateBuilder();
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapPost("/todo", (Todo todo) => { });
app.MapPost("/project", (Project project) => { });
app.MapPost("/board", (ProjectBoard.BoardItem boardItem) => { });
app.MapPost("/project-record", (ProjectRecord project) => { });
app.MapPost("/todo-with-description", (TodoWithDescription todo) => { });
app.MapPost("/type-with-examples", (TypeWithExamples typeWithExamples) => { });
app.Run();
""";

var librarySource = """
using System;
namespace ClassLibrary;
/// <summary>
/// This is a todo item.
/// </summary>
public class Todo
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
/// <summary>
/// The project that contains <see cref="Todo"/> items.
/// </summary>
public record Project(string Name, string Description);
public class ProjectBoard
{
/// <summary>
/// An item on the board.
/// </summary>
public class BoardItem
{
public string Name { get; set; }
}
}
/// <summary>
/// The project that contains <see cref="Todo"/> items.
/// </summary>
/// <param name="Name">The name of the project.</param>
/// <param name="Description">The description of the project.</param>
public record ProjectRecord(string Name, string Description);
public class TodoWithDescription
{
/// <summary>
/// The identifier of the todo.
/// </summary>
public int Id { get; set; }
/// <value>
/// The name of the todo.
/// </value>
public string Name { get; set; }
/// <summary>
/// A description of the the todo.
/// </summary>
/// <value>Another description of the todo.</value>
public string Description { get; set; }
}
public class TypeWithExamples
{
/// <example>true</example>
public bool BooleanType { get; set; }
/// <example>42</example>
public int IntegerType { get; set; }
/// <example>1234567890123456789</example>
public long LongType { get; set; }
/// <example>3.14</example>
public double DoubleType { get; set; }
/// <example>3.14</example>
public float FloatType { get; set; }
/// <example>2022-01-01T00:00:00Z</example>
public DateTime DateTimeType { get; set; }
/// <example>2022-01-01</example>
public DateOnly DateOnlyType { get; set; }
/// <example>Hello, World!</example>
public string StringType { get; set; }
/// <example>2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d</example>
public Guid GuidType { get; set; }
/// <example>12:30:45</example>
public TimeOnly TimeOnlyType { get; set; }
/// <example>P3DT4H5M</example>
public TimeSpan TimeSpanType { get; set; }
/// <example>255</example>
public byte ByteType { get; set; }
/// <example>3.14159265359</example>
public decimal DecimalType { get; set; }
/// <example>https://example.com</example>
public Uri UriType { get; set; }
}
""";
var references = new Dictionary<string, List<string>>
{
{ "ClassLibrary", [librarySource] }
};

var generator = new XmlCommentGenerator();
await SnapshotTestHelper.Verify(source, generator, references, out var compilation, out var additionalAssemblies);
await SnapshotTestHelper.VerifyOpenApi(compilation, additionalAssemblies, document =>
{
var path = document.Paths["/todo"].Operations[OperationType.Post];
var todo = path.RequestBody.Content["application/json"].Schema;
Assert.Equal("This is a todo item.", todo.Description);

path = document.Paths["/project"].Operations[OperationType.Post];
var project = path.RequestBody.Content["application/json"].Schema;
Assert.Equal("The project that contains Todo items.", project.Description);

path = document.Paths["/board"].Operations[OperationType.Post];
var board = path.RequestBody.Content["application/json"].Schema;
Assert.Equal("An item on the board.", board.Description);

path = document.Paths["/project-record"].Operations[OperationType.Post];
project = path.RequestBody.Content["application/json"].Schema;

Assert.Equal("The name of the project.", project.Properties["name"].Description);
Assert.Equal("The description of the project.", project.Properties["description"].Description);

path = document.Paths["/todo-with-description"].Operations[OperationType.Post];
todo = path.RequestBody.Content["application/json"].Schema;
Assert.Equal("The identifier of the todo.", todo.Properties["id"].Description);
Assert.Equal("The name of the todo.", todo.Properties["name"].Description);
Assert.Equal("Another description of the todo.", todo.Properties["description"].Description);

path = document.Paths["/type-with-examples"].Operations[OperationType.Post];
var typeWithExamples = path.RequestBody.Content["application/json"].Schema;

var booleanTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["booleanType"].Example);
Assert.True(booleanTypeExample.GetValue<bool>());

var integerTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["integerType"].Example);
Assert.Equal(42, integerTypeExample.GetValue<int>());

var longTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["longType"].Example);
Assert.Equal(1234567890123456789, longTypeExample.GetValue<long>());

// Broken due to https://github.com/microsoft/OpenAPI.NET/issues/2137
// var doubleTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["doubleType"].Example);
// Assert.Equal("3.14", doubleTypeExample.GetValue<string>());

// var floatTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["floatType"].Example);
// Assert.Equal(3.14f, floatTypeExample.GetValue<float>());

// var dateTimeTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["dateTimeType"].Example);
// Assert.Equal(DateTime.Parse("2022-01-01T00:00:00Z", CultureInfo.InvariantCulture), dateTimeTypeExample.GetValue<DateTime>());

// var dateOnlyTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["dateOnlyType"].Example);
// Assert.Equal(DateOnly.Parse("2022-01-01", CultureInfo.InvariantCulture), dateOnlyTypeExample.GetValue<DateOnly>());

var stringTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["stringType"].Example);
Assert.Equal("Hello, World!", stringTypeExample.GetValue<string>());

var guidTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["guidType"].Example);
Assert.Equal("2d8f1eac-b5c6-4e29-8c62-4d9d75ef3d3d", guidTypeExample.GetValue<string>());

var byteTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["byteType"].Example);
Assert.Equal(255, byteTypeExample.GetValue<int>());

// Broken due to https://github.com/microsoft/OpenAPI.NET/issues/2137
// var timeOnlyTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["timeOnlyType"].Example);
// Assert.Equal(TimeOnly.Parse("12:30:45", CultureInfo.InvariantCulture), timeOnlyTypeExample.GetValue<TimeOnly>());

// var timeSpanTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["timeSpanType"].Example);
// Assert.Equal(TimeSpan.Parse("P3DT4H5M", CultureInfo.InvariantCulture), timeSpanTypeExample.GetValue<TimeSpan>());

// var decimalTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["decimalType"].Example);
// Assert.Equal(3.14159265359m, decimalTypeExample.GetValue<decimal>());

var uriTypeExample = Assert.IsAssignableFrom<JsonNode>(typeWithExamples.Properties["uriType"].Example);
Assert.Equal("https://example.com", uriTypeExample.GetValue<string>());
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public async Task SupportsAllXmlTagsOnSchemas()
{
var source = """
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

Expand Down Expand Up @@ -180,6 +181,29 @@ public static int Add(int left, int right)

return left + right;
}

/// <summary>
/// This method is an example of a method that
/// returns an awaitable item.
/// </summary>
public static Task<int> AddAsync(int left, int right)
{
return Task.FromResult(Add(left, right));
}

/// <summary>
/// This method is an example of a method that consumes
/// an params array.
/// </summary>
public static int AddNumbers(params int[] numbers)
{
var sum = 0;
foreach (var number in numbers)
{
sum += number;
}
return sum;
}
}

/// <summary>
Expand Down
Loading
Loading