diff --git a/Volo.Abp.sln b/Volo.Abp.sln index d6e55fe8f7..4c62a97e82 100644 --- a/Volo.Abp.sln +++ b/Volo.Abp.sln @@ -128,9 +128,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.MemoryDb.Tests", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.TestApp.Tests", "test\Volo.Abp.TestApp.Tests\Volo.Abp.TestApp.Tests.csproj", "{4C2F7B03-C598-4432-A43A-B065D9D0712F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Http", "src\Volo.Abp.Http\Volo.Abp.Http.csproj", "{01A70034-D353-4BF9-821D-F2B6F7641532}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Http", "src\Volo.Abp.Http\Volo.Abp.Http.csproj", "{01A70034-D353-4BF9-821D-F2B6F7641532}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Http.Tests", "test\Volo.Abp.Http.Tests\Volo.Abp.Http.Tests.csproj", "{5B86E837-0C89-4058-A84B-0B51F2F573D4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Http.Client", "src\Volo.Abp.Http.Client\Volo.Abp.Http.Client.csproj", "{D5E2FB37-0194-480A-B952-5FFECC1200EB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Http.Client.Tests", "test\Volo.Abp.Http.Client.Tests\Volo.Abp.Http.Client.Tests.csproj", "{703BD43C-02B9-413F-854C-9CBA0C963196}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -326,10 +328,14 @@ Global {01A70034-D353-4BF9-821D-F2B6F7641532}.Debug|Any CPU.Build.0 = Debug|Any CPU {01A70034-D353-4BF9-821D-F2B6F7641532}.Release|Any CPU.ActiveCfg = Release|Any CPU {01A70034-D353-4BF9-821D-F2B6F7641532}.Release|Any CPU.Build.0 = Release|Any CPU - {5B86E837-0C89-4058-A84B-0B51F2F573D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5B86E837-0C89-4058-A84B-0B51F2F573D4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5B86E837-0C89-4058-A84B-0B51F2F573D4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5B86E837-0C89-4058-A84B-0B51F2F573D4}.Release|Any CPU.Build.0 = Release|Any CPU + {D5E2FB37-0194-480A-B952-5FFECC1200EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5E2FB37-0194-480A-B952-5FFECC1200EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5E2FB37-0194-480A-B952-5FFECC1200EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5E2FB37-0194-480A-B952-5FFECC1200EB}.Release|Any CPU.Build.0 = Release|Any CPU + {703BD43C-02B9-413F-854C-9CBA0C963196}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {703BD43C-02B9-413F-854C-9CBA0C963196}.Debug|Any CPU.Build.0 = Debug|Any CPU + {703BD43C-02B9-413F-854C-9CBA0C963196}.Release|Any CPU.ActiveCfg = Release|Any CPU + {703BD43C-02B9-413F-854C-9CBA0C963196}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -391,7 +397,8 @@ Global {D0279C94-E9A3-4A1B-968B-D3BBF3E06FD8} = {37087D1B-3693-4E96-983D-A69F210BDE53} {4C2F7B03-C598-4432-A43A-B065D9D0712F} = {37087D1B-3693-4E96-983D-A69F210BDE53} {01A70034-D353-4BF9-821D-F2B6F7641532} = {4C753F64-0C93-4D65-96C2-A40893AFC1E8} - {5B86E837-0C89-4058-A84B-0B51F2F573D4} = {37087D1B-3693-4E96-983D-A69F210BDE53} + {D5E2FB37-0194-480A-B952-5FFECC1200EB} = {4C753F64-0C93-4D65-96C2-A40893AFC1E8} + {703BD43C-02B9-413F-854C-9CBA0C963196} = {37087D1B-3693-4E96-983D-A69F210BDE53} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/src/Volo.Abp.AspNetCore.Mvc/Properties/AssemblyInfo.cs b/src/Volo.Abp.AspNetCore.Mvc/Properties/AssemblyInfo.cs index 214a7fbd4f..61be6f3536 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Properties/AssemblyInfo.cs +++ b/src/Volo.Abp.AspNetCore.Mvc/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -9,6 +10,8 @@ using System.Runtime.InteropServices; [assembly: AssemblyProduct("Volo.Abp.AspNetCore.Mvc")] [assembly: AssemblyTrademark("")] +[assembly:InternalsVisibleTo("Volo.Abp.AspNetCore.Mvc.Tests")] + // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj b/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj index 828bdfbb27..da9a2c3a86 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAppServiceConvention.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAppServiceConvention.cs index c04e563097..25e9265ab1 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAppServiceConvention.cs +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAppServiceConvention.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Options; using Volo.Abp.Application.Services; using Volo.Abp.DependencyInjection; using Volo.Abp.Http; +using Volo.Abp.Http.Modeling; using Volo.Abp.Reflection; namespace Volo.Abp.AspNetCore.Mvc @@ -245,7 +246,7 @@ namespace Volo.Abp.AspNetCore.Mvc protected virtual string GetModuleNameOrDefault(Type controllerType) { return GetControllerSettingOrNull(controllerType)?.ModuleName ?? - AbpControllerAssemblySetting.DefaultServiceModuleName; + ModuleApiDescriptionModel.DefaultServiceModuleName; } [CanBeNull] diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpControllerAssemblySetting.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpControllerAssemblySetting.cs index ed89e7ae9b..56f4fa0142 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpControllerAssemblySetting.cs +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpControllerAssemblySetting.cs @@ -6,11 +6,6 @@ namespace Volo.Abp.AspNetCore.Mvc { public class AbpControllerAssemblySetting { - /// - /// "app". - /// - public const string DefaultServiceModuleName = "app"; - public string ModuleName { get; } public Assembly Assembly { get; } diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ActionDescriptorExtensions.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ActionDescriptorExtensions.cs new file mode 100644 index 0000000000..e12fae5152 --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ActionDescriptorExtensions.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; + +namespace Volo.Abp.AspNetCore.Mvc +{ + public static class ActionDescriptorExtensions + { + public static ControllerActionDescriptor AsControllerActionDescriptor(this ActionDescriptor actionDescriptor) + { + if (!actionDescriptor.IsControllerAction()) + { + throw new AbpException($"{nameof(actionDescriptor)} should be type of {typeof(ControllerActionDescriptor).AssemblyQualifiedName}"); + } + + return actionDescriptor as ControllerActionDescriptor; + } + + public static MethodInfo GetMethodInfo(this ActionDescriptor actionDescriptor) + { + return actionDescriptor.AsControllerActionDescriptor().MethodInfo; + } + + public static bool IsControllerAction(this ActionDescriptor actionDescriptor) + { + return actionDescriptor is ControllerActionDescriptor; + } + } +} diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AppServiceControllerOptions.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AppServiceControllerOptions.cs index dd1944b3f8..659b939e3c 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AppServiceControllerOptions.cs +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AppServiceControllerOptions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reflection; using Microsoft.AspNetCore.Http; +using Volo.Abp.Http.Modeling; namespace Volo.Abp.AspNetCore.Mvc { @@ -30,7 +31,7 @@ namespace Volo.Abp.AspNetCore.Mvc public AbpControllerAssemblySettingBuilder CreateFor( Assembly assembly, - string moduleName = AbpControllerAssemblySetting.DefaultServiceModuleName, + string moduleName = ModuleApiDescriptionModel.DefaultServiceModuleName, bool useConventionalHttpVerbs = true) { var setting = new AbpControllerAssemblySetting(moduleName, assembly, useConventionalHttpVerbs); diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs new file mode 100644 index 0000000000..799b65ac6e --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs @@ -0,0 +1,181 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.Application.Services; +using Volo.Abp.AspNetCore.Mvc.Utils; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http.Modeling; + +namespace Volo.Abp.AspNetCore.Mvc +{ + public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvider, ISingletonDependency + { + public ILogger Logger { get; set; } + + private readonly IApiDescriptionGroupCollectionProvider _descriptionProvider; + private readonly AbpAspNetCoreMvcOptions _options; + + public AspNetCoreApiDescriptionModelProvider( + IApiDescriptionGroupCollectionProvider descriptionProvider, + IOptions options) + { + _descriptionProvider = descriptionProvider; + _options = options.Value; + + Logger = NullLogger.Instance; + } + + public ApplicationApiDescriptionModel CreateModel() + { + var model = ApplicationApiDescriptionModel.Create(); + + foreach (var descriptionGroupItem in _descriptionProvider.ApiDescriptionGroups.Items) + { + foreach (var apiDescription in descriptionGroupItem.Items) + { + if (!apiDescription.ActionDescriptor.IsControllerAction()) + { + continue; + } + + AddApiDescriptionToModel(apiDescription, model); + } + } + + return model; + } + + private void AddApiDescriptionToModel(ApiDescription apiDescription, ApplicationApiDescriptionModel model) + { + var controllerType = apiDescription.ActionDescriptor.AsControllerActionDescriptor().ControllerTypeInfo.AsType(); + + var moduleModel = model.GetOrAddModule(GetModuleName(controllerType)); + + var controllerModel = moduleModel.GetOrAddController(GetControllerName(apiDescription), controllerType); + + var method = apiDescription.ActionDescriptor.GetMethodInfo(); + + var uniqueMethodName = GetUniqueActionName(method); + if (controllerModel.Actions.ContainsKey(uniqueMethodName)) + { + Logger.LogWarning($"Controller '{controllerModel.ControllerName}' contains more than one action with name '{uniqueMethodName}' for module '{moduleModel.Name}'. Ignored: " + method); + return; + } + + var actionModel = controllerModel.AddAction(ActionApiDescriptionModel.Create( + method, + uniqueMethodName, + apiDescription.RelativePath, + apiDescription.HttpMethod + )); + + AddParameterDescriptionsToModel(actionModel, method, apiDescription); + } + + private static string GetControllerName(ApiDescription apiDescription) + { + return apiDescription.GroupName?.RemovePostFix(ApplicationService.CommonPostfixes) + ?? apiDescription.ActionDescriptor.AsControllerActionDescriptor().ControllerName; + } + + private static string GetUniqueActionName(MethodInfo method) + { + var methodNameBuilder = new StringBuilder(method.Name); + + var parameters = method.GetParameters(); + if (parameters.Any()) + { + methodNameBuilder.Append("By"); + + for (var i = 0; i < parameters.Length; i++) + { + if (i > 0) + { + methodNameBuilder.Append("And"); + } + + methodNameBuilder.Append(parameters[i].Name.ToPascalCase()); + } + } + + return methodNameBuilder.ToString(); + } + + private void AddParameterDescriptionsToModel(ActionApiDescriptionModel actionModel, MethodInfo method, ApiDescription apiDescription) + { + if (!apiDescription.ParameterDescriptions.Any()) + { + return; + } + + var matchedMethodParamNames = ArrayMatcher.Match( + apiDescription.ParameterDescriptions.Select(p => p.Name).ToArray(), + method.GetParameters().Select(GetMethodParamName).ToArray() + ); + + for (var i = 0; i < apiDescription.ParameterDescriptions.Count; i++) + { + var parameterDescription = apiDescription.ParameterDescriptions[i]; + var matchedMethodParamName = matchedMethodParamNames.Length > i + ? matchedMethodParamNames[i] + : parameterDescription.Name; + + actionModel.AddParameter(ParameterApiDescriptionModel.Create( + parameterDescription.Name, + matchedMethodParamName, + parameterDescription.Type, + parameterDescription.RouteInfo?.IsOptional ?? false, + parameterDescription.RouteInfo?.DefaultValue, + parameterDescription.RouteInfo?.Constraints?.Select(c => c.GetType().Name).ToArray(), + parameterDescription.Source.Id + ) + ); + } + } + + public string GetMethodParamName(ParameterInfo parameterInfo) + { + var modelNameProvider = parameterInfo.GetCustomAttributes() + .OfType() + .FirstOrDefault(); + + if (modelNameProvider == null) + { + return parameterInfo.Name; + } + + return modelNameProvider.Name; + } + + private string GetModuleName(Type controllerType) + { + if (controllerType == null) + { + return ModuleApiDescriptionModel.DefaultServiceModuleName; + } + + foreach (var controllerSetting in _options.AppServiceControllers.ControllerAssemblySettings) + { + if (Equals(controllerType.Assembly, controllerSetting.Assembly)) + { + return controllerSetting.ModuleName; + } + } + + var areaAttr = controllerType.GetCustomAttributes().OfType().FirstOrDefault(); + if (areaAttr != null) + { + return areaAttr.RouteValue; + } + + return ModuleApiDescriptionModel.DefaultServiceModuleName; + } + } +} diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ControllerAssemblySettingList.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ControllerAssemblySettingList.cs index 36112a9ccc..c7701994bc 100644 --- a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ControllerAssemblySettingList.cs +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ControllerAssemblySettingList.cs @@ -10,7 +10,7 @@ namespace Volo.Abp.AspNetCore.Mvc [CanBeNull] public AbpControllerAssemblySetting GetSettingOrNull(Type controllerType) { - return this.FirstOrDefault(controllerSetting => controllerSetting.Assembly == controllerType.GetAssembly()); + return this.FirstOrDefault(controllerSetting => controllerSetting.Assembly == controllerType.Assembly); } } } \ No newline at end of file diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Controllers/AbpApiDefinitionController.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Controllers/AbpApiDefinitionController.cs new file mode 100644 index 0000000000..0a8c6fb8ef --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Controllers/AbpApiDefinitionController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Http.Modeling; + +namespace Volo.Abp.AspNetCore.Mvc.Controllers +{ + [Area("abp")] + public class AbpApiDefinitionController : AbpController + { + private readonly IApiDescriptionModelProvider _modelProvider; + + public AbpApiDefinitionController(IApiDescriptionModelProvider modelProvider) + { + _modelProvider = modelProvider; + } + + [HttpGet] + [Route("api/abp/api-description")] + public ApplicationApiDescriptionModel Get() + { + return _modelProvider.CreateModel(); + } + } +} diff --git a/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMatcher.cs b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMatcher.cs new file mode 100644 index 0000000000..f1b5b2b8e6 --- /dev/null +++ b/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMatcher.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Mvc.Utils +{ + internal static class ArrayMatcher + { + public static T[] Match(T[] sourceArray, T[] destinationArray) + { + var result = new List(); + + var currentMethodParamIndex = 0; + var parentItem = default(T); + + foreach (var sourceItem in sourceArray) + { + if (currentMethodParamIndex < destinationArray.Length) + { + var destinationItem = destinationArray[currentMethodParamIndex]; + + if (EqualityComparer.Default.Equals(sourceItem, destinationItem)) + { + parentItem = default(T); + currentMethodParamIndex++; + } + else + { + if (parentItem == null) + { + parentItem = destinationItem; + currentMethodParamIndex++; + } + } + } + + var resultItem = EqualityComparer.Default.Equals(parentItem, default(T)) ? sourceItem : parentItem; + result.Add(resultItem); + } + + return result.ToArray(); + } + } +} diff --git a/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj b/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj index c6ff8966ba..7090e4bf39 100644 --- a/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj +++ b/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpAspNetCoreIntegratedTestBase.cs b/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpAspNetCoreIntegratedTestBase.cs index 4f9da35c91..81c85197b1 100644 --- a/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpAspNetCoreIntegratedTestBase.cs +++ b/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpAspNetCoreIntegratedTestBase.cs @@ -5,6 +5,7 @@ using System.Net.Http; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; namespace Volo.Abp.AspNetCore.TestBase { @@ -22,8 +23,9 @@ namespace Volo.Abp.AspNetCore.TestBase var builder = CreateWebHostBuilder(); Server = CreateTestServer(builder); Client = Server.CreateClient(); - ServiceProvider = Server.Host.Services; + + ServiceProvider.GetRequiredService().Server = Server; } protected virtual IWebHostBuilder CreateWebHostBuilder() diff --git a/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpAspNetCoreTestBaseModule.cs b/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpAspNetCoreTestBaseModule.cs index 6f98ab1614..c6828feea5 100644 --- a/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpAspNetCoreTestBaseModule.cs +++ b/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpAspNetCoreTestBaseModule.cs @@ -1,8 +1,10 @@ using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Http.Client; using Volo.Abp.Modularity; namespace Volo.Abp.AspNetCore.TestBase { + [DependsOn(typeof(AbpHttpClientModule))] [DependsOn(typeof(AbpAspNetCoreModule))] public class AbpAspNetCoreTestBaseModule : AbpModule { diff --git a/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/DynamicProxying/AspNetCoreTestDynamicProxyHttpClientFactory.cs b/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/DynamicProxying/AspNetCoreTestDynamicProxyHttpClientFactory.cs new file mode 100644 index 0000000000..e0253f8f8b --- /dev/null +++ b/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/DynamicProxying/AspNetCoreTestDynamicProxyHttpClientFactory.cs @@ -0,0 +1,22 @@ +using System.Net.Http; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http.Client.DynamicProxying; + +namespace Volo.Abp.AspNetCore.TestBase.DynamicProxying +{ + [Dependency(ReplaceServices = true)] + public class AspNetCoreTestDynamicProxyHttpClientFactory : IDynamicProxyHttpClientFactory, ITransientDependency + { + private readonly ITestServerAccessor _testServerAccessor; + + public AspNetCoreTestDynamicProxyHttpClientFactory(ITestServerAccessor testServerAccessor) + { + _testServerAccessor = testServerAccessor; + } + + public HttpClient Create() + { + return _testServerAccessor.Server.CreateClient(); + } + } +} diff --git a/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/ITestServerAccessor.cs b/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/ITestServerAccessor.cs new file mode 100644 index 0000000000..86247877cb --- /dev/null +++ b/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/ITestServerAccessor.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.TestHost; + +namespace Volo.Abp.AspNetCore.TestBase +{ + public interface ITestServerAccessor + { + TestServer Server { get; set; } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/TestServerAccessor.cs b/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/TestServerAccessor.cs new file mode 100644 index 0000000000..f848529536 --- /dev/null +++ b/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/TestServerAccessor.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.TestHost; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.TestBase +{ + public class TestServerAccessor : ITestServerAccessor, ISingletonDependency + { + public TestServer Server { get; set; } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Castle.Core/Volo/Abp/Castle/DynamicProxy/CastleAbpInterceptorAdapter.cs b/src/Volo.Abp.Castle.Core/Volo/Abp/Castle/DynamicProxy/CastleAbpInterceptorAdapter.cs index 56675ec73f..1b1b958ff1 100644 --- a/src/Volo.Abp.Castle.Core/Volo/Abp/Castle/DynamicProxy/CastleAbpInterceptorAdapter.cs +++ b/src/Volo.Abp.Castle.Core/Volo/Abp/Castle/DynamicProxy/CastleAbpInterceptorAdapter.cs @@ -17,7 +17,9 @@ namespace Volo.Abp.Castle.DynamicProxy public void Intercept(IInvocation invocation) { - if (invocation.MethodInvocationTarget.IsAsync()) + var method = invocation.MethodInvocationTarget ?? invocation.Method; + + if (method.IsAsync()) { InterceptAsyncMethod(invocation); } diff --git a/src/Volo.Abp.Castle.Core/Volo/Abp/Castle/DynamicProxy/CastleAbpMethodInvocationAdapter.cs b/src/Volo.Abp.Castle.Core/Volo/Abp/Castle/DynamicProxy/CastleAbpMethodInvocationAdapter.cs index 2ce330ec66..6758f138b0 100644 --- a/src/Volo.Abp.Castle.Core/Volo/Abp/Castle/DynamicProxy/CastleAbpMethodInvocationAdapter.cs +++ b/src/Volo.Abp.Castle.Core/Volo/Abp/Castle/DynamicProxy/CastleAbpMethodInvocationAdapter.cs @@ -15,7 +15,7 @@ namespace Volo.Abp.Castle.DynamicProxy public object TargetObject => Invocation.InvocationTarget; - public MethodInfo Method => Invocation.MethodInvocationTarget; + public MethodInfo Method => Invocation.MethodInvocationTarget ?? Invocation.Method; public object ReturnValue { diff --git a/src/Volo.Abp.Http.Client/Microsoft/Extensions/DependencyInjection/ServiceCollectionDynamicHttpClientProxyExtensions.cs b/src/Volo.Abp.Http.Client/Microsoft/Extensions/DependencyInjection/ServiceCollectionDynamicHttpClientProxyExtensions.cs new file mode 100644 index 0000000000..17f5d51f7a --- /dev/null +++ b/src/Volo.Abp.Http.Client/Microsoft/Extensions/DependencyInjection/ServiceCollectionDynamicHttpClientProxyExtensions.cs @@ -0,0 +1,42 @@ +using System; +using Castle.DynamicProxy; +using Volo.Abp.Castle.DynamicProxy; +using Volo.Abp.Http.Client; +using Volo.Abp.Http.Client.DynamicProxying; +using Volo.Abp.Http.Modeling; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionDynamicHttpClientProxyExtensions + { + private static readonly ProxyGenerator ProxyGeneratorInstance = new ProxyGenerator(); + + //TODO: AddHttpClientProxies for adding all services from single assembly! + + public static IServiceCollection AddHttpClientProxy(this IServiceCollection services, string baseUrl, string moduleName = ModuleApiDescriptionModel.DefaultServiceModuleName) + { + return services.AddHttpClientProxy(typeof(T), baseUrl, moduleName); + } + + public static IServiceCollection AddHttpClientProxy(this IServiceCollection services, Type type, string baseUrl, string moduleName = ModuleApiDescriptionModel.DefaultServiceModuleName) + { + services.Configure(options => + { + options.HttpClientProxies[type] = new DynamicHttpClientProxyConfig(moduleName, baseUrl, type); + }); + + var interceptorType = typeof(DynamicHttpProxyInterceptor<>).MakeGenericType(type); + services.AddTransient(interceptorType); + + var interceptorAdapterType = typeof(CastleAbpInterceptorAdapter<>).MakeGenericType(interceptorType); + return services.AddTransient( + type, + serviceProvider => ProxyGeneratorInstance + .CreateInterfaceProxyWithoutTarget( + type, + (IInterceptor) serviceProvider.GetRequiredService(interceptorAdapterType) + ) + ); + } + } +} diff --git a/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj b/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj new file mode 100644 index 0000000000..ff4dd91623 --- /dev/null +++ b/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + Volo.Abp.Http.Client + Volo.Abp.Http.Client + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + \ No newline at end of file diff --git a/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientOptions.cs b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientOptions.cs new file mode 100644 index 0000000000..5adcd7525f --- /dev/null +++ b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientOptions.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Http.Client.DynamicProxying; + +namespace Volo.Abp.Http.Client +{ + public class AbpHttpClientOptions + { + public Dictionary HttpClientProxies { get; set; } + + public AbpHttpClientOptions() + { + HttpClientProxies = new Dictionary(); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpModule.cs b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpModule.cs new file mode 100644 index 0000000000..37ddfe2755 --- /dev/null +++ b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpModule.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; + +namespace Volo.Abp.Http.Client +{ + [DependsOn(typeof(AbpHttpModule))] + public class AbpHttpClientModule : AbpModule + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddAssemblyOf(); + } + } +} diff --git a/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/ApplicationApiDescriptionModelManager.cs b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/ApplicationApiDescriptionModelManager.cs new file mode 100644 index 0000000000..79cac746e5 --- /dev/null +++ b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/ApplicationApiDescriptionModelManager.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Nito.AsyncEx; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http.Modeling; + +namespace Volo.Abp.Http.Client.DynamicProxying +{ + public class ApplicationApiDescriptionModelManager : IApplicationApiDescriptionModelManager, ISingletonDependency + { + private readonly IDynamicProxyHttpClientFactory _httpClientFactory; + + private readonly Dictionary _cache; + private readonly AsyncLock _asyncLock; + + public ApplicationApiDescriptionModelManager(IDynamicProxyHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + + _cache = new Dictionary(); + _asyncLock = new AsyncLock(); + } + + public async Task GetAsync(string baseUrl, CancellationToken cancellationToken = default(CancellationToken)) + { + using (await _asyncLock.LockAsync(cancellationToken)) + { + var model = _cache.GetOrDefault(baseUrl); + if (model == null) + { + _cache[baseUrl] = model = await GetFromServerAsync(baseUrl); + } + + return model; + } + } + + private async Task GetFromServerAsync(string baseUrl) + { + using (var client = _httpClientFactory.Create()) + { + var response = await client.GetAsync(baseUrl + "api/abp/api-description"); + if (!response.IsSuccessStatusCode) + { + throw new AbpException("Remote service returns error!"); + } + + var content = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject( + content, + typeof(ApplicationApiDescriptionModel), + new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + + return (ApplicationApiDescriptionModel)result; + } + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DefaultDynamicProxyHttpClientFactory.cs b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DefaultDynamicProxyHttpClientFactory.cs new file mode 100644 index 0000000000..6c827f5237 --- /dev/null +++ b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DefaultDynamicProxyHttpClientFactory.cs @@ -0,0 +1,13 @@ +using System.Net.Http; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Http.Client.DynamicProxying +{ + public class DefaultDynamicProxyHttpClientFactory : IDynamicProxyHttpClientFactory, ITransientDependency + { + public HttpClient Create() + { + return new HttpClient(); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpClientProxyConfig.cs b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpClientProxyConfig.cs new file mode 100644 index 0000000000..20df2dfd7e --- /dev/null +++ b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpClientProxyConfig.cs @@ -0,0 +1,20 @@ +using System; + +namespace Volo.Abp.Http.Client.DynamicProxying +{ + public class DynamicHttpClientProxyConfig + { + public string BaseUrl { get; } + + public string ModuleName { get; } + + public Type Type { get; } + + public DynamicHttpClientProxyConfig(string moduleName, string baseUrl, Type type) + { + BaseUrl = baseUrl; + ModuleName = moduleName; + Type = type; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs new file mode 100644 index 0000000000..b19d901bda --- /dev/null +++ b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DynamicProxy; +using Volo.Abp.Http.Modeling; +using Volo.Abp.Threading; + +namespace Volo.Abp.Http.Client.DynamicProxying +{ + public class DynamicHttpProxyInterceptor : AbpInterceptor, ITransientDependency + { + private static readonly MethodInfo GenericInterceptAsyncMethod; + + private readonly IDynamicProxyHttpClientFactory _httpClientFactory; + private readonly IApplicationApiDescriptionModelManager _discoverManager; + private readonly AbpHttpClientOptions _options; + + static DynamicHttpProxyInterceptor() + { + GenericInterceptAsyncMethod = typeof(DynamicHttpProxyInterceptor) + .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + .First(m => m.Name == nameof(InterceptAsync) && m.IsGenericMethodDefinition); + } + + public DynamicHttpProxyInterceptor( + IDynamicProxyHttpClientFactory httpClientFactory, + IOptions options, + IApplicationApiDescriptionModelManager discoverManager) + { + _httpClientFactory = httpClientFactory; + _discoverManager = discoverManager; + _options = options.Value; + } + + public override void Intercept(IAbpMethodInvocation invocation) + { + AsyncHelper.RunSync(() => InterceptAsync(invocation)); + } + + public override Task InterceptAsync(IAbpMethodInvocation invocation) + { + invocation.ReturnValue = GenericInterceptAsyncMethod + .MakeGenericMethod(invocation.Method.ReturnType.GenericTypeArguments[0]) + .Invoke(this, new object[] { invocation }); + + return Task.CompletedTask; + } + + private async Task InterceptAsync(IAbpMethodInvocation invocation) + { + var config = _options.HttpClientProxies.GetOrDefault(typeof(TService)); + if (config == null) + { + throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {typeof(T).FullName}."); + } + + var apiDescriptionModel = await _discoverManager.GetAsync(config.BaseUrl); + + var action = FindAction(apiDescriptionModel, invocation.Method, config); + + using (var client = _httpClientFactory.Create()) + { + var response = await client.GetAsync(config.BaseUrl + action.Url); + if (!response.IsSuccessStatusCode) + { + throw new AbpException("Remote service returns error!"); + } + + var content = await response.Content.ReadAsStringAsync(); + + var result = JsonConvert.DeserializeObject( + content, + typeof(T), + new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + + return (T)result; + } + } + + private ActionApiDescriptionModel FindAction(ApplicationApiDescriptionModel apiDescriptionModel, MethodInfo method, DynamicHttpClientProxyConfig config) + { + var methodParameters = method.GetParameters().ToArray(); + + foreach (var module in apiDescriptionModel.Modules.Values) + { + if (module.Name != config.ModuleName) + { + continue; + } + + foreach (var controller in module.Controllers.Values) + { + if (controller.Interfaces.All(i => i.TypeAsString != typeof(TService).FullName)) + { + continue; + } + + foreach (var action in controller.Actions.Values) + { + if (action.NameOnClass == method.Name && action.ParametersOnMethod.Count == methodParameters.Length) + { + var found = true; + + for (int i = 0; i < methodParameters.Length; i++) + { + if (action.ParametersOnMethod[i].TypeAsString != methodParameters[i].ParameterType.FullName) + { + found = false; + break; + } + } + + if (found) + { + return action; + } + } + } + } + } + + throw new AbpException("Could not found remote action for method: " + method); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/IApplicationApiDescriptionModelManager.cs b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/IApplicationApiDescriptionModelManager.cs new file mode 100644 index 0000000000..bdc7f3d6af --- /dev/null +++ b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/IApplicationApiDescriptionModelManager.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Http.Modeling; + +namespace Volo.Abp.Http.Client.DynamicProxying +{ + public interface IApplicationApiDescriptionModelManager + { + Task GetAsync(string baseUrl, CancellationToken cancellationToken = default(CancellationToken)); + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/IDynamicProxyHttpClientFactory.cs b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/IDynamicProxyHttpClientFactory.cs new file mode 100644 index 0000000000..aaf821d0f9 --- /dev/null +++ b/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/IDynamicProxyHttpClientFactory.cs @@ -0,0 +1,9 @@ +using System.Net.Http; + +namespace Volo.Abp.Http.Client.DynamicProxying +{ + public interface IDynamicProxyHttpClientFactory + { + HttpClient Create(); + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http/Volo.Abp.Http.csproj b/src/Volo.Abp.Http/Volo.Abp.Http.csproj index 475e1a8af2..d4031e3399 100644 --- a/src/Volo.Abp.Http/Volo.Abp.Http.csproj +++ b/src/Volo.Abp.Http/Volo.Abp.Http.csproj @@ -12,11 +12,8 @@ + - - - - \ No newline at end of file diff --git a/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ActionApiDescriptionModel.cs b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ActionApiDescriptionModel.cs new file mode 100644 index 0000000000..aaac0bfba9 --- /dev/null +++ b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ActionApiDescriptionModel.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Volo.Abp.Http.Modeling +{ + [Serializable] + public class ActionApiDescriptionModel + { + public string UniqueName { get; set; } + + public string NameOnClass { get; set; } + + public string HttpMethod { get; set; } + + public string Url { get; set; } + + public IList ParametersOnMethod { get; set; } + + public IList Parameters { get; set; } + + public ReturnValueApiDescriptionModel ReturnValue { get; set; } + + private ActionApiDescriptionModel() + { + + } + + public static ActionApiDescriptionModel Create(MethodInfo method, string uniqueName, string url, string httpMethod = null) + { + return new ActionApiDescriptionModel + { + UniqueName = uniqueName, + NameOnClass = method.Name, + Url = url, + HttpMethod = httpMethod, + ReturnValue = new ReturnValueApiDescriptionModel(method.ReturnType), + Parameters = new List(), + ParametersOnMethod = method + .GetParameters() + .Select(MethodParameterApiDescriptionModel.Create) + .ToList() + }; + } + + public ParameterApiDescriptionModel AddParameter(ParameterApiDescriptionModel parameter) + { + Parameters.Add(parameter); + return parameter; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModel.cs b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModel.cs new file mode 100644 index 0000000000..63f9aef716 --- /dev/null +++ b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApplicationApiDescriptionModel.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Volo.Abp.Http.Modeling +{ + [Serializable] + public class ApplicationApiDescriptionModel + { + public IDictionary Modules { get; set; } + + private ApplicationApiDescriptionModel() + { + + } + + public static ApplicationApiDescriptionModel Create() + { + return new ApplicationApiDescriptionModel + { + //TODO: Why ConcurrentDictionary? + Modules = new ConcurrentDictionary() + }; + } + + public ModuleApiDescriptionModel AddModule(ModuleApiDescriptionModel module) + { + if (Modules.ContainsKey(module.Name)) + { + throw new AbpException("There is already a module with same name: " + module.Name); + } + + return Modules[module.Name] = module; + } + + public ModuleApiDescriptionModel GetOrAddModule(string name) + { + return Modules.GetOrAdd(name, () => ModuleApiDescriptionModel.Create(name)); + } + + public ApplicationApiDescriptionModel CreateSubModel(string[] modules = null, string[] controllers = null, string[] actions = null) + { + var subModel = new ApplicationApiDescriptionModel(); + + foreach (var module in Modules.Values) + { + if (modules == null || modules.Contains(module.Name)) + { + subModel.AddModule(module.CreateSubModel(controllers, actions)); + } + } + + return subModel; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs new file mode 100644 index 0000000000..b4bd4df80e --- /dev/null +++ b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Volo.Abp.Http.Modeling +{ + [Serializable] + public class ControllerApiDescriptionModel + { + public string ControllerName { get; set; } + + public string TypeAsString { get; set; } + + public List Interfaces { get; set; } + + public Dictionary Actions { get; set; } + + private ControllerApiDescriptionModel() + { + + } + + public static ControllerApiDescriptionModel Create(string controllerName, Type type) + { + return new ControllerApiDescriptionModel + { + ControllerName = controllerName, + TypeAsString = type.FullName, + Actions = new Dictionary(), + Interfaces = type + .GetInterfaces() + .Select(ControllerInterfaceApiDescriptionModel.Create) + .ToList() + }; + } + + public ActionApiDescriptionModel AddAction(ActionApiDescriptionModel action) + { + if (Actions.ContainsKey(action.UniqueName)) + { + throw new AbpException( + $"Can not add more than one action with same name to the same controller. Controller: {ControllerName}, Action: {action.UniqueName}." + ); + } + + return Actions[action.UniqueName] = action; + } + + public ControllerApiDescriptionModel CreateSubModel(string[] actions) + { + var subModel = new ControllerApiDescriptionModel + { + TypeAsString = TypeAsString, + Interfaces = Interfaces, + ControllerName = ControllerName, + Actions = new Dictionary() + }; + + foreach (var action in Actions.Values) + { + if (actions == null || actions.Contains(action.UniqueName)) + { + subModel.AddAction(action); + } + } + + return subModel; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerInterfaceApiDescriptionModel.cs b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerInterfaceApiDescriptionModel.cs new file mode 100644 index 0000000000..3868ec94f9 --- /dev/null +++ b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerInterfaceApiDescriptionModel.cs @@ -0,0 +1,23 @@ +using System; + +namespace Volo.Abp.Http.Modeling +{ + [Serializable] + public class ControllerInterfaceApiDescriptionModel + { + public string TypeAsString { get; set; } + + private ControllerInterfaceApiDescriptionModel() + { + + } + + public static ControllerInterfaceApiDescriptionModel Create(Type type) + { + return new ControllerInterfaceApiDescriptionModel + { + TypeAsString = type.FullName + }; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs new file mode 100644 index 0000000000..35334fae33 --- /dev/null +++ b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/IApiDescriptionModelProvider.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Http.Modeling +{ + public interface IApiDescriptionModelProvider + { + ApplicationApiDescriptionModel CreateModel(); + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs new file mode 100644 index 0000000000..fdc50090d2 --- /dev/null +++ b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs @@ -0,0 +1,33 @@ +using System; +using System.Reflection; + +namespace Volo.Abp.Http.Modeling +{ + [Serializable] + public class MethodParameterApiDescriptionModel + { + public string Name { get; set; } + + public string TypeAsString { get; set; } + + public bool IsOptional { get; set; } + + public object DefaultValue { get; set; } + + private MethodParameterApiDescriptionModel() + { + + } + + public static MethodParameterApiDescriptionModel Create(ParameterInfo parameterInfo) + { + return new MethodParameterApiDescriptionModel + { + Name = parameterInfo.Name, + TypeAsString = parameterInfo.ParameterType.FullName, + IsOptional = parameterInfo.IsOptional, + DefaultValue = parameterInfo.HasDefaultValue ? parameterInfo.DefaultValue : null + }; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModuleApiDescriptionModel.cs b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModuleApiDescriptionModel.cs new file mode 100644 index 0000000000..20ce707dff --- /dev/null +++ b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModuleApiDescriptionModel.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Volo.Abp.Http.Modeling +{ + [Serializable] + public class ModuleApiDescriptionModel + { + /// + /// "app". + /// + public const string DefaultServiceModuleName = "app"; + + public string Name { get; set; } + + public IDictionary Controllers { get; set; } + + private ModuleApiDescriptionModel() + { + + } + + public static ModuleApiDescriptionModel Create(string name) + { + return new ModuleApiDescriptionModel + { + Name = name, + Controllers = new Dictionary() + }; + } + + public ControllerApiDescriptionModel AddController(ControllerApiDescriptionModel controller) + { + if (Controllers.ContainsKey(controller.ControllerName)) + { + throw new AbpException($"There is already a controller with name: {controller.ControllerName} in module: {Name}"); + } + + return Controllers[controller.ControllerName] = controller; + } + + public ControllerApiDescriptionModel GetOrAddController(string name, Type type) + { + return Controllers.GetOrAdd(name, () => ControllerApiDescriptionModel.Create(name, type)); + } + + public ModuleApiDescriptionModel CreateSubModel(string[] controllers, string[] actions) + { + var subModel = ModuleApiDescriptionModel.Create(Name); + + foreach (var controller in Controllers.Values) + { + if (controllers == null || controllers.Contains(controller.ControllerName)) + { + subModel.AddController(controller.CreateSubModel(actions)); + } + } + + return subModel; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs new file mode 100644 index 0000000000..d3e9a5fbab --- /dev/null +++ b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs @@ -0,0 +1,44 @@ +using System; + +namespace Volo.Abp.Http.Modeling +{ + [Serializable] + public class ParameterApiDescriptionModel + { + public string NameOnMethod { get; set; } + + public string Name { get; set; } + + public Type Type { get; set; } + + public string TypeAsString { get; set; } + + public bool IsOptional { get; set; } + + public object DefaultValue { get; set; } + + public string[] ConstraintTypes { get; set; } + + public string BindingSourceId { get; set; } + + private ParameterApiDescriptionModel() + { + + } + + public static ParameterApiDescriptionModel Create(string name, string nameOnMethod, Type type, bool isOptional = false, object defaultValue = null, string[] constraintTypes = null, string bindingSourceId = null) + { + return new ParameterApiDescriptionModel + { + Name = name, + NameOnMethod = nameOnMethod, + Type = type, + TypeAsString = type.FullName, + IsOptional = isOptional, + DefaultValue = defaultValue, + ConstraintTypes = constraintTypes, + BindingSourceId = bindingSourceId + }; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs new file mode 100644 index 0000000000..83b4cba346 --- /dev/null +++ b/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs @@ -0,0 +1,17 @@ +using System; + +namespace Volo.Abp.Http.Modeling +{ + [Serializable] + public class ReturnValueApiDescriptionModel + { + public Type Type { get; } + public string TypeAsString { get; } + + public ReturnValueApiDescriptionModel(Type type) + { + Type = type; + TypeAsString = type.FullName; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/System/AbpTypeExtensions.cs b/src/Volo.Abp/System/AbpTypeExtensions.cs deleted file mode 100644 index 2cbd8dea77..0000000000 --- a/src/Volo.Abp/System/AbpTypeExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Reflection; - -namespace System -{ - public static class AbpTypeExtensions - { - //TODO: This method can be removed because not needed anymore! - public static Assembly GetAssembly(this Type type) - { - return type.GetTypeInfo().Assembly; - } - } -} diff --git a/src/Volo.Abp/Volo.Abp.csproj b/src/Volo.Abp/Volo.Abp.csproj index d02cb6b1b3..be558fc887 100644 --- a/src/Volo.Abp/Volo.Abp.csproj +++ b/src/Volo.Abp/Volo.Abp.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Volo.Abp/Volo/Abp/Modularity/AbpModuleDescriptor.cs b/src/Volo.Abp/Volo/Abp/Modularity/AbpModuleDescriptor.cs index 69fada953c..7413e1154f 100644 --- a/src/Volo.Abp/Volo/Abp/Modularity/AbpModuleDescriptor.cs +++ b/src/Volo.Abp/Volo/Abp/Modularity/AbpModuleDescriptor.cs @@ -33,7 +33,7 @@ namespace Volo.Abp.Modularity } Type = type; - Assembly = type.GetAssembly(); + Assembly = type.Assembly; Instance = instance; IsLoadedAsPlugIn = isLoadedAsPlugIn; diff --git a/src/Volo.Abp/Volo/Abp/Reflection/AssemblyFinder.cs b/src/Volo.Abp/Volo/Abp/Reflection/AssemblyFinder.cs index 6d90a8fb5c..8178f1046b 100644 --- a/src/Volo.Abp/Volo/Abp/Reflection/AssemblyFinder.cs +++ b/src/Volo.Abp/Volo/Abp/Reflection/AssemblyFinder.cs @@ -30,7 +30,7 @@ namespace Volo.Abp.Reflection foreach (var module in _moduleContainer.Modules) { - assemblies.Add(module.Type.GetAssembly()); + assemblies.Add(module.Type.Assembly); } return assemblies.Distinct().ToImmutableList(); diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkInterceptor.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkInterceptor.cs index 53b6efb2fa..1bf813905a 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkInterceptor.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkInterceptor.cs @@ -32,7 +32,7 @@ namespace Volo.Abp.Uow { if (!UnitOfWorkHelper.IsUnitOfWorkMethod(invocation.Method)) { - invocation.Proceed(); + await invocation.ProceedAsync(); return; } diff --git a/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMacther_Tests.cs b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMacther_Tests.cs new file mode 100644 index 0000000000..a066622748 --- /dev/null +++ b/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMacther_Tests.cs @@ -0,0 +1,19 @@ +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.Utils +{ + public class ArrayMacther_Tests + { + [Theory] + [InlineData( + new[] { "p1", "p2.a1", "p2.a2.b1", "p2.a2.b2", "p3", "p4", "p5.c1", "p5.c2" }, + new[] { "p1", "p2", "p3", "p4", "p5" }, + new[] { "p1", "p2", "p2", "p2", "p3", "p4", "p5", "p5" }) + ] + public void Should_Find_Correct_Items(string[] sourceArray, string[] destinationArray, string[] expectedArray) + { + var result = ArrayMatcher.Match(sourceArray, destinationArray); + Assert.Equal(expectedArray, result); + } + } +} diff --git a/test/Volo.Abp.AspNetCore.Tests/Volo/Abp/AspNetCore/AppTestBase.cs b/test/Volo.Abp.AspNetCore.Tests/Volo/Abp/AspNetCore/AppTestBase.cs index d13cb810bf..6bb82c3656 100644 --- a/test/Volo.Abp.AspNetCore.Tests/Volo/Abp/AspNetCore/AppTestBase.cs +++ b/test/Volo.Abp.AspNetCore.Tests/Volo/Abp/AspNetCore/AppTestBase.cs @@ -6,7 +6,7 @@ using Newtonsoft.Json.Serialization; using Shouldly; using Volo.Abp.AspNetCore.TestBase; -namespace Volo.Abp.AspNetCore.App +namespace Volo.Abp.AspNetCore { public abstract class AbpAspNetCoreTestBase : AbpAspNetCoreIntegratedTestBase where TStartup : class diff --git a/test/Volo.Abp.Http.Tests/Volo.Abp.Http.Tests.csproj b/test/Volo.Abp.Http.Client.Tests/Volo.Abp.Http.Client.Tests.csproj similarity index 84% rename from test/Volo.Abp.Http.Tests/Volo.Abp.Http.Tests.csproj rename to test/Volo.Abp.Http.Client.Tests/Volo.Abp.Http.Client.Tests.csproj index fd4ae111e2..6593fa5d39 100644 --- a/test/Volo.Abp.Http.Tests/Volo.Abp.Http.Tests.csproj +++ b/test/Volo.Abp.Http.Client.Tests/Volo.Abp.Http.Client.Tests.csproj @@ -3,7 +3,7 @@ netcoreapp2.0 Volo.Abp.Http.Tests - Volo.Abp.Http.Tests + Volo.Abp.Http.Client.Tests true false false @@ -12,7 +12,7 @@ - + diff --git a/test/Volo.Abp.Http.Tests/Volo/Abp/Http/AbpHttpTestBase.cs b/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpTestBase.cs similarity index 83% rename from test/Volo.Abp.Http.Tests/Volo/Abp/Http/AbpHttpTestBase.cs rename to test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpTestBase.cs index 35db320555..f950193109 100644 --- a/test/Volo.Abp.Http.Tests/Volo/Abp/Http/AbpHttpTestBase.cs +++ b/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpTestBase.cs @@ -1,3 +1,4 @@ +using Volo.Abp.AspNetCore; using Volo.Abp.AspNetCore.App; namespace Volo.Abp.Http diff --git a/test/Volo.Abp.Http.Tests/Volo/Abp/Http/AbpHttpTestModule.cs b/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpTestModule.cs similarity index 63% rename from test/Volo.Abp.Http.Tests/Volo/Abp/Http/AbpHttpTestModule.cs rename to test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpTestModule.cs index ae2cb91658..4a150a2c19 100644 --- a/test/Volo.Abp.Http.Tests/Volo/Abp/Http/AbpHttpTestModule.cs +++ b/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpTestModule.cs @@ -1,15 +1,18 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.App; +using Volo.Abp.Http.Client; using Volo.Abp.Modularity; +using Volo.Abp.TestApp.Application; namespace Volo.Abp.Http { - [DependsOn(typeof(AbpAspNetCoreMvcTestModule), typeof(AbpHttpModule))] + [DependsOn(typeof(AbpAspNetCoreMvcTestModule), typeof(AbpHttpClientModule))] public class AbpHttpTestModule : AbpModule { public override void ConfigureServices(IServiceCollection services) { - services.AddAssemblyOf(); + services.AddAssemblyOf(); + services.AddHttpClientProxy("/"); } } } diff --git a/test/Volo.Abp.Http.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs b/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs similarity index 94% rename from test/Volo.Abp.Http.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs rename to test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs index abd73531c2..4d6c18ceca 100644 --- a/test/Volo.Abp.Http.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs +++ b/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs @@ -13,7 +13,6 @@ namespace Volo.Abp.Http.DynamicProxying public PersonAppServiceClientProxy_Tests() { - //TODO: Should actually test the proxy! _peopleAppService = ServiceProvider.GetRequiredService(); } diff --git a/test/Volo.Abp.Http.Tests/Volo/Abp/Http/Startup.cs b/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/Startup.cs similarity index 100% rename from test/Volo.Abp.Http.Tests/Volo/Abp/Http/Startup.cs rename to test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/Startup.cs diff --git a/test/Volo.Abp.Tests/Volo/Abp/Reflection/AssemblyFinder_Tests.cs b/test/Volo.Abp.Tests/Volo/Abp/Reflection/AssemblyFinder_Tests.cs index 92cd5aaf64..37c5b01518 100644 --- a/test/Volo.Abp.Tests/Volo/Abp/Reflection/AssemblyFinder_Tests.cs +++ b/test/Volo.Abp.Tests/Volo/Abp/Reflection/AssemblyFinder_Tests.cs @@ -30,7 +30,7 @@ namespace Volo.Abp.Reflection foreach (var moduleType in moduleTypes) { - assemblyFinder.Assemblies.ShouldContain(moduleType.GetAssembly()); + assemblyFinder.Assemblies.ShouldContain(moduleType.Assembly); } } diff --git a/test/Volo.Abp.Tests/Volo/Abp/Reflection/TypeFinder_Tests.cs b/test/Volo.Abp.Tests/Volo/Abp/Reflection/TypeFinder_Tests.cs index 4bbaf34400..c9876875ed 100644 --- a/test/Volo.Abp.Tests/Volo/Abp/Reflection/TypeFinder_Tests.cs +++ b/test/Volo.Abp.Tests/Volo/Abp/Reflection/TypeFinder_Tests.cs @@ -18,8 +18,8 @@ namespace Volo.Abp.Reflection var fakeAssemblyFinder = Substitute.For(); fakeAssemblyFinder.Assemblies.Returns(new List { - typeof(AbpKernelModule).GetAssembly(), - typeof(TypeFinder_Tests).GetAssembly() + typeof(AbpKernelModule).Assembly, + typeof(TypeFinder_Tests).Assembly }); //Act