Browse Source

feat: Refactor API documentation handling to support asynchronous operations

pull/25022/head
maliming 3 weeks ago
parent
commit
778abc8aaa
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 7
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController.cs
  2. 15
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/IXmlDocumentationProvider.cs
  3. 87
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProvider.cs
  4. 159
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs
  5. 5
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs
  6. 301
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Description_Tests.cs
  7. 26
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DocumentedAppService.cs
  8. 10
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IDocumentedAppService.cs
  9. 20
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IInterfaceOnlyDocumentedAppService.cs
  10. 12
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/InterfaceOnlyDocumentedAppService.cs

7
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController.cs

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Http.Modeling;
namespace Volo.Abp.AspNetCore.Mvc.ApiExploring;
@ -16,8 +17,8 @@ public class AbpApiDefinitionController : AbpController, IRemoteService
}
[HttpGet]
public virtual ApplicationApiDescriptionModel Get(ApplicationApiDescriptionModelRequestDto model)
public virtual async Task<ApplicationApiDescriptionModel> Get(ApplicationApiDescriptionModelRequestDto model)
{
return ModelProvider.CreateApiModel(model);
return await ModelProvider.CreateApiModelAsync(model);
}
}

15
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/IXmlDocumentationProvider.cs

@ -1,21 +1,22 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
namespace Volo.Abp.AspNetCore.Mvc.ApiExploring;
public interface IXmlDocumentationProvider
{
string? GetSummary(Type type);
Task<string?> GetSummaryAsync(Type type);
string? GetRemarks(Type type);
Task<string?> GetRemarksAsync(Type type);
string? GetSummary(MethodInfo method);
Task<string?> GetSummaryAsync(MethodInfo method);
string? GetRemarks(MethodInfo method);
Task<string?> GetRemarksAsync(MethodInfo method);
string? GetReturns(MethodInfo method);
Task<string?> GetReturnsAsync(MethodInfo method);
string? GetParameterSummary(MethodInfo method, string parameterName);
Task<string?> GetParameterSummaryAsync(MethodInfo method, string parameterName);
string? GetSummary(PropertyInfo property);
Task<string?> GetSummaryAsync(PropertyInfo property);
}

87
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProvider.cs

