mirror of https://github.com/abpframework/abp.git
committed by
GitHub
53 changed files with 1094 additions and 47 deletions
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<AspNetCoreApiDescriptionModelProvider> Logger { get; set; } |
||||
|
|
||||
|
private readonly IApiDescriptionGroupCollectionProvider _descriptionProvider; |
||||
|
private readonly AbpAspNetCoreMvcOptions _options; |
||||
|
|
||||
|
public AspNetCoreApiDescriptionModelProvider( |
||||
|
IApiDescriptionGroupCollectionProvider descriptionProvider, |
||||
|
IOptions<AbpAspNetCoreMvcOptions> options) |
||||
|
{ |
||||
|
_descriptionProvider = descriptionProvider; |
||||
|
_options = options.Value; |
||||
|
|
||||
|
Logger = NullLogger<AspNetCoreApiDescriptionModelProvider>.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<IModelNameProvider>() |
||||
|
.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<AreaAttribute>().FirstOrDefault(); |
||||
|
if (areaAttr != null) |
||||
|
{ |
||||
|
return areaAttr.RouteValue; |
||||
|
} |
||||
|
|
||||
|
return ModuleApiDescriptionModel.DefaultServiceModuleName; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.Utils |
||||
|
{ |
||||
|
internal static class ArrayMatcher |
||||
|
{ |
||||
|
public static T[] Match<T>(T[] sourceArray, T[] destinationArray) |
||||
|
{ |
||||
|
var result = new List<T>(); |
||||
|
|
||||
|
var currentMethodParamIndex = 0; |
||||
|
var parentItem = default(T); |
||||
|
|
||||
|
foreach (var sourceItem in sourceArray) |
||||
|
{ |
||||
|
if (currentMethodParamIndex < destinationArray.Length) |
||||
|
{ |
||||
|
var destinationItem = destinationArray[currentMethodParamIndex]; |
||||
|
|
||||
|
if (EqualityComparer<T>.Default.Equals(sourceItem, destinationItem)) |
||||
|
{ |
||||
|
parentItem = default(T); |
||||
|
currentMethodParamIndex++; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (parentItem == null) |
||||
|
{ |
||||
|
parentItem = destinationItem; |
||||
|
currentMethodParamIndex++; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var resultItem = EqualityComparer<T>.Default.Equals(parentItem, default(T)) ? sourceItem : parentItem; |
||||
|
result.Add(resultItem); |
||||
|
} |
||||
|
|
||||
|
return result.ToArray(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using Microsoft.AspNetCore.TestHost; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.TestBase |
||||
|
{ |
||||
|
public interface ITestServerAccessor |
||||
|
{ |
||||
|
TestServer Server { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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<T>(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<AbpHttpClientOptions>(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) |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<AssemblyName>Volo.Abp.Http.Client</AssemblyName> |
||||
|
<PackageId>Volo.Abp.Http.Client</PackageId> |
||||
|
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
||||
|
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
||||
|
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
||||
|
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\Volo.Abp.Castle.Core\Volo.Abp.Castle.Core.csproj" /> |
||||
|
<ProjectReference Include="..\Volo.Abp.Http\Volo.Abp.Http.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -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<Type, DynamicHttpClientProxyConfig> HttpClientProxies { get; set; } |
||||
|
|
||||
|
public AbpHttpClientOptions() |
||||
|
{ |
||||
|
HttpClientProxies = new Dictionary<Type, DynamicHttpClientProxyConfig>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<AbpHttpClientModule>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<string, ApplicationApiDescriptionModel> _cache; |
||||
|
private readonly AsyncLock _asyncLock; |
||||
|
|
||||
|
public ApplicationApiDescriptionModelManager(IDynamicProxyHttpClientFactory httpClientFactory) |
||||
|
{ |
||||
|
_httpClientFactory = httpClientFactory; |
||||
|
|
||||
|
_cache = new Dictionary<string, ApplicationApiDescriptionModel>(); |
||||
|
_asyncLock = new AsyncLock(); |
||||
|
} |
||||
|
|
||||
|
public async Task<ApplicationApiDescriptionModel> 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<ApplicationApiDescriptionModel> 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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<TService> : AbpInterceptor, ITransientDependency |
||||
|
{ |
||||
|
private static readonly MethodInfo GenericInterceptAsyncMethod; |
||||
|
|
||||
|
private readonly IDynamicProxyHttpClientFactory _httpClientFactory; |
||||
|
private readonly IApplicationApiDescriptionModelManager _discoverManager; |
||||
|
private readonly AbpHttpClientOptions _options; |
||||
|
|
||||
|
static DynamicHttpProxyInterceptor() |
||||
|
{ |
||||
|
GenericInterceptAsyncMethod = typeof(DynamicHttpProxyInterceptor<TService>) |
||||
|
.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) |
||||
|
.First(m => m.Name == nameof(InterceptAsync) && m.IsGenericMethodDefinition); |
||||
|
} |
||||
|
|
||||
|
public DynamicHttpProxyInterceptor( |
||||
|
IDynamicProxyHttpClientFactory httpClientFactory, |
||||
|
IOptions<AbpHttpClientOptions> 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<T> InterceptAsync<T>(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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<ApplicationApiDescriptionModel> GetAsync(string baseUrl, CancellationToken cancellationToken = default(CancellationToken)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System.Net.Http; |
||||
|
|
||||
|
namespace Volo.Abp.Http.Client.DynamicProxying |
||||
|
{ |
||||
|
public interface IDynamicProxyHttpClientFactory |
||||
|
{ |
||||
|
HttpClient Create(); |
||||
|
} |
||||
|
} |
||||
@ -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<MethodParameterApiDescriptionModel> ParametersOnMethod { get; set; } |
||||
|
|
||||
|
public IList<ParameterApiDescriptionModel> 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<ParameterApiDescriptionModel>(), |
||||
|
ParametersOnMethod = method |
||||
|
.GetParameters() |
||||
|
.Select(MethodParameterApiDescriptionModel.Create) |
||||
|
.ToList() |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public ParameterApiDescriptionModel AddParameter(ParameterApiDescriptionModel parameter) |
||||
|
{ |
||||
|
Parameters.Add(parameter); |
||||
|
return parameter; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<string, ModuleApiDescriptionModel> Modules { get; set; } |
||||
|
|
||||
|
private ApplicationApiDescriptionModel() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public static ApplicationApiDescriptionModel Create() |
||||
|
{ |
||||
|
return new ApplicationApiDescriptionModel |
||||
|
{ |
||||
|
//TODO: Why ConcurrentDictionary?
|
||||
|
Modules = new ConcurrentDictionary<string, ModuleApiDescriptionModel>() |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<ControllerInterfaceApiDescriptionModel> Interfaces { get; set; } |
||||
|
|
||||
|
public Dictionary<string, ActionApiDescriptionModel> Actions { get; set; } |
||||
|
|
||||
|
private ControllerApiDescriptionModel() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public static ControllerApiDescriptionModel Create(string controllerName, Type type) |
||||
|
{ |
||||
|
return new ControllerApiDescriptionModel |
||||
|
{ |
||||
|
ControllerName = controllerName, |
||||
|
TypeAsString = type.FullName, |
||||
|
Actions = new Dictionary<string, ActionApiDescriptionModel>(), |
||||
|
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<string, ActionApiDescriptionModel>() |
||||
|
}; |
||||
|
|
||||
|
foreach (var action in Actions.Values) |
||||
|
{ |
||||
|
if (actions == null || actions.Contains(action.UniqueName)) |
||||
|
{ |
||||
|
subModel.AddAction(action); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return subModel; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace Volo.Abp.Http.Modeling |
||||
|
{ |
||||
|
public interface IApiDescriptionModelProvider |
||||
|
{ |
||||
|
ApplicationApiDescriptionModel CreateModel(); |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Volo.Abp.Http.Modeling |
||||
|
{ |
||||
|
[Serializable] |
||||
|
public class ModuleApiDescriptionModel |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// "app".
|
||||
|
/// </summary>
|
||||
|
public const string DefaultServiceModuleName = "app"; |
||||
|
|
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public IDictionary<string, ControllerApiDescriptionModel> Controllers { get; set; } |
||||
|
|
||||
|
private ModuleApiDescriptionModel() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public static ModuleApiDescriptionModel Create(string name) |
||||
|
{ |
||||
|
return new ModuleApiDescriptionModel |
||||
|
{ |
||||
|
Name = name, |
||||
|
Controllers = new Dictionary<string, ControllerApiDescriptionModel>() |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,3 +1,4 @@ |
|||||
|
using Volo.Abp.AspNetCore; |
||||
using Volo.Abp.AspNetCore.App; |
using Volo.Abp.AspNetCore.App; |
||||
|
|
||||
namespace Volo.Abp.Http |
namespace Volo.Abp.Http |
||||
@ -1,15 +1,18 @@ |
|||||
using Microsoft.Extensions.DependencyInjection; |
using Microsoft.Extensions.DependencyInjection; |
||||
using Volo.Abp.AspNetCore.App; |
using Volo.Abp.AspNetCore.App; |
||||
|
using Volo.Abp.Http.Client; |
||||
using Volo.Abp.Modularity; |
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.TestApp.Application; |
||||
|
|
||||
namespace Volo.Abp.Http |
namespace Volo.Abp.Http |
||||
{ |
{ |
||||
[DependsOn(typeof(AbpAspNetCoreMvcTestModule), typeof(AbpHttpModule))] |
[DependsOn(typeof(AbpAspNetCoreMvcTestModule), typeof(AbpHttpClientModule))] |
||||
public class AbpHttpTestModule : AbpModule |
public class AbpHttpTestModule : AbpModule |
||||
{ |
{ |
||||
public override void ConfigureServices(IServiceCollection services) |
public override void ConfigureServices(IServiceCollection services) |
||||
{ |
{ |
||||
services.AddAssemblyOf<AbpAspNetCoreMvcTestModule>(); |
services.AddAssemblyOf<AbpHttpTestModule>(); |
||||
|
services.AddHttpClientProxy<IPeopleAppService>("/"); |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
Loading…
Reference in new issue