Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ namespace Asp.Versioning;
/// Represents API version parser from a type namespace.
/// </summary>
/// <remarks>
/// The namespace identifier can use 'v', 'V', or '_' as a prefix. The '_' prefix is useful when
/// a folder starts with a number because Visual Studio automatically prefixes it with an underscore.
/// For example, <c>Contoso.Api._2018_04_01.Controllers</c> is equivalent to <c>Contoso.Api.v2018_04_01.Controllers</c>.
/// The namespace identifier can use <c>'v'</c>, <c>'V'</c>, or <c>'_'</c> as a prefix. The <c>'_'</c> prefix is useful
/// when the source folder starts with a number and the editor automatically prefixes it with an underscore. As an
/// example, <c>Api._2018_04_01.Controllers</c> is equivalent to <c>Api.v2018_04_01.Controllers</c>.
/// </remarks>
public class NamespaceParser
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,8 @@ private static void AddApiExplorerServices( IApiVersioningBuilder builder )

var services = builder.Services;

// registers DefaultApiDescriptionProvider, which discovers controller-based endpoints
services.AddMvcCore().AddApiExplorer();

// registers EndpointMetadataApiDescriptionProvider, which discovers minimal API endpoints.
// both providers are required so that OpenApiDocumentService sees all endpoints.
services.AddEndpointsApiExplorer();
services.AddMvcCore().AddApiExplorer();
services.TryAddSingleton<IOptionsFactory<ApiExplorerOptions>, ApiExplorerOptionsFactory<ApiExplorerOptions>>();
services.TryAddTransient<IApiVersionDescriptionProviderFactory, ApiVersionDescriptionProviderFactory>();
services.TryAddSingleton( static sp => sp.GetRequiredService<IApiVersionDescriptionProviderFactory>().Create() );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.OpenApi" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.*" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,11 @@ private void AddLinkExtensions( OpenApiDocument document, ApiVersionDescription

if ( array.Count > 0 )
{
var obj = new JsonObject();
var extensions = document.Extensions ??= new Dictionary<string, IOpenApiExtension>();
extensions[ExtensionName] = new JsonNodeExtension( array );

obj["links"] = array;
extensions[ExtensionName] = new JsonNodeExtension( obj );
}
}

Expand Down Expand Up @@ -347,7 +350,7 @@ protected virtual JsonObject ToJson( LinkHeaderValue link )

if ( link.Languages.Count > 0 )
{
obj["lang"] = new JsonArray( link.Languages.Select( l => JsonNode.Parse( l.ToString() ) ).ToArray() );
obj["lang"] = new JsonArray( [.. link.Languages.Select( l => JsonNode.Parse( l.ToString() ) )] );
}

foreach ( var (key, value) in link.Extensions )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,40 +42,48 @@ public class XmlComments
/// Gets the <c>summary</c> from the specified member, if any.
/// </summary>
/// <param name="member">The member to get the summary from.</param>
/// <returns>The corresponding summary or an empty string.</returns>
/// <returns>The corresponding <c>&lt;summary&gt;</c> or an empty string.</returns>
public string GetSummary( MemberInfo member )
=> GetMember( member )?.Element( "summary" )?.Value.Trim() ?? string.Empty;

/// <summary>
/// Gets the <c>description</c> from the specified member, if any.
/// </summary>
/// <param name="member">The member to get the description from.</param>
/// <returns>The corresponding description or an empty string.</returns>
/// <returns>The corresponding <c>&lt;description&gt;</c> or an empty string.</returns>
public string GetDescription( MemberInfo member )
=> GetMember( member )?.Element( "description" )?.Value.Trim() ?? string.Empty;

/// <summary>
/// Gets the <c>remarks</c> from the specified member, if any.
/// </summary>
/// <param name="member">The member to get the remarks from.</param>
/// <returns>The corresponding remarks or an empty string.</returns>
/// <returns>The corresponding <c>&lt;remarks&gt;</c> or an empty string.</returns>
public string GetRemarks( MemberInfo member )
=> GetMember( member )?.Element( "remarks" )?.Value.Trim() ?? string.Empty;

/// <summary>
/// Gets the <c>returns</c> from the specified member, if any.
/// </summary>
/// <param name="member">The member to get the returns from.</param>
/// <returns>The corresponding returns or an empty string.</returns>
/// <returns>The corresponding <c>&lt;returns&gt;</c> or an empty string.</returns>
public string GetReturns( MemberInfo member )
=> GetMember( member )?.Element( "returns" )?.Value.Trim() ?? string.Empty;

