mirror of https://github.com/abpframework/abp.git
16 changed files with 645 additions and 14 deletions
@ -0,0 +1,21 @@ |
|||
using System; |
|||
using System.Reflection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; |
|||
|
|||
public interface IXmlDocumentationProvider |
|||
{ |
|||
string? GetSummary(Type type); |
|||
|
|||
string? GetRemarks(Type type); |
|||
|
|||
string? GetSummary(MethodInfo method); |
|||
|
|||
string? GetRemarks(MethodInfo method); |
|||
|
|||
string? GetReturns(MethodInfo method); |
|||
|
|||
string? GetParameterSummary(MethodInfo method, string parameterName); |
|||
|
|||
string? GetSummary(PropertyInfo property); |
|||
} |
|||
@ -0,0 +1,175 @@ |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using System.Text.RegularExpressions; |
|||
using System.Xml.Linq; |
|||
using System.Xml.XPath; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; |
|||
|
|||
public class XmlDocumentationProvider : IXmlDocumentationProvider, ISingletonDependency |
|||
{ |
|||
private readonly ConcurrentDictionary<Assembly, XDocument?> _xmlDocCache = new(); |
|||
|
|||
public string? GetSummary(Type type) |
|||
{ |
|||
var memberName = GetMemberNameForType(type); |
|||
return GetDocumentationElement(type.Assembly, memberName, "summary"); |
|||
} |
|||
|
|||
public string? GetRemarks(Type type) |
|||
{ |
|||
var memberName = GetMemberNameForType(type); |
|||
return GetDocumentationElement(type.Assembly, memberName, "remarks"); |
|||
} |
|||
|
|||
public string? GetSummary(MethodInfo method) |
|||
{ |
|||
var memberName = GetMemberNameForMethod(method); |
|||
return GetDocumentationElement(method.DeclaringType!.Assembly, memberName, "summary"); |
|||
} |
|||
|
|||
public string? GetRemarks(MethodInfo method) |
|||
{ |
|||
var memberName = GetMemberNameForMethod(method); |
|||
return GetDocumentationElement(method.DeclaringType!.Assembly, memberName, "remarks"); |
|||
} |
|||
|
|||
public string? GetReturns(MethodInfo method) |
|||
{ |
|||
var memberName = GetMemberNameForMethod(method); |
|||
return GetDocumentationElement(method.DeclaringType!.Assembly, memberName, "returns"); |
|||
} |
|||
|
|||
public string? GetParameterSummary(MethodInfo method, string parameterName) |
|||
{ |
|||
var memberName = GetMemberNameForMethod(method); |
|||
var doc = LoadXmlDocumentation(method.DeclaringType!.Assembly); |
|||
if (doc == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var memberNode = doc.XPathSelectElement($"//member[@name='{memberName}']"); |
|||
var paramNode = memberNode?.XPathSelectElement($"param[@name='{parameterName}']"); |
|||
return CleanXmlText(paramNode); |
|||
} |
|||
|
|||
public string? GetSummary(PropertyInfo property) |
|||
{ |
|||
var memberName = GetMemberNameForProperty(property); |
|||
return GetDocumentationElement(property.DeclaringType!.Assembly, memberName, "summary"); |
|||
} |
|||
|
|||
private string? GetDocumentationElement(Assembly assembly, string memberName, string elementName) |
|||
{ |
|||
var doc = LoadXmlDocumentation(assembly); |
|||
if (doc == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var memberNode = doc.XPathSelectElement($"//member[@name='{memberName}']"); |
|||
var element = memberNode?.Element(elementName); |
|||
return CleanXmlText(element); |
|||
} |
|||
|
|||
private XDocument? LoadXmlDocumentation(Assembly assembly) |
|||
{ |
|||
return _xmlDocCache.GetOrAdd(assembly, static asm => |
|||
{ |
|||
if (string.IsNullOrEmpty(asm.Location)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var xmlFilePath = Path.ChangeExtension(asm.Location, ".xml"); |
|||
if (!File.Exists(xmlFilePath)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
return XDocument.Load(xmlFilePath); |
|||
} |
|||
catch |
|||
{ |
|||
return null; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private static string? CleanXmlText(XElement? element) |
|||
{ |
|||
if (element == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var text = element.Value; |
|||
if (string.IsNullOrWhiteSpace(text)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return Regex.Replace(text.Trim(), @"\s+", " "); |
|||
} |
|||
|
|||
private static string GetMemberNameForType(Type type) |
|||
{ |
|||
return $"T:{GetTypeFullName(type)}"; |
|||
} |
|||
|
|||
private static string GetMemberNameForMethod(MethodInfo method) |
|||
{ |
|||
var typeName = GetTypeFullName(method.DeclaringType!); |
|||
var parameters = method.GetParameters(); |
|||
if (parameters.Length == 0) |
|||
{ |
|||
return $"M:{typeName}.{method.Name}"; |
|||
} |
|||
|
|||
var paramTypes = string.Join(",", |
|||
parameters.Select(p => GetParameterTypeName(p.ParameterType))); |
|||
return $"M:{typeName}.{method.Name}({paramTypes})"; |
|||
} |
|||
|
|||
private static string GetMemberNameForProperty(PropertyInfo property) |
|||
{ |
|||
var typeName = GetTypeFullName(property.DeclaringType!); |
|||
return $"P:{typeName}.{property.Name}"; |
|||
} |
|||
|
|||
private static string GetTypeFullName(Type type) |
|||
{ |
|||
return type.FullName?.Replace('+', '.') ?? type.Name; |
|||
} |
|||
|
|||
private static string GetParameterTypeName(Type type) |
|||
{ |
|||
if (type.IsGenericType) |
|||
{ |
|||
var genericDef = type.GetGenericTypeDefinition(); |
|||
var defName = genericDef.FullName!; |
|||
defName = defName[..defName.IndexOf('`')]; |
|||
var args = string.Join(",", type.GetGenericArguments().Select(GetParameterTypeName)); |
|||
return $"{defName}{{{args}}}"; |
|||
} |
|||
|
|||
if (type.IsArray) |
|||
{ |
|||
return GetParameterTypeName(type.GetElementType()!) + "[]"; |
|||
} |
|||
|
|||
if (type.IsByRef) |
|||
{ |
|||
return GetParameterTypeName(type.GetElementType()!) + "@"; |
|||
} |
|||
|
|||
return type.FullName ?? type.Name; |
|||
} |
|||
} |
|||
@ -0,0 +1,205 @@ |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Http.Modeling; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; |
|||
|
|||
public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBase |
|||
{ |
|||
[Fact] |
|||
public async Task Default_Should_Not_Include_Descriptions() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition"); |
|||
|
|||
var controller = GetDocumentedController(model); |
|||
controller.Summary.ShouldBeNull(); |
|||
controller.Remarks.ShouldBeNull(); |
|||
controller.Description.ShouldBeNull(); |
|||
controller.DisplayName.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IncludeDescriptions_Should_Populate_Controller_Summary() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition?includeDescriptions=true"); |
|||
|
|||
var controller = GetDocumentedController(model); |
|||
controller.Summary.ShouldNotBeNullOrEmpty(); |
|||
controller.Summary.ShouldContain("documented application service"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IncludeDescriptions_Should_Populate_Controller_Remarks() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition?includeDescriptions=true"); |
|||
|
|||
var controller = GetDocumentedController(model); |
|||
controller.Remarks.ShouldNotBeNullOrEmpty(); |
|||
controller.Remarks.ShouldContain("integration tests"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IncludeDescriptions_Should_Populate_Controller_Description_Attribute() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition?includeDescriptions=true"); |
|||
|
|||
var controller = GetDocumentedController(model); |
|||
controller.Description.ShouldBe("Documented service description from attribute"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IncludeDescriptions_Should_Populate_Controller_DisplayName_Attribute() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition?includeDescriptions=true"); |
|||
|
|||
var controller = GetDocumentedController(model); |
|||
controller.DisplayName.ShouldBe("Documented Service"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IncludeDescriptions_Should_Populate_Action_Descriptions() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition?includeDescriptions=true"); |
|||
|
|||
var controller = GetDocumentedController(model); |
|||
var action = GetAction(controller, "GetGreeting"); |
|||
|
|||
action.Summary.ShouldNotBeNullOrEmpty(); |
|||
action.Summary.ShouldContain("greeting message"); |
|||
action.Remarks.ShouldBeNull(); |
|||
action.Description.ShouldBe("Get greeting description from attribute"); |
|||
action.DisplayName.ShouldBe("Get Greeting"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IncludeDescriptions_Should_Populate_ReturnValue_Summary() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition?includeDescriptions=true"); |
|||
|
|||
var controller = GetDocumentedController(model); |
|||
var action = GetAction(controller, "GetGreeting"); |
|||
|
|||
action.ReturnValue.Summary.ShouldNotBeNullOrEmpty(); |
|||
action.ReturnValue.Summary.ShouldContain("personalized greeting"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IncludeDescriptions_Should_Populate_ParameterOnMethod_Summary() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition?includeDescriptions=true"); |
|||
|
|||
var controller = GetDocumentedController(model); |
|||
var action = GetAction(controller, "GetGreeting"); |
|||
|
|||
var param = action.ParametersOnMethod.FirstOrDefault(p => p.Name == "name"); |
|||
param.ShouldNotBeNull(); |
|||
param.Summary.ShouldNotBeNullOrEmpty(); |
|||
param.Summary.ShouldContain("name of the person"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IncludeDescriptions_Should_Populate_Parameter_Summary() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition?includeDescriptions=true"); |
|||
|
|||
var controller = GetDocumentedController(model); |
|||
var action = GetAction(controller, "GetGreeting"); |
|||
|
|||
var param = action.Parameters.FirstOrDefault(p => p.NameOnMethod == "name"); |
|||
param.ShouldNotBeNull(); |
|||
param.Summary.ShouldNotBeNullOrEmpty(); |
|||
param.Summary.ShouldContain("name of the person"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IncludeDescriptions_With_IncludeTypes_Should_Populate_Type_Descriptions() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition?includeDescriptions=true&includeTypes=true"); |
|||
|
|||
model.Types.ShouldNotBeEmpty(); |
|||
|
|||
var documentedDtoType = model.Types.FirstOrDefault(t => t.Key.Contains("DocumentedDto")); |
|||
documentedDtoType.Value.ShouldNotBeNull(); |
|||
documentedDtoType.Value.Summary.ShouldNotBeNullOrEmpty(); |
|||
documentedDtoType.Value.Summary.ShouldContain("documented DTO"); |
|||
documentedDtoType.Value.Description.ShouldBe("Documented DTO description from attribute"); |
|||
documentedDtoType.Value.DisplayName.ShouldBe("Documented DTO"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task IncludeDescriptions_With_IncludeTypes_Should_Populate_Property_Descriptions() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition?includeDescriptions=true&includeTypes=true"); |
|||
|
|||
var documentedDtoType = model.Types.FirstOrDefault(t => t.Key.Contains("DocumentedDto")); |
|||
documentedDtoType.Value.ShouldNotBeNull(); |
|||
documentedDtoType.Value.Properties.ShouldNotBeNull(); |
|||
|
|||
var nameProp = documentedDtoType.Value.Properties!.FirstOrDefault(p => p.Name == "Name"); |
|||
nameProp.ShouldNotBeNull(); |
|||
nameProp.Summary.ShouldNotBeNullOrEmpty(); |
|||
nameProp.Summary.ShouldContain("name of the documented item"); |
|||
nameProp.Description.ShouldBe("Name description from attribute"); |
|||
nameProp.DisplayName.ShouldBe("Item Name"); |
|||
|
|||
var valueProp = documentedDtoType.Value.Properties!.FirstOrDefault(p => p.Name == "Value"); |
|||
valueProp.ShouldNotBeNull(); |
|||
valueProp.Summary.ShouldNotBeNullOrEmpty(); |
|||
valueProp.Description.ShouldBe("Value description from attribute"); |
|||
valueProp.DisplayName.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Default_Should_Not_Include_Type_Descriptions() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition?includeTypes=true"); |
|||
|
|||
var documentedDtoType = model.Types.FirstOrDefault(t => t.Key.Contains("DocumentedDto")); |
|||
documentedDtoType.Value.ShouldNotBeNull(); |
|||
documentedDtoType.Value.Summary.ShouldBeNull(); |
|||
documentedDtoType.Value.Description.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Action_Without_Descriptions_Should_Have_Null_Properties() |
|||
{ |
|||
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
|||
"/api/abp/api-definition?includeDescriptions=true"); |
|||
|
|||
var peopleController = model.Modules.Values |
|||
.SelectMany(m => m.Controllers.Values) |
|||
.First(c => c.ControllerName == "People"); |
|||
|
|||
var action = peopleController.Actions.Values.First(a => a.Name == "GetPhones"); |
|||
action.Summary.ShouldBeNull(); |
|||
action.Description.ShouldBeNull(); |
|||
action.DisplayName.ShouldBeNull(); |
|||
} |
|||
|
|||
private static ControllerApiDescriptionModel GetDocumentedController(ApplicationApiDescriptionModel model) |
|||
{ |
|||
return model.Modules.Values |
|||
.SelectMany(m => m.Controllers.Values) |
|||
.First(c => c.ControllerName == "Documented"); |
|||
} |
|||
|
|||
private static ActionApiDescriptionModel GetAction(ControllerApiDescriptionModel controller, string actionName) |
|||
{ |
|||
return controller.Actions.Values |
|||
.First(a => a.Name == actionName + "Async" || a.Name == actionName); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using System.ComponentModel; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.TestApp.Application.Dto; |
|||
|
|||
namespace Volo.Abp.TestApp.Application; |
|||
|
|||
/// <summary>
|
|||
/// A documented application service for testing API descriptions.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This service is used in integration tests to verify XML doc extraction.
|
|||
/// </remarks>
|
|||
[Description("Documented service description from attribute")] |
|||
[Display(Name = "Documented Service")] |
|||
public class DocumentedAppService : ApplicationService, IDocumentedAppService |
|||
{ |
|||
/// <summary>
|
|||
/// Gets a greeting message for the specified name.
|
|||
/// </summary>
|
|||
/// <param name="name">The name of the person to greet.</param>
|
|||
/// <returns>A personalized greeting message.</returns>
|
|||
[Description("Get greeting description from attribute")] |
|||
[Display(Name = "Get Greeting")] |
|||
public Task<string> GetGreetingAsync(string name) |
|||
{ |
|||
return Task.FromResult($"Hello, {name}!"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a documented item.
|
|||
/// </summary>
|
|||
/// <param name="input">The input for creating a documented item.</param>
|
|||
/// <returns>The created documented item.</returns>
|
|||
public Task<DocumentedDto> CreateAsync(DocumentedDto input) |
|||
{ |
|||
return Task.FromResult(input); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using System.ComponentModel; |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace Volo.Abp.TestApp.Application.Dto; |
|||
|
|||
/// <summary>
|
|||
/// A documented DTO for testing type and property descriptions.
|
|||
/// </summary>
|
|||
[Description("Documented DTO description from attribute")] |
|||
[Display(Name = "Documented DTO")] |
|||
public class DocumentedDto |
|||
{ |
|||
/// <summary>
|
|||
/// The name of the documented item.
|
|||
/// </summary>
|
|||
[Description("Name description from attribute")] |
|||
[Display(Name = "Item Name")] |
|||
public string Name { get; set; } = default!; |
|||
|
|||
/// <summary>
|
|||
/// The value of the documented item.
|
|||
/// </summary>
|
|||
[Description("Value description from attribute")] |
|||
public int Value { get; set; } |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.TestApp.Application.Dto; |
|||
|
|||
namespace Volo.Abp.TestApp.Application; |
|||
|
|||
/// <summary>
|
|||
/// A documented application service for testing API descriptions.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This service is used in integration tests to verify XML doc extraction.
|
|||
/// </remarks>
|
|||
public interface IDocumentedAppService : IApplicationService |
|||
{ |
|||
/// <summary>
|
|||
/// Gets a greeting message for the specified name.
|
|||
/// </summary>
|
|||
/// <param name="name">The name of the person to greet.</param>
|
|||
/// <returns>A personalized greeting message.</returns>
|
|||
Task<string> GetGreetingAsync(string name); |
|||
|
|||
/// <summary>
|
|||
/// Creates a documented item.
|
|||
/// </summary>
|
|||
/// <param name="input">The input for creating a documented item.</param>
|
|||
/// <returns>The created documented item.</returns>
|
|||
Task<DocumentedDto> CreateAsync(DocumentedDto input); |
|||
} |
|||
Loading…
Reference in new issue