committed by
GitHub
22 changed files with 1094 additions and 21 deletions
@ -0,0 +1,13 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net5.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Dapr.Client" Version="1.1.0" /> |
|||
<PackageReference Include="Volo.Abp.Http.Client" Version="4.2.1" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,21 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Http.Client; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpHttpClientModule))] |
|||
public class AbpDaprClientModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
Configure<AbpDaprRemoteServiceOptions>(configuration); |
|||
Configure<AbpDaprClientOptions>(configuration.GetSection("Dapr:Client")); |
|||
|
|||
// DaprClient应该配置为单例的实现
|
|||
context.Services.AddDaprClient(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using Grpc.Net.Client; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client |
|||
{ |
|||
public class AbpDaprClientOptions |
|||
{ |
|||
public string GrpcEndpoint { get; set; } |
|||
public string HttpEndpoint { get; set; } |
|||
public GrpcChannelOptions GrpcChannelOptions { get; set; } |
|||
public AbpDaprClientOptions() |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
namespace LINGYUN.Abp.Dapr.Client |
|||
{ |
|||
public class AbpDaprRemoteServiceOptions |
|||
{ |
|||
public DaprRemoteServiceConfigurationDictionary RemoteServices { get; set; } |
|||
|
|||
public AbpDaprRemoteServiceOptions() |
|||
{ |
|||
RemoteServices = new DaprRemoteServiceConfigurationDictionary(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client.Authentication |
|||
{ |
|||
public interface IRemoteServiceDaprClientAuthenticator |
|||
{ |
|||
Task AuthenticateAsync(RemoteServiceDaprClientAuthenticateContext context); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client.Authentication |
|||
{ |
|||
[Dependency(TryRegister = true)] |
|||
public class NullRemoteServiceDaprClientAuthenticator : IRemoteServiceDaprClientAuthenticator, ISingletonDependency |
|||
{ |
|||
public Task AuthenticateAsync(RemoteServiceDaprClientAuthenticateContext context) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using System.Net.Http; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client.Authentication |
|||
{ |
|||
public class RemoteServiceDaprClientAuthenticateContext |
|||
{ |
|||
public HttpRequestMessage Request { get; } |
|||
public DaprRemoteServiceConfiguration RemoteService { get; } |
|||
|
|||
public string RemoteServiceName { get; } |
|||
public RemoteServiceDaprClientAuthenticateContext( |
|||
HttpRequestMessage request, |
|||
DaprRemoteServiceConfiguration remoteService, |
|||
string remoteServiceName) |
|||
{ |
|||
Request = request; |
|||
RemoteService = remoteService; |
|||
RemoteServiceName = remoteServiceName; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
using Dapr.Client; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Json.SystemTextJson; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client |
|||
{ |
|||
public class DaprClientFactory : IDaprClientFactory, ISingletonDependency |
|||
{ |
|||
protected AbpDaprClientOptions DaprClientOptions { get; } |
|||
protected AbpSystemTextJsonSerializerOptions JsonSerializerOptions { get; } |
|||
|
|||
private readonly Lazy<DaprClient> _daprClientLazy; |
|||
|
|||
public DaprClientFactory( |
|||
IOptions<AbpDaprClientOptions> daprClientOptions, |
|||
IOptions<AbpSystemTextJsonSerializerOptions> jsonSerializarOptions) |
|||
{ |
|||
DaprClientOptions = daprClientOptions.Value; |
|||
JsonSerializerOptions = jsonSerializarOptions.Value; |
|||
|
|||
_daprClientLazy = new Lazy<DaprClient>(() => CreateDaprClient()); |
|||
} |
|||
|
|||
public DaprClient Create() => _daprClientLazy.Value; |
|||
|
|||
protected virtual DaprClient CreateDaprClient() |
|||
{ |
|||
var builder = new DaprClientBuilder() |
|||
.UseHttpEndpoint(DaprClientOptions.HttpEndpoint) |
|||
.UseJsonSerializationOptions(JsonSerializerOptions.JsonSerializerOptions); |
|||
|
|||
if (!DaprClientOptions.GrpcEndpoint.IsNullOrWhiteSpace() && |
|||
DaprClientOptions.GrpcChannelOptions != null) |
|||
{ |
|||
builder |
|||
.UseGrpcEndpoint(DaprClientOptions.GrpcEndpoint) |
|||
.UseGrpcChannelOptions(DaprClientOptions.GrpcChannelOptions); |
|||
} |
|||
|
|||
return builder.Build(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client |
|||
{ |
|||
public class DaprRemoteServiceConfiguration : Dictionary<string, string> |
|||
{ |
|||
/// <summary>
|
|||
/// Base AppId.
|
|||
/// </summary>
|
|||
public string AppId |
|||
{ |
|||
get => this.GetOrDefault(nameof(AppId)); |
|||
set => this[nameof(AppId)] = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Version.
|
|||
/// </summary>
|
|||
public string Version |
|||
{ |
|||
get => this.GetOrDefault(nameof(Version)); |
|||
set => this[nameof(Version)] = value; |
|||
} |
|||
|
|||
public DaprRemoteServiceConfiguration() |
|||
{ |
|||
} |
|||
|
|||
public DaprRemoteServiceConfiguration( |
|||
string appId, |
|||
string version) |
|||
{ |
|||
this[nameof(AppId)] = appId; |
|||
this[nameof(Version)] = version; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client |
|||
{ |
|||
public class DaprRemoteServiceConfigurationDictionary : Dictionary<string, DaprRemoteServiceConfiguration> |
|||
{ |
|||
public const string DefaultName = "Default"; |
|||
|
|||
public DaprRemoteServiceConfiguration Default |
|||
{ |
|||
get => this.GetOrDefault(DefaultName); |
|||
set => this[DefaultName] = value; |
|||
} |
|||
|
|||
public DaprRemoteServiceConfiguration GetConfigurationOrDefault(string name) |
|||
{ |
|||
return this.GetOrDefault(name) |
|||
?? Default |
|||
?? throw new AbpException($"Dapr remote service '{name}' was not found and there is no default configuration."); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client.DynamicProxying |
|||
{ |
|||
public class AbpDaprClientProxyOptions |
|||
{ |
|||
public Dictionary<Type, DynamicDaprClientProxyConfig> DaprClientProxies { get; set; } |
|||
|
|||
public AbpDaprClientProxyOptions() |
|||
{ |
|||
DaprClientProxies = new Dictionary<Type, DynamicDaprClientProxyConfig>(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,160 @@ |
|||
using Dapr.Client; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Net.Http; |
|||
using System.Net.Http.Headers; |
|||
using System.Reflection; |
|||
using System.Text.Json; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Http.Client.DynamicProxying; |
|||
using Volo.Abp.Http.Modeling; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Threading; |
|||
using Volo.Abp.Tracing; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client.DynamicProxying |
|||
{ |
|||
public class DaprApiDescriptionFinder : IDaprApiDescriptionFinder, ITransientDependency |
|||
{ |
|||
public ICancellationTokenProvider CancellationTokenProvider { get; set; } |
|||
protected IApiDescriptionCache Cache { get; } |
|||
protected AbpCorrelationIdOptions AbpCorrelationIdOptions { get; } |
|||
protected ICorrelationIdProvider CorrelationIdProvider { get; } |
|||
protected ICurrentTenant CurrentTenant { get; } |
|||
|
|||
protected DaprClient DaprClient { get; } |
|||
public DaprApiDescriptionFinder( |
|||
DaprClient daprClient, |
|||
IApiDescriptionCache cache, |
|||
IOptions<AbpCorrelationIdOptions> abpCorrelationIdOptions, |
|||
ICorrelationIdProvider correlationIdProvider, |
|||
ICurrentTenant currentTenant) |
|||
{ |
|||
DaprClient = daprClient; |
|||
|
|||
Cache = cache; |
|||
AbpCorrelationIdOptions = abpCorrelationIdOptions.Value; |
|||
CorrelationIdProvider = correlationIdProvider; |
|||
CurrentTenant = currentTenant; |
|||
CancellationTokenProvider = NullCancellationTokenProvider.Instance; |
|||
} |
|||
|
|||
public virtual async Task<ActionApiDescriptionModel> FindActionAsync(string appId, Type serviceType, MethodInfo method) |
|||
{ |
|||
var apiDescription = await GetApiDescriptionAsync(appId); |
|||
|
|||
//TODO: Cache finding?
|
|||
|
|||
var methodParameters = method.GetParameters().ToArray(); |
|||
|
|||
foreach (var module in apiDescription.Modules.Values) |
|||
{ |
|||
foreach (var controller in module.Controllers.Values) |
|||
{ |
|||
if (!controller.Implements(serviceType)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
foreach (var action in controller.Actions.Values) |
|||
{ |
|||
if (action.Name == method.Name && action.ParametersOnMethod.Count == methodParameters.Length) |
|||
{ |
|||
var found = true; |
|||
|
|||
for (int i = 0; i < methodParameters.Length; i++) |
|||
{ |
|||
if (!TypeMatches(action.ParametersOnMethod[i], methodParameters[i])) |
|||
{ |
|||
found = false; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (found) |
|||
{ |
|||
return action; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
throw new AbpException($"Could not found remote action for method: {method} on the appId: {appId}"); |
|||
} |
|||
|
|||
public virtual async Task<ApplicationApiDescriptionModel> GetApiDescriptionAsync(string appId) |
|||
{ |
|||
return await Cache.GetAsync(appId, () => GetApiDescriptionFromServerAsync(appId)); |
|||
} |
|||
|
|||
protected virtual async Task<ApplicationApiDescriptionModel> GetApiDescriptionFromServerAsync(string appId) |
|||
{ |
|||
var requestMessage = DaprClient.CreateInvokeMethodRequest(HttpMethod.Get, appId, "api/abp/api-definition"); |
|||
|
|||
AddHeaders(requestMessage); |
|||
|
|||
var response = await DaprClient.InvokeMethodWithResponseAsync( |
|||
requestMessage, |
|||
CancellationTokenProvider.Token); |
|||
|
|||
if (!response.IsSuccessStatusCode) |
|||
{ |
|||
throw new AbpException("Remote service returns error! StatusCode = " + response.StatusCode); |
|||
} |
|||
|
|||
var content = await response.Content.ReadAsStringAsync(); |
|||
|
|||
var result = JsonSerializer.Deserialize<ApplicationApiDescriptionModel>(content, new JsonSerializerOptions |
|||
{ |
|||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase |
|||
}); |
|||
|
|||
return (ApplicationApiDescriptionModel)result; |
|||
} |
|||
|
|||
protected virtual void AddHeaders(HttpRequestMessage requestMessage) |
|||
{ |
|||
//CorrelationId
|
|||
requestMessage.Headers.Add(AbpCorrelationIdOptions.HttpHeaderName, CorrelationIdProvider.Get()); |
|||
|
|||
//TenantId
|
|||
if (CurrentTenant.Id.HasValue) |
|||
{ |
|||
//TODO: Use AbpAspNetCoreMultiTenancyOptions to get the key
|
|||
requestMessage.Headers.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString()); |
|||
} |
|||
|
|||
//Culture
|
|||
//TODO: Is that the way we want? Couldn't send the culture (not ui culture)
|
|||
var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name; |
|||
if (!currentCulture.IsNullOrEmpty()) |
|||
{ |
|||
requestMessage.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(currentCulture)); |
|||
} |
|||
|
|||
//X-Requested-With
|
|||
requestMessage.Headers.Add("X-Requested-With", "XMLHttpRequest"); |
|||
} |
|||
|
|||
protected virtual bool TypeMatches(MethodParameterApiDescriptionModel actionParameter, ParameterInfo methodParameter) |
|||
{ |
|||
return NormalizeTypeName(actionParameter.TypeAsString) == |
|||
NormalizeTypeName(methodParameter.ParameterType.GetFullNameWithAssemblyName()); |
|||
} |
|||
|
|||
protected virtual string NormalizeTypeName(string typeName) |
|||
{ |
|||
const string placeholder = "%COREFX%"; |
|||
const string netCoreLib = "System.Private.CoreLib"; |
|||
const string netFxLib = "mscorlib"; |
|||
|
|||
return typeName.Replace(netCoreLib, placeholder).Replace(netFxLib, placeholder); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client.DynamicProxying |
|||
{ |
|||
public class DynamicDaprClientProxyConfig |
|||
{ |
|||
public Type Type { get; } |
|||
|
|||
public string RemoteServiceName { get; } |
|||
|
|||
public DynamicDaprClientProxyConfig(Type type, string remoteServiceName) |
|||
{ |
|||
Type = type; |
|||
RemoteServiceName = remoteServiceName; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,299 @@ |
|||
using Dapr.Client; |
|||
using LINGYUN.Abp.Dapr.Client.Authentication; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Net.Http; |
|||
using System.Net.Http.Headers; |
|||
using System.Reflection; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Content; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.DynamicProxy; |
|||
using Volo.Abp.Http; |
|||
using Volo.Abp.Http.Client; |
|||
using Volo.Abp.Http.Client.DynamicProxying; |
|||
using Volo.Abp.Http.Modeling; |
|||
using Volo.Abp.Http.ProxyScripting.Generators; |
|||
using Volo.Abp.Json; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Threading; |
|||
using Volo.Abp.Tracing; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client.DynamicProxying |
|||
{ |
|||
public class DynamicDaprClientProxyInterceptor<TService> : AbpInterceptor, ITransientDependency |
|||
{ |
|||
protected static MethodInfo MakeRequestAndGetResultAsyncMethod { get; } |
|||
|
|||
protected DaprClient DaprClient { get; } |
|||
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
|||
protected ICorrelationIdProvider CorrelationIdProvider { get; } |
|||
protected ICurrentTenant CurrentTenant { get; } |
|||
protected AbpCorrelationIdOptions AbpCorrelationIdOptions { get; } |
|||
protected IDaprApiDescriptionFinder ApiDescriptionFinder { get; } |
|||
protected AbpDaprRemoteServiceOptions AbpRemoteServiceOptions { get; } |
|||
protected AbpDaprClientProxyOptions ClientProxyOptions { get; } |
|||
protected IJsonSerializer JsonSerializer { get; } |
|||
protected IRemoteServiceDaprClientAuthenticator ClientAuthenticator { get; } |
|||
|
|||
public ILogger<DynamicDaprClientProxyInterceptor<TService>> Logger { get; set; } |
|||
|
|||
static DynamicDaprClientProxyInterceptor() |
|||
{ |
|||
MakeRequestAndGetResultAsyncMethod = typeof(DynamicDaprClientProxyInterceptor<TService>) |
|||
.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) |
|||
.First(m => m.Name == nameof(MakeRequestAndGetResultAsync) && m.IsGenericMethodDefinition); |
|||
} |
|||
|
|||
public DynamicDaprClientProxyInterceptor( |
|||
DaprClient daprClient, |
|||
IOptions<AbpDaprClientProxyOptions> clientProxyOptions, |
|||
IOptionsSnapshot<AbpDaprRemoteServiceOptions> remoteServiceOptions, |
|||
IDaprApiDescriptionFinder apiDescriptionFinder, |
|||
IJsonSerializer jsonSerializer, |
|||
IRemoteServiceDaprClientAuthenticator clientAuthenticator, |
|||
ICancellationTokenProvider cancellationTokenProvider, |
|||
ICorrelationIdProvider correlationIdProvider, |
|||
IOptions<AbpCorrelationIdOptions> correlationIdOptions, |
|||
ICurrentTenant currentTenant) |
|||
{ |
|||
DaprClient = daprClient; |
|||
CancellationTokenProvider = cancellationTokenProvider; |
|||
CorrelationIdProvider = correlationIdProvider; |
|||
CurrentTenant = currentTenant; |
|||
AbpCorrelationIdOptions = correlationIdOptions.Value; |
|||
ApiDescriptionFinder = apiDescriptionFinder; |
|||
JsonSerializer = jsonSerializer; |
|||
ClientAuthenticator = clientAuthenticator; |
|||
ClientProxyOptions = clientProxyOptions.Value; |
|||
AbpRemoteServiceOptions = remoteServiceOptions.Value; |
|||
|
|||
Logger = NullLogger<DynamicDaprClientProxyInterceptor<TService>>.Instance; |
|||
} |
|||
|
|||
public override async Task InterceptAsync(IAbpMethodInvocation invocation) |
|||
{ |
|||
if (invocation.Method.ReturnType.GenericTypeArguments.IsNullOrEmpty()) |
|||
{ |
|||
await MakeRequestAsync(invocation); |
|||
} |
|||
else |
|||
{ |
|||
var result = (Task)MakeRequestAndGetResultAsyncMethod |
|||
.MakeGenericMethod(invocation.Method.ReturnType.GenericTypeArguments[0]) |
|||
.Invoke(this, new object[] { invocation }); |
|||
|
|||
invocation.ReturnValue = await GetResultAsync( |
|||
result, |
|||
invocation.Method.ReturnType.GetGenericArguments()[0] |
|||
); |
|||
} |
|||
} |
|||
|
|||
private async Task<object> GetResultAsync(Task task, Type resultType) |
|||
{ |
|||
await task; |
|||
return typeof(Task<>) |
|||
.MakeGenericType(resultType) |
|||
.GetProperty(nameof(Task<object>.Result), BindingFlags.Instance | BindingFlags.Public) |
|||
.GetValue(task); |
|||
} |
|||
|
|||
private async Task<T> MakeRequestAndGetResultAsync<T>(IAbpMethodInvocation invocation) |
|||
{ |
|||
var responseContent = await MakeRequestAsync(invocation); |
|||
|
|||
if (typeof(T) == typeof(IRemoteStreamContent)) |
|||
{ |
|||
/* returning a class that holds a reference to response |
|||
* content just to be sure that GC does not dispose of |
|||
* it before we finish doing our work with the stream */ |
|||
return (T)(object)new RemoteStreamContent(await responseContent.ReadAsStreamAsync()) |
|||
{ |
|||
ContentType = responseContent.Headers.ContentType?.ToString() |
|||
}; |
|||
} |
|||
|
|||
var stringContent = await responseContent.ReadAsStringAsync(); |
|||
if (typeof(T) == typeof(string)) |
|||
{ |
|||
return (T)(object)stringContent; |
|||
} |
|||
|
|||
if (stringContent.IsNullOrWhiteSpace()) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
return JsonSerializer.Deserialize<T>(stringContent); |
|||
} |
|||
|
|||
private async Task<HttpContent> MakeRequestAsync(IAbpMethodInvocation invocation) |
|||
{ |
|||
var clientConfig = ClientProxyOptions.DaprClientProxies.GetOrDefault(typeof(TService)) ?? throw new AbpException($"Could not get DynamicDaprClientProxyConfig for {typeof(TService).FullName}."); |
|||
var remoteServiceConfig = AbpRemoteServiceOptions.RemoteServices.GetConfigurationOrDefault(clientConfig.RemoteServiceName); |
|||
|
|||
// 遵循远端 api/abp/api-definition
|
|||
var action = await ApiDescriptionFinder.FindActionAsync( |
|||
remoteServiceConfig.AppId, |
|||
typeof(TService), |
|||
invocation.Method |
|||
); |
|||
|
|||
var apiVersion = GetApiVersionInfo(action); |
|||
|
|||
// See: https://docs.dapr.io/reference/api/service_invocation_api/#examples
|
|||
// 需要合并端点作为dapr远程调用的方法名称
|
|||
var methodName = UrlBuilder.GenerateUrlWithParameters(action, invocation.ArgumentsDictionary, apiVersion); |
|||
|
|||
var requestMessage = DaprClient.CreateInvokeMethodRequest( |
|||
action.GetHttpMethod(), |
|||
remoteServiceConfig.AppId, |
|||
methodName); |
|||
|
|||
requestMessage.Content = RequestPayloadBuilder.BuildContent(action, invocation.ArgumentsDictionary, JsonSerializer, apiVersion); |
|||
|
|||
AddHeaders(invocation, action, requestMessage, apiVersion); |
|||
|
|||
await ClientAuthenticator.AuthenticateAsync( |
|||
new RemoteServiceDaprClientAuthenticateContext( |
|||
requestMessage, |
|||
remoteServiceConfig, |
|||
clientConfig.RemoteServiceName |
|||
) |
|||
); |
|||
|
|||
var response = await DaprClient.InvokeMethodWithResponseAsync(requestMessage, GetCancellationToken()); |
|||
|
|||
if (!response.IsSuccessStatusCode) |
|||
{ |
|||
await ThrowExceptionForResponseAsync(response); |
|||
} |
|||
|
|||
return response.Content; |
|||
} |
|||
|
|||
private ApiVersionInfo GetApiVersionInfo(ActionApiDescriptionModel action) |
|||
{ |
|||
var apiVersion = FindBestApiVersion(action); |
|||
|
|||
//TODO: Make names configurable?
|
|||
var versionParam = action.Parameters.FirstOrDefault(p => p.Name == "apiVersion" && p.BindingSourceId == ParameterBindingSources.Path) ?? |
|||
action.Parameters.FirstOrDefault(p => p.Name == "api-version" && p.BindingSourceId == ParameterBindingSources.Query); |
|||
|
|||
return new ApiVersionInfo(versionParam?.BindingSourceId, apiVersion); |
|||
} |
|||
|
|||
private string FindBestApiVersion(ActionApiDescriptionModel action) |
|||
{ |
|||
var configuredVersion = GetConfiguredApiVersion(); |
|||
|
|||
if (action.SupportedVersions.IsNullOrEmpty()) |
|||
{ |
|||
return configuredVersion ?? "1.0"; |
|||
} |
|||
|
|||
if (action.SupportedVersions.Contains(configuredVersion)) |
|||
{ |
|||
return configuredVersion; |
|||
} |
|||
|
|||
return action.SupportedVersions.Last(); //TODO: Ensure to get the latest version!
|
|||
} |
|||
|
|||
protected virtual void AddHeaders( |
|||
IAbpMethodInvocation invocation, |
|||
ActionApiDescriptionModel action, |
|||
HttpRequestMessage requestMessage, |
|||
ApiVersionInfo apiVersion) |
|||
{ |
|||
//API Version
|
|||
if (!apiVersion.Version.IsNullOrEmpty()) |
|||
{ |
|||
//TODO: What about other media types?
|
|||
requestMessage.Headers.Add("accept", $"{MimeTypes.Text.Plain}; v={apiVersion.Version}"); |
|||
requestMessage.Headers.Add("accept", $"{MimeTypes.Application.Json}; v={apiVersion.Version}"); |
|||
requestMessage.Headers.Add("api-version", apiVersion.Version); |
|||
} |
|||
|
|||
//Header parameters
|
|||
var headers = action.Parameters.Where(p => p.BindingSourceId == ParameterBindingSources.Header).ToArray(); |
|||
foreach (var headerParameter in headers) |
|||
{ |
|||
var value = HttpActionParameterHelper.FindParameterValue(invocation.ArgumentsDictionary, headerParameter); |
|||
if (value != null) |
|||
{ |
|||
requestMessage.Headers.Add(headerParameter.Name, value.ToString()); |
|||
} |
|||
} |
|||
|
|||
//CorrelationId
|
|||
requestMessage.Headers.Add(AbpCorrelationIdOptions.HttpHeaderName, CorrelationIdProvider.Get()); |
|||
|
|||
//TenantId
|
|||
if (CurrentTenant.Id.HasValue) |
|||
{ |
|||
//TODO: Use AbpAspNetCoreMultiTenancyOptions to get the key
|
|||
requestMessage.Headers.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString()); |
|||
} |
|||
|
|||
//Culture
|
|||
//TODO: Is that the way we want? Couldn't send the culture (not ui culture)
|
|||
var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name; |
|||
if (!currentCulture.IsNullOrEmpty()) |
|||
{ |
|||
requestMessage.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(currentCulture)); |
|||
} |
|||
|
|||
//X-Requested-With
|
|||
requestMessage.Headers.Add("X-Requested-With", "XMLHttpRequest"); |
|||
} |
|||
|
|||
private string GetConfiguredApiVersion() |
|||
{ |
|||
var clientConfig = ClientProxyOptions.DaprClientProxies.GetOrDefault(typeof(TService)) |
|||
?? throw new AbpException($"Could not get DynamicDaprClientProxyConfig for {typeof(TService).FullName}."); |
|||
|
|||
return AbpRemoteServiceOptions.RemoteServices.GetOrDefault(clientConfig.RemoteServiceName)?.Version |
|||
?? AbpRemoteServiceOptions.RemoteServices.Default?.Version; |
|||
} |
|||
|
|||
private async Task ThrowExceptionForResponseAsync(HttpResponseMessage response) |
|||
{ |
|||
if (response.Headers.Contains(AbpHttpConsts.AbpErrorFormat)) |
|||
{ |
|||
var errorResponse = JsonSerializer.Deserialize<RemoteServiceErrorResponse>( |
|||
await response.Content.ReadAsStringAsync() |
|||
); |
|||
|
|||
throw new AbpRemoteCallException(errorResponse.Error) |
|||
{ |
|||
HttpStatusCode = (int)response.StatusCode |
|||
}; |
|||
} |
|||
|
|||
throw new AbpRemoteCallException( |
|||
new RemoteServiceErrorInfo |
|||
{ |
|||
Message = response.ReasonPhrase, |
|||
Code = response.StatusCode.ToString() |
|||
} |
|||
) |
|||
{ |
|||
HttpStatusCode = (int)response.StatusCode |
|||
}; |
|||
} |
|||
|
|||
protected virtual CancellationToken GetCancellationToken() |
|||
{ |
|||
return CancellationTokenProvider.Token; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Http.Modeling; |
|||
using Volo.Abp.Reflection; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client.DynamicProxying |
|||
{ |
|||
internal static class HttpActionParameterHelper |
|||
{ |
|||
public static object FindParameterValue(IReadOnlyDictionary<string, object> methodArguments, ParameterApiDescriptionModel apiParameter) |
|||
{ |
|||
var value = methodArguments.GetOrDefault(apiParameter.NameOnMethod); |
|||
if (value == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (apiParameter.Name == apiParameter.NameOnMethod) |
|||
{ |
|||
return value; |
|||
} |
|||
|
|||
return ReflectionHelper.GetValueByPath(value, value.GetType(), apiParameter.Name); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
using System.Reflection; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Http.Modeling; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client.DynamicProxying |
|||
{ |
|||
public interface IDaprApiDescriptionFinder |
|||
{ |
|||
Task<ActionApiDescriptionModel> FindActionAsync(string appId, Type serviceType, MethodInfo invocationMethod); |
|||
|
|||
Task<ApplicationApiDescriptionModel> GetApiDescriptionAsync(string appId); |
|||
} |
|||
} |
|||
@ -0,0 +1,144 @@ |
|||
using JetBrains.Annotations; |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Http.Client.DynamicProxying; |
|||
using Volo.Abp.Http.Modeling; |
|||
using Volo.Abp.Http.ProxyScripting.Generators; |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client.DynamicProxying |
|||
{ |
|||
internal static class UrlBuilder |
|||
{ |
|||
public static string GenerateUrlWithParameters(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion) |
|||
{ |
|||
var urlBuilder = new StringBuilder(action.Url); |
|||
|
|||
ReplacePathVariables(urlBuilder, action.Parameters, methodArguments, apiVersion); |
|||
AddQueryStringParameters(urlBuilder, action.Parameters, methodArguments, apiVersion); |
|||
|
|||
return urlBuilder.ToString(); |
|||
} |
|||
|
|||
private static void ReplacePathVariables(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion) |
|||
{ |
|||
var pathParameters = actionParameters |
|||
.Where(p => p.BindingSourceId == ParameterBindingSources.Path) |
|||
.ToArray(); |
|||
|
|||
if (!pathParameters.Any()) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (pathParameters.Any(p => p.Name == "apiVersion")) |
|||
{ |
|||
urlBuilder = urlBuilder.Replace("{apiVersion}", apiVersion.Version); |
|||
} |
|||
|
|||
foreach (var pathParameter in pathParameters.Where(p => p.Name != "apiVersion")) //TODO: Constant!
|
|||
{ |
|||
var value = HttpActionParameterHelper.FindParameterValue(methodArguments, pathParameter); |
|||
|
|||
if (value == null) |
|||
{ |
|||
if (pathParameter.IsOptional) |
|||
{ |
|||
urlBuilder = urlBuilder.Replace($"{{{pathParameter.Name}}}", ""); |
|||
} |
|||
else if (pathParameter.DefaultValue != null) |
|||
{ |
|||
urlBuilder = urlBuilder.Replace($"{{{pathParameter.Name}}}", pathParameter.DefaultValue.ToString()); |
|||
} |
|||
else |
|||
{ |
|||
throw new AbpException($"Missing path parameter value for {pathParameter.Name} ({pathParameter.NameOnMethod})"); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
urlBuilder = urlBuilder.Replace($"{{{pathParameter.Name}}}", value.ToString()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void AddQueryStringParameters(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion) |
|||
{ |
|||
var queryStringParameters = actionParameters |
|||
.Where(p => p.BindingSourceId.IsIn(ParameterBindingSources.ModelBinding, ParameterBindingSources.Query)) |
|||
.ToArray(); |
|||
|
|||
var isFirstParam = true; |
|||
|
|||
foreach (var queryStringParameter in queryStringParameters) |
|||
{ |
|||
var value = HttpActionParameterHelper.FindParameterValue(methodArguments, queryStringParameter); |
|||
if (value == null) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (AddQueryStringParameter(urlBuilder, isFirstParam, queryStringParameter.Name, value)) |
|||
{ |
|||
isFirstParam = false; |
|||
} |
|||
} |
|||
|
|||
if (apiVersion.ShouldSendInQueryString()) |
|||
{ |
|||
AddQueryStringParameter(urlBuilder, isFirstParam, "api-version", apiVersion.Version); //TODO: Constant!
|
|||
} |
|||
} |
|||
|
|||
private static bool AddQueryStringParameter( |
|||
StringBuilder urlBuilder, |
|||
bool isFirstParam, |
|||
string name, |
|||
[NotNull] object value) |
|||
{ |
|||
if (value.GetType().IsArray || (value.GetType().IsGenericType && value is IEnumerable)) |
|||
{ |
|||
var index = 0; |
|||
foreach (var item in (IEnumerable)value) |
|||
{ |
|||
if (index == 0) |
|||
{ |
|||
urlBuilder.Append(isFirstParam ? "?" : "&"); |
|||
} |
|||
urlBuilder.Append(name + $"[{index++}]=" + System.Net.WebUtility.UrlEncode(ConvertValueToString(item)) + "&"); |
|||
} |
|||
|
|||
if (index > 0) |
|||
{ |
|||
//remove & at the end of the urlBuilder.
|
|||
urlBuilder.Remove(urlBuilder.Length - 1, 1); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
urlBuilder.Append(isFirstParam ? "?" : "&"); |
|||
urlBuilder.Append(name + "=" + System.Net.WebUtility.UrlEncode(ConvertValueToString(value))); |
|||
return true; |
|||
} |
|||
|
|||
private static string ConvertValueToString([NotNull] object value) |
|||
{ |
|||
using (CultureHelper.Use(CultureInfo.InvariantCulture)) |
|||
{ |
|||
if (value is DateTime dateTimeValue) |
|||
{ |
|||
return dateTimeValue.ToUniversalTime().ToString("u"); |
|||
} |
|||
|
|||
return value.ToString(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using Dapr.Client; |
|||
|
|||
namespace LINGYUN.Abp.Dapr.Client |
|||
{ |
|||
public interface IDaprClientFactory |
|||
{ |
|||
DaprClient Create(); |
|||
} |
|||
} |
|||
@ -0,0 +1,114 @@ |
|||
using Castle.DynamicProxy; |
|||
using JetBrains.Annotations; |
|||
using LINGYUN.Abp.Dapr.Client; |
|||
using LINGYUN.Abp.Dapr.Client.DynamicProxying; |
|||
using Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using System; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Castle.DynamicProxy; |
|||
using Volo.Abp.Validation; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection |
|||
{ |
|||
public static class ServiceCollectionDynamicDaprClientProxyExtensions |
|||
{ |
|||
private static readonly ProxyGenerator ProxyGeneratorInstance = new ProxyGenerator(); |
|||
|
|||
public static IServiceCollection AddDaprClient( |
|||
[NotNull] this IServiceCollection services) |
|||
{ |
|||
Check.NotNull(services, nameof(services)); |
|||
|
|||
services.TryAddSingleton(provider => provider.GetRequiredService<IDaprClientFactory>().Create()); |
|||
|
|||
return services; |
|||
} |
|||
|
|||
public static IServiceCollection AddDaprClientProxies( |
|||
[NotNull] this IServiceCollection services, |
|||
[NotNull] Assembly assembly, |
|||
[NotNull] string remoteServiceConfigurationName = DaprRemoteServiceConfigurationDictionary.DefaultName, |
|||
bool asDefaultServices = true) |
|||
{ |
|||
Check.NotNull(services, nameof(assembly)); |
|||
|
|||
var serviceTypes = assembly.GetTypes().Where(IsSuitableForDynamicActorProxying).ToArray(); |
|||
|
|||
foreach (var serviceType in serviceTypes) |
|||
{ |
|||
services.AddDaprClientProxy( |
|||
serviceType, |
|||
remoteServiceConfigurationName, |
|||
asDefaultServices |
|||
); |
|||
} |
|||
|
|||
return services; |
|||
} |
|||
|
|||
public static IServiceCollection AddDaprClientProxy<T>( |
|||
[NotNull] this IServiceCollection services, |
|||
[NotNull] string remoteServiceConfigurationName = DaprRemoteServiceConfigurationDictionary.DefaultName, |
|||
bool asDefaultService = true) |
|||
{ |
|||
return services.AddDaprClientProxy( |
|||
typeof(T), |
|||
remoteServiceConfigurationName, |
|||
asDefaultService |
|||
); |
|||
} |
|||
|
|||
public static IServiceCollection AddDaprClientProxy( |
|||
[NotNull] this IServiceCollection services, |
|||
[NotNull] Type type, |
|||
[NotNull] string remoteServiceConfigurationName = DaprRemoteServiceConfigurationDictionary.DefaultName, |
|||
bool asDefaultService = true) |
|||
{ |
|||
Check.NotNull(services, nameof(services)); |
|||
Check.NotNull(type, nameof(type)); |
|||
Check.NotNullOrWhiteSpace(remoteServiceConfigurationName, nameof(remoteServiceConfigurationName)); |
|||
|
|||
// AddHttpClientFactory(services, remoteServiceConfigurationName);
|
|||
|
|||
services.Configure<AbpDaprClientProxyOptions>(options => |
|||
{ |
|||
options.DaprClientProxies[type] = new DynamicDaprClientProxyConfig(type, remoteServiceConfigurationName); |
|||
}); |
|||
|
|||
var interceptorType = typeof(DynamicDaprClientProxyInterceptor<>).MakeGenericType(type); |
|||
services.AddTransient(interceptorType); |
|||
|
|||
var interceptorAdapterType = typeof(AbpAsyncDeterminationInterceptor<>).MakeGenericType(interceptorType); |
|||
|
|||
var validationInterceptorAdapterType = |
|||
typeof(AbpAsyncDeterminationInterceptor<>).MakeGenericType(typeof(ValidationInterceptor)); |
|||
|
|||
if (asDefaultService) |
|||
{ |
|||
services.AddTransient( |
|||
type, |
|||
serviceProvider => ProxyGeneratorInstance |
|||
.CreateInterfaceProxyWithoutTarget( |
|||
type, |
|||
(IInterceptor)serviceProvider.GetRequiredService(validationInterceptorAdapterType), |
|||
(IInterceptor)serviceProvider.GetRequiredService(interceptorAdapterType) |
|||
) |
|||
); |
|||
} |
|||
|
|||
return services; |
|||
} |
|||
|
|||
private static bool IsSuitableForDynamicActorProxying(Type type) |
|||
{ |
|||
//TODO: Add option to change type filter
|
|||
|
|||
return type.IsInterface |
|||
&& type.IsPublic |
|||
&& !type.IsGenericType |
|||
&& typeof(IRemoteService).IsAssignableFrom(type); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
# LINGYUN.Abp.Dapr.Client |
|||
|
|||
实现了Dapr文档中的服务间调用,项目设计与Volo.Abp.Http.Client一致,通过配置文件即可无缝替代Volo.Abp.Http.Client |
|||
|
|||
配置参考 [AbpRemoteServiceOptions](https://docs.abp.io/zh-Hans/abp/latest/API/Dynamic-CSharp-API-Clients#abpremoteserviceoptions) |
|||
|
|||
## 配置使用 |
|||
|
|||
模块按需引用 |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpDaprClientModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
// 注册代理类似于 Volo.Abp.Http.Client 模块 |
|||
context.Services.AddDaprClientProxies( |
|||
typeof(YouProjectActorInterfaceModule).Assembly, // 搜索 YouProjectActorInterfaceModule 模块下的远程服务定义 |
|||
RemoteServiceName |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
## 配置项说明 |
|||
|
|||
* AbpDaprClientOptions.GrpcEndpoint Dapr暴露的Grpc端点, 对应 **DaprClientBuilder.GrpcEndpoint** |
|||
* AbpDaprClientOptions.HttpEndpoint Dapr暴露的Http端点, 对应 **DaprClientBuilder.HttpEndpoint** |
|||
* AbpDaprClientOptions.GrpcChannelOptions 通过Grpc调用远程服务的配置项, 对应 **DaprClientBuilder.GrpcChannelOptions** |
|||
|
|||
* AbpDaprRemoteServiceOptions.RemoteServices 配置Dapr.AppId |
|||
|
|||
```json |
|||
|
|||
{ |
|||
"Dapr": { |
|||
"Client": { |
|||
"HttpEndpoint": "http://127.0.0.1:50000" |
|||
} |
|||
}, |
|||
"RemoteServices": { |
|||
"System": { |
|||
"AppId": "myapp" |
|||
} |
|||
} |
|||
} |
|||
|
|||
``` |
|||
|
|||
|
|||
## 其他 |
|||
Loading…
Reference in new issue