@ -4,6 +4,8 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.Xml.XPath;
using Volo.Abp.DependencyInjection;
@ -12,42 +14,44 @@ namespace Volo.Abp.AspNetCore.Mvc.ApiExploring;
public class XmlDocumentationProvider : IXmlDocumentationProvider, ISingletonDependency
{
private readonly ConcurrentDictionary<Assembly, XDocument?> _xmlDocCache = new();
private static readonly Regex WhitespaceRegex = new(@"\s+", RegexOptions.Compiled);
public string? GetSummary(Type type)
private readonly ConcurrentDictionary<Assembly, Task<XDocument?>> _xmlDocCache = new();
public virtual async Task<string?> GetSummaryAsync(Type type)
{
var memberName = GetMemberNameForType(type);
return GetDocumentationElement(type.Assembly, memberName, "summary");
return await GetDocumentationElementAsync(type.Assembly, memberName, "summary");
}
public string? GetRemarks(Type type)
public virtual async Task<string?> GetRemarksAsync(Type type)
{
var memberName = GetMemberNameForType(type);
return GetDocumentationElement(type.Assembly, memberName, "remarks");
return await GetDocumentationElementAsync(type.Assembly, memberName, "remarks");
}
public string? GetSummary(MethodInfo method)
public virtual async Task<string?> GetSummaryAsync(MethodInfo method)
{
var memberName = GetMemberNameForMethod(method);
return GetDocumentationElement(method.DeclaringType!.Assembly, memberName, "summary");
return await GetDocumentationElementAsync(method.DeclaringType!.Assembly, memberName, "summary");
}
public string? GetRemarks(MethodInfo method)
public virtual async Task<string?> GetRemarksAsync(MethodInfo method)
{
var memberName = GetMemberNameForMethod(method);
return GetDocumentationElement(method.DeclaringType!.Assembly, memberName, "remarks");
return await GetDocumentationElementAsync(method.DeclaringType!.Assembly, memberName, "remarks");
}
public string? GetReturns(MethodInfo method)
public virtual async Task<string?> GetReturnsAsync(MethodInfo method)
{
var memberName = GetMemberNameForMethod(method);
return GetDocumentationElement(method.DeclaringType!.Assembly, memberName, "returns");
return await GetDocumentationElementAsync(method.DeclaringType!.Assembly, memberName, "returns");
}
public string? GetParameterSummary(MethodInfo method, string parameterName)
public virtual async Task<string?> GetParameterSummaryAsync(MethodInfo method, string parameterName)
{
var memberName = GetMemberNameForMethod(method);
var doc = LoadXmlDocumentation(method.DeclaringType!.Assembly);
var doc = await LoadXmlDocumentationAsync(method.DeclaringType!.Assembly);
if (doc == null)
{
return null;
@ -58,15 +62,15 @@ public class XmlDocumentationProvider : IXmlDocumentationProvider, ISingletonDep
return CleanXmlText(paramNode);
}
public string? GetSummary(PropertyInfo property)
public virtual async Task<string?> GetSummaryAsync(PropertyInfo property)
{
var memberName = GetMemberNameForProperty(property);
return GetDocumentationElement(property.DeclaringType!.Assembly, memberName, "summary");
return await GetDocumentationElementAsync(property.DeclaringType!.Assembly, memberName, "summary");
}
private string? GetDocumentationElement(Assembly assembly, string memberName, string elementName)
protected virtual async Task<string?> GetDocumentationElementAsync(Assembly assembly, string memberName, string elementName)
{
var doc = LoadXmlDocumentation(assembly);
var doc = await LoadXmlDocumentationAsync(assembly);
if (doc == null)
{
return null;
@ -77,30 +81,33 @@ public class XmlDocumentationProvider : IXmlDocumentationProvider, ISingletonDep
return CleanXmlText(element);
}
private XDocument? LoadXmlDocumentation(Assembly assembly)
protected virtual Task<XDocument?> LoadXmlDocumentationAsync(Assembly assembly)
{
return _xmlDocCache.GetOrAdd(assembly, LoadXmlDocumentationFromDiskAsync);
}
protected virtual async Task<XDocument?> LoadXmlDocumentationFromDiskAsync(Assembly assembly)
{
return _xmlDocCache.GetOrAdd(assembly, static asm =>
if (string.IsNullOrEmpty(assembly.Location))
{
return null;
}
var xmlFilePath = Path.ChangeExtension(assembly.Location, ".xml");
if (!File.Exists(xmlFilePath))
{
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;
}
});
return null;
}
try
{
await using var stream = new FileStream(xmlFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true);
return await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None);
}
catch
{
return null;
}
}
private static string? CleanXmlText(XElement? element)
@ -116,7 +123,7 @@ public class XmlDocumentationProvider : IXmlDocumentationProvider, ISingletonDep
return null;
}
return Regex.Replace(text.Trim(), @"\s+", " ");
return WhitespaceRegex.Replace(text.Trim(), " ");
}
private static string GetMemberNameForType(Type type)

