Browse Source

feat: Enhance XmlDocumentationProvider and related classes for improved XML documentation handling and caching

pull/25022/head
maliming 3 weeks ago
parent
commit
68ac1cee60
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 26
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProvider.cs
  2. 70
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs
  3. 4
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs
  4. 9
      framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManagerCache.cs
  5. 8
      framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs
  6. 34
      framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManagerCache.cs
  7. 80
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Description_Tests.cs
  8. 214
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProviderTests.cs

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

@ -8,20 +8,32 @@ 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, Task<XDocument?>> _xmlDocCache = new();
private readonly ConcurrentDictionary<Assembly, Lazy<Task<XDocument?>>> _xmlDocCache = new();
public virtual async Task<string?> GetSummaryAsync(Type type)
{
@ -88,7 +100,12 @@ public class XmlDocumentationProvider : IXmlDocumentationProvider, ISingletonDep
protected virtual Task<XDocument?> LoadXmlDocumentationAsync(Assembly assembly)
{
return _xmlDocCache.GetOrAdd(assembly, LoadXmlDocumentationFromDiskAsync);
return _xmlDocCache.GetOrAdd(
assembly,
asm => new Lazy<Task<XDocument?>>(
() => LoadXmlDocumentationFromDiskAsync(asm),
LazyThreadSafetyMode.ExecutionAndPublication)
).Value;
}
protected virtual async Task<XDocument?> LoadXmlDocumentationFromDiskAsync(Assembly assembly)
@ -109,8 +126,9 @@ public class XmlDocumentationProvider : IXmlDocumentationProvider, ISingletonDep
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
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to load XML documentation from {XmlFilePath}.", xmlFilePath);
return null;
}
}
@ -147,7 +165,7 @@ public class XmlDocumentationProvider : IXmlDocumentationProvider, ISingletonDep
});
// Strip any remaining XML tags (e.g. <c>, <code>, <para>, <b>, etc.)
inner = Regex.Replace(inner, @"<[^>]+>", string.Empty);
inner = XmlTagRegex.Replace(inner, string.Empty);
if (string.IsNullOrWhiteSpace(inner))
{

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

@ -148,10 +148,21 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
var implementFrom = controllerType.FullName;
var interfaceType = controllerType.GetInterfaces().FirstOrDefault(i => i.GetMethods().Any(x => x.ToString() == method.ToString()));
if (interfaceType != null)
foreach (var iface in controllerType.GetInterfaces())
{
implementFrom = TypeHelper.GetFullNameHandlingNullableAndGenerics(interfaceType);
try
{
var map = controllerType.GetInterfaceMap(iface);
if (Array.IndexOf(map.TargetMethods, method) >= 0)
{
implementFrom = TypeHelper.GetFullNameHandlingNullableAndGenerics(iface);
break;
}
}
catch (ArgumentException)
{
// GetInterfaceMap is not supported for some generic interface edge cases
}
}
var actionModel = controllerModel.AddAction(
@ -182,8 +193,9 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
await PopulateControllerDescriptionsAsync(controllerModel, controllerType);
}
await PopulateActionDescriptionsAsync(actionModel, method);
await PopulateParameterDescriptionsAsync(actionModel, method);
var interfaceMethod = GetInterfaceMethod(method);
await PopulateActionDescriptionsAsync(actionModel, method, interfaceMethod);
await PopulateParameterDescriptionsAsync(actionModel, method, interfaceMethod);
}
}
@ -447,7 +459,7 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
if (controllerModel.Summary == null && controllerModel.Remarks == null)
{
foreach (var interfaceType in GetDirectInterfaces(controllerType))
foreach (var interfaceType in GetDirectInterfaces(controllerType).Where(i => !_modelOptions.IgnoredInterfaces.Contains(i)))
{
controllerModel.Summary = await _xmlDocProvider.GetSummaryAsync(interfaceType);
controllerModel.Remarks = await _xmlDocProvider.GetRemarksAsync(interfaceType);
@ -462,38 +474,29 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
controllerModel.DisplayName = controllerType.GetCustomAttribute<DisplayAttribute>()?.Name;
}
protected virtual async Task PopulateActionDescriptionsAsync(ActionApiDescriptionModel actionModel, MethodInfo method)
protected virtual async Task PopulateActionDescriptionsAsync(ActionApiDescriptionModel actionModel, MethodInfo method, MethodInfo? interfaceMethod)
{
actionModel.Summary = await _xmlDocProvider.GetSummaryAsync(method);
actionModel.Remarks = await _xmlDocProvider.GetRemarksAsync(method);
if (actionModel.Summary == null && actionModel.Remarks == null)
if (actionModel.Summary == null && actionModel.Remarks == null && interfaceMethod != null)
{
var interfaceMethod = GetInterfaceMethod(method);
if (interfaceMethod != null)
{
actionModel.Summary = await _xmlDocProvider.GetSummaryAsync(interfaceMethod);
actionModel.Remarks = await _xmlDocProvider.GetRemarksAsync(interfaceMethod);
}
actionModel.Summary = await _xmlDocProvider.GetSummaryAsync(interfaceMethod);
actionModel.Remarks = await _xmlDocProvider.GetRemarksAsync(interfaceMethod);
}
actionModel.Description = method.GetCustomAttribute<DescriptionAttribute>()?.Description;
actionModel.DisplayName = method.GetCustomAttribute<DisplayAttribute>()?.Name;
actionModel.ReturnValue.Summary = await _xmlDocProvider.GetReturnsAsync(method);
if (actionModel.ReturnValue.Summary == null)
if (actionModel.ReturnValue.Summary == null && interfaceMethod != null)
{
var interfaceMethod = GetInterfaceMethod(method);
if (interfaceMethod != null)
{
actionModel.ReturnValue.Summary = await _xmlDocProvider.GetReturnsAsync(interfaceMethod);
}
actionModel.ReturnValue.Summary = await _xmlDocProvider.GetReturnsAsync(interfaceMethod);
}
}
protected virtual async Task PopulateParameterDescriptionsAsync(ActionApiDescriptionModel actionModel, MethodInfo method)
protected virtual async Task PopulateParameterDescriptionsAsync(ActionApiDescriptionModel actionModel, MethodInfo method, MethodInfo? interfaceMethod)
{
var interfaceMethod = GetInterfaceMethod(method);
var methodParameters = method.GetParameters();
foreach (var param in actionModel.ParametersOnMethod)
@ -516,6 +519,13 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
foreach (var param in actionModel.Parameters)
{
// Skip expanded properties from complex types - their descriptions
// should come from type-level documentation (PopulateTypeDescriptionsAsync)
if (!string.IsNullOrEmpty(param.DescriptorName) && param.Name != param.NameOnMethod)
{
continue;
}
param.Summary = await _xmlDocProvider.GetParameterSummaryAsync(method, param.NameOnMethod);
if (param.Summary == null && interfaceMethod != null)
{
@ -531,7 +541,7 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
}
}
private static MethodInfo? GetInterfaceMethod(MethodInfo method)
private MethodInfo? GetInterfaceMethod(MethodInfo method)
{
var declaringType = method.DeclaringType;
if (declaringType == null || declaringType.IsInterface)
@ -539,13 +549,15 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
return null;
}
foreach (var interfaceType in GetDirectInterfaces(declaringType))
foreach (var interfaceType in GetDirectInterfaces(declaringType).Where(i => !_modelOptions.IgnoredInterfaces.Contains(i)))
{
var interfaceMethod = interfaceType.GetMethods()
.FirstOrDefault(m => m.ToString() == method.ToString());
if (interfaceMethod != null)
var map = declaringType.GetInterfaceMap(interfaceType);
for (var i = 0; i < map.TargetMethods.Length; i++)
{
return interfaceMethod;
if (map.TargetMethods[i] == method)
{
return map.InterfaceMethods[i];
}
}
}
@ -573,7 +585,7 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
foreach (var propModel in typeModel.Properties)
{
var propInfo = type.GetProperty(propModel.Name, BindingFlags.Instance | BindingFlags.Public);
var propInfo = type.GetProperty(propModel.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
if (propInfo == null)
{
continue;

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

@ -74,6 +74,10 @@ public class ControllerApiDescriptionModel
Type = Type,
Interfaces = Interfaces,
ControllerName = ControllerName,
ControllerGroupName = ControllerGroupName,
IsRemoteService = IsRemoteService,
IsIntegrationService = IsIntegrationService,
ApiVersion = ApiVersion,
Summary = Summary,
Remarks = Remarks,
Description = Description,

9
framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManagerCache.cs

@ -1,12 +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);
bool TryGet(string key, out string? value);
void Set(string key, string value);
Task<string> GetOrAddAsync(string key, Func<Task<string>> factory);
}

8
framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs

@ -37,14 +37,12 @@ public class ProxyScriptManager : IProxyScriptManager, ITransientDependency
{
var cacheKey = CreateCacheKey(scriptingModel);
if (scriptingModel.UseCache && _cache.TryGet(cacheKey, out var cached))
if (scriptingModel.UseCache)
{
return cached!;
return await _cache.GetOrAddAsync(cacheKey, () => CreateScriptAsync(scriptingModel));
}
var script = await CreateScriptAsync(scriptingModel);
_cache.Set(cacheKey, script);
return script;
return await CreateScriptAsync(scriptingModel);
}
private async Task<string> CreateScriptAsync(ProxyScriptingModel scriptingModel)

34
framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManagerCache.cs

@ -1,31 +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 bool TryGet(string key, out string? value)
{
return _cache.TryGetValue(key, out value);
}
_cache[key] = result;
_asyncCache.TryRemove(key, out _);
public void Set(string key, string value)
{
_cache[key] = value;
return result;
}
}

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

@ -434,6 +434,86 @@ public class AbpApiDefinitionController_Description_Tests : AspNetCoreMvcTestBas
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

214
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProviderTests.cs

@ -192,6 +192,220 @@ public class XmlDocumentationProviderTests
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>

Loading…
Cancel
Save