diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProvider.cs index 1febf730f7..538a79a611 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProvider.cs +++ b/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 Logger { get; set; } + + public XmlDocumentationProvider() + { + Logger = NullLogger.Instance; + } + private static readonly Regex WhitespaceRegex = new(@"\s+", RegexOptions.Compiled); + // Matches any remaining XML tags like , , , , etc. + private static readonly Regex XmlTagRegex = new(@"<[^>]+>", RegexOptions.Compiled); + // Matches , , , private static readonly Regex XmlRefTagRegex = new( @"<(see|paramref|typeparamref)\s+(cref|name|langword)=""([TMFPE]:)?(?[^""]+)""\s*/?>", RegexOptions.Compiled); - private readonly ConcurrentDictionary> _xmlDocCache = new(); + private readonly ConcurrentDictionary>> _xmlDocCache = new(); public virtual async Task GetSummaryAsync(Type type) { @@ -88,7 +100,12 @@ public class XmlDocumentationProvider : IXmlDocumentationProvider, ISingletonDep protected virtual Task LoadXmlDocumentationAsync(Assembly assembly) { - return _xmlDocCache.GetOrAdd(assembly, LoadXmlDocumentationFromDiskAsync); + return _xmlDocCache.GetOrAdd( + assembly, + asm => new Lazy>( + () => LoadXmlDocumentationFromDiskAsync(asm), + LazyThreadSafetyMode.ExecutionAndPublication) + ).Value; } protected virtual async Task 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. , , , , etc.) - inner = Regex.Replace(inner, @"<[^>]+>", string.Empty); + inner = XmlTagRegex.Replace(inner, string.Empty); if (string.IsNullOrWhiteSpace(inner)) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs index 7960b9319a..464981cb3c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs +++ b/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()?.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()?.Description; actionModel.DisplayName = method.GetCustomAttribute()?.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; diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs index e27459d7e7..04daecc72f 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs +++ b/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, diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManagerCache.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManagerCache.cs index 74149251e0..10d812e3ce 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/IProxyScriptManagerCache.cs +++ b/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 factory); - - bool TryGet(string key, out string? value); - - void Set(string key, string value); + Task GetOrAddAsync(string key, Func> factory); } diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs index 11bb25a1fe..178637b7ea 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManager.cs +++ b/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 CreateScriptAsync(ProxyScriptingModel scriptingModel) diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManagerCache.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManagerCache.cs index 67c1b39d7f..95bb999c9d 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/ProxyScriptManagerCache.cs +++ b/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 _cache; + private readonly ConcurrentDictionary _cache = new(); + private readonly ConcurrentDictionary>> _asyncCache = new(); - public ProxyScriptManagerCache() + public async Task GetOrAddAsync(string key, Func> factory) { - _cache = new ConcurrentDictionary(); - } + if (_cache.TryGetValue(key, out var cached)) + { + return cached; + } - public string GetOrAdd(string key, Func factory) - { - return _cache.GetOrAdd(key, factory); - } + var result = await _asyncCache.GetOrAdd( + key, + _ => new Lazy>(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; } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Description_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Description_Tests.cs index 09baed4aa6..9308026cd8 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpApiDefinitionController_Description_Tests.cs +++ b/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( + "/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( + "/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( + "/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 diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProviderTests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProviderTests.cs index 01b5f6b060..6f6c34ba53 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApiExploring/XmlDocumentationProviderTests.cs +++ b/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( + $@"Summary text.Some remarks here."); + + 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( + $@""); + + 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( + $@"See for details."); + + 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( + $@"See property."); + + 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( + $@"First. code bold end."); + + 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( + $@"Gets a value by key."); + + 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( + $@"No params method."); + + 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( + $@"The resolved value."); + + 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( + $@"Gets a value."); + + 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( + $@"The lookup key.Max results."); + + 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( + $@"The key."); + + 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( + $@"The name property."); + + 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( + $@"Implementation note."); + + 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( + $@"Summary only."); + + var result = await provider.GetRemarksAsync(method); + + result.ShouldBeNull(); + } + /// /// A fake provider that loads XML from an in-memory string instead of the file system. ///