159
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Asp.Versioning;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
@ -51,10 +52,16 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
}
public ApplicationApiDescriptionModel CreateApiModel(ApplicationApiDescriptionModelRequestDto input)
{
return AsyncHelper.RunSync(() => CreateApiModelAsync(input));
}
public virtual async Task<ApplicationApiDescriptionModel> CreateApiModelAsync(ApplicationApiDescriptionModelRequestDto input)
{
//TODO: Can cache the model?
var model = ApplicationApiDescriptionModel.Create();
var populatedControllers = new HashSet<ControllerApiDescriptionModel>();
foreach (var descriptionGroupItem in _descriptionProvider.ApiDescriptionGroups.Items)
{
@ -65,7 +72,7 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
continue;
}
AddApiDescriptionToModel(apiDescription, model, input);
await AddApiDescriptionToModelAsync(apiDescription, model, input, populatedControllers);
}
}
@ -86,10 +93,11 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
return model;
}
private void AddApiDescriptionToModel(
private async Task AddApiDescriptionToModelAsync(
ApiDescription apiDescription,
ApplicationApiDescriptionModel applicationModel,
ApplicationApiDescriptionModelRequestDto input)
ApplicationApiDescriptionModelRequestDto input,
HashSet<ControllerApiDescriptionModel> populatedControllers)
{
var controllerType = apiDescription
.ActionDescriptor
@ -167,20 +175,20 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
if (input.IncludeTypes)
{
AddCustomTypesToModel(applicationModel, method, input.IncludeDescriptions);
await AddCustomTypesToModelAsync(applicationModel, method, input.IncludeDescriptions);
}
AddParameterDescriptionsToModel(actionModel, method, apiDescription);
if (input.IncludeDescriptions)
{
if (controllerModel.Summary == null && controllerModel.Description == null && controllerModel.DisplayName == null)
if (populatedControllers.Add(controllerModel))
{
PopulateControllerDescriptions(controllerModel, controllerType);
await PopulateControllerDescriptionsAsync(controllerModel, controllerType);
}
PopulateActionDescriptions(actionModel, method);
PopulateParameterDescriptions(actionModel, method);
await PopulateActionDescriptionsAsync(actionModel, method);
await PopulateParameterDescriptionsAsync(actionModel, method);
}
}
@ -208,17 +216,17 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
return supportedVersions.Select(v => v.ToString()).Distinct().ToList();
}
private void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel, MethodInfo method, bool includeDescriptions)
private async Task AddCustomTypesToModelAsync(ApplicationApiDescriptionModel applicationModel, MethodInfo method, bool includeDescriptions)
{
foreach (var parameterInfo in method.GetParameters())
{
AddCustomTypesToModel(applicationModel, parameterInfo.ParameterType, includeDescriptions);
await AddCustomTypesToModelAsync(applicationModel, parameterInfo.ParameterType, includeDescriptions);
}
AddCustomTypesToModel(applicationModel, method.ReturnType, includeDescriptions);
await AddCustomTypesToModelAsync(applicationModel, method.ReturnType, includeDescriptions);
}
private void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel,
private async Task AddCustomTypesToModelAsync(ApplicationApiDescriptionModel applicationModel,
Type? type, bool includeDescriptions)
{
if (type == null)
@ -246,14 +254,14 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
if (TypeHelper.IsDictionary(type, out var keyType, out var valueType))
{
AddCustomTypesToModel(applicationModel, keyType, includeDescriptions);
AddCustomTypesToModel(applicationModel, valueType, includeDescriptions);
await AddCustomTypesToModelAsync(applicationModel, keyType, includeDescriptions);
await AddCustomTypesToModelAsync(applicationModel, valueType, includeDescriptions);
return;
}
if (TypeHelper.IsEnumerable(type, out var itemType))
{
AddCustomTypesToModel(applicationModel, itemType, includeDescriptions);
await AddCustomTypesToModelAsync(applicationModel, itemType, includeDescriptions);
return;
}
@ -261,11 +269,11 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
{
var genericTypeDefinition = type.GetGenericTypeDefinition();
AddCustomTypesToModel(applicationModel, genericTypeDefinition, includeDescriptions);
await AddCustomTypesToModelAsync(applicationModel, genericTypeDefinition, includeDescriptions);
foreach (var genericArgument in type.GetGenericArguments())
{
AddCustomTypesToModel(applicationModel, genericArgument, includeDescriptions);
await AddCustomTypesToModelAsync(applicationModel, genericArgument, includeDescriptions);
}
return;
@ -281,14 +289,14 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
if (includeDescriptions)
{
PopulateTypeDescriptions(applicationModel.Types[typeName], type);
await PopulateTypeDescriptionsAsync(applicationModel.Types[typeName], type);
}
AddCustomTypesToModel(applicationModel, type.BaseType, includeDescriptions);
await AddCustomTypesToModelAsync(applicationModel, type.BaseType, includeDescriptions);
foreach (var propertyInfo in type.GetProperties().Where(p => p.DeclaringType == type))
{
AddCustomTypesToModel(applicationModel, propertyInfo.PropertyType, includeDescriptions);
await AddCustomTypesToModelAsync(applicationModel, propertyInfo.PropertyType, includeDescriptions);
}
}
@ -437,48 +445,129 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
return null;
}
private void PopulateControllerDescriptions(ControllerApiDescriptionModel controllerModel, Type controllerType)
protected virtual async Task PopulateControllerDescriptionsAsync(ControllerApiDescriptionModel controllerModel, Type controllerType)
{
controllerModel.Summary = _xmlDocProvider.GetSummary(controllerType);
controllerModel.Remarks = _xmlDocProvider.GetRemarks(controllerType);
controllerModel.Summary = await _xmlDocProvider.GetSummaryAsync(controllerType);
controllerModel.Remarks = await _xmlDocProvider.GetRemarksAsync(controllerType);
if (controllerModel.Summary == null && controllerModel.Remarks == null)
{
foreach (var interfaceType in GetDirectInterfaces(controllerType))
{
controllerModel.Summary = await _xmlDocProvider.GetSummaryAsync(interfaceType);
controllerModel.Remarks = await _xmlDocProvider.GetRemarksAsync(interfaceType);
if (controllerModel.Summary != null || controllerModel.Remarks != null)
{
break;
}
}
}
controllerModel.Description = controllerType.GetCustomAttribute<DescriptionAttribute>()?.Description;
controllerModel.DisplayName = controllerType.GetCustomAttribute<DisplayAttribute>()?.Name;
}
private void PopulateActionDescriptions(ActionApiDescriptionModel actionModel, MethodInfo method)
protected virtual async Task PopulateActionDescriptionsAsync(ActionApiDescriptionModel actionModel, MethodInfo method)
{
actionModel.Summary = _xmlDocProvider.GetSummary(method);
actionModel.Remarks = _xmlDocProvider.GetRemarks(method);
actionModel.Summary = await _xmlDocProvider.GetSummaryAsync(method);
actionModel.Remarks = await _xmlDocProvider.GetRemarksAsync(method);
if (actionModel.Summary == null && actionModel.Remarks == null)
{
var interfaceMethod = GetInterfaceMethod(method);
if (interfaceMethod != null)
{
actionModel.Summary = await _xmlDocProvider.GetSummaryAsync(interfaceMethod);
actionModel.Remarks = await _xmlDocProvider.GetRemarksAsync(interfaceMethod);
}
}
actionModel.Description = method.GetCustomAttribute<DescriptionAttribute>()?.Description;
actionModel.DisplayName = method.GetCustomAttribute<DisplayAttribute>()?.Name;
actionModel.ReturnValue.Summary = _xmlDocProvider.GetReturns(method);
actionModel.ReturnValue.Summary = await _xmlDocProvider.GetReturnsAsync(method);
if (actionModel.ReturnValue.Summary == null)
{
var interfaceMethod = GetInterfaceMethod(method);
if (interfaceMethod != null)
{
actionModel.ReturnValue.Summary = await _xmlDocProvider.GetReturnsAsync(interfaceMethod);
}
}
}
private void PopulateParameterDescriptions(ActionApiDescriptionModel actionModel, MethodInfo method)
protected virtual async Task PopulateParameterDescriptionsAsync(ActionApiDescriptionModel actionModel, MethodInfo method)
{
var interfaceMethod = GetInterfaceMethod(method);
var methodParameters = method.GetParameters();
foreach (var param in actionModel.ParametersOnMethod)
{
var paramInfo = method.GetParameters().FirstOrDefault(p => p.Name == param.Name);
var paramInfo = methodParameters.FirstOrDefault(p => p.Name == param.Name);
if (paramInfo == null)
{
continue;
}
param.Summary = _xmlDocProvider.GetParameterSummary(method, param.Name);
param.Summary = await _xmlDocProvider.GetParameterSummaryAsync(method, param.Name);
if (param.Summary == null && interfaceMethod != null)
{
param.Summary = await _xmlDocProvider.GetParameterSummaryAsync(interfaceMethod, param.Name);
}
param.Description = paramInfo.GetCustomAttribute<DescriptionAttribute>()?.Description;
param.DisplayName = paramInfo.GetCustomAttribute<DisplayAttribute>()?.Name;
}
foreach (var param in actionModel.Parameters)
{
param.Summary = _xmlDocProvider.GetParameterSummary(method, param.NameOnMethod);
param.Summary = await _xmlDocProvider.GetParameterSummaryAsync(method, param.NameOnMethod);
if (param.Summary == null && interfaceMethod != null)
{
param.Summary = await _xmlDocProvider.GetParameterSummaryAsync(interfaceMethod, param.NameOnMethod);
}
var paramInfo = methodParameters.FirstOrDefault(p => p.Name == param.NameOnMethod);
if (paramInfo != null)
{
param.Description = paramInfo.GetCustomAttribute<DescriptionAttribute>()?.Description;
param.DisplayName = paramInfo.GetCustomAttribute<DisplayAttribute>()?.Name;
}
}
}
private void PopulateTypeDescriptions(TypeApiDescriptionModel typeModel, Type type)
private static MethodInfo? GetInterfaceMethod(MethodInfo method)
{
var declaringType = method.DeclaringType;
if (declaringType == null || declaringType.IsInterface)
{
return null;
}
foreach (var interfaceType in GetDirectInterfaces(declaringType))
{
var interfaceMethod = interfaceType.GetMethods()
.FirstOrDefault(m => m.ToString() == method.ToString());
if (interfaceMethod != null)
{
return interfaceMethod;
}
}
return null;
}
private static IEnumerable<Type> GetDirectInterfaces(Type type)
{
var allInterfaces = type.GetInterfaces();
var baseInterfaces = type.BaseType?.GetInterfaces() ?? Type.EmptyTypes;
return allInterfaces.Except(baseInterfaces);
}
protected virtual async Task PopulateTypeDescriptionsAsync(TypeApiDescriptionModel typeModel, Type type)
{
typeModel.Summary = _xmlDocProvider.GetSummary(type);
typeModel.Remarks = _xmlDocProvider.GetRemarks(type);
typeModel.Summary = await _xmlDocProvider.GetSummaryAsync(type);
typeModel.Remarks = await _xmlDocProvider.GetRemarksAsync(type);
typeModel.Description = type.GetCustomAttribute<DescriptionAttribute>()?.Description;
typeModel.DisplayName = type.GetCustomAttribute<DisplayAttribute>()?.Name;
@ -495,7 +584,7 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
continue;
}
propModel.Summary = _xmlDocProvider.GetSummary(propInfo);
propModel.Summary = await _xmlDocProvider.GetSummaryAsync(propInfo);
propModel.Description = propInfo.GetCustomAttribute<DescriptionAttribute>()?.Description;
propModel.DisplayName = propInfo.GetCustomAttribute<DisplayAttribute>()?.Name;
}

5
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs

@ -1,6 +1,11 @@
using System.Threading.Tasks;
namespace Volo.Abp.Http.Modeling;
public interface IApiDescriptionModelProvider
{
ApplicationApiDescriptionModel CreateApiModel(ApplicationApiDescriptionModelRequestDto input);
Task<ApplicationApiDescriptionModel> CreateApiModelAsync(ApplicationApiDescriptionModelRequestDto input)
=> Task.FromResult(CreateApiModel(input));
}

301
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Description_Tests.cs

@ -9,7 +9,7 @@ namespace Volo.Abp.AspNetCore.Mvc.ApiExploring;
public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBase
{
[Fact]
public async Task Default_Should_Not_Include_Descriptions()
public async Task Default_Should_Not_Include_Controller_Descriptions()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition");
@ -21,6 +21,55 @@ public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBas
controller.DisplayName.ShouldBeNull();
}
[Fact]
public async Task Default_Should_Not_Include_Action_Descriptions()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition");
var controller = GetDocumentedController(model);
var action = GetAction(controller, "GetGreeting");
action.Summary.ShouldBeNull();
action.Remarks.ShouldBeNull();
action.Description.ShouldBeNull();
action.DisplayName.ShouldBeNull();
action.ReturnValue.Summary.ShouldBeNull();
}
[Fact]
public async Task Default_Should_Not_Include_Parameter_Descriptions()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition");
var controller = GetDocumentedController(model);
var action = GetAction(controller, "GetGreeting");
var methodParam = action.ParametersOnMethod.FirstOrDefault(p => p.Name == "name");
methodParam.ShouldNotBeNull();
methodParam.Summary.ShouldBeNull();
methodParam.Description.ShouldBeNull();
methodParam.DisplayName.ShouldBeNull();
var httpParam = action.Parameters.FirstOrDefault(p => p.NameOnMethod == "name");
httpParam.ShouldNotBeNull();
httpParam.Summary.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.Remarks.ShouldBeNull();
documentedDtoType.Value.Description.ShouldBeNull();
documentedDtoType.Value.DisplayName.ShouldBeNull();
}
[Fact]
public async Task IncludeDescriptions_Should_Populate_Controller_Summary()
{
@ -64,18 +113,56 @@ public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBas
}
[Fact]
public async Task IncludeDescriptions_Should_Populate_Action_Descriptions()
public async Task Controller_Descriptions_Should_Be_Populated_Only_Once_For_Multiple_Actions()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var controller = GetDocumentedController(model);
var action = GetAction(controller, "GetGreeting");
controller.Actions.Count.ShouldBeGreaterThan(1);
controller.Summary.ShouldNotBeNullOrEmpty();
controller.Summary.ShouldContain("documented application service");
}
[Fact]
public async Task IncludeDescriptions_Should_Populate_Action_Summary()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var action = GetAction(GetDocumentedController(model), "GetGreeting");
action.Summary.ShouldNotBeNullOrEmpty();
action.Summary.ShouldContain("greeting message");
}
[Fact]
public async Task IncludeDescriptions_Should_Leave_Action_Remarks_Null_When_Not_Documented()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var action = GetAction(GetDocumentedController(model), "GetGreeting");
action.Remarks.ShouldBeNull();
}
[Fact]
public async Task IncludeDescriptions_Should_Populate_Action_Description_Attribute()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var action = GetAction(GetDocumentedController(model), "GetGreeting");
action.Description.ShouldBe("Get greeting description from attribute");
}
[Fact]
public async Task IncludeDescriptions_Should_Populate_Action_DisplayName_Attribute()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var action = GetAction(GetDocumentedController(model), "GetGreeting");
action.DisplayName.ShouldBe("Get Greeting");
}
@ -85,22 +172,32 @@ public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBas
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var controller = GetDocumentedController(model);
var action = GetAction(controller, "GetGreeting");
var action = GetAction(GetDocumentedController(model), "GetGreeting");
action.ReturnValue.Summary.ShouldNotBeNullOrEmpty();
action.ReturnValue.Summary.ShouldContain("personalized greeting");
}
[Fact]
public async Task IncludeDescriptions_Should_Populate_ParameterOnMethod_Summary()
public async Task Undocumented_Action_Should_Have_Null_Descriptions()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var controller = GetDocumentedController(model);
var action = GetAction(controller, "GetGreeting");
var action = GetAction(GetDocumentedController(model), "Delete");
action.Summary.ShouldBeNull();
action.Remarks.ShouldBeNull();
action.Description.ShouldBeNull();
action.DisplayName.ShouldBeNull();
action.ReturnValue.Summary.ShouldBeNull();
}
[Fact]
public async Task IncludeDescriptions_Should_Populate_ParameterOnMethod_Summary()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var action = GetAction(GetDocumentedController(model), "GetGreeting");
var param = action.ParametersOnMethod.FirstOrDefault(p => p.Name == "name");
param.ShouldNotBeNull();
param.Summary.ShouldNotBeNullOrEmpty();
@ -108,14 +205,41 @@ public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBas
}
[Fact]
public async Task IncludeDescriptions_Should_Populate_Parameter_Summary()
public async Task IncludeDescriptions_Should_Populate_ParameterOnMethod_Description_And_DisplayName_From_Attribute()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var controller = GetDocumentedController(model);
var action = GetAction(controller, "GetGreeting");
var action = GetAction(GetDocumentedController(model), "Search");
var param = action.ParametersOnMethod.FirstOrDefault(p => p.Name == "query");
param.ShouldNotBeNull();
param.Summary.ShouldNotBeNullOrEmpty();
param.Summary.ShouldContain("search query");
param.Description.ShouldBe("Query param description from attribute");
param.DisplayName.ShouldBe("Search Query");
}
[Fact]
public async Task IncludeDescriptions_Should_Leave_Parameter_Attributes_Null_When_Not_Annotated()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var action = GetAction(GetDocumentedController(model), "Search");
var param = action.ParametersOnMethod.FirstOrDefault(p => p.Name == "maxResults");
param.ShouldNotBeNull();
param.Summary.ShouldNotBeNullOrEmpty();
param.Description.ShouldBeNull();
param.DisplayName.ShouldBeNull();
}
[Fact]
public async Task IncludeDescriptions_Should_Populate_Parameter_Summary()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var action = GetAction(GetDocumentedController(model), "GetGreeting");
var param = action.Parameters.FirstOrDefault(p => p.NameOnMethod == "name");
param.ShouldNotBeNull();
param.Summary.ShouldNotBeNullOrEmpty();
@ -123,23 +247,58 @@ public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBas
}
[Fact]
public async Task IncludeDescriptions_With_IncludeTypes_Should_Populate_Type_Descriptions()
public async Task IncludeDescriptions_Should_Populate_Parameter_Description_And_DisplayName_From_Attribute()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true&includeTypes=true");
"/api/abp/api-definition?includeDescriptions=true");
var action = GetAction(GetDocumentedController(model), "Search");
var param = action.Parameters.FirstOrDefault(p => p.NameOnMethod == "query");
param.ShouldNotBeNull();
param.Summary.ShouldNotBeNullOrEmpty();
param.Description.ShouldBe("Query param description from attribute");
param.DisplayName.ShouldBe("Search Query");
}
model.Types.ShouldNotBeEmpty();
[Fact]
public async Task IncludeDescriptions_Should_Leave_Parameter_Attributes_Null_When_Not_Annotated_Http()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var action = GetAction(GetDocumentedController(model), "Search");
var param = action.Parameters.FirstOrDefault(p => p.NameOnMethod == "maxResults");
param.ShouldNotBeNull();
param.Description.ShouldBeNull();
param.DisplayName.ShouldBeNull();
}
[Fact]
public async Task IncludeDescriptions_With_IncludeTypes_Should_Populate_Type_Summary()
{
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.Summary.ShouldNotBeNullOrEmpty();
documentedDtoType.Value.Summary.ShouldContain("documented DTO");
}
[Fact]
public async Task IncludeDescriptions_With_IncludeTypes_Should_Populate_Type_Description_And_DisplayName()
{
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.Description.ShouldBe("Documented DTO description from attribute");
documentedDtoType.Value.DisplayName.ShouldBe("Documented DTO");
}
[Fact]
public async Task IncludeDescriptions_With_IncludeTypes_Should_Populate_Property_Descriptions()
public async Task IncludeDescriptions_With_IncludeTypes_Should_Populate_Property_Summary()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true&includeTypes=true");
@ -152,8 +311,31 @@ public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBas
nameProp.ShouldNotBeNull();
nameProp.Summary.ShouldNotBeNullOrEmpty();
nameProp.Summary.ShouldContain("name of the documented item");
}
[Fact]
public async Task IncludeDescriptions_With_IncludeTypes_Should_Populate_Property_Description_And_DisplayName()
{
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();
var nameProp = documentedDtoType.Value.Properties!.FirstOrDefault(p => p.Name == "Name");
nameProp.ShouldNotBeNull();
nameProp.Description.ShouldBe("Name description from attribute");
nameProp.DisplayName.ShouldBe("Item Name");
}
[Fact]
public async Task IncludeDescriptions_With_IncludeTypes_Should_Leave_Property_DisplayName_Null_When_Not_Set()
{
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();
var valueProp = documentedDtoType.Value.Properties!.FirstOrDefault(p => p.Name == "Value");
valueProp.ShouldNotBeNull();
@ -163,7 +345,7 @@ public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBas
}
[Fact]
public async Task Default_Should_Not_Include_Type_Descriptions()
public async Task IncludeTypes_Without_IncludeDescriptions_Should_Not_Populate_Type_Descriptions()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeTypes=true");
@ -171,23 +353,85 @@ public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBas
var documentedDtoType = model.Types.FirstOrDefault(t => t.Key.Contains("DocumentedDto"));
documentedDtoType.Value.ShouldNotBeNull();
documentedDtoType.Value.Summary.ShouldBeNull();
documentedDtoType.Value.Remarks.ShouldBeNull();
documentedDtoType.Value.Description.ShouldBeNull();
documentedDtoType.Value.DisplayName.ShouldBeNull();
if (documentedDtoType.Value.Properties != null)
{
foreach (var prop in documentedDtoType.Value.Properties)
{
prop.Summary.ShouldBeNull();
prop.Description.ShouldBeNull();
prop.DisplayName.ShouldBeNull();
}
}
}
[Fact]
public async Task Action_Without_Descriptions_Should_Have_Null_Properties()
public async Task IncludeDescriptions_Should_Fallback_To_Interface_For_Controller_Summary()
{
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 controller = GetInterfaceOnlyController(model);
controller.Summary.ShouldNotBeNullOrEmpty();
controller.Summary.ShouldContain("documented only on the interface");
}
var action = peopleController.Actions.Values.First(a => a.Name == "GetPhones");
action.Summary.ShouldBeNull();
action.Description.ShouldBeNull();
action.DisplayName.ShouldBeNull();
[Fact]
public async Task IncludeDescriptions_Should_Fallback_To_Interface_For_Controller_Remarks()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var controller = GetInterfaceOnlyController(model);
controller.Remarks.ShouldNotBeNullOrEmpty();
controller.Remarks.ShouldContain("resolved from the interface");
}
[Fact]
public async Task IncludeDescriptions_Should_Fallback_To_Interface_For_Action_Summary()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var controller = GetInterfaceOnlyController(model);
var action = GetAction(controller, "GetMessage");
action.Summary.ShouldNotBeNullOrEmpty();
action.Summary.ShouldContain("documented only on the interface");
}
[Fact]
public async Task IncludeDescriptions_Should_Fallback_To_Interface_For_Action_ReturnValue_Summary()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var controller = GetInterfaceOnlyController(model);
var action = GetAction(controller, "GetMessage");
action.ReturnValue.Summary.ShouldNotBeNullOrEmpty();
action.ReturnValue.Summary.ShouldContain("resolved message");
}
[Fact]
public async Task IncludeDescriptions_Should_Fallback_To_Interface_For_Parameter_Summary()
{
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>(
"/api/abp/api-definition?includeDescriptions=true");
var controller = GetInterfaceOnlyController(model);
var action = GetAction(controller, "GetMessage");
var methodParam = action.ParametersOnMethod.FirstOrDefault(p => p.Name == "key");
methodParam.ShouldNotBeNull();
methodParam.Summary.ShouldNotBeNullOrEmpty();
methodParam.Summary.ShouldContain("message key");
var httpParam = action.Parameters.FirstOrDefault(p => p.NameOnMethod == "key");
httpParam.ShouldNotBeNull();
httpParam.Summary.ShouldNotBeNullOrEmpty();
httpParam.Summary.ShouldContain("message key");
}
private static ControllerApiDescriptionModel GetDocumentedController(ApplicationApiDescriptionModel model)
@ -197,6 +441,13 @@ public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBas
.First(c => c.ControllerName == "Documented");
}
private static ControllerApiDescriptionModel GetInterfaceOnlyController(ApplicationApiDescriptionModel model)
{
return model.Modules.Values
.SelectMany(m => m.Controllers.Values)
.First(c => c.ControllerName == "InterfaceOnlyDocumented");
}
private static ActionApiDescriptionModel GetAction(ControllerApiDescriptionModel controller, string actionName)
{
return controller.Actions.Values

26
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DocumentedAppService.cs

@ -23,9 +23,9 @@ public class DocumentedAppService : ApplicationService, IDocumentedAppService
/// <returns>A personalized greeting message.</returns>
[Description("Get greeting description from attribute")]
[Display(Name = "Get Greeting")]
public Task<string> GetGreetingAsync(string name)
public async Task<string> GetGreetingAsync(string name)
{
return Task.FromResult($"Hello, {name}!");
return await Task.FromResult($"Hello, {name}!");
}
/// <summary>
@ -33,8 +33,26 @@ public class DocumentedAppService : ApplicationService, IDocumentedAppService
/// </summary>
/// <param name="input">The input for creating a documented item.</param>
/// <returns>The created documented item.</returns>
public Task<DocumentedDto> CreateAsync(DocumentedDto input)
public async Task<DocumentedDto> CreateAsync(DocumentedDto input)
{
return Task.FromResult(input);
return await Task.FromResult(input);
}
/// <summary>
/// Searches for items matching the query.
/// </summary>
/// <param name="query">The search query string.</param>
/// <param name="maxResults">The maximum number of results to return.</param>
/// <returns>A list of matching item names.</returns>
public async Task<string> SearchAsync(
[Description("Query param description from attribute")] [Display(Name = "Search Query")] string query,
int maxResults)
{
return await Task.FromResult($"Results for {query}");
}
public async Task DeleteAsync(int id)
{
await Task.CompletedTask;
}
}

10
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IDocumentedAppService.cs

@ -25,4 +25,14 @@ public interface IDocumentedAppService : IApplicationService
/// <param name="input">The input for creating a documented item.</param>
/// <returns>The created documented item.</returns>
Task<DocumentedDto> CreateAsync(DocumentedDto input);
/// <summary>
/// Searches for items matching the query.
/// </summary>
/// <param name="query">The search query string.</param>
/// <param name="maxResults">The maximum number of results to return.</param>
/// <returns>A list of matching item names.</returns>
Task<string> SearchAsync(string query, int maxResults);
Task DeleteAsync(int id);
}

20
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IInterfaceOnlyDocumentedAppService.cs

@ -0,0 +1,20 @@
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace Volo.Abp.TestApp.Application;
/// <summary>
/// A service documented only on the interface to test XML doc fallback.
/// </summary>
/// <remarks>
/// Used to verify that documentation is resolved from the interface when the implementation has none.
/// </remarks>
public interface IInterfaceOnlyDocumentedAppService : IApplicationService
{
/// <summary>
/// Gets a message documented only on the interface.
/// </summary>
/// <param name="key">The message key.</param>
/// <returns>The resolved message.</returns>
Task<string> GetMessageAsync(string key);
}

12
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/InterfaceOnlyDocumentedAppService.cs

@ -0,0 +1,12 @@
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace Volo.Abp.TestApp.Application;
public class InterfaceOnlyDocumentedAppService : ApplicationService, IInterfaceOnlyDocumentedAppService
{
public async Task<string> GetMessageAsync(string key)
{
return await Task.FromResult(key);
}
}
Loading…
Cancel
Save