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; |
|||
|
|||
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; |
|||
|
|||
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; |
|||
|
|||
public interface IProxyScriptManagerCache |
|||
{ |
|||
string GetOrAdd(string key, Func<string> factory); |
|||
|
|||
void Set(string key, string value); |
|||
Task<string> GetOrAddAsync(string key, Func<Task<string>> factory); |
|||
} |
|||
|
|||
@ -1,26 +1,31 @@ |
|||
using System; |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Http.ProxyScripting; |
|||
|
|||
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) |
|||
{ |
|||
return _cache.GetOrAdd(key, factory); |
|||
} |
|||
var result = await _asyncCache.GetOrAdd( |
|||
key, |
|||
_ => new Lazy<Task<string>>(factory, LazyThreadSafetyMode.ExecutionAndPublication) |
|||
).Value; |
|||
|
|||
public void Set(string key, string value) |
|||
{ |
|||
_cache[key] = value; |
|||
_cache[key] = result; |
|||
_asyncCache.TryRemove(key, out _); |
|||
|
|||
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