mirror of https://github.com/abpframework/abp.git
committed by
GitHub
26 changed files with 1668 additions and 53 deletions
@ -0,0 +1,22 @@ |
|||||
|
using System; |
||||
|
using System.Reflection; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; |
||||
|
|
||||
|
public interface IXmlDocumentationProvider |
||||
|
{ |
||||
|
Task<string?> GetSummaryAsync(Type type); |
||||
|
|
||||
|
Task<string?> GetRemarksAsync(Type type); |
||||
|
|
||||
|
Task<string?> GetSummaryAsync(MethodInfo method); |
||||
|
|
||||
|
Task<string?> GetRemarksAsync(MethodInfo method); |
||||
|
|
||||
|
Task<string?> GetReturnsAsync(MethodInfo method); |
||||
|
|
||||
|
Task<string?> GetParameterSummaryAsync(MethodInfo method, string parameterName); |
||||
|
|
||||
|
Task<string?> GetSummaryAsync(PropertyInfo property); |
||||
|
} |
||||
@ -0,0 +1,231 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Concurrent; |
||||
|
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 Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Logging.Abstractions; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; |
||||
|
|
||||
|
public class XmlDocumentationProvider : IXmlDocumentationProvider, ISingletonDependency |
||||
|
{ |
||||
|
public ILogger<XmlDocumentationProvider> Logger { get; set; } |
||||
|
|
||||
|
public XmlDocumentationProvider() |
||||
|
{ |
||||
|
Logger = NullLogger<XmlDocumentationProvider>.Instance; |
||||
|
} |
||||
|
|
||||
|
private static readonly Regex WhitespaceRegex = new(@"\s+", RegexOptions.Compiled); |
||||
|
|
||||
|
// Matches any remaining XML tags like <c>, <code>, <para>, <b>, etc.
|
||||
|
private static readonly Regex XmlTagRegex = new(@"<[^>]+>", RegexOptions.Compiled); |
||||
|
|
||||
|
// Matches <see cref="T:Foo.Bar"/>, <see langword="null"/>, <paramref name="x"/>, <typeparamref name="T"/>
|
||||
|
private static readonly Regex XmlRefTagRegex = new( |
||||
|
@"<(see|paramref|typeparamref)\s+(cref|name|langword)=""([TMFPE]:)?(?<display>[^""]+)""\s*/?>", |
||||
|
RegexOptions.Compiled); |
||||
|
|
||||
|
private readonly ConcurrentDictionary<Assembly, Lazy<Task<XDocument?>>> _xmlDocCache = new(); |
||||
|
|
||||
|
public virtual async Task<string?> GetSummaryAsync(Type type) |
||||
|
{ |
||||
|
var memberName = GetMemberNameForType(type); |
||||
|
return await GetDocumentationElementAsync(type.Assembly, memberName, "summary"); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<string?> GetRemarksAsync(Type type) |
||||
|
{ |
||||
|
var memberName = GetMemberNameForType(type); |
||||
|
return await GetDocumentationElementAsync(type.Assembly, memberName, "remarks"); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<string?> GetSummaryAsync(MethodInfo method) |
||||
|
{ |
||||
|
var memberName = GetMemberNameForMethod(method); |
||||
|
return await GetDocumentationElementAsync(method.DeclaringType!.Assembly, memberName, "summary"); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<string?> GetRemarksAsync(MethodInfo method) |
||||
|
{ |
||||
|
var memberName = GetMemberNameForMethod(method); |
||||
|
return await GetDocumentationElementAsync(method.DeclaringType!.Assembly, memberName, "remarks"); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<string?> GetReturnsAsync(MethodInfo method) |
||||
|
{ |
||||
|
var memberName = GetMemberNameForMethod(method); |
||||
|
return await GetDocumentationElementAsync(method.DeclaringType!.Assembly, memberName, "returns"); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<string?> GetParameterSummaryAsync(MethodInfo method, string parameterName) |
||||
|
{ |
||||
|
var memberName = GetMemberNameForMethod(method); |
||||
|
var doc = await LoadXmlDocumentationAsync(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 virtual async Task<string?> GetSummaryAsync(PropertyInfo property) |
||||
|
{ |
||||
|
var memberName = GetMemberNameForProperty(property); |
||||
|
return await GetDocumentationElementAsync(property.DeclaringType!.Assembly, memberName, "summary"); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<string?> GetDocumentationElementAsync(Assembly assembly, string memberName, string elementName) |
||||
|
{ |
||||
|
var doc = await LoadXmlDocumentationAsync(assembly); |
||||
|
if (doc == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var memberNode = doc.XPathSelectElement($"//member[@name='{memberName}']"); |
||||
|
var element = memberNode?.Element(elementName); |
||||
|
return CleanXmlText(element); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task<XDocument?> LoadXmlDocumentationAsync(Assembly assembly) |
||||
|
{ |
||||
|
return _xmlDocCache.GetOrAdd( |
||||
|
assembly, |
||||
|
asm => new Lazy<Task<XDocument?>>( |
||||
|
() => LoadXmlDocumentationFromDiskAsync(asm), |
||||
|
LazyThreadSafetyMode.ExecutionAndPublication) |
||||
|
).Value; |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<XDocument?> LoadXmlDocumentationFromDiskAsync(Assembly assembly) |
||||
|
{ |
||||
|
if (string.IsNullOrEmpty(assembly.Location)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var xmlFilePath = Path.ChangeExtension(assembly.Location, ".xml"); |
||||
|
if (!File.Exists(xmlFilePath)) |
||||
|
{ |
||||
|
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 (Exception ex) |
||||
|
{ |
||||
|
Logger.LogWarning(ex, "Failed to load XML documentation from {XmlFilePath}.", xmlFilePath); |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static string? CleanXmlText(XElement? element) |
||||
|
{ |
||||
|
if (element == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// Convert to string first so we can process inline XML tags like <see cref="..."/>
|
||||
|
var raw = element.ToString(); |
||||
|
|
||||
|
// Strip the outer element tags (e.g. <summary>...</summary>)
|
||||
|
var start = raw.IndexOf('>') + 1; |
||||
|
var end = raw.LastIndexOf('<'); |
||||
|
if (start >= end) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var inner = raw[start..end]; |
||||
|
|
||||
|
// Replace <see cref="T:Foo.Bar"/> with the short name "Bar"
|
||||
|
// Replace <see langword="null"/> with "null"
|
||||
|
// Replace <paramref name="x"/> and <typeparamref name="T"/> with the name
|
||||
|
inner = XmlRefTagRegex.Replace(inner, m => |
||||
|
{ |
||||
|
var display = m.Groups["display"].Value; |
||||
|
// For cref values like "T:Foo.Bar.Baz", return only "Baz"
|
||||
|
var dot = display.LastIndexOf('.'); |
||||
|
return dot >= 0 ? display[(dot + 1)..] : display; |
||||
|
}); |
||||
|
|
||||
|
// Strip any remaining XML tags (e.g. <c>, <code>, <para>, <b>, etc.)
|
||||
|
inner = XmlTagRegex.Replace(inner, string.Empty); |
||||
|
|
||||
|
if (string.IsNullOrWhiteSpace(inner)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return WhitespaceRegex.Replace(inner.Trim(), " "); |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
@ -1,6 +1,8 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
namespace Volo.Abp.Http.Modeling; |
namespace Volo.Abp.Http.Modeling; |
||||
|
|
||||
public interface IApiDescriptionModelProvider |
public interface IApiDescriptionModelProvider |
||||
{ |
{ |
||||
ApplicationApiDescriptionModel CreateApiModel(ApplicationApiDescriptionModelRequestDto input); |
Task<ApplicationApiDescriptionModel> CreateApiModelAsync(ApplicationApiDescriptionModelRequestDto input); |
||||
} |
} |
||||
|
|||||
@ -1,6 +1,8 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
namespace Volo.Abp.Http.ProxyScripting; |
namespace Volo.Abp.Http.ProxyScripting; |
||||
|
|
||||
public interface IProxyScriptManager |
public interface IProxyScriptManager |
||||
{ |
{ |
||||
string GetScript(ProxyScriptingModel scriptingModel); |
Task<string> GetScriptAsync(ProxyScriptingModel scriptingModel); |
||||
} |
} |
||||
|
|||||
@ -1,10 +1,9 @@ |
|||||
using System; |
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
namespace Volo.Abp.Http.ProxyScripting; |
namespace Volo.Abp.Http.ProxyScripting; |
||||
|
|
||||
public interface IProxyScriptManagerCache |
public interface IProxyScriptManagerCache |
||||
{ |
{ |
||||
string GetOrAdd(string key, Func<string> factory); |
Task<string> GetOrAddAsync(string key, Func<Task<string>> factory); |
||||
|
|
||||
void Set(string key, string value); |
|
||||
} |
} |
||||
|
|||||
@ -1,26 +1,31 @@ |
|||||
using System; |
using System; |
||||
using System.Collections.Concurrent; |
using System.Collections.Concurrent; |
||||
using System.Collections.Generic; |
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
using Volo.Abp.DependencyInjection; |
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
namespace Volo.Abp.Http.ProxyScripting; |
namespace Volo.Abp.Http.ProxyScripting; |
||||
|
|
||||
public class ProxyScriptManagerCache : IProxyScriptManagerCache, ISingletonDependency |
public class ProxyScriptManagerCache : IProxyScriptManagerCache, ISingletonDependency |
||||
{ |
{ |
||||
private readonly ConcurrentDictionary<string, string> _cache; |
private readonly ConcurrentDictionary<string, string> _cache = new(); |
||||
|
private readonly ConcurrentDictionary<string, Lazy<Task<string>>> _asyncCache = new(); |
||||
|
|
||||
public ProxyScriptManagerCache() |
public async Task<string> GetOrAddAsync(string key, Func<Task<string>> factory) |
||||
{ |
{ |
||||
_cache = new ConcurrentDictionary<string, string>(); |
if (_cache.TryGetValue(key, out var cached)) |
||||
} |
{ |
||||
|
return cached; |
||||
|
} |
||||
|
|
||||
public string GetOrAdd(string key, Func<string> factory) |
var result = await _asyncCache.GetOrAdd( |
||||
{ |
key, |
||||
return _cache.GetOrAdd(key, factory); |
_ => new Lazy<Task<string>>(factory, LazyThreadSafetyMode.ExecutionAndPublication) |
||||
} |
).Value; |
||||
|
|
||||
public void Set(string key, string value) |
_cache[key] = result; |
||||
{ |
_asyncCache.TryRemove(key, out _); |
||||
_cache[key] = value; |
|
||||
|
return result; |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,536 @@ |
|||||
|
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_Controller_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 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() |
||||
|
{ |
||||
|
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 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); |
||||
|
|
||||
|
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"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task IncludeDescriptions_Should_Populate_ReturnValue_Summary() |
||||
|
{ |
||||
|
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
||||
|
"/api/abp/api-definition?includeDescriptions=true"); |
||||
|
|
||||
|
var action = GetAction(GetDocumentedController(model), "GetGreeting"); |
||||
|
action.ReturnValue.Summary.ShouldNotBeNullOrEmpty(); |
||||
|
action.ReturnValue.Summary.ShouldContain("personalized greeting"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Undocumented_Action_Should_Have_Null_Descriptions() |
||||
|
{ |
||||
|
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
||||
|
"/api/abp/api-definition?includeDescriptions=true"); |
||||
|
|
||||
|
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(); |
||||
|
param.Summary.ShouldContain("name of the person"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task IncludeDescriptions_Should_Populate_ParameterOnMethod_Description_And_DisplayName_From_Attribute() |
||||
|
{ |
||||
|
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 == "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(); |
||||
|
param.Summary.ShouldContain("name of the person"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task IncludeDescriptions_Should_Populate_Parameter_Description_And_DisplayName_From_Attribute() |
||||
|
{ |
||||
|
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 == "query"); |
||||
|
param.ShouldNotBeNull(); |
||||
|
param.Summary.ShouldNotBeNullOrEmpty(); |
||||
|
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_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_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.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"); |
||||
|
} |
||||
|
|
||||
|
[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(); |
||||
|
valueProp.Summary.ShouldNotBeNullOrEmpty(); |
||||
|
valueProp.Description.ShouldBe("Value description from attribute"); |
||||
|
valueProp.DisplayName.ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task IncludeTypes_Without_IncludeDescriptions_Should_Not_Populate_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(); |
||||
|
|
||||
|
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 IncludeDescriptions_Should_Fallback_To_Interface_For_Controller_Summary() |
||||
|
{ |
||||
|
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
||||
|
"/api/abp/api-definition?includeDescriptions=true"); |
||||
|
|
||||
|
var controller = GetInterfaceOnlyController(model); |
||||
|
controller.Summary.ShouldNotBeNullOrEmpty(); |
||||
|
controller.Summary.ShouldContain("documented only on the interface"); |
||||
|
} |
||||
|
|
||||
|
[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"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task IncludeDescriptions_Should_Not_Apply_Container_Param_Summary_To_Expanded_Properties() |
||||
|
{ |
||||
|
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
||||
|
"/api/abp/api-definition?includeDescriptions=true"); |
||||
|
|
||||
|
var controller = GetDocumentedController(model); |
||||
|
var action = GetAction(controller, "Create"); |
||||
|
|
||||
|
// Expanded properties from DocumentedDto should not have the container parameter's summary
|
||||
|
var expandedParams = action.Parameters |
||||
|
.Where(p => !string.IsNullOrEmpty(p.DescriptorName) && p.Name != p.NameOnMethod) |
||||
|
.ToList(); |
||||
|
|
||||
|
foreach (var param in expandedParams) |
||||
|
{ |
||||
|
param.Summary.ShouldBeNull(); |
||||
|
param.Description.ShouldBeNull(); |
||||
|
param.DisplayName.ShouldBeNull(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Action_ImplementFrom_Should_Point_To_Implemented_Interface() |
||||
|
{ |
||||
|
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
||||
|
"/api/abp/api-definition"); |
||||
|
|
||||
|
var controller = GetDocumentedController(model); |
||||
|
var action = GetAction(controller, "GetGreeting"); |
||||
|
|
||||
|
action.ImplementFrom.ShouldNotBeNullOrEmpty(); |
||||
|
action.ImplementFrom.ShouldContain("IDocumentedAppService"); |
||||
|
action.ImplementFrom.ShouldNotContain("DocumentedAppService."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Action_ImplementFrom_Should_Point_To_Interface_When_Only_Documented_On_Interface() |
||||
|
{ |
||||
|
var model = await GetResponseAsObjectAsync<ApplicationApiDescriptionModel>( |
||||
|
"/api/abp/api-definition"); |
||||
|
|
||||
|
var controller = GetInterfaceOnlyController(model); |
||||
|
var action = GetAction(controller, "GetMessage"); |
||||
|
|
||||
|
action.ImplementFrom.ShouldNotBeNullOrEmpty(); |
||||
|
action.ImplementFrom.ShouldContain("IInterfaceOnlyDocumentedAppService"); |
||||
|
action.ImplementFrom.ShouldNotContain("InterfaceOnlyDocumentedAppService."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CreateSubModel_Should_Preserve_All_Controller_Properties() |
||||
|
{ |
||||
|
var controller = ControllerApiDescriptionModel.Create( |
||||
|
"TestController", |
||||
|
"TestGroup", |
||||
|
isRemoteService: true, |
||||
|
isIntegrationService: false, |
||||
|
apiVersion: "1.0", |
||||
|
typeof(AbpApiDefinitionController_Description_Tests)); |
||||
|
|
||||
|
controller.Summary = "Test summary"; |
||||
|
controller.Remarks = "Test remarks"; |
||||
|
controller.Description = "Test description"; |
||||
|
controller.DisplayName = "Test display name"; |
||||
|
|
||||
|
var subModel = controller.CreateSubModel(null); |
||||
|
|
||||
|
subModel.ControllerName.ShouldBe("TestController"); |
||||
|
subModel.ControllerGroupName.ShouldBe("TestGroup"); |
||||
|
subModel.IsRemoteService.ShouldBeTrue(); |
||||
|
subModel.IsIntegrationService.ShouldBeFalse(); |
||||
|
subModel.ApiVersion.ShouldBe("1.0"); |
||||
|
subModel.Summary.ShouldBe("Test summary"); |
||||
|
subModel.Remarks.ShouldBe("Test remarks"); |
||||
|
subModel.Description.ShouldBe("Test description"); |
||||
|
subModel.DisplayName.ShouldBe("Test display name"); |
||||
|
subModel.Type.ShouldBe(controller.Type); |
||||
|
} |
||||
|
|
||||
|
private static ControllerApiDescriptionModel GetDocumentedController(ApplicationApiDescriptionModel model) |
||||
|
{ |
||||
|
return model.Modules.Values |
||||
|
.SelectMany(m => m.Controllers.Values) |
||||
|
.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 |
||||
|
.First(a => a.Name == actionName + "Async" || a.Name == actionName); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,427 @@ |
|||||
|
#nullable enable |
||||
|
using System; |
||||
|
using System.Reflection; |
||||
|
using System.Threading.Tasks; |
||||
|
using System.Xml.Linq; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.ApiExploring; |
||||
|
|
||||
|
public class XmlDocumentationProviderTests |
||||
|
{ |
||||
|
// A stub type so we can construct member-name keys that the provider can look up.
|
||||
|
private class StubType { } |
||||
|
|
||||
|
private static XmlDocumentationProvider CreateProvider(string xmlDocBody) |
||||
|
{ |
||||
|
var xml = $@"<?xml version=""1.0""?>
|
||||
|
<doc> |
||||
|
<members> |
||||
|
{xmlDocBody} |
||||
|
</members> |
||||
|
</doc>";
|
||||
|
return new FakeXmlDocumentationProvider(xml); |
||||
|
} |
||||
|
|
||||
|
private static string StubTypeMemberName(string elementName, string xmlContent) |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
return $@"<member name=""{elementName}:{typeName}"">
|
||||
|
{xmlContent} |
||||
|
</member>";
|
||||
|
} |
||||
|
|
||||
|
// Tests for CleanXmlText via GetSummaryAsync(Type)
|
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Returns_PlainText() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>A simple summary.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("A simple summary."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Expands_SeeCref_To_ShortTypeName() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>Returns a <see cref=""T:System.String"" /> value.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("Returns a String value."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Expands_SeeCref_NestedType() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>See <see cref=""T:System.Collections.Generic.List`1"" /> for details.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("See List`1 for details."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Expands_SeeLangword() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>Returns <see langword=""null"" /> when not found.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("Returns null when not found."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Strips_CodeTag() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>Use <c>DoSomething()</c> to start.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("Use DoSomething() to start."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Strips_ParaTag() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary><para>First paragraph.</para></summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("First paragraph."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Collapses_Whitespace() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>
|
||||
|
Multiple |
||||
|
spaces here. |
||||
|
</summary></member>");
|
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("Multiple spaces here."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Returns_Null_When_Member_Not_Found() |
||||
|
{ |
||||
|
var provider = CreateProvider(string.Empty); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Returns_Null_When_Summary_Is_Empty() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary> </summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Expands_Paramref() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>Use the <paramref name=""input"" /> parameter.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("Use the input parameter."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Expands_Typeparamref() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>Returns <typeparamref name=""T"" /> instance.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("Returns T instance."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Expands_Mixed_Tags() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>Returns <see cref=""T:System.String"" /> or <see langword=""null"" /> if <paramref name=""key"" /> not found.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("Returns String or null if key not found."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetRemarks_Returns_Null_When_No_Remarks_Element() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>Only summary.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetRemarksAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetRemarks_Returns_Remarks_Content() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>Summary text.</summary><remarks>Some remarks here.</remarks></member>"); |
||||
|
|
||||
|
var result = await provider.GetRemarksAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("Some remarks here."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Returns_Null_For_SelfClosing_Summary_Tag() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary/></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Expands_SeeCref_Without_TypePrefix() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>See <see cref=""M:Foo.Bar.DoWork"" /> for details.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("See DoWork for details."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Expands_SeeCref_Property() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary>See <see cref=""P:Foo.Bar.Name"" /> property.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("See Name property."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_Strips_Multiple_Xml_Tags() |
||||
|
{ |
||||
|
var typeName = typeof(StubType).FullName!.Replace('+', '.'); |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""T:{typeName}""><summary><para>First.</para> <c>code</c> <b>bold</b> end.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(typeof(StubType)); |
||||
|
|
||||
|
result.ShouldBe("First. code bold end."); |
||||
|
} |
||||
|
|
||||
|
// Tests for GetSummaryAsync(MethodInfo) and GetReturnsAsync(MethodInfo)
|
||||
|
|
||||
|
private class StubService |
||||
|
{ |
||||
|
public string GetValue(string key, int count) => key; |
||||
|
public string NoParams() => string.Empty; |
||||
|
public string Name { get; set; } = default!; |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_For_Method_Returns_Summary() |
||||
|
{ |
||||
|
var typeName = typeof(StubService).FullName!.Replace('+', '.'); |
||||
|
var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""M:{typeName}.GetValue(System.String,System.Int32)""><summary>Gets a value by key.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(method); |
||||
|
|
||||
|
result.ShouldBe("Gets a value by key."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_For_Method_Without_Parameters_Returns_Summary() |
||||
|
{ |
||||
|
var typeName = typeof(StubService).FullName!.Replace('+', '.'); |
||||
|
var method = typeof(StubService).GetMethod(nameof(StubService.NoParams))!; |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""M:{typeName}.NoParams""><summary>No params method.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(method); |
||||
|
|
||||
|
result.ShouldBe("No params method."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetReturns_For_Method_Returns_Content() |
||||
|
{ |
||||
|
var typeName = typeof(StubService).FullName!.Replace('+', '.'); |
||||
|
var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""M:{typeName}.GetValue(System.String,System.Int32)""><returns>The resolved value.</returns></member>"); |
||||
|
|
||||
|
var result = await provider.GetReturnsAsync(method); |
||||
|
|
||||
|
result.ShouldBe("The resolved value."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetReturns_Returns_Null_When_No_Returns_Element() |
||||
|
{ |
||||
|
var typeName = typeof(StubService).FullName!.Replace('+', '.'); |
||||
|
var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""M:{typeName}.GetValue(System.String,System.Int32)""><summary>Gets a value.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetReturnsAsync(method); |
||||
|
|
||||
|
result.ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetParameterSummary_Returns_Content() |
||||
|
{ |
||||
|
var typeName = typeof(StubService).FullName!.Replace('+', '.'); |
||||
|
var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""M:{typeName}.GetValue(System.String,System.Int32)""><param name=""key"">The lookup key.</param><param name=""count"">Max results.</param></member>"); |
||||
|
|
||||
|
var result = await provider.GetParameterSummaryAsync(method, "key"); |
||||
|
result.ShouldBe("The lookup key."); |
||||
|
|
||||
|
var result2 = await provider.GetParameterSummaryAsync(method, "count"); |
||||
|
result2.ShouldBe("Max results."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetParameterSummary_Returns_Null_When_Param_Not_Found() |
||||
|
{ |
||||
|
var typeName = typeof(StubService).FullName!.Replace('+', '.'); |
||||
|
var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""M:{typeName}.GetValue(System.String,System.Int32)""><param name=""key"">The key.</param></member>"); |
||||
|
|
||||
|
var result = await provider.GetParameterSummaryAsync(method, "nonExistent"); |
||||
|
|
||||
|
result.ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetParameterSummary_Returns_Null_When_Member_Not_Found() |
||||
|
{ |
||||
|
var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; |
||||
|
var provider = CreateProvider(string.Empty); |
||||
|
|
||||
|
var result = await provider.GetParameterSummaryAsync(method, "key"); |
||||
|
|
||||
|
result.ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
// Tests for GetSummaryAsync(PropertyInfo)
|
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_For_Property_Returns_Summary() |
||||
|
{ |
||||
|
var typeName = typeof(StubService).FullName!.Replace('+', '.'); |
||||
|
var property = typeof(StubService).GetProperty(nameof(StubService.Name))!; |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""P:{typeName}.Name""><summary>The name property.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(property); |
||||
|
|
||||
|
result.ShouldBe("The name property."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetSummary_For_Property_Returns_Null_When_Not_Found() |
||||
|
{ |
||||
|
var property = typeof(StubService).GetProperty(nameof(StubService.Name))!; |
||||
|
var provider = CreateProvider(string.Empty); |
||||
|
|
||||
|
var result = await provider.GetSummaryAsync(property); |
||||
|
|
||||
|
result.ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
// Tests for GetRemarksAsync(MethodInfo)
|
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetRemarks_For_Method_Returns_Content() |
||||
|
{ |
||||
|
var typeName = typeof(StubService).FullName!.Replace('+', '.'); |
||||
|
var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""M:{typeName}.GetValue(System.String,System.Int32)""><remarks>Implementation note.</remarks></member>"); |
||||
|
|
||||
|
var result = await provider.GetRemarksAsync(method); |
||||
|
|
||||
|
result.ShouldBe("Implementation note."); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetRemarks_For_Method_Returns_Null_When_Not_Found() |
||||
|
{ |
||||
|
var typeName = typeof(StubService).FullName!.Replace('+', '.'); |
||||
|
var method = typeof(StubService).GetMethod(nameof(StubService.GetValue))!; |
||||
|
var provider = CreateProvider( |
||||
|
$@"<member name=""M:{typeName}.GetValue(System.String,System.Int32)""><summary>Summary only.</summary></member>"); |
||||
|
|
||||
|
var result = await provider.GetRemarksAsync(method); |
||||
|
|
||||
|
result.ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// A fake provider that loads XML from an in-memory string instead of the file system.
|
||||
|
/// </summary>
|
||||
|
[DisableConventionalRegistration] |
||||
|
private sealed class FakeXmlDocumentationProvider : XmlDocumentationProvider |
||||
|
{ |
||||
|
private readonly XDocument _document; |
||||
|
|
||||
|
public FakeXmlDocumentationProvider(string xml) |
||||
|
{ |
||||
|
_document = XDocument.Parse(xml); |
||||
|
} |
||||
|
|
||||
|
protected override Task<XDocument?> LoadXmlDocumentationFromDiskAsync(Assembly assembly) |
||||
|
{ |
||||
|
return Task.FromResult<XDocument?>(_document); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
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 async Task<string> GetGreetingAsync(string name) |
||||
|
{ |
||||
|
return await 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 async Task<DocumentedDto> CreateAsync(DocumentedDto 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; |
||||
|
} |
||||
|
} |
||||
@ -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,38 @@ |
|||||
|
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); |
||||
|
|
||||
|
/// <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); |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -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…
Reference in new issue