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