Browse Source

documentation support for abp api description

pull/25022/head
Nico Lachmuth 4 weeks ago
parent
commit
15e467517d
  1. 21
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/IXmlDocumentationProvider.cs
  2. 175
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProvider.cs
  3. 114
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs
  4. 8
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ActionApiDescriptionModel.cs
  5. 2
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModelRequestDto.cs
  6. 12
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs
  7. 6
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs
  8. 6
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs
  9. 6
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs
  10. 2
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs
  11. 8
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs
  12. 205
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Description_Tests.cs
  13. 1
      framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj
  14. 40
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DocumentedAppService.cs
  15. 25
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/DocumentedDto.cs
  16. 28
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IDocumentedAppService.cs

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

@ -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);
}

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

@ -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;
}
}

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

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using Asp.Versioning;
@ -12,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.ApiExploring;
using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.AspNetCore.Mvc.Utils;
using Volo.Abp.DependencyInjection;
@ -29,17 +32,20 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
private readonly IApiDescriptionGroupCollectionProvider _descriptionProvider;
private readonly AbpAspNetCoreMvcOptions _abpAspNetCoreMvcOptions;
private readonly AbpApiDescriptionModelOptions _modelOptions;
private readonly IXmlDocumentationProvider _xmlDocProvider;
public AspNetCoreApiDescriptionModelProvider(
IOptions<AspNetCoreApiDescriptionModelProviderOptions> options,
IApiDescriptionGroupCollectionProvider descriptionProvider,
IOptions<AbpAspNetCoreMvcOptions> abpAspNetCoreMvcOptions,
IOptions<AbpApiDescriptionModelOptions> modelOptions)
IOptions<AbpApiDescriptionModelOptions> modelOptions,
IXmlDocumentationProvider xmlDocProvider)
{
_options = options.Value;
_descriptionProvider = descriptionProvider;
_abpAspNetCoreMvcOptions = abpAspNetCoreMvcOptions.Value;
_modelOptions = modelOptions.Value;
_xmlDocProvider = xmlDocProvider;
Logger = NullLogger<AspNetCoreApiDescriptionModelProvider>.Instance;
}
@ -161,10 +167,21 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
if (input.IncludeTypes)
{
AddCustomTypesToModel(applicationModel, method);
AddCustomTypesToModel(applicationModel, method, input.IncludeDescriptions);
}
AddParameterDescriptionsToModel(actionModel, method, apiDescription);
if (input.IncludeDescriptions)
{
if (controllerModel.Summary == null && controllerModel.Description == null && controllerModel.DisplayName == null)
{
PopulateControllerDescriptions(controllerModel, controllerType);
}
PopulateActionDescriptions(actionModel, method);
PopulateParameterDescriptions(actionModel, method);
}
}
private static List<string> GetSupportedVersions(Type controllerType, MethodInfo method,
@ -191,18 +208,18 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
return supportedVersions.Select(v => v.ToString()).Distinct().ToList();
}
private void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel, MethodInfo method)
private void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel, MethodInfo method, bool includeDescriptions)
{
foreach (var parameterInfo in method.GetParameters())
{
AddCustomTypesToModel(applicationModel, parameterInfo.ParameterType);
AddCustomTypesToModel(applicationModel, parameterInfo.ParameterType, includeDescriptions);
}
AddCustomTypesToModel(applicationModel, method.ReturnType);
AddCustomTypesToModel(applicationModel, method.ReturnType, includeDescriptions);
}
private static void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel,
Type? type)
private void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel,
Type? type, bool includeDescriptions)
{
if (type == null)
{
@ -229,14 +246,14 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
if (TypeHelper.IsDictionary(type, out var keyType, out var valueType))
{
AddCustomTypesToModel(applicationModel, keyType);
AddCustomTypesToModel(applicationModel, valueType);
AddCustomTypesToModel(applicationModel, keyType, includeDescriptions);
AddCustomTypesToModel(applicationModel, valueType, includeDescriptions);
return;
}
if (TypeHelper.IsEnumerable(type, out var itemType))
{
AddCustomTypesToModel(applicationModel, itemType);
AddCustomTypesToModel(applicationModel, itemType, includeDescriptions);
return;
}
@ -244,11 +261,11 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
{
var genericTypeDefinition = type.GetGenericTypeDefinition();
AddCustomTypesToModel(applicationModel, genericTypeDefinition);
AddCustomTypesToModel(applicationModel, genericTypeDefinition, includeDescriptions);
foreach (var genericArgument in type.GetGenericArguments())
{
AddCustomTypesToModel(applicationModel, genericArgument);
AddCustomTypesToModel(applicationModel, genericArgument, includeDescriptions);
}
return;
@ -262,11 +279,16 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
applicationModel.Types[typeName] = TypeApiDescriptionModel.Create(type);
AddCustomTypesToModel(applicationModel, type.BaseType);
if (includeDescriptions)
{
PopulateTypeDescriptions(applicationModel.Types[typeName], type);
}
AddCustomTypesToModel(applicationModel, type.BaseType, includeDescriptions);
foreach (var propertyInfo in type.GetProperties().Where(p => p.DeclaringType == type))
{
AddCustomTypesToModel(applicationModel, propertyInfo.PropertyType);
AddCustomTypesToModel(applicationModel, propertyInfo.PropertyType, includeDescriptions);
}
}
@ -414,4 +436,68 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
return null;
}
private void PopulateControllerDescriptions(ControllerApiDescriptionModel controllerModel, Type controllerType)
{
controllerModel.Summary = _xmlDocProvider.GetSummary(controllerType);
controllerModel.Remarks = _xmlDocProvider.GetRemarks(controllerType);
controllerModel.Description = controllerType.GetCustomAttribute<DescriptionAttribute>()?.Description;
controllerModel.DisplayName = controllerType.GetCustomAttribute<DisplayAttribute>()?.Name;
}
private void PopulateActionDescriptions(ActionApiDescriptionModel actionModel, MethodInfo method)
{
actionModel.Summary = _xmlDocProvider.GetSummary(method);
actionModel.Remarks = _xmlDocProvider.GetRemarks(method);
actionModel.Description = method.GetCustomAttribute<DescriptionAttribute>()?.Description;
actionModel.DisplayName = method.GetCustomAttribute<DisplayAttribute>()?.Name;
actionModel.ReturnValue.Summary = _xmlDocProvider.GetReturns(method);
}
private void PopulateParameterDescriptions(ActionApiDescriptionModel actionModel, MethodInfo method)
{
foreach (var param in actionModel.ParametersOnMethod)
{
var paramInfo = method.GetParameters().FirstOrDefault(p => p.Name == param.Name);
if (paramInfo == null)
{
continue;
}
param.Summary = _xmlDocProvider.GetParameterSummary(method, 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);
}
}
private void PopulateTypeDescriptions(TypeApiDescriptionModel typeModel, Type type)
{
typeModel.Summary = _xmlDocProvider.GetSummary(type);
typeModel.Remarks = _xmlDocProvider.GetRemarks(type);
typeModel.Description = type.GetCustomAttribute<DescriptionAttribute>()?.Description;
typeModel.DisplayName = type.GetCustomAttribute<DisplayAttribute>()?.Name;
if (typeModel.Properties == null)
{
return;
}
foreach (var propModel in typeModel.Properties)
{
var propInfo = type.GetProperty(propModel.Name, BindingFlags.Instance | BindingFlags.Public);
if (propInfo == null)
{
continue;
}
propModel.Summary = _xmlDocProvider.GetSummary(propInfo);
propModel.Description = propInfo.GetCustomAttribute<DescriptionAttribute>()?.Description;
propModel.DisplayName = propInfo.GetCustomAttribute<DisplayAttribute>()?.Name;
}
}
}

8
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ActionApiDescriptionModel.cs

@ -32,6 +32,14 @@ public class ActionApiDescriptionModel
public string? ImplementFrom { get; set; }
public string? Summary { get; set; }
public string? Remarks { get; set; }
public string? Description { get; set; }
public string? DisplayName { get; set; }
public ActionApiDescriptionModel()
{

2
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModelRequestDto.cs

@ -3,4 +3,6 @@
public class ApplicationApiDescriptionModelRequestDto
{
public bool IncludeTypes { get; set; }
public bool IncludeDescriptions { get; set; }
}

12
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs

@ -19,6 +19,14 @@ public class ControllerApiDescriptionModel
public string Type { get; set; } = default!;
public string? Summary { get; set; }
public string? Remarks { get; set; }
public string? Description { get; set; }
public string? DisplayName { get; set; }
public List<ControllerInterfaceApiDescriptionModel> Interfaces { get; set; } = default!;
public Dictionary<string, ActionApiDescriptionModel> Actions { get; set; } = default!;
@ -66,6 +74,10 @@ public class ControllerApiDescriptionModel
Type = Type,
Interfaces = Interfaces,
ControllerName = ControllerName,
Summary = Summary,
Remarks = Remarks,
Description = Description,
DisplayName = DisplayName,
Actions = new Dictionary<string, ActionApiDescriptionModel>()
};

6
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs

@ -19,6 +19,12 @@ public class MethodParameterApiDescriptionModel
public object? DefaultValue { get; set; }
public string? Summary { get; set; }
public string? Description { get; set; }
public string? DisplayName { get; set; }
public MethodParameterApiDescriptionModel()
{

6
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs

@ -26,6 +26,12 @@ public class ParameterApiDescriptionModel
public string? DescriptorName { get; set; }
public string? Summary { get; set; }
public string? Description { get; set; }
public string? DisplayName { get; set; }
public ParameterApiDescriptionModel()
{

6
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs

@ -32,6 +32,12 @@ public class PropertyApiDescriptionModel
public bool IsNullable { get; set; }
public string? Summary { get; set; }
public string? Description { get; set; }
public string? DisplayName { get; set; }
public static PropertyApiDescriptionModel Create(PropertyInfo propertyInfo)
{
var customAttributes = propertyInfo.GetCustomAttributes(true);

2
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs

@ -11,6 +11,8 @@ public class ReturnValueApiDescriptionModel
public string TypeSimple { get; set; } = default!;
public string? Summary { get; set; }
public ReturnValueApiDescriptionModel()
{

8
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs

@ -20,6 +20,14 @@ public class TypeApiDescriptionModel
public PropertyApiDescriptionModel[]? Properties { get; set; }
public string? Summary { get; set; }
public string? Remarks { get; set; }
public string? Description { get; set; }
public string? DisplayName { get; set; }
public TypeApiDescriptionModel()
{

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

@ -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);
}
}

1
framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj

@ -6,6 +6,7 @@
<TargetFramework>net10.0</TargetFramework>
<RootNamespace />
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>

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

@ -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);
}
}

25
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/DocumentedDto.cs

@ -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; }
}

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

@ -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…
Cancel
Save