/// <summary>
/// Gets the <c>example</c> from the specified member, if any.
/// </summary>
/// <param name="member">The member to get the example from.</param>
/// <returns>The corresponding <c>&lt;example&gt;</c> or an empty string.</returns>
public string GetExample( MemberInfo member )
=> GetMember( member )?.Element( "example" )?.Value.Trim() ?? string.Empty;

/// <summary>
/// Gets the <c>param</c> description from the specified member, if any.
/// </summary>
/// <param name="member">The member to get the parameter from.</param>
/// <param name="name">The name of the parameter.</param>
/// <returns>The corresponding returns or an empty string.</returns>
/// <returns>The corresponding description or an empty string.</returns>
public string GetParameterDescription( MemberInfo member, string name )
{
if ( GetMember( member ) is { } element )
Expand All @@ -89,6 +97,50 @@ public string GetParameterDescription( MemberInfo member, string name )
return string.Empty;
}

/// <summary>
/// Gets the parameter <c>example</c> from the specified member, if any.
/// </summary>
/// <param name="member">The member to get the parameter from.</param>
/// <param name="name">The name of the parameter.</param>
/// <returns>The corresponding <c>&lt;example&gt;</c> or an empty string.</returns>
public string GetParameterExample( MemberInfo member, string name )
{
if ( GetMember( member ) is { } element )
{
return element.Elements( "param" )
.FirstOrDefault( x => x.Attribute( "name" )?.Value == name )?
.Attribute( "example" )?
.Value
.Trim() ?? string.Empty;
}

return string.Empty;
}

/// <summary>
/// Gets the <c>deprecated</c> attribute from the specified member, if any.
/// </summary>
/// <param name="member">The member to get the parameter from.</param>
/// <param name="name">The name of the parameter.</param>
/// <returns><c>true</c> if the <c>deprecated</c> attribute is present with a value of <c>"true"</c>;
/// otherwise <c>false</c>.</returns>
public bool IsParameterDeprecated( MemberInfo member, string name )
{
if ( GetMember( member ) is { } element )
{
var deprecated = element.Elements( "param" )
.FirstOrDefault( x => x.Attribute( "name" )?.Value == name )?
.Attribute( "deprecated" )?.Value;

if ( deprecated is { } value )
{
return StringComparer.OrdinalIgnoreCase.Equals( value, bool.TrueString );
}
}

return false;
}

/// <summary>
/// Gets the <c>response</c> description from the specified member, if any.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public XmlCommentsFile( Assembly[] assemblies, IHostEnvironment environment )
for ( var i = 0; i < assemblies.Length; i++ )
{
var assembly = assemblies[i];
var fileName = FilePath.ChangeExtension( assembly.GetName().Name, ".xml" );
var fileName = assembly.GetName().Name + ".xml";

if ( string.IsNullOrEmpty( fileName ) )
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ namespace Asp.Versioning.OpenApi.Transformers;
using Microsoft.OpenApi;
using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using static System.Reflection.BindingFlags;

Expand Down Expand Up @@ -45,24 +47,46 @@ public virtual Task TransformAsync(
ArgumentNullException.ThrowIfNull( schema );
ArgumentNullException.ThrowIfNull( context );

if ( schema.Properties is not { } properties
|| context.JsonTypeInfo?.Type is not Type type )
if ( context.JsonTypeInfo?.Type is not Type type )
{
return Task.CompletedTask;
}

if ( string.IsNullOrEmpty( schema.Description ) )
var description = schema.Description;

if ( string.IsNullOrEmpty( description )
&& !string.IsNullOrEmpty( description = Documentation.GetSummary( type ) ) )
{
schema.Description = Documentation.GetSummary( type );
schema.Description = description;
}

if ( schema.Example is null && ToJson( Documentation.GetExample( type ) ) is { } example )
{
schema.Example = example;
}

if ( schema.Properties is not { } properties )
{
return Task.CompletedTask;
}

foreach ( var (name, prop) in properties )
{
if ( prop is not null
&& string.IsNullOrEmpty( prop.Description )
&& type.GetProperty( name, IgnoreCase | Instance | Public ) is { } property )
&& type.GetProperty( name, IgnoreCase | Instance | Public ) is { } property )
{
prop.Description = Documentation.GetSummary( property );
if ( string.IsNullOrEmpty( prop.Description )
&& !string.IsNullOrEmpty( description = Documentation.GetSummary( property ) ) )
{
prop.Description = description;
}

if ( prop.Example is null
&& prop.Examples is not null
&& ( example = ToJson( Documentation.GetExample( property ) ) ) is not null )
{
prop.Examples.Add( example );
}
}
}

Expand All @@ -88,16 +112,19 @@ public virtual Task TransformAsync(
operation.Summary = Documentation.GetSummary( method );
}

if ( string.IsNullOrEmpty( operation.Description ) )
var description = operation.Description;

if ( string.IsNullOrEmpty( description )
&& !string.IsNullOrEmpty( description = Documentation.GetDescription( method ) ) )
{
operation.Description = Documentation.GetDescription( method );
operation.Description = description;
}

if ( operation.Responses is { } responses )
{
foreach ( var (statusCode, response) in responses )
{
var description = Documentation.GetResponseDescription( method, statusCode );
description = Documentation.GetResponseDescription( method, statusCode );

if ( !string.IsNullOrEmpty( description ) )
{
Expand All @@ -118,18 +145,40 @@ public virtual Task TransformAsync(
{
var parameter = parameters[i];

if ( !string.IsNullOrEmpty( parameter.Name ) && string.IsNullOrEmpty( parameter.Description ) )
if ( string.IsNullOrEmpty( parameter.Name ) )
{
for ( var j = 0; j < args.Count; j++ )
continue;
}

for ( var j = 0; j < args.Count; j++ )
{
var arg = args[j];

if ( arg.Name != parameter.Name )
{
var arg = args[i];
continue;
}

var name = arg.ParameterDescriptor.Name;

if ( string.IsNullOrEmpty( parameter.Description )
&& !string.IsNullOrEmpty( description = Documentation.GetParameterDescription( method, name ) ) )
{
parameter.Description = description;
}

if ( arg.Name == parameter.Name )
if ( parameter is OpenApiParameter param )
{
if ( param.Example is null
&& ToJson( Documentation.GetParameterExample( method, name ) ) is { } example )
{
var name = arg.ParameterDescriptor.Name;
parameter.Description = Documentation.GetParameterDescription( method, name );
param.Example = example;
}

param.Deprecated |= Documentation.IsParameterDeprecated( method, name );
}

break;
}
}

Expand Down Expand Up @@ -159,4 +208,21 @@ private static bool TryResolveMethod( ActionDescriptor action, [MaybeNullWhen( f
method = default;
return false;
}

private static JsonNode? ToJson( string? example )
{
if ( string.IsNullOrEmpty( example ) )
{
return default;
}

try
{
return JsonNode.Parse( example );
}
catch ( JsonException )
{
return JsonNode.Parse( $"\"{example}\"" );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@

<ItemGroup>
<Compile Include="$(SourceFile)" Condition="Exists('$(SourceFile)')" Link="%(Filename)%(Extension)" Visible="false" />
<Compile Include="$(RootDir)\src\Abstractions\test\Asp.Versioning.Abstractions.Tests\AssumeCultureAttribute.cs" Link="AssumeCultureAttribute.cs" />
</ItemGroup>

<ItemGroup>
<None Include="Content\**\*.*" CopyToOutputDirectory="Always" />
<None Include="Content\**\*.*" CopyToOutputDirectory="Always" />
</ItemGroup>

<Target Name="GenerateFilePaths" AfterTargets="BeforeBuild">
<Target Name="GenerateFilePaths" BeforeTargets="PrepareForBuild">

<ItemGroup>
<Code Include="namespace $(RootNamespace)%3B" />
Expand All @@ -32,10 +33,15 @@
<Code Include="}" />
</ItemGroup>

<PropertyGroup>
<FirstRun>true</FirstRun>
<FirstRun Condition="Exists('$(SourceFile)')">false</FirstRun>
</PropertyGroup>

<WriteLinesToFile File="$(SourceFile)" Lines="@(Code)" Overwrite="true" />

<ItemGroup>
<Compile Include="$(SourceFile)" Visible="false" />
<Compile Include="$(SourceFile)" Condition="$(FirstRun)" />
</ItemGroup>

</Target>
Expand Down
Loading
Loading