From 5abd71f6c7691f63f66bca3589e32aeab7198547 Mon Sep 17 00:00:00 2001 From: Gideon de Swardt Date: Sat, 3 Apr 2021 22:32:37 +0100 Subject: [PATCH 01/31] Add Azure Service Bus module Added an ABP module for Azure Service Bus that implements common configuration, services, and factories that can be reused in any module. Added extension methods to the ServiceBusAdministrationClient to check if a topic and subscription exist and if not create it. Improved the performance of publishing and processing messages by creating a pool for ServiceBusAdministrationClient, ServiceBusClient, ServiceBusSender and ServiceBusProcessor. --- framework/Volo.Abp.sln | 7 ++ .../Volo.Abp.AzureServiceBus/FodyWeavers.xml | 3 + .../Volo.Abp.AzureServiceBus/FodyWeavers.xsd | 30 +++++ .../Volo.Abp.AzureServiceBus.csproj | 17 +++ .../AbpAzureServiceBusModule.cs | 20 ++++ .../AbpAzureServiceBusOptions.cs | 12 ++ .../AzureServiceBusConnections.cs | 31 ++++++ .../AzureServiceBusMessageConsumer.cs | 104 ++++++++++++++++++ .../AzureServiceBusMessageConsumerFactory.cs | 29 +++++ .../Volo/Abp/AzureServiceBus/ClientConfig.cs | 16 +++ .../Abp/AzureServiceBus/ConnectionPool.cs | 88 +++++++++++++++ .../IAzureServiceBusMessageConsumer.cs | 11 ++ .../IAzureServiceBusMessageConsumerFactory.cs | 21 ++++ .../IAzureServiceBusSerializer.cs | 11 ++ .../Abp/AzureServiceBus/IConnectionPool.cs | 13 +++ .../Abp/AzureServiceBus/IProcessorPool.cs | 11 ++ .../Abp/AzureServiceBus/IPublisherPool.cs | 11 ++ .../Volo/Abp/AzureServiceBus/ProcessorPool.cs | 82 ++++++++++++++ .../Volo/Abp/AzureServiceBus/PublisherPool.cs | 66 +++++++++++ ...erviceBusAdministrationClientExtensions.cs | 25 +++++ .../Utf8JsonAzureServiceBusSerializer.cs | 27 +++++ 21 files changed, 635 insertions(+) create mode 100644 framework/src/Volo.Abp.AzureServiceBus/FodyWeavers.xml create mode 100644 framework/src/Volo.Abp.AzureServiceBus/FodyWeavers.xsd create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo.Abp.AzureServiceBus.csproj create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AbpAzureServiceBusModule.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AbpAzureServiceBusOptions.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AzureServiceBusConnections.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AzureServiceBusMessageConsumer.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AzureServiceBusMessageConsumerFactory.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ClientConfig.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ConnectionPool.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusMessageConsumer.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusMessageConsumerFactory.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusSerializer.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IConnectionPool.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IProcessorPool.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IPublisherPool.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ProcessorPool.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/PublisherPool.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ServiceBusAdministrationClientExtensions.cs create mode 100644 framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/Utf8JsonAzureServiceBusSerializer.cs diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 67d2fa2573..56fd40cee3 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -383,6 +383,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Compone EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions", "src\Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions\Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions.csproj", "{E9CE58DB-0789-4D18-8B63-474F7D7B14B4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AzureServiceBus", "src\Volo.Abp.AzureServiceBus\Volo.Abp.AzureServiceBus.csproj", "{808EC18E-C8CC-4F5C-82B6-984EADBBF85D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1141,6 +1143,10 @@ Global {E9CE58DB-0789-4D18-8B63-474F7D7B14B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E9CE58DB-0789-4D18-8B63-474F7D7B14B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {E9CE58DB-0789-4D18-8B63-474F7D7B14B4}.Release|Any CPU.Build.0 = Release|Any CPU + {808EC18E-C8CC-4F5C-82B6-984EADBBF85D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {808EC18E-C8CC-4F5C-82B6-984EADBBF85D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {808EC18E-C8CC-4F5C-82B6-984EADBBF85D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {808EC18E-C8CC-4F5C-82B6-984EADBBF85D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1334,6 +1340,7 @@ Global {863C18F9-2407-49F9-9ADC-F6229AF3B385} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {B4B6B7DE-9798-4007-B1DF-7EE7929E392A} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {E9CE58DB-0789-4D18-8B63-474F7D7B14B4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {808EC18E-C8CC-4F5C-82B6-984EADBBF85D} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.AzureServiceBus/FodyWeavers.xml b/framework/src/Volo.Abp.AzureServiceBus/FodyWeavers.xml new file mode 100644 index 0000000000..be0de3a908 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/FodyWeavers.xsd b/framework/src/Volo.Abp.AzureServiceBus/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo.Abp.AzureServiceBus.csproj b/framework/src/Volo.Abp.AzureServiceBus/Volo.Abp.AzureServiceBus.csproj new file mode 100644 index 0000000000..77ccf8f772 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo.Abp.AzureServiceBus.csproj @@ -0,0 +1,17 @@ + + + + + + + netstandard2.0 + latest + + + + + + + + + diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AbpAzureServiceBusModule.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AbpAzureServiceBusModule.cs new file mode 100644 index 0000000000..e9d1c20cd3 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AbpAzureServiceBusModule.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Json; +using Volo.Abp.Modularity; +using Volo.Abp.Threading; + +namespace Volo.Abp.AzureServiceBus +{ + [DependsOn( + typeof(AbpJsonModule), + typeof(AbpThreadingModule) + )] + public class AbpAzureServiceBusModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + Configure(configuration.GetSection("Azure:ServiceBus")); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AbpAzureServiceBusOptions.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AbpAzureServiceBusOptions.cs new file mode 100644 index 0000000000..7672502d28 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AbpAzureServiceBusOptions.cs @@ -0,0 +1,12 @@ +namespace Volo.Abp.AzureServiceBus +{ + public class AbpAzureServiceBusOptions + { + public AzureServiceBusConnections Connections { get; } + + public AbpAzureServiceBusOptions() + { + Connections = new AzureServiceBusConnections(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AzureServiceBusConnections.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AzureServiceBusConnections.cs new file mode 100644 index 0000000000..f8b0683d96 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AzureServiceBusConnections.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Volo.Abp.AzureServiceBus +{ + [Serializable] + public class AzureServiceBusConnections : Dictionary + { + public const string DefaultConnectionName = "Default"; + + [NotNull] + public ClientConfig Default + { + get => this[DefaultConnectionName]; + set => this[DefaultConnectionName] = Check.NotNull(value, nameof(value)); + } + + public AzureServiceBusConnections() + { + Default = new ClientConfig(); + } + + public ClientConfig GetOrDefault(string connectionName) + { + return TryGetValue(connectionName, out var connectionFactory) + ? connectionFactory + : Default; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AzureServiceBusMessageConsumer.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AzureServiceBusMessageConsumer.cs new file mode 100644 index 0000000000..6eeb8f7693 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AzureServiceBusMessageConsumer.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.ExceptionHandling; +using Volo.Abp.Threading; + +namespace Volo.Abp.AzureServiceBus +{ + public class AzureServiceBusMessageConsumer : IAzureServiceBusMessageConsumer, ITransientDependency + { + public ILogger Logger { get; set; } + + private readonly IExceptionNotifier _exceptionNotifier; + private readonly IProcessorPool _processorPool; + private readonly ConcurrentBag> _callbacks; + private string _connectionName; + private string _subscriptionName; + private string _topicName; + + public AzureServiceBusMessageConsumer( + IExceptionNotifier exceptionNotifier, + IProcessorPool processorPool) + { + _exceptionNotifier = exceptionNotifier; + _processorPool = processorPool; + Logger = NullLogger.Instance; + _callbacks = new ConcurrentBag>(); + } + + public virtual void Initialize( + [NotNull] string topicName, + [NotNull] string subscriptionName, + string connectionName) + { + Check.NotNull(topicName, nameof(topicName)); + Check.NotNull(subscriptionName, nameof(subscriptionName)); + + _topicName = topicName; + _connectionName = connectionName ?? AzureServiceBusConnections.DefaultConnectionName; + _subscriptionName = subscriptionName; + StartProcessing(); + } + + public void OnMessageReceived(Func callback) + { + _callbacks.Add(callback); + } + + protected virtual void StartProcessing() + { + Task.Factory.StartNew(function: async () => + { + var serviceBusProcessor = await _processorPool.GetAsync(_subscriptionName, _topicName, _connectionName); + serviceBusProcessor.ProcessErrorAsync += HandleIncomingError; + serviceBusProcessor.ProcessMessageAsync += HandleIncomingMessage; + + if (!serviceBusProcessor.IsProcessing) + { + await serviceBusProcessor.StartProcessingAsync(); + } + + while (true) + { + Thread.Sleep(1000); + } + }, TaskCreationOptions.LongRunning); + } + + protected virtual async Task HandleIncomingMessage(ProcessMessageEventArgs args) + { + try + { + foreach (var callback in _callbacks) + { + await callback(args.Message); + } + + await args.CompleteMessageAsync(args.Message); + } + catch (Exception exception) + { + await HandleError(exception); + } + } + + protected virtual async Task HandleIncomingError(ProcessErrorEventArgs args) + { + await HandleError(args.Exception); + } + + protected virtual async Task HandleError(Exception exception) + { + Logger.LogException(exception); + await _exceptionNotifier.NotifyAsync(exception); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AzureServiceBusMessageConsumerFactory.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AzureServiceBusMessageConsumerFactory.cs new file mode 100644 index 0000000000..d373de43cf --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/AzureServiceBusMessageConsumerFactory.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AzureServiceBus +{ + public class AzureServiceBusMessageConsumerFactory : IAzureServiceBusMessageConsumerFactory, ISingletonDependency, IDisposable + { + protected IServiceScope ServiceScope { get; } + + public AzureServiceBusMessageConsumerFactory(IServiceScopeFactory serviceScopeFactory) + { + ServiceScope = serviceScopeFactory.CreateScope(); + } + + public IAzureServiceBusMessageConsumer CreateMessageConsumer(string topicName, string subscriptionName, string connectionName) + { + var processor = ServiceScope.ServiceProvider.GetRequiredService(); + processor.Initialize(topicName, subscriptionName, connectionName); + return processor; + } + + public void Dispose() + { + ServiceScope?.Dispose(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ClientConfig.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ClientConfig.cs new file mode 100644 index 0000000000..9a4c8c2856 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ClientConfig.cs @@ -0,0 +1,16 @@ +using Azure.Messaging.ServiceBus; +using Azure.Messaging.ServiceBus.Administration; + +namespace Volo.Abp.AzureServiceBus +{ + public class ClientConfig + { + public string ConnectionString { get; set; } + + public ServiceBusAdministrationClientOptions Admin { get; set; } = new(); + + public ServiceBusClientOptions Client { get; set; } = new(); + + public ServiceBusProcessorOptions Processor { get; set; } = new(); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ConnectionPool.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ConnectionPool.cs new file mode 100644 index 0000000000..4f38250dec --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ConnectionPool.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Azure.Messaging.ServiceBus.Administration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AzureServiceBus +{ + public class ConnectionPool : IConnectionPool, ISingletonDependency + { + public ILogger Logger { get; set; } + + private bool _isDisposed; + private readonly AbpAzureServiceBusOptions _options; + private readonly ConcurrentDictionary> _clients; + private readonly ConcurrentDictionary> _adminClients; + + public ConnectionPool(IOptions options) + { + _options = options.Value; + _clients = new ConcurrentDictionary>(); + _adminClients = new ConcurrentDictionary>(); + Logger = new NullLogger(); + } + + public ServiceBusClient GetClient(string connectionName) + { + connectionName ??= AzureServiceBusConnections.DefaultConnectionName; + return _clients.GetOrAdd( + connectionName, new Lazy(() => + { + var config = _options.Connections.GetOrDefault(connectionName); + return new ServiceBusClient(config.ConnectionString, config.Client); + }) + ).Value; + } + + public ServiceBusAdministrationClient GetAdministrationClient(string connectionName) + { + connectionName ??= AzureServiceBusConnections.DefaultConnectionName; + return _adminClients.GetOrAdd( + connectionName, new Lazy(() => + { + var config = _options.Connections.GetOrDefault(connectionName); + return new ServiceBusAdministrationClient(config.ConnectionString); + }) + ).Value; + } + + public async ValueTask DisposeAsync() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + if (!_clients.Any()) + { + Logger.LogDebug($"Disposed connection pool with no connection in the pool."); + return; + } + + Logger.LogInformation($"Disposing connection pool ({_clients.Count} connections)."); + + foreach (var connection in _clients.Values) + { + await connection.Value.DisposeAsync(); + } + + _clients.Clear(); + + if (!_adminClients.Any()) + { + Logger.LogDebug($"Disposed admin connection pool with no admin connection in the pool."); + return; + } + + Logger.LogInformation($"Disposing admin connection pool ({_adminClients.Count} admin connections)."); + _adminClients.Clear(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusMessageConsumer.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusMessageConsumer.cs new file mode 100644 index 0000000000..7a47e42e40 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusMessageConsumer.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; + +namespace Volo.Abp.AzureServiceBus +{ + public interface IAzureServiceBusMessageConsumer + { + void OnMessageReceived(Func callback); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusMessageConsumerFactory.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusMessageConsumerFactory.cs new file mode 100644 index 0000000000..0f9f20f0aa --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusMessageConsumerFactory.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.AzureServiceBus +{ + public interface IAzureServiceBusMessageConsumerFactory + { + /// + /// Creates a new . + /// Avoid to create too many consumers since they are + /// not disposed until end of the application. + /// + /// + /// + /// + /// + IAzureServiceBusMessageConsumer CreateMessageConsumer( + string topicName, + string subscriptionName, + string connectionName); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusSerializer.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusSerializer.cs new file mode 100644 index 0000000000..04f56d4470 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusSerializer.cs @@ -0,0 +1,11 @@ +using System; + +namespace Volo.Abp.AzureServiceBus +{ + public interface IAzureServiceBusSerializer + { + byte[] Serialize(object obj); + + object Deserialize(BinaryData value, Type type); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IConnectionPool.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IConnectionPool.cs new file mode 100644 index 0000000000..43ed188022 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IConnectionPool.cs @@ -0,0 +1,13 @@ +using System; +using Azure.Messaging.ServiceBus; +using Azure.Messaging.ServiceBus.Administration; + +namespace Volo.Abp.AzureServiceBus +{ + public interface IConnectionPool : IAsyncDisposable + { + ServiceBusClient GetClient(string connectionName); + + ServiceBusAdministrationClient GetAdministrationClient(string connectionName); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IProcessorPool.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IProcessorPool.cs new file mode 100644 index 0000000000..338bee393c --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IProcessorPool.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; + +namespace Volo.Abp.AzureServiceBus +{ + public interface IProcessorPool : IAsyncDisposable + { + Task GetAsync(string subscriptionName, string topicName, string connectionName); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IPublisherPool.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IPublisherPool.cs new file mode 100644 index 0000000000..9ae141a252 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IPublisherPool.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; + +namespace Volo.Abp.AzureServiceBus +{ + public interface IPublisherPool : IAsyncDisposable + { + Task GetAsync(string topicName, string connectionName); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ProcessorPool.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ProcessorPool.cs new file mode 100644 index 0000000000..daa804e930 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ProcessorPool.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AzureServiceBus +{ + public class ProcessorPool : IProcessorPool, ISingletonDependency + { + public ILogger Logger { get; set; } + + private bool _isDisposed; + private readonly AbpAzureServiceBusOptions _options; + private readonly IConnectionPool _connectionPool; + private readonly ConcurrentDictionary> _processors; + + public ProcessorPool( + IOptions options, + IConnectionPool connectionPool) + { + _options = options.Value; + _connectionPool = connectionPool; + _processors = new ConcurrentDictionary>(); + Logger = new NullLogger(); + } + + public async Task GetAsync(string subscriptionName, string topicName, string connectionName) + { + var admin = _connectionPool.GetAdministrationClient(connectionName); + await admin.SetupSubscriptionAsync(topicName, subscriptionName); + + return _processors.GetOrAdd( + $"{topicName}-{subscriptionName}", new Lazy(() => + { + var config = _options.Connections.GetOrDefault(connectionName); + var client = _connectionPool.GetClient(connectionName); + return client.CreateProcessor(topicName, subscriptionName, config.Processor); + }) + ).Value; + } + + public async ValueTask DisposeAsync() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + if (!_processors.Any()) + { + Logger.LogDebug($"Disposed processor pool with no processors in the pool."); + return; + } + + Logger.LogInformation($"Disposing processor pool ({_processors.Count} processors)."); + + foreach (var item in _processors.Values) + { + var processor = item.Value; + if (processor.IsProcessing) + { + await processor.StopProcessingAsync(); + } + + if (!processor.IsClosed) + { + await processor.CloseAsync(); + } + + await processor.DisposeAsync(); + } + + _processors.Clear(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/PublisherPool.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/PublisherPool.cs new file mode 100644 index 0000000000..57008e07cb --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/PublisherPool.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AzureServiceBus +{ + public class PublisherPool : IPublisherPool, ISingletonDependency + { + public ILogger Logger { get; set; } + + private bool _isDisposed; + private readonly IConnectionPool _connectionPool; + private readonly ConcurrentDictionary> _publishers; + + public PublisherPool(IConnectionPool connectionPool) + { + _connectionPool = connectionPool; + _publishers = new ConcurrentDictionary>(); + Logger = new NullLogger(); + } + + public async Task GetAsync(string topicName, string connectionName) + { + var admin = _connectionPool.GetAdministrationClient(connectionName); + await admin.SetupTopicAsync(topicName); + + return _publishers.GetOrAdd( + topicName, new Lazy(() => + { + var client = _connectionPool.GetClient(connectionName); + return client.CreateSender(topicName); + }) + ).Value; + } + + public async ValueTask DisposeAsync() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + if (!_publishers.Any()) + { + Logger.LogDebug($"Disposed publisher pool with no publisher in the pool."); + return; + } + + Logger.LogInformation($"Disposing publisher pool ({_publishers.Count} publishers)."); + + foreach (var publisher in _publishers.Values) + { + await publisher.Value.CloseAsync(); + await publisher.Value.DisposeAsync(); + } + + _publishers.Clear(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ServiceBusAdministrationClientExtensions.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ServiceBusAdministrationClientExtensions.cs new file mode 100644 index 0000000000..c6a09a155b --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ServiceBusAdministrationClientExtensions.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus.Administration; + +namespace Volo.Abp.AzureServiceBus +{ + public static class ServiceBusAdministrationClientExtensions + { + public static async Task SetupTopicAsync(this ServiceBusAdministrationClient client, string topicName) + { + if (!await client.TopicExistsAsync(topicName)) + { + await client.CreateTopicAsync(topicName); + } + } + + public static async Task SetupSubscriptionAsync(this ServiceBusAdministrationClient client, string topicName, string subscriptionName) + { + await client.SetupTopicAsync(topicName); + if (!await client.SubscriptionExistsAsync(topicName, subscriptionName)) + { + await client.CreateSubscriptionAsync(topicName, subscriptionName); + } + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/Utf8JsonAzureServiceBusSerializer.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/Utf8JsonAzureServiceBusSerializer.cs new file mode 100644 index 0000000000..0a87e22934 --- /dev/null +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/Utf8JsonAzureServiceBusSerializer.cs @@ -0,0 +1,27 @@ +using System; +using System.Text; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; + +namespace Volo.Abp.AzureServiceBus +{ + public class Utf8JsonAzureServiceBusSerializer : IAzureServiceBusSerializer, ITransientDependency + { + private readonly IJsonSerializer _jsonSerializer; + + public Utf8JsonAzureServiceBusSerializer(IJsonSerializer jsonSerializer) + { + _jsonSerializer = jsonSerializer; + } + + public byte[] Serialize(object obj) + { + return Encoding.UTF8.GetBytes(_jsonSerializer.Serialize(obj)); + } + + public object Deserialize(BinaryData value, Type type) + { + return _jsonSerializer.Deserialize(type, Encoding.UTF8.GetString(value.ToArray())); + } + } +} \ No newline at end of file From 08e7aedbd9213b55d213363bbcef28a0a7ee8c28 Mon Sep 17 00:00:00 2001 From: Gideon de Swardt Date: Sat, 3 Apr 2021 22:31:37 +0100 Subject: [PATCH 02/31] Implement Distribute Events Bus with Azure Implement a new instance of the IDistributedEventBus that uses the AzureServiceBus module. Added a service that will replace the existing instance in the service registry for the distributed event bus with the Azure instance. Added configuration options for the Azure Event Bus. --- framework/Volo.Abp.sln | 7 + .../Volo.Abp.EventBus.Azure/FodyWeavers.xml | 3 + .../Volo.Abp.EventBus.Azure/FodyWeavers.xsd | 30 +++ .../Volo.Abp.EventBus.Azure.csproj | 13 ++ .../EventBus/Azure/AbpAzureEventBusOptions.cs | 11 + .../EventBus/Azure/AbpEventBusAzureModule.cs | 28 +++ .../Azure/AzureDistributedEventBus.cs | 189 ++++++++++++++++++ 7 files changed, 281 insertions(+) create mode 100644 framework/src/Volo.Abp.EventBus.Azure/FodyWeavers.xml create mode 100644 framework/src/Volo.Abp.EventBus.Azure/FodyWeavers.xsd create mode 100644 framework/src/Volo.Abp.EventBus.Azure/Volo.Abp.EventBus.Azure.csproj create mode 100644 framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AbpAzureEventBusOptions.cs create mode 100644 framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AbpEventBusAzureModule.cs create mode 100644 framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 56fd40cee3..53c15f8ea9 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -385,6 +385,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Mvc.UI. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AzureServiceBus", "src\Volo.Abp.AzureServiceBus\Volo.Abp.AzureServiceBus.csproj", "{808EC18E-C8CC-4F5C-82B6-984EADBBF85D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EventBus.Azure", "src\Volo.Abp.EventBus.Azure\Volo.Abp.EventBus.Azure.csproj", "{FB27F78E-F10E-4810-9B8E-BCD67DCFC8A2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1147,6 +1149,10 @@ Global {808EC18E-C8CC-4F5C-82B6-984EADBBF85D}.Debug|Any CPU.Build.0 = Debug|Any CPU {808EC18E-C8CC-4F5C-82B6-984EADBBF85D}.Release|Any CPU.ActiveCfg = Release|Any CPU {808EC18E-C8CC-4F5C-82B6-984EADBBF85D}.Release|Any CPU.Build.0 = Release|Any CPU + {FB27F78E-F10E-4810-9B8E-BCD67DCFC8A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB27F78E-F10E-4810-9B8E-BCD67DCFC8A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB27F78E-F10E-4810-9B8E-BCD67DCFC8A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB27F78E-F10E-4810-9B8E-BCD67DCFC8A2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1341,6 +1347,7 @@ Global {B4B6B7DE-9798-4007-B1DF-7EE7929E392A} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {E9CE58DB-0789-4D18-8B63-474F7D7B14B4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {808EC18E-C8CC-4F5C-82B6-984EADBBF85D} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {FB27F78E-F10E-4810-9B8E-BCD67DCFC8A2} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.EventBus.Azure/FodyWeavers.xml b/framework/src/Volo.Abp.EventBus.Azure/FodyWeavers.xml new file mode 100644 index 0000000000..be0de3a908 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Azure/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.EventBus.Azure/FodyWeavers.xsd b/framework/src/Volo.Abp.EventBus.Azure/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Azure/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo.Abp.EventBus.Azure.csproj b/framework/src/Volo.Abp.EventBus.Azure/Volo.Abp.EventBus.Azure.csproj new file mode 100644 index 0000000000..c913b79bfc --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo.Abp.EventBus.Azure.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.0 + latest + + + + + + + + diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AbpAzureEventBusOptions.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AbpAzureEventBusOptions.cs new file mode 100644 index 0000000000..cb56de424a --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AbpAzureEventBusOptions.cs @@ -0,0 +1,11 @@ +namespace Volo.Abp.EventBus.Azure +{ + public class AbpAzureEventBusOptions + { + public string ConnectionName { get; set; } + + public string SubscriberName { get; set; } + + public string TopicName { get; set; } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AbpEventBusAzureModule.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AbpEventBusAzureModule.cs new file mode 100644 index 0000000000..08feabd366 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AbpEventBusAzureModule.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AzureServiceBus; +using Volo.Abp.Modularity; + +namespace Volo.Abp.EventBus.Azure +{ + [DependsOn( + typeof(AbpEventBusModule), + typeof(AbpAzureServiceBusModule) + )] + public class AbpEventBusAzureModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + Configure(configuration.GetSection("Azure:EventBus")); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + context + .ServiceProvider + .GetRequiredService() + .Initialize(); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs new file mode 100644 index 0000000000..9b0527ce69 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.AzureServiceBus; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Threading; + +namespace Volo.Abp.EventBus.Azure +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(IDistributedEventBus), typeof(AzureDistributedEventBus))] + public class AzureDistributedEventBus : EventBusBase, IDistributedEventBus, ISingletonDependency + { + private readonly AbpAzureEventBusOptions _options; + private readonly AbpDistributedEventBusOptions _distributedEventBusOptions; + private readonly IAzureServiceBusMessageConsumerFactory _messageConsumerFactory; + private readonly IPublisherPool _publisherPool; + private readonly IAzureServiceBusSerializer _serializer; + private readonly ConcurrentDictionary> _handlerFactories; + private readonly ConcurrentDictionary _eventTypes; + private IAzureServiceBusMessageConsumer _consumer; + + public AzureDistributedEventBus( + IServiceScopeFactory serviceScopeFactory, + ICurrentTenant currentTenant, + IOptions abpAzureEventBusOptions, + IOptions abpDistributedEventBusOptions, + IAzureServiceBusSerializer serializer, + IAzureServiceBusMessageConsumerFactory messageConsumerFactory, + IPublisherPool publisherPool) + : base(serviceScopeFactory, currentTenant) + { + _options = abpAzureEventBusOptions.Value; + _distributedEventBusOptions = abpDistributedEventBusOptions.Value; + _serializer = serializer; + _messageConsumerFactory = messageConsumerFactory; + _publisherPool = publisherPool; + _handlerFactories = new ConcurrentDictionary>(); + _eventTypes = new ConcurrentDictionary(); + } + + public void Initialize() + { + _consumer = _messageConsumerFactory.CreateMessageConsumer( + _options.TopicName, + _options.SubscriberName, + _options.ConnectionName); + + _consumer.OnMessageReceived(ProcessEventAsync); + SubscribeHandlers(_distributedEventBusOptions.Handlers); + } + + private async Task ProcessEventAsync(ServiceBusReceivedMessage message) + { + var eventName = message.Subject; + var eventType = _eventTypes.GetOrDefault(eventName); + if (eventType == null) + { + return; + } + + var eventData = _serializer.Deserialize(message.Body, eventType); + + await TriggerHandlersAsync(eventType, eventData); + } + + public IDisposable Subscribe(IDistributedEventHandler handler) where TEvent : class + { + return Subscribe(typeof(TEvent), handler); + } + + public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) + { + var handlerFactories = GetOrCreateHandlerFactories(eventType); + + if (factory.IsInFactories(handlerFactories)) + { + return NullDisposable.Instance; + } + + handlerFactories.Add(factory); + + return new EventHandlerFactoryUnregistrar(this, eventType, factory); + } + + public override void Unsubscribe(Func action) + { + Check.NotNull(action, nameof(action)); + + GetOrCreateHandlerFactories(typeof(TEvent)) + .Locking(factories => + { + factories.RemoveAll( + factory => + { + var singleInstanceFactory = factory as SingleInstanceHandlerFactory; + if (singleInstanceFactory == null) + { + return false; + } + + var actionHandler = singleInstanceFactory.HandlerInstance as ActionEventHandler; + if (actionHandler == null) + { + return false; + } + + return actionHandler.Action == action; + }); + }); + } + + public override void Unsubscribe(Type eventType, IEventHandler handler) + { + GetOrCreateHandlerFactories(eventType) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory handlerFactory && + handlerFactory.HandlerInstance == handler + ); + }); + } + + public override void Unsubscribe(Type eventType, IEventHandlerFactory factory) + { + GetOrCreateHandlerFactories(eventType) + .Locking(factories => factories.Remove(factory)); + } + + public override void UnsubscribeAll(Type eventType) + { + GetOrCreateHandlerFactories(eventType) + .Locking(factories => factories.Clear()); + } + + public override async Task PublishAsync(Type eventType, object eventData) + { + var eventName = EventNameAttribute.GetNameOrDefault(eventType); + var body = _serializer.Serialize(eventData); + + var message = new ServiceBusMessage(body) + { + Subject = eventName + }; + + var publisher = await _publisherPool.GetAsync( + _options.TopicName, + _options.ConnectionName); + + await publisher.SendMessageAsync(message); + } + + protected override IEnumerable GetHandlerFactories(Type eventType) + { + return _handlerFactories + .Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key)) + .Select(handlerFactory => + new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)) + .ToArray(); + } + + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) + { + return handlerEventType == targetEventType || handlerEventType.IsAssignableFrom(targetEventType); + } + + private List GetOrCreateHandlerFactories(Type eventType) + { + return _handlerFactories.GetOrAdd( + eventType, + type => + { + var eventName = EventNameAttribute.GetNameOrDefault(type); + _eventTypes[eventName] = type; + return new List(); + } + ); + } + } +} \ No newline at end of file From e6a80c0380c0b54f8a7cac11c6196a6d096ed2e6 Mon Sep 17 00:00:00 2001 From: Gideon de Swardt Date: Sat, 3 Apr 2021 23:19:24 +0100 Subject: [PATCH 03/31] Add documentation Added documentation on how to configure and setup the Azure Service Bus as the Distributed Event Bus. --- ...Distributed-Event-Bus-Azure-Integration.md | 133 ++++++++++++++++++ docs/en/Distributed-Event-Bus.md | 1 + docs/en/docs-nav.json | 4 + 3 files changed, 138 insertions(+) create mode 100644 docs/en/Distributed-Event-Bus-Azure-Integration.md diff --git a/docs/en/Distributed-Event-Bus-Azure-Integration.md b/docs/en/Distributed-Event-Bus-Azure-Integration.md new file mode 100644 index 0000000000..ca66cbca8a --- /dev/null +++ b/docs/en/Distributed-Event-Bus-Azure-Integration.md @@ -0,0 +1,133 @@ +# Distributed Event Bus Azure Integration + +> This document explains **how to configure the [Azure Service Bus](https://azure.microsoft.com/en-us/services/service-bus/)** as the distributed event bus provider. See the [distributed event bus document](Distributed-Event-Bus.md) to learn how to use the distributed event bus system + +## Installation + +Use the ABP CLI to add [Volo.Abp.EventBus.Azure](https://www.nuget.org/packages/Volo.Abp.EventBus.Azure) NuGet package to your project: + +* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. +* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.EventBus.Azure` package. +* Run `abp add-package Volo.Abp.EventBus.Azure` command. + +If you want to do it manually, install the [Volo.Abp.EventBus.Azure](https://www.nuget.org/packages/Volo.Abp.EventBus.Azure) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusAzureModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. + +## Configuration + +You can configure using the standard [configuration system](Configuration.md), like using the `appsettings.json` file, or using the [options](Options.md) classes. + +### `appsettings.json` file configuration + +This is the simplest way to configure the Azure Service Bus settings. It is also very strong since you can use any other configuration source (like environment variables) that is [supported by the AspNet Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/). + +**Example: The minimal configuration to connect to Azure Service Bus Namespace with default configurations** + +````json +{ + "Azure": { + "ServiceBus": { + "Connections": { + "Default": { + "ConnectionString": "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName={{Policy Name}};SharedAccessKey={};EntityPath=marketing-consent" + } + } + }, + "EventBus": { + "ConnectionName": "Default", + "SubscriberName": "MySubscriberName", + "TopicName": "MyTopicName" + } + } +} +```` + +* `MySubscriberName` is the name of this subscription, which is used as the **Subscriber** on the Azure Service Bus. +* `MyTopicName` is the **topic name**. + +See [the Azure Service Bus document](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-queues-topics-subscriptions) to understand these options better. + +#### Connections + +If you need to connect to another Azure Service Bus Namespace the Default, you need to configure the connection properties. + +**Example: Declare two connections and use one of them for the event bus** + +````json +{ + "Azure": { + "ServiceBus": { + "Connections": { + "Default": { + "ConnectionString": "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey={{SharedAccessKey}}" + }, + "SecondConnection": { + "ConnectionString": "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName={{Policy Name}};SharedAccessKey={{SharedAccessKey}}" + } + } + }, + "EventBus": { + "ConnectionName": "SecondConnection", + "SubscriberName": "MySubscriberName", + "TopicName": "MyTopicName" + } + } +} +```` + +This allows you to use multiple Azure Service Bus namespaces in your application, but select one of them for the event bus. + +You can use any of the [ServiceBusAdministrationClientOptions](https://docs.microsoft.com/en-us/dotnet/api/azure.messaging.servicebus.administration.servicebusadministrationclientoptions?view=azure-dotnet), [ServiceBusClientOptions](https://docs.microsoft.com/en-us/dotnet/api/azure.messaging.servicebus.servicebusclientoptions?view=azure-dotnet), [ServiceBusProcessorOptions](https://docs.microsoft.com/en-us/dotnet/api/azure.messaging.servicebus.servicebusprocessoroptions?view=azure-dotnet) properties for the connection. + +**Example: Specify the Admin, Client and Processor options** + +````json +{ + "Azure": { + "ServiceBus": { + "Connections": { + "Default": { + "ConnectionString": "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName={{Policy Name}};SharedAccessKey={};EntityPath=marketing-consent", + "Admin": { + "Retry": { + "MaxRetries": 3 + } + }, + "Client": { + "RetryOptions": { + "MaxRetries": 1 + } + }, + "Processor": { + "AutoCompleteMessages": true, + "ReceiveMode": "ReceiveAndDelete" + } + } + } + }, + "EventBus": { + "ConnectionName": "Default", + "SubscriberName": "MySubscriberName", + "TopicName": "MyTopicName" + } + } +} +```` + +### The Options Classes + +`AbpAzureServiceBusOptions` and `AbpAzureEventBusOptions` classes can be used to configure the connection strings and event bus options for Azure Service Bus. + +You can configure this options inside the `ConfigureServices` of your [module](Module-Development-Basics.md). + +**Example: Configure the connection** + +````csharp +Configure(options => +{ + options.Connections.Default.ConnectionString = "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName={{Policy Name}};SharedAccessKey={}"; + options.Connections.Default.Admin.Retry.MaxRetries = 3; + options.Connections.Default.Client.RetryOptions.MaxRetries = 1; +}); +```` + +Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file. diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index 02216c7ac8..70d5fbe391 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/docs/en/Distributed-Event-Bus.md @@ -7,6 +7,7 @@ Distributed Event bus system allows to **publish** and **subscribe** to events t Distributed event bus system provides an **abstraction** that can be implemented by any vendor/provider. There are two providers implemented out of the box: * `LocalDistributedEventBus` is the default implementation that implements the distributed event bus to work as in-process. Yes! The **default implementation works just like the [local event bus](Local-Event-Bus.md)**, if you don't configure a real distributed provider. +* `AzureDistributedEventBus` implements the distributed event bus with the [Azure Service Bus](https://azure.microsoft.com/en-us/services/service-bus/). See the [Azure Service Bus integration document](Distributed-Event-Bus-Azure-Integration.md) to learn how to configure it. * `RabbitMqDistributedEventBus` implements the distributed event bus with the [RabbitMQ](https://www.rabbitmq.com/). See the [RabbitMQ integration document](Distributed-Event-Bus-RabbitMQ-Integration.md) to learn how to configure it. * `KafkaDistributedEventBus` implements the distributed event bus with the [Kafka](https://kafka.apache.org/). See the [Kafka integration document](Distributed-Event-Bus-Kafka-Integration.md) to learn how to configure it. * `RebusDistributedEventBus` implements the distributed event bus with the [Rebus](http://mookid.dk/category/rebus/). See the [Rebus integration document](Distributed-Event-Bus-Rebus-Integration.md) to learn how to configure it. diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index efca73a484..77c1476735 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -296,6 +296,10 @@ "text": "Distributed Event Bus", "path": "Distributed-Event-Bus.md", "items": [ + { + "text": "Azure Service Bus Integration", + "path": "Distributed-Event-Bus-Azure-Integration.md" + }, { "text": "RabbitMQ Integration", "path": "Distributed-Event-Bus-RabbitMQ-Integration.md" From 159bc23f2d45fd6526bbdd55d913ccf325555260 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 13 Jul 2021 13:29:59 +0800 Subject: [PATCH 04/31] Update AzureDistributedEventBus.cs --- .../Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index 9b0527ce69..456284d1e8 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -26,16 +26,16 @@ namespace Volo.Abp.EventBus.Azure private readonly ConcurrentDictionary> _handlerFactories; private readonly ConcurrentDictionary _eventTypes; private IAzureServiceBusMessageConsumer _consumer; - public AzureDistributedEventBus( IServiceScopeFactory serviceScopeFactory, ICurrentTenant currentTenant, + IEventErrorHandler errorHandler, IOptions abpAzureEventBusOptions, IOptions abpDistributedEventBusOptions, IAzureServiceBusSerializer serializer, IAzureServiceBusMessageConsumerFactory messageConsumerFactory, IPublisherPool publisherPool) - : base(serviceScopeFactory, currentTenant) + : base(serviceScopeFactory, currentTenant, errorHandler) { _options = abpAzureEventBusOptions.Value; _distributedEventBusOptions = abpDistributedEventBusOptions.Value; @@ -186,4 +186,4 @@ namespace Volo.Abp.EventBus.Azure ); } } -} \ No newline at end of file +} From ae7c867679e0ca6f1edf8e42dab7fa3a02577f88 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 13 Jul 2021 14:19:25 +0800 Subject: [PATCH 05/31] Add packages to common.ps1 --- .../Volo.Abp.AzureServiceBus.csproj | 10 ++++++++-- .../Volo.Abp.EventBus.Azure.csproj | 11 ++++++++++- nupkg/common.ps1 | 2 ++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo.Abp.AzureServiceBus.csproj b/framework/src/Volo.Abp.AzureServiceBus/Volo.Abp.AzureServiceBus.csproj index 77ccf8f772..51cab2b9bf 100644 --- a/framework/src/Volo.Abp.AzureServiceBus/Volo.Abp.AzureServiceBus.csproj +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo.Abp.AzureServiceBus.csproj @@ -2,10 +2,16 @@ - + netstandard2.0 - latest + Volo.Abp.AzureServiceBus + Volo.Abp.AzureServiceBus + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo.Abp.EventBus.Azure.csproj b/framework/src/Volo.Abp.EventBus.Azure/Volo.Abp.EventBus.Azure.csproj index c913b79bfc..2ffd853cdc 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo.Abp.EventBus.Azure.csproj +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo.Abp.EventBus.Azure.csproj @@ -1,8 +1,17 @@ + + + netstandard2.0 - latest + Volo.Abp.EventBus.Azure + Volo.Abp.EventBus.Azure + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + diff --git a/nupkg/common.ps1 b/nupkg/common.ps1 index b71ebccfd0..1b07380f42 100644 --- a/nupkg/common.ps1 +++ b/nupkg/common.ps1 @@ -64,6 +64,7 @@ $projects = ( "framework/src/Volo.Abp.Autofac", "framework/src/Volo.Abp.Autofac.WebAssembly", "framework/src/Volo.Abp.AutoMapper", + "framework/src/Volo.Abp.AzureServiceBus", "framework/src/Volo.Abp.BackgroundJobs.Abstractions", "framework/src/Volo.Abp.BackgroundJobs", "framework/src/Volo.Abp.BackgroundJobs.HangFire", @@ -100,6 +101,7 @@ $projects = ( "framework/src/Volo.Abp.EntityFrameworkCore.SqlServer", "framework/src/Volo.Abp.EventBus.Abstractions", "framework/src/Volo.Abp.EventBus", + "framework/src/Volo.Abp.EventBus.Azure", "framework/src/Volo.Abp.EventBus.RabbitMQ", "framework/src/Volo.Abp.EventBus.Kafka", "framework/src/Volo.Abp.EventBus.Rebus", From 39c99f4bfd21aaae125d3618a521015b3d77e4cf Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 9 Aug 2021 10:26:11 +0200 Subject: [PATCH 06/31] Create da.json --- .../Account/Localization/Resources/da.json | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/da.json diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/da.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/da.json new file mode 100644 index 0000000000..38d81e8516 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/da.json @@ -0,0 +1,67 @@ +{ + "culture": "da", + "texts": { + "UserName": "Brugernavn", + "EmailAddress": "Email", + "UserNameOrEmailAddress": "Brugernavn eller email", + "Password": "Kodeord", + "RememberMe": "Husk mig", + "UseAnotherServiceToLogin": "Brug en anden service til at logge ind", + "UserLockedOutMessage": "Brugerkontoen er blevet låst grundet forkerte login forsøg. Vent venligst og prøv igen.", + "InvalidUserNameOrPassword": "Forkert brugernavn eller kodeord!", + "LoginIsNotAllowed": "Du kan ikke logge ind! Du skal bekræfte den angivne email/telefonnummer.", + "SelfRegistrationDisabledMessage": "Selvregistrering er deaktiveret for denne applikation. Kontakt venligst applikationens administrator for at registrere en ny bruger.", + "LocalLoginDisabledMessage": "Lokalt login er deaktiveret for denne appliaktion.", + "Login": "Login", + "Cancel": "Annullér", + "Register": "Registrér", + "AreYouANewUser": "Er du ny bruger?", + "AlreadyRegistered": "Allerede registreret?", + "InvalidLoginRequest": "Forkert login forespørgsel", + "ThereAreNoLoginSchemesConfiguredForThisClient": "Der er ingen konfigurerede login skemaer for denne klient.", + "LogInUsingYourProviderAccount": "Login med din {0} konto", + "DisplayName:CurrentPassword": "Nuværende kodeord", + "DisplayName:NewPassword": "Nyt kodeord", + "DisplayName:NewPasswordConfirm": "Bekræft nyt kodeord", + "PasswordChangedMessage": "Dit kodeord er blevet ændret.", + "DisplayName:UserName": "Brugernavn", + "DisplayName:Email": "Email", + "DisplayName:Name": "Fornavn", + "DisplayName:Surname": "Efternavn", + "DisplayName:Password": "Kodeord", + "DisplayName:EmailAddress": "Email", + "DisplayName:PhoneNumber": "Telefonnummer", + "PersonalSettings": "Personlige indstillinger", + "PersonalSettingsSaved": "Personlige indstillinger gemt", + "PasswordChanged": "Kodeord gemt", + "NewPasswordConfirmFailed": "Bekræft venligst det nye kodeord.", + "Manage": "Administrer", + "MyAccount": "Min konto", + "DisplayName:Abp.Account.IsSelfRegistrationEnabled": "Er registrering aktiveret", + "Description:Abp.Account.IsSelfRegistrationEnabled": "Om en bruger kan registrere kontoen selv.", + "DisplayName:Abp.Account.EnableLocalLogin": "Godkende med en lokal konto", + "Description:Abp.Account.EnableLocalLogin": "Indikerer om serveren vil tillade brugere at logge ind med lokal konto.", + "LoggedOutTitle": "Logget ud", + "LoggedOutText": "Du er blevet logget ud og vil blive videresendt snarest.", + "ReturnToText": "Klik her for at returnere til applikationen", + "OrLoginWith": "Eller login med:", + "ForgotPassword": "Glemt kodeord?", + "SendPasswordResetLink_Information": "Et link til at nulstille dit kodeord vil blive sendt til din email. Hvis du ikke modtager en email indenfor et par minutter, venligst prøv igen.", + "PasswordResetMailSentMessage": "Gendannelse af konto er sendt til din email. Hvis du ikke har modtaget emailen indenfor 15 minutter, kig i din spam folder. Hvis du finder den dér, venligst markér den som -Ikke Spam-. ", + "ResetPassword": "Nulstil Kodeord", + "ConfirmPassword": "Bekræft (gentag) kodeordet", + "ResetPassword_Information": "Venligst indtast dit nye kodeord.", + "YourPasswordIsSuccessfullyReset": "Dit kodeord blev ændret.", + "GoToTheApplication": "Gå til applikationen", + "BackToLogin": "Tilbage til login", + "ProfileTab:Password": "Skift kodeord", + "ProfileTab:PersonalInfo": "Personlig info", + "ReturnToApplication": "Gå tilbage til applikationen", + "Volo.Account:InvalidEmailAddress": "Kan ikke finde den angivne email: {0}", + "PasswordReset": "Nulstil kodeord", + "PasswordResetInfoInEmail": "Vi har modtaget en forespørgsel for gendannelse af konto! Hvis du har lavet denne forespørgsel, klik på det efterfølgende link for at nulstille dit kodeord.", + "ResetMyPassword": "Nulstil mit kodeord", + "AccessDenied": "Adgang nægtet!", + "AccessDeniedMessage": "Du har ikke adgang til denne ressource." + } +} From 0b62e07236f167b51c588294dc1b91e13ffa412b Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 4 Oct 2021 11:01:47 +0800 Subject: [PATCH 07/31] Inherit `DistributedEventBusBase`. --- .../IAzureServiceBusSerializer.cs | 6 +- .../Utf8JsonAzureServiceBusSerializer.cs | 11 ++- .../Azure/AzureDistributedEventBus.cs | 67 +++++++++++++++---- 3 files changed, 67 insertions(+), 17 deletions(-) diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusSerializer.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusSerializer.cs index 04f56d4470..61fc9e5ca0 100644 --- a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusSerializer.cs +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/IAzureServiceBusSerializer.cs @@ -6,6 +6,8 @@ namespace Volo.Abp.AzureServiceBus { byte[] Serialize(object obj); - object Deserialize(BinaryData value, Type type); + object Deserialize(byte[] value, Type type); + + T Deserialize(byte[] value); } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/Utf8JsonAzureServiceBusSerializer.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/Utf8JsonAzureServiceBusSerializer.cs index 0a87e22934..d8d6fb82a2 100644 --- a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/Utf8JsonAzureServiceBusSerializer.cs +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/Utf8JsonAzureServiceBusSerializer.cs @@ -19,9 +19,14 @@ namespace Volo.Abp.AzureServiceBus return Encoding.UTF8.GetBytes(_jsonSerializer.Serialize(obj)); } - public object Deserialize(BinaryData value, Type type) + public object Deserialize(byte[] value, Type type) { - return _jsonSerializer.Deserialize(type, Encoding.UTF8.GetString(value.ToArray())); + return _jsonSerializer.Deserialize(type, Encoding.UTF8.GetString(value)); + } + + public T Deserialize(byte[] value) + { + return _jsonSerializer.Deserialize(Encoding.UTF8.GetString(value)); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index 456284d1e8..f4fbca13b4 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -9,36 +9,47 @@ using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.EventBus.Distributed; using Volo.Abp.AzureServiceBus; +using Volo.Abp.Guids; using Volo.Abp.MultiTenancy; using Volo.Abp.Threading; +using Volo.Abp.Timing; +using Volo.Abp.Uow; namespace Volo.Abp.EventBus.Azure { [Dependency(ReplaceServices = true)] [ExposeServices(typeof(IDistributedEventBus), typeof(AzureDistributedEventBus))] - public class AzureDistributedEventBus : EventBusBase, IDistributedEventBus, ISingletonDependency + public class AzureDistributedEventBus : DistributedEventBusBase, IDistributedEventBus, ISingletonDependency { private readonly AbpAzureEventBusOptions _options; - private readonly AbpDistributedEventBusOptions _distributedEventBusOptions; private readonly IAzureServiceBusMessageConsumerFactory _messageConsumerFactory; private readonly IPublisherPool _publisherPool; private readonly IAzureServiceBusSerializer _serializer; private readonly ConcurrentDictionary> _handlerFactories; private readonly ConcurrentDictionary _eventTypes; private IAzureServiceBusMessageConsumer _consumer; + public AzureDistributedEventBus( IServiceScopeFactory serviceScopeFactory, ICurrentTenant currentTenant, + IUnitOfWorkManager unitOfWorkManager, IEventErrorHandler errorHandler, - IOptions abpAzureEventBusOptions, IOptions abpDistributedEventBusOptions, + IGuidGenerator guidGenerator, + IClock clock, + IOptions abpAzureEventBusOptions, IAzureServiceBusSerializer serializer, IAzureServiceBusMessageConsumerFactory messageConsumerFactory, IPublisherPool publisherPool) - : base(serviceScopeFactory, currentTenant, errorHandler) + : base(serviceScopeFactory, + currentTenant, + unitOfWorkManager, + errorHandler, + abpDistributedEventBusOptions, + guidGenerator, + clock) { _options = abpAzureEventBusOptions.Value; - _distributedEventBusOptions = abpDistributedEventBusOptions.Value; _serializer = serializer; _messageConsumerFactory = messageConsumerFactory; _publisherPool = publisherPool; @@ -54,7 +65,7 @@ namespace Volo.Abp.EventBus.Azure _options.ConnectionName); _consumer.OnMessageReceived(ProcessEventAsync); - SubscribeHandlers(_distributedEventBusOptions.Handlers); + SubscribeHandlers(AbpDistributedEventBusOptions.Handlers); } private async Task ProcessEventAsync(ServiceBusReceivedMessage message) @@ -66,14 +77,36 @@ namespace Volo.Abp.EventBus.Azure return; } - var eventData = _serializer.Deserialize(message.Body, eventType); + var eventData = _serializer.Deserialize(message.Body.ToArray(), eventType); await TriggerHandlersAsync(eventType, eventData); } - public IDisposable Subscribe(IDistributedEventHandler handler) where TEvent : class + public override async Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) + { + await PublishAsync(outgoingEvent.EventName, outgoingEvent.EventData); + } + + public override async Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) + { + var eventType = _eventTypes.GetOrDefault(incomingEvent.EventName); + if (eventType == null) + { + return; + } + + var eventData = _serializer.Deserialize(incomingEvent.EventData, eventType); + var exceptions = new List(); + await TriggerHandlersAsync(eventType, eventData, exceptions, inboxConfig); + if (exceptions.Any()) + { + ThrowOriginalExceptions(eventType, exceptions); + } + } + + protected override byte[] Serialize(object eventData) { - return Subscribe(typeof(TEvent), handler); + return _serializer.Serialize(eventData); } public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) @@ -142,16 +175,25 @@ namespace Volo.Abp.EventBus.Azure .Locking(factories => factories.Clear()); } - public override async Task PublishAsync(Type eventType, object eventData) + protected override async Task PublishToEventBusAsync(Type eventType, object eventData) + { + await PublishAsync(eventType, eventData); + } + + protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord) + { + unitOfWork.AddOrReplaceDistributedEvent(eventRecord); + } + + protected virtual async Task PublishAsync(string eventName, object eventData) { - var eventName = EventNameAttribute.GetNameOrDefault(eventType); var body = _serializer.Serialize(eventData); var message = new ServiceBusMessage(body) { Subject = eventName }; - + var publisher = await _publisherPool.GetAsync( _options.TopicName, _options.ConnectionName); @@ -185,5 +227,6 @@ namespace Volo.Abp.EventBus.Azure } ); } + } } From 43c4d80b6dcd0531f58129d2a7750d8a1ba244b2 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 4 Oct 2021 22:59:28 +0800 Subject: [PATCH 08/31] Add `IObjectToQueryString`. --- .../Volo/Abp/Http/IObjectToQueryString.cs | 9 +++ .../Volo/Abp/Reflection/ReflectionHelper.cs | 7 +- .../Client/ClientProxying/ClientProxyBase.cs | 4 +- .../ClientProxying/ClientProxyUrlBuilder.cs | 81 +++++++++++++++---- .../PersonAppServiceClientProxy_Tests.cs | 27 +++++++ .../Dto/GetParamsFromQueryInput.cs | 44 ++++++++++ .../TestApp/Application/IPeopleAppService.cs | 1 + .../TestApp/Application/PeopleAppService.cs | 8 ++ 8 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToQueryString.cs create mode 100644 framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsFromQueryInput.cs diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToQueryString.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToQueryString.cs new file mode 100644 index 0000000000..19696e546c --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToQueryString.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Http +{ + public interface IObjectToQueryString + { + Task ConvertAsync(TValue value); + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs index 132c198aa9..e0b999ac3d 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs @@ -134,7 +134,7 @@ namespace Volo.Abp.Reflection var currentType = objectType; var objectPath = currentType.FullName; var absolutePropertyPath = propertyPath; - if (objectPath != null && absolutePropertyPath.StartsWith(objectPath)) + if (objectPath != null && absolutePropertyPath.StartsWith(objectPath)) { absolutePropertyPath = absolutePropertyPath.Replace(objectPath + ".", ""); } @@ -144,7 +144,10 @@ namespace Volo.Abp.Reflection var property = currentType.GetProperty(propertyName); if (property != null) { - value = property.GetValue(value, null); + if (value != null) + { + value = property.GetValue(value, null); + } currentType = property.PropertyType; } else diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs index 5254b10238..68ebc87543 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs @@ -156,9 +156,9 @@ namespace Volo.Abp.Http.Client.ClientProxying return new ApiVersionInfo(versionParam?.BindingSourceId, apiVersion); } - protected virtual Task GetUrlWithParametersAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion) + protected virtual async Task GetUrlWithParametersAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion) { - return Task.FromResult(ClientProxyUrlBuilder.GenerateUrlWithParameters(requestContext.Action, requestContext.Arguments, apiVersion)); + return await ClientProxyUrlBuilder.GenerateUrlWithParametersAsync(requestContext.Action, requestContext.Arguments, apiVersion); } protected virtual Task GetHttpContentAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion) diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs index e6a9b8ff71..1eb07e88cf 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs @@ -3,8 +3,11 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Reflection; using System.Text; +using System.Threading.Tasks; using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Client.Proxying; using Volo.Abp.Http.Modeling; @@ -15,7 +18,23 @@ namespace Volo.Abp.Http.Client.ClientProxying { public class ClientProxyUrlBuilder : ITransientDependency { - public string GenerateUrlWithParameters(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) + protected static MethodInfo CallObjectToQueryStringAsyncMethod { get; } + + static ClientProxyUrlBuilder() + { + CallObjectToQueryStringAsyncMethod = typeof(ClientProxyUrlBuilder) + .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + .First(m => m.Name == nameof(ObjectToQueryStringAsync) && m.IsGenericMethodDefinition); + } + + protected IServiceScopeFactory ServiceScopeFactory { get; } + + public ClientProxyUrlBuilder(IServiceScopeFactory serviceScopeFactory) + { + ServiceScopeFactory = serviceScopeFactory; + } + + public async Task GenerateUrlWithParametersAsync(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) { // The ASP.NET Core route value provider and query string value provider: // Treat values as invariant culture. @@ -24,14 +43,14 @@ namespace Volo.Abp.Http.Client.ClientProxying { var urlBuilder = new StringBuilder(action.Url); - ReplacePathVariables(urlBuilder, action.Parameters, methodArguments, apiVersion); - AddQueryStringParameters(urlBuilder, action.Parameters, methodArguments, apiVersion); + await ReplacePathVariablesAsync(urlBuilder, action.Parameters, methodArguments, apiVersion); + await AddQueryStringParametersAsync(urlBuilder, action.Parameters, methodArguments, apiVersion); return urlBuilder.ToString(); } } - protected virtual void ReplacePathVariables(StringBuilder urlBuilder, IList actionParameters, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) + protected virtual Task ReplacePathVariablesAsync(StringBuilder urlBuilder, IList actionParameters, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) { var pathParameters = actionParameters .Where(p => p.BindingSourceId == ParameterBindingSources.Path) @@ -39,7 +58,7 @@ namespace Volo.Abp.Http.Client.ClientProxying if (!pathParameters.Any()) { - return; + return Task.CompletedTask; } if (pathParameters.Any(p => p.Name == "apiVersion")) @@ -71,9 +90,11 @@ namespace Volo.Abp.Http.Client.ClientProxying urlBuilder = urlBuilder.Replace($"{{{pathParameter.Name}}}", value.ToString()); } } + + return Task.CompletedTask; } - protected virtual void AddQueryStringParameters(StringBuilder urlBuilder, IList actionParameters, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) + protected virtual async Task AddQueryStringParametersAsync(StringBuilder urlBuilder, IList actionParameters, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) { var queryStringParameters = actionParameters .Where(p => p.BindingSourceId.IsIn(ParameterBindingSources.ModelBinding, ParameterBindingSources.Query)) @@ -89,7 +110,26 @@ namespace Volo.Abp.Http.Client.ClientProxying continue; } - if (AddQueryStringParameter(urlBuilder, isFirstParam, queryStringParameter.Name, value)) + using (var scope = ServiceScopeFactory.CreateScope()) + { + var objectToQuery = scope.ServiceProvider.GetService(typeof(IObjectToQueryString<>).MakeGenericType(value.GetType())); + if (objectToQuery != null) + { + var queryString = await (Task)CallObjectToQueryStringAsyncMethod + .MakeGenericMethod(value.GetType()) + .Invoke(this, new object[] { value }); + + if (!queryString.IsNullOrWhiteSpace()) + { + urlBuilder.Append(isFirstParam ? "?" : "&"); + urlBuilder.Append(queryString); + isFirstParam = false; + continue; + } + } + } + + if (await AddQueryStringParameterAsync(urlBuilder, isFirstParam, queryStringParameter.Name, value)) { isFirstParam = false; } @@ -97,11 +137,24 @@ namespace Volo.Abp.Http.Client.ClientProxying if (apiVersion.ShouldSendInQueryString()) { - AddQueryStringParameter(urlBuilder, isFirstParam, "api-version", apiVersion.Version); //TODO: Constant! + await AddQueryStringParameterAsync(urlBuilder, isFirstParam, "api-version", apiVersion.Version); //TODO: Constant! + } + } + + protected virtual async Task ObjectToQueryStringAsync(T value) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var objectToQueryString = scope.ServiceProvider.GetService>(); + if (objectToQueryString != null) + { + return await objectToQueryString.ConvertAsync(value); + } } + return null; } - protected virtual bool AddQueryStringParameter( + protected virtual async Task AddQueryStringParameterAsync( StringBuilder urlBuilder, bool isFirstParam, string name, @@ -116,7 +169,7 @@ namespace Volo.Abp.Http.Client.ClientProxying { urlBuilder.Append(isFirstParam ? "?" : "&"); } - urlBuilder.Append(name + $"[{index++}]=" + System.Net.WebUtility.UrlEncode(ConvertValueToString(item)) + "&"); + urlBuilder.Append(name + $"[{index++}]=" + System.Net.WebUtility.UrlEncode(await ConvertValueToStringAsync(item)) + "&"); } if (index > 0) @@ -130,18 +183,18 @@ namespace Volo.Abp.Http.Client.ClientProxying } urlBuilder.Append(isFirstParam ? "?" : "&"); - urlBuilder.Append(name + "=" + System.Net.WebUtility.UrlEncode(ConvertValueToString(value))); + urlBuilder.Append(name + "=" + System.Net.WebUtility.UrlEncode(await ConvertValueToStringAsync(value))); return true; } - protected virtual string ConvertValueToString([CanBeNull] object value) + protected virtual Task ConvertValueToStringAsync([CanBeNull] object value) { if (value is DateTime dateTimeValue) { - return dateTimeValue.ToUniversalTime().ToString("O"); + return Task.FromResult(dateTimeValue.ToUniversalTime().ToString("O")); } - return value?.ToString(); + return Task.FromResult(value?.ToString()); } } } diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs index b040c3e3d4..98225cf424 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs @@ -276,5 +276,32 @@ namespace Volo.Abp.Http.DynamicProxying }); result.ShouldBe("123.rtf:File1:application/rtf:1-1.rtf123.rtf:File2:application/rtf2:1-2.rtf789.rtf:File3:application/rtf3:i-789.rtf"); } + + [Fact] + public async Task GetParamsFromQueryAsync() + { + var result = await _peopleAppService.GetParamsFromQueryAsync(new GetParamsFromQueryInput() + { + NameValues = new List() + { + new GetParamsFromQueryInputNameValue() + { + Name = "name1", + Value = "value1" + }, + new GetParamsFromQueryInputNameValue() + { + Name = "name2", + Value = "value2" + } + }, + NameValue = new GetParamsFromQueryInputNameValue() + { + Name = "name3", + Value = "value3" + } + }); + result.ShouldBe("name1-value1:name2-value2:name3-value3"); + } } } diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsFromQueryInput.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsFromQueryInput.cs new file mode 100644 index 0000000000..764a6390f0 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsFromQueryInput.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http; + +namespace Volo.Abp.TestApp.Application.Dto +{ + public class GetParamsFromQueryInput + { + public List NameValues { get; set; } + + public GetParamsFromQueryInputNameValue NameValue { get; set; } + } + + public class GetParamsFromQueryInputNameValue + { + public string Name { get; set; } + + public string Value { get; set; } + } + + [ExposeServices(typeof(IObjectToQueryString>))] + public class TestInputToQueryString : IObjectToQueryString>, ITransientDependency + { + public Task ConvertAsync(List values) + { + if (values.IsNullOrEmpty()) + { + return null; + } + + var sb = new StringBuilder(); + + for (var i = 0; i < values.Count; i++) + { + sb.Append($"NameValues[{i}].Name={values[i].Name}&NameValues[{i}].Value={values[i].Value}&"); + } + + sb.Remove(sb.Length - 1, 1); + return Task.FromResult(sb.ToString()); + } + } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs index 844e7fd383..ec63c279f2 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs @@ -32,5 +32,6 @@ namespace Volo.Abp.TestApp.Application Task CreateMultipleFileAsync(CreateMultipleFileInput input); + Task GetParamsFromQueryAsync(GetParamsFromQueryInput input); } } diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs index a3d8764423..f211a9198f 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs @@ -125,5 +125,13 @@ namespace Volo.Abp.TestApp.Application return str; } + + public Task GetParamsFromQueryAsync(GetParamsFromQueryInput input) + { + return Task.FromResult(input.NameValues?.FirstOrDefault()?.Name + "-" + + input.NameValues?.FirstOrDefault()?.Value + ":" + + input.NameValues?.LastOrDefault()?.Name + "-" + input.NameValues?.LastOrDefault()?.Value + ":" + + input.NameValue?.Name + "-" + input.NameValue?.Value); + } } } From 8b1d93a7e500ecfc70e9d65ad276062cc0eac1ba Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 4 Oct 2021 23:01:35 +0800 Subject: [PATCH 09/31] Update ClientProxyUrlBuilder.cs --- .../ClientProxying/ClientProxyUrlBuilder.cs | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs index 1eb07e88cf..0d97e6d348 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs @@ -110,23 +110,16 @@ namespace Volo.Abp.Http.Client.ClientProxying continue; } - using (var scope = ServiceScopeFactory.CreateScope()) + var queryString = await (Task)CallObjectToQueryStringAsyncMethod + .MakeGenericMethod(value.GetType()) + .Invoke(this, new object[] { value }); + + if (!queryString.IsNullOrWhiteSpace()) { - var objectToQuery = scope.ServiceProvider.GetService(typeof(IObjectToQueryString<>).MakeGenericType(value.GetType())); - if (objectToQuery != null) - { - var queryString = await (Task)CallObjectToQueryStringAsyncMethod - .MakeGenericMethod(value.GetType()) - .Invoke(this, new object[] { value }); - - if (!queryString.IsNullOrWhiteSpace()) - { - urlBuilder.Append(isFirstParam ? "?" : "&"); - urlBuilder.Append(queryString); - isFirstParam = false; - continue; - } - } + urlBuilder.Append(isFirstParam ? "?" : "&"); + urlBuilder.Append(queryString); + isFirstParam = false; + continue; } if (await AddQueryStringParameterAsync(urlBuilder, isFirstParam, queryStringParameter.Name, value)) From fe32230aae3e039be95d5cdae940836b64947777 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 5 Oct 2021 14:40:21 +0800 Subject: [PATCH 10/31] Add `IObjectToFormData`. --- .../Volo/Abp/Http/IObjectToFormData.cs | 11 ++ .../Client/ClientProxying/ClientProxyBase.cs | 24 ++-- .../ClientProxyRequestPayloadBuilder.cs | 135 ++++++++++-------- .../ClientProxying/ClientProxyUrlBuilder.cs | 2 +- .../PersonAppServiceClientProxy_Tests.cs | 37 ++++- .../Volo.Abp.TestApp/Volo.Abp.TestApp.csproj | 2 +- .../Dto/GetParamsFromQueryInput.cs | 44 ------ .../TestApp/Application/Dto/GetParamsInput.cs | 66 +++++++++ .../TestApp/Application/IPeopleAppService.cs | 4 +- .../TestApp/Application/PeopleAppService.cs | 11 +- 10 files changed, 215 insertions(+), 121 deletions(-) create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToFormData.cs delete mode 100644 framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsFromQueryInput.cs create mode 100644 framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsInput.cs diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToFormData.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToFormData.cs new file mode 100644 index 0000000000..166b51239e --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToFormData.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Volo.Abp.Http +{ + public interface IObjectToFormData + { + Task>> ConvertAsync(TValue value); + } +} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs index 68ebc87543..9b47208141 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs @@ -28,15 +28,15 @@ namespace Volo.Abp.Http.Client.ClientProxying protected IClientProxyApiDescriptionFinder ClientProxyApiDescriptionFinder => LazyServiceProvider.LazyGetRequiredService(); protected ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetRequiredService(); protected ICorrelationIdProvider CorrelationIdProvider => LazyServiceProvider.LazyGetRequiredService(); - protected ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService(); - protected IOptions AbpCorrelationIdOptions => LazyServiceProvider.LazyGetRequiredService>(); - protected IProxyHttpClientFactory HttpClientFactory => LazyServiceProvider.LazyGetRequiredService(); - protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider => LazyServiceProvider.LazyGetRequiredService(); - protected IOptions ClientOptions => LazyServiceProvider.LazyGetRequiredService>(); - protected IJsonSerializer JsonSerializer => LazyServiceProvider.LazyGetRequiredService(); - protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator => LazyServiceProvider.LazyGetRequiredService(); - protected ClientProxyRequestPayloadBuilder ClientProxyRequestPayloadBuilder => LazyServiceProvider.LazyGetRequiredService(); - protected ClientProxyUrlBuilder ClientProxyUrlBuilder => LazyServiceProvider.LazyGetRequiredService(); + protected ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService(); + protected IOptions AbpCorrelationIdOptions => LazyServiceProvider.LazyGetRequiredService>(); + protected IProxyHttpClientFactory HttpClientFactory => LazyServiceProvider.LazyGetRequiredService(); + protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider => LazyServiceProvider.LazyGetRequiredService(); + protected IOptions ClientOptions => LazyServiceProvider.LazyGetRequiredService>(); + protected IJsonSerializer JsonSerializer => LazyServiceProvider.LazyGetRequiredService(); + protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator => LazyServiceProvider.LazyGetRequiredService(); + protected ClientProxyRequestPayloadBuilder ClientProxyRequestPayloadBuilder => LazyServiceProvider.LazyGetRequiredService(); + protected ClientProxyUrlBuilder ClientProxyUrlBuilder => LazyServiceProvider.LazyGetRequiredService(); protected virtual async Task RequestAsync(string methodName, ClientProxyRequestTypeValue arguments = null) { @@ -114,7 +114,7 @@ namespace Volo.Abp.Http.Client.ClientProxying var requestMessage = new HttpRequestMessage(requestContext.Action.GetHttpMethod(), url) { - Content = ClientProxyRequestPayloadBuilder.BuildContent(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion) + Content = await ClientProxyRequestPayloadBuilder.BuildContentAsync(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion) }; AddHeaders(requestContext.Arguments, requestContext.Action, requestMessage, apiVersion); @@ -161,9 +161,9 @@ namespace Volo.Abp.Http.Client.ClientProxying return await ClientProxyUrlBuilder.GenerateUrlWithParametersAsync(requestContext.Action, requestContext.Arguments, apiVersion); } - protected virtual Task GetHttpContentAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion) + protected virtual async Task GetHttpContentAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion) { - return Task.FromResult(ClientProxyRequestPayloadBuilder.BuildContent(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion)); + return await ClientProxyRequestPayloadBuilder.BuildContentAsync(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion); } protected virtual async Task FindBestApiVersionAsync(ClientProxyRequestContext requestContext) diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs index 3ba7026c8e..f76afae6a3 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs @@ -3,8 +3,11 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Reflection; using System.Text; +using System.Threading.Tasks; using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Content; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Client.Proxying; @@ -16,21 +19,37 @@ namespace Volo.Abp.Http.Client.ClientProxying { public class ClientProxyRequestPayloadBuilder : ITransientDependency { + protected static MethodInfo CallObjectToFormDataAsyncMethod { get; } + + static ClientProxyRequestPayloadBuilder() + { + CallObjectToFormDataAsyncMethod = typeof(ClientProxyRequestPayloadBuilder) + .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + .First(m => m.Name == nameof(ObjectToFormDataAsync) && m.IsGenericMethodDefinition); + } + + protected IServiceScopeFactory ServiceScopeFactory { get; } + + public ClientProxyRequestPayloadBuilder(IServiceScopeFactory serviceScopeFactory) + { + ServiceScopeFactory = serviceScopeFactory; + } + [CanBeNull] - public virtual HttpContent BuildContent(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion) + public virtual async Task BuildContentAsync(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion) { - var body = GenerateBody(action, methodArguments, jsonSerializer); + var body = await GenerateBodyAsync(action, methodArguments, jsonSerializer); if (body != null) { return body; } - body = GenerateFormPostData(action, methodArguments); + body = await GenerateFormPostDataAsync(action, methodArguments); return body; } - protected virtual HttpContent GenerateBody(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer) + protected virtual Task GenerateBodyAsync(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer) { var parameters = action .Parameters @@ -39,7 +58,7 @@ namespace Volo.Abp.Http.Client.ClientProxying if (parameters.Length <= 0) { - return null; + return Task.FromResult(null); } if (parameters.Length > 1) @@ -52,13 +71,13 @@ namespace Volo.Abp.Http.Client.ClientProxying var value = HttpActionParameterHelper.FindParameterValue(methodArguments, parameters[0]); if (value == null) { - return null; + return Task.FromResult(null); } - return new StringContent(jsonSerializer.Serialize(value), Encoding.UTF8, MimeTypes.Application.Json); + return Task.FromResult(new StringContent(jsonSerializer.Serialize(value), Encoding.UTF8, MimeTypes.Application.Json)); } - protected virtual HttpContent GenerateFormPostData(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments) + protected virtual async Task GenerateFormPostDataAsync(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments) { var parameters = action .Parameters @@ -70,71 +89,75 @@ namespace Volo.Abp.Http.Client.ClientProxying return null; } - if (parameters.Any(x => x.BindingSourceId == ParameterBindingSources.FormFile)) + var formData = new MultipartFormDataContent(); + + foreach (var parameter in parameters) { - var formData = new MultipartFormDataContent(); - foreach (var parameter in parameters) + var value = HttpActionParameterHelper.FindParameterValue(methodArguments, parameter); + if (value == null) + { + continue; + } + + var formDataContents = await (Task>>)CallObjectToFormDataAsyncMethod + .MakeGenericMethod(value.GetType()) + .Invoke(this, new object[] { value }); + + if (formDataContents != null) { - var value = HttpActionParameterHelper.FindParameterValue(methodArguments, parameter); - if (value == null) + foreach (var content in formDataContents) { - continue; + formData.Add(content.Value, content.Key); } - if (value is IRemoteStreamContent remoteStreamContent) + continue; + } + + if (value is IRemoteStreamContent remoteStreamContent) + { + var stream = remoteStreamContent.GetStream(); + var streamContent = new StreamContent(stream); + if (!remoteStreamContent.ContentType.IsNullOrWhiteSpace()) { - var stream = remoteStreamContent.GetStream(); - var streamContent = new StreamContent(stream); - if (!remoteStreamContent.ContentType.IsNullOrWhiteSpace()) - { - streamContent.Headers.ContentType = new MediaTypeHeaderValue(remoteStreamContent.ContentType); - } - streamContent.Headers.ContentLength = remoteStreamContent.ContentLength; - formData.Add(streamContent, parameter.Name, remoteStreamContent.FileName ?? parameter.Name); + streamContent.Headers.ContentType = new MediaTypeHeaderValue(remoteStreamContent.ContentType); } - else if (value is IEnumerable remoteStreamContents) + streamContent.Headers.ContentLength = remoteStreamContent.ContentLength; + formData.Add(streamContent, parameter.Name, remoteStreamContent.FileName ?? parameter.Name); + } + else if (value is IEnumerable remoteStreamContents) + { + foreach (var content in remoteStreamContents) { - foreach (var content in remoteStreamContents) + var stream = content.GetStream(); + var streamContent = new StreamContent(stream); + if (!content.ContentType.IsNullOrWhiteSpace()) { - var stream = content.GetStream(); - var streamContent = new StreamContent(stream); - if (!content.ContentType.IsNullOrWhiteSpace()) - { - streamContent.Headers.ContentType = new MediaTypeHeaderValue(content.ContentType); - } - streamContent.Headers.ContentLength = content.ContentLength; - formData.Add(streamContent, parameter.Name, content.FileName ?? parameter.Name); + streamContent.Headers.ContentType = new MediaTypeHeaderValue(content.ContentType); } - } - else - { - formData.Add(new StringContent(value.ToString(), Encoding.UTF8), parameter.Name); + streamContent.Headers.ContentLength = content.ContentLength; + formData.Add(streamContent, parameter.Name, content.FileName ?? parameter.Name); } } - - return formData; - } - else - { - var postDataBuilder = new StringBuilder(); - - var isFirstParam = true; - foreach (var parameter in parameters.Where(p => p.BindingSourceId == ParameterBindingSources.Form)) + else { - var value = HttpActionParameterHelper.FindParameterValue(methodArguments, parameter); - if (value == null) - { - continue; - } + formData.Add(new StringContent(value.ToString(), Encoding.UTF8), parameter.Name); + } + } - postDataBuilder.Append(isFirstParam ? "?" : "&"); - postDataBuilder.Append(parameter.Name + "=" + System.Net.WebUtility.UrlEncode(value.ToString())); + return formData; + } - isFirstParam = false; + protected virtual async Task>> ObjectToFormDataAsync(T value) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var objectToFormData = scope.ServiceProvider.GetService>(); + if (objectToFormData != null) + { + return await objectToFormData.ConvertAsync(value); } - - return new StringContent(postDataBuilder.ToString(), Encoding.UTF8, MimeTypes.Application.XWwwFormUrlencoded); } + return null; } } } diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs index 0d97e6d348..97b9d747cf 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs @@ -114,7 +114,7 @@ namespace Volo.Abp.Http.Client.ClientProxying .MakeGenericMethod(value.GetType()) .Invoke(this, new object[] { value }); - if (!queryString.IsNullOrWhiteSpace()) + if (queryString != null) { urlBuilder.Append(isFirstParam ? "?" : "&"); urlBuilder.Append(queryString); diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs index 98225cf424..789c552861 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs @@ -280,22 +280,49 @@ namespace Volo.Abp.Http.DynamicProxying [Fact] public async Task GetParamsFromQueryAsync() { - var result = await _peopleAppService.GetParamsFromQueryAsync(new GetParamsFromQueryInput() + var result = await _peopleAppService.GetParamsFromQueryAsync(new GetParamsInput() { - NameValues = new List() + NameValues = new List() { - new GetParamsFromQueryInputNameValue() + new GetParamsNameValue() { Name = "name1", Value = "value1" }, - new GetParamsFromQueryInputNameValue() + new GetParamsNameValue() { Name = "name2", Value = "value2" } }, - NameValue = new GetParamsFromQueryInputNameValue() + NameValue = new GetParamsNameValue() + { + Name = "name3", + Value = "value3" + } + }); + result.ShouldBe("name1-value1:name2-value2:name3-value3"); + } + + [Fact] + public async Task GetParamsFromFormAsync() + { + var result = await _peopleAppService.GetParamsFromFormAsync(new GetParamsInput() + { + NameValues = new List() + { + new GetParamsNameValue() + { + Name = "name1", + Value = "value1" + }, + new GetParamsNameValue() + { + Name = "name2", + Value = "value2" + } + }, + NameValue = new GetParamsNameValue() { Name = "name3", Value = "value3" diff --git a/framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj b/framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj index 6dc99c23c3..0664ad2b7b 100644 --- a/framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj +++ b/framework/test/Volo.Abp.TestApp/Volo.Abp.TestApp.csproj @@ -1,4 +1,4 @@ - + diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsFromQueryInput.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsFromQueryInput.cs deleted file mode 100644 index 764a6390f0..0000000000 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsFromQueryInput.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Http; - -namespace Volo.Abp.TestApp.Application.Dto -{ - public class GetParamsFromQueryInput - { - public List NameValues { get; set; } - - public GetParamsFromQueryInputNameValue NameValue { get; set; } - } - - public class GetParamsFromQueryInputNameValue - { - public string Name { get; set; } - - public string Value { get; set; } - } - - [ExposeServices(typeof(IObjectToQueryString>))] - public class TestInputToQueryString : IObjectToQueryString>, ITransientDependency - { - public Task ConvertAsync(List values) - { - if (values.IsNullOrEmpty()) - { - return null; - } - - var sb = new StringBuilder(); - - for (var i = 0; i < values.Count; i++) - { - sb.Append($"NameValues[{i}].Name={values[i].Name}&NameValues[{i}].Value={values[i].Value}&"); - } - - sb.Remove(sb.Length - 1, 1); - return Task.FromResult(sb.ToString()); - } - } -} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsInput.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsInput.cs new file mode 100644 index 0000000000..3cbb1d4794 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsInput.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http; + +namespace Volo.Abp.TestApp.Application.Dto +{ + public class GetParamsInput + { + public List NameValues { get; set; } + + public GetParamsNameValue NameValue { get; set; } + } + + public class GetParamsNameValue + { + public string Name { get; set; } + + public string Value { get; set; } + } + + [ExposeServices(typeof(IObjectToQueryString>))] + public class TestObjectToQueryString : IObjectToQueryString>, ITransientDependency + { + public Task ConvertAsync(List values) + { + if (values.IsNullOrEmpty()) + { + return null; + } + + var sb = new StringBuilder(); + + for (var i = 0; i < values.Count; i++) + { + sb.Append($"NameValues[{i}].Name={values[i].Name}&NameValues[{i}].Value={values[i].Value}&"); + } + + sb.Remove(sb.Length - 1, 1); + return Task.FromResult(sb.ToString()); + } + } + + [ExposeServices(typeof(IObjectToFormData>))] + public class TestObjectToFormData : IObjectToFormData>, ITransientDependency + { + public Task>> ConvertAsync(List values) + { + if (values.IsNullOrEmpty()) + { + return null; + } + + var formDataContents = new List>(); + for (var i = 0; i < values.Count; i++) + { + formDataContents.Add(new KeyValuePair($"NameValues[{i}].Name", new StringContent(values[i].Name, Encoding.UTF8))); + formDataContents.Add(new KeyValuePair($"NameValues[{i}].Value", new StringContent(values[i].Value, Encoding.UTF8))); + } + + return Task.FromResult(formDataContents); + } + } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs index ec63c279f2..b9cb2e0887 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs @@ -32,6 +32,8 @@ namespace Volo.Abp.TestApp.Application Task CreateMultipleFileAsync(CreateMultipleFileInput input); - Task GetParamsFromQueryAsync(GetParamsFromQueryInput input); + Task GetParamsFromQueryAsync(GetParamsInput input); + + Task GetParamsFromFormAsync(GetParamsInput input); } } diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs index f211a9198f..4baa2a4ec2 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Volo.Abp.Application.Dtos; using Volo.Abp.TestApp.Domain; using Volo.Abp.Domain.Repositories; @@ -126,7 +127,15 @@ namespace Volo.Abp.TestApp.Application return str; } - public Task GetParamsFromQueryAsync(GetParamsFromQueryInput input) + public Task GetParamsFromQueryAsync([FromQuery]GetParamsInput input) + { + return Task.FromResult(input.NameValues?.FirstOrDefault()?.Name + "-" + + input.NameValues?.FirstOrDefault()?.Value + ":" + + input.NameValues?.LastOrDefault()?.Name + "-" + input.NameValues?.LastOrDefault()?.Value + ":" + + input.NameValue?.Name + "-" + input.NameValue?.Value); + } + + public Task GetParamsFromFormAsync([FromForm]GetParamsInput input) { return Task.FromResult(input.NameValues?.FirstOrDefault()?.Name + "-" + input.NameValues?.FirstOrDefault()?.Value + ":" + From 4551acda13776c481467723e96cff3261b6b144d Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 6 Oct 2021 11:09:26 +0800 Subject: [PATCH 11/31] Move interface to `Http.Client`. --- .../ClientProxying}/IObjectToFormData.cs | 2 +- .../ClientProxying}/IObjectToQueryString.cs | 2 +- .../DynamicProxying/TestObjectToFormData.cs | 31 ++++++++++++ .../TestObjectToQueryString.cs | 31 ++++++++++++ .../TestApp/Application/Dto/GetParamsInput.cs | 48 ------------------- 5 files changed, 64 insertions(+), 50 deletions(-) rename framework/src/{Volo.Abp.Core/Volo/Abp/Http => Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying}/IObjectToFormData.cs (83%) rename framework/src/{Volo.Abp.Core/Volo/Abp/Http => Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying}/IObjectToQueryString.cs (76%) create mode 100644 framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToFormData.cs create mode 100644 framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToQueryString.cs diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToFormData.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/IObjectToFormData.cs similarity index 83% rename from framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToFormData.cs rename to framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/IObjectToFormData.cs index 166b51239e..5aec4252e3 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToFormData.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/IObjectToFormData.cs @@ -2,7 +2,7 @@ using System.Net.Http; using System.Threading.Tasks; -namespace Volo.Abp.Http +namespace Volo.Abp.Http.Client.ClientProxying { public interface IObjectToFormData { diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToQueryString.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/IObjectToQueryString.cs similarity index 76% rename from framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToQueryString.cs rename to framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/IObjectToQueryString.cs index 19696e546c..ef223a903d 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Http/IObjectToQueryString.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/IObjectToQueryString.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -namespace Volo.Abp.Http +namespace Volo.Abp.Http.Client.ClientProxying { public interface IObjectToQueryString { diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToFormData.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToFormData.cs new file mode 100644 index 0000000000..a82923c0ac --- /dev/null +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToFormData.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http.Client.ClientProxying; +using Volo.Abp.TestApp.Application.Dto; + +namespace Volo.Abp.Http.DynamicProxying +{ + [ExposeServices(typeof(IObjectToFormData>))] + public class TestObjectToFormData : IObjectToFormData>, ITransientDependency + { + public Task>> ConvertAsync(List values) + { + if (values.IsNullOrEmpty()) + { + return null; + } + + var formDataContents = new List>(); + for (var i = 0; i < values.Count; i++) + { + formDataContents.Add(new KeyValuePair($"NameValues[{i}].Name", new StringContent(values[i].Name, Encoding.UTF8))); + formDataContents.Add(new KeyValuePair($"NameValues[{i}].Value", new StringContent(values[i].Value, Encoding.UTF8))); + } + + return Task.FromResult(formDataContents); + } + } +} diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToQueryString.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToQueryString.cs new file mode 100644 index 0000000000..13c470666a --- /dev/null +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToQueryString.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http.Client.ClientProxying; +using Volo.Abp.TestApp.Application.Dto; + +namespace Volo.Abp.Http.DynamicProxying +{ + [ExposeServices(typeof(IObjectToQueryString>))] + public class TestObjectToQueryString : IObjectToQueryString>, ITransientDependency + { + public Task ConvertAsync(List values) + { + if (values.IsNullOrEmpty()) + { + return null; + } + + var sb = new StringBuilder(); + + for (var i = 0; i < values.Count; i++) + { + sb.Append($"NameValues[{i}].Name={values[i].Name}&NameValues[{i}].Value={values[i].Value}&"); + } + + sb.Remove(sb.Length - 1, 1); + return Task.FromResult(sb.ToString()); + } + } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsInput.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsInput.cs index 3cbb1d4794..93a643491c 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsInput.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/GetParamsInput.cs @@ -1,9 +1,4 @@ using System.Collections.Generic; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Http; namespace Volo.Abp.TestApp.Application.Dto { @@ -20,47 +15,4 @@ namespace Volo.Abp.TestApp.Application.Dto public string Value { get; set; } } - - [ExposeServices(typeof(IObjectToQueryString>))] - public class TestObjectToQueryString : IObjectToQueryString>, ITransientDependency - { - public Task ConvertAsync(List values) - { - if (values.IsNullOrEmpty()) - { - return null; - } - - var sb = new StringBuilder(); - - for (var i = 0; i < values.Count; i++) - { - sb.Append($"NameValues[{i}].Name={values[i].Name}&NameValues[{i}].Value={values[i].Value}&"); - } - - sb.Remove(sb.Length - 1, 1); - return Task.FromResult(sb.ToString()); - } - } - - [ExposeServices(typeof(IObjectToFormData>))] - public class TestObjectToFormData : IObjectToFormData>, ITransientDependency - { - public Task>> ConvertAsync(List values) - { - if (values.IsNullOrEmpty()) - { - return null; - } - - var formDataContents = new List>(); - for (var i = 0; i < values.Count; i++) - { - formDataContents.Add(new KeyValuePair($"NameValues[{i}].Name", new StringContent(values[i].Name, Encoding.UTF8))); - formDataContents.Add(new KeyValuePair($"NameValues[{i}].Value", new StringContent(values[i].Value, Encoding.UTF8))); - } - - return Task.FromResult(formDataContents); - } - } } From 6808effa17fb09ae8dc090bb94af746a9945d0c9 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 6 Oct 2021 11:53:19 +0800 Subject: [PATCH 12/31] Add `AbpHttpClientProxyingOptions` --- .../AbpHttpClientProxyingOptions.cs | 18 ++++++++ .../ClientProxyRequestPayloadBuilder.cs | 46 +++++++++++-------- .../ClientProxying/ClientProxyUrlBuilder.cs | 46 +++++++++++-------- .../Volo/Abp/Http/AbpHttpClientTestModule.cs | 11 ++++- 4 files changed, 80 insertions(+), 41 deletions(-) create mode 100644 framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/AbpHttpClientProxyingOptions.cs diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/AbpHttpClientProxyingOptions.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/AbpHttpClientProxyingOptions.cs new file mode 100644 index 0000000000..87ee7de37c --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/AbpHttpClientProxyingOptions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.Http.Client.ClientProxying +{ + public class AbpHttpClientProxyingOptions + { + public Dictionary QueryStringConverts { get; set; } + + public Dictionary FormDataConverts { get; set; } + + public AbpHttpClientProxyingOptions() + { + QueryStringConverts = new Dictionary(); + FormDataConverts = new Dictionary(); + } + } +} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs index f76afae6a3..cee57d7663 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Volo.Abp.Content; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Client.Proxying; @@ -30,9 +31,12 @@ namespace Volo.Abp.Http.Client.ClientProxying protected IServiceScopeFactory ServiceScopeFactory { get; } - public ClientProxyRequestPayloadBuilder(IServiceScopeFactory serviceScopeFactory) + protected AbpHttpClientProxyingOptions HttpClientProxyingOptions { get; } + + public ClientProxyRequestPayloadBuilder(IServiceScopeFactory serviceScopeFactory, IOptions httpClientProxyingOptions) { ServiceScopeFactory = serviceScopeFactory; + HttpClientProxyingOptions = httpClientProxyingOptions.Value; } [CanBeNull] @@ -99,18 +103,28 @@ namespace Volo.Abp.Http.Client.ClientProxying continue; } - var formDataContents = await (Task>>)CallObjectToFormDataAsyncMethod - .MakeGenericMethod(value.GetType()) - .Invoke(this, new object[] { value }); - - if (formDataContents != null) + if (HttpClientProxyingOptions.FormDataConverts.ContainsKey(value.GetType())) { - foreach (var content in formDataContents) + using (var scope = ServiceScopeFactory.CreateScope()) { - formData.Add(content.Value, content.Key); + var formDataContents = await (Task>>)CallObjectToFormDataAsyncMethod + .MakeGenericMethod(value.GetType()) + .Invoke(this, new object[] + { + scope.ServiceProvider.GetRequiredService( + typeof(IObjectToFormData<>).MakeGenericType(value.GetType())), + value + }); + + if (formDataContents != null) + { + foreach (var content in formDataContents) + { + formData.Add(content.Value, content.Key); + } + continue; + } } - - continue; } if (value is IRemoteStreamContent remoteStreamContent) @@ -147,17 +161,9 @@ namespace Volo.Abp.Http.Client.ClientProxying return formData; } - protected virtual async Task>> ObjectToFormDataAsync(T value) + protected virtual async Task>> ObjectToFormDataAsync(IObjectToFormData converter, T value) { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var objectToFormData = scope.ServiceProvider.GetService>(); - if (objectToFormData != null) - { - return await objectToFormData.ConvertAsync(value); - } - } - return null; + return await converter.ConvertAsync(value); } } } diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs index 97b9d747cf..e59cac6911 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Client.Proxying; using Volo.Abp.Http.Modeling; @@ -28,10 +29,12 @@ namespace Volo.Abp.Http.Client.ClientProxying } protected IServiceScopeFactory ServiceScopeFactory { get; } + protected AbpHttpClientProxyingOptions HttpClientProxyingOptions { get; } - public ClientProxyUrlBuilder(IServiceScopeFactory serviceScopeFactory) + public ClientProxyUrlBuilder(IServiceScopeFactory serviceScopeFactory, IOptions httpClientProxyingOptions) { ServiceScopeFactory = serviceScopeFactory; + HttpClientProxyingOptions = httpClientProxyingOptions.Value; } public async Task GenerateUrlWithParametersAsync(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, ApiVersionInfo apiVersion) @@ -110,16 +113,27 @@ namespace Volo.Abp.Http.Client.ClientProxying continue; } - var queryString = await (Task)CallObjectToQueryStringAsyncMethod - .MakeGenericMethod(value.GetType()) - .Invoke(this, new object[] { value }); - - if (queryString != null) + if (HttpClientProxyingOptions.QueryStringConverts.ContainsKey(value.GetType())) { - urlBuilder.Append(isFirstParam ? "?" : "&"); - urlBuilder.Append(queryString); - isFirstParam = false; - continue; + using (var scope = ServiceScopeFactory.CreateScope()) + { + var queryString = await (Task)CallObjectToQueryStringAsyncMethod + .MakeGenericMethod(value.GetType()) + .Invoke(this, new object[] + { + scope.ServiceProvider.GetRequiredService( + typeof(IObjectToQueryString<>).MakeGenericType(value.GetType())), + value + }); + + if (queryString != null) + { + urlBuilder.Append(isFirstParam ? "?" : "&"); + urlBuilder.Append(queryString); + isFirstParam = false; + continue; + } + } } if (await AddQueryStringParameterAsync(urlBuilder, isFirstParam, queryStringParameter.Name, value)) @@ -134,17 +148,9 @@ namespace Volo.Abp.Http.Client.ClientProxying } } - protected virtual async Task ObjectToQueryStringAsync(T value) + protected virtual async Task ObjectToQueryStringAsync(IObjectToQueryString converter, T value) { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var objectToQueryString = scope.ServiceProvider.GetService>(); - if (objectToQueryString != null) - { - return await objectToQueryString.ConvertAsync(value); - } - } - return null; + return await converter.ConvertAsync(value); } protected virtual async Task AddQueryStringParameterAsync( diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs index 966ab01840..85713929cf 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs @@ -1,7 +1,9 @@ -using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.Conventions; using Volo.Abp.Http.Client; +using Volo.Abp.Http.Client.ClientProxying; using Volo.Abp.Http.DynamicProxying; using Volo.Abp.Http.Localization; using Volo.Abp.Localization; @@ -52,6 +54,13 @@ namespace Volo.Abp.Http options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateFileInput)); options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateMultipleFileInput)); }); + + + Configure(options => + { + options.QueryStringConverts.Add(typeof(List), typeof(TestObjectToQueryString)); + options.FormDataConverts.Add(typeof(List), typeof(TestObjectToFormData)); + }); } } } From ac9e24fbc605970a65113ed92971e282ff6f8fea Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 6 Oct 2021 12:05:41 +0800 Subject: [PATCH 13/31] Use type to resolve service. --- .../Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs | 3 +-- .../Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs | 3 +-- .../Volo/Abp/Http/AbpHttpClientTestModule.cs | 1 - .../Volo/Abp/Http/DynamicProxying/TestObjectToFormData.cs | 1 - .../Volo/Abp/Http/DynamicProxying/TestObjectToQueryString.cs | 1 - 5 files changed, 2 insertions(+), 7 deletions(-) diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs index cee57d7663..a91a9904de 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs @@ -111,8 +111,7 @@ namespace Volo.Abp.Http.Client.ClientProxying .MakeGenericMethod(value.GetType()) .Invoke(this, new object[] { - scope.ServiceProvider.GetRequiredService( - typeof(IObjectToFormData<>).MakeGenericType(value.GetType())), + scope.ServiceProvider.GetRequiredService(HttpClientProxyingOptions.FormDataConverts[value.GetType()]), value }); diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs index e59cac6911..12f7bc1ac8 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs @@ -121,8 +121,7 @@ namespace Volo.Abp.Http.Client.ClientProxying .MakeGenericMethod(value.GetType()) .Invoke(this, new object[] { - scope.ServiceProvider.GetRequiredService( - typeof(IObjectToQueryString<>).MakeGenericType(value.GetType())), + scope.ServiceProvider.GetRequiredService(HttpClientProxyingOptions.QueryStringConverts[value.GetType()]), value }); diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs index 85713929cf..d90ef50006 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs @@ -55,7 +55,6 @@ namespace Volo.Abp.Http options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateMultipleFileInput)); }); - Configure(options => { options.QueryStringConverts.Add(typeof(List), typeof(TestObjectToQueryString)); diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToFormData.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToFormData.cs index a82923c0ac..65b65fc54b 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToFormData.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToFormData.cs @@ -8,7 +8,6 @@ using Volo.Abp.TestApp.Application.Dto; namespace Volo.Abp.Http.DynamicProxying { - [ExposeServices(typeof(IObjectToFormData>))] public class TestObjectToFormData : IObjectToFormData>, ITransientDependency { public Task>> ConvertAsync(List values) diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToQueryString.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToQueryString.cs index 13c470666a..ae11a7bf2f 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToQueryString.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/TestObjectToQueryString.cs @@ -7,7 +7,6 @@ using Volo.Abp.TestApp.Application.Dto; namespace Volo.Abp.Http.DynamicProxying { - [ExposeServices(typeof(IObjectToQueryString>))] public class TestObjectToQueryString : IObjectToQueryString>, ITransientDependency { public Task ConvertAsync(List values) From ad19fb7a53ab251be26a4b20f42985a79ac3163e Mon Sep 17 00:00:00 2001 From: Berkan Sasmaz Date: Wed, 6 Oct 2021 10:30:16 +0300 Subject: [PATCH 14/31] Update en.json --- .../AbpIoLocalization/Admin/Localization/Resources/en.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 9410a7813f..2713c83604 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -346,9 +346,12 @@ "Volo.AbpIo.Commercial:030002": "Once activated, trial license cannot be set to requested!", "Volo.AbpIo.Commercial:030003": "There is no such status!", "Volo.AbpIo.Commercial:030004": "Status could not be changed due to an unexpected error!", - "Volo.AbpIo.Commercial:030005": "Start date and end date cannot be given when the trial license is in the requested state!", + "Volo.AbpIo.Commercial:030005": "Start date and end date only can be given when the trial license is in the activated state!", "Volo.AbpIo.Commercial:030006": "End date must always be greater than start date!", "Volo.AbpIo.Commercial:030007": "This trial license has already been activated once!", "Volo.AbpIo.Commercial:030008": "Purchase date can be set only when status is Purchased!", + "Volo.AbpIo.Commercial:030009": "User not found!", + "Volo.AbpIo.Commercial:030010": "To purchase the trial license, you need to activate the trial license!", + "Volo.AbpIo.Commercial:030011": "Trial license cannot be deleted if Purchased!" } } From 3a7dbad256591879a8328a9c94645857661f9932 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 8 Oct 2021 14:41:54 +0800 Subject: [PATCH 15/31] Upgrade `Npgsql.EntityFrameworkCore.PostgreSQL` to 6.0.0-rc.1 --- .../Volo.Abp.EntityFrameworkCore.PostgreSql.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo.Abp.EntityFrameworkCore.PostgreSql.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo.Abp.EntityFrameworkCore.PostgreSql.csproj index 5704a8a2c0..9acb67c7ea 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo.Abp.EntityFrameworkCore.PostgreSql.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo.Abp.EntityFrameworkCore.PostgreSql.csproj @@ -19,7 +19,7 @@ - + From 55dbc12e8641ea42b0604dd2956ca0ba891c1a17 Mon Sep 17 00:00:00 2001 From: Maik Stegemann Date: Fri, 8 Oct 2021 11:08:01 +0200 Subject: [PATCH 16/31] export /models/index.ts from public-api.ts --- npm/ng-packs/packages/feature-management/src/public-api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/npm/ng-packs/packages/feature-management/src/public-api.ts b/npm/ng-packs/packages/feature-management/src/public-api.ts index c52b944e86..b9073df803 100644 --- a/npm/ng-packs/packages/feature-management/src/public-api.ts +++ b/npm/ng-packs/packages/feature-management/src/public-api.ts @@ -2,3 +2,4 @@ export * from './lib/components'; export * from './lib/directives'; export * from './lib/enums/components'; export * from './lib/feature-management.module'; +export * from './lib/models'; \ No newline at end of file From b4e720655c74f1b9596b95c157afcd68a6d994c9 Mon Sep 17 00:00:00 2001 From: Ebicoglu Date: Fri, 8 Oct 2021 13:19:15 +0300 Subject: [PATCH 17/31] Update en.json --- .../AbpIoLocalization/Admin/Localization/Resources/en.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 2713c83604..923535cfdd 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -346,12 +346,12 @@ "Volo.AbpIo.Commercial:030002": "Once activated, trial license cannot be set to requested!", "Volo.AbpIo.Commercial:030003": "There is no such status!", "Volo.AbpIo.Commercial:030004": "Status could not be changed due to an unexpected error!", - "Volo.AbpIo.Commercial:030005": "Start date and end date only can be given when the trial license is in the activated state!", + "Volo.AbpIo.Commercial:030005": "Start and end date can be updated when the trial license is in the -activated- status!", "Volo.AbpIo.Commercial:030006": "End date must always be greater than start date!", "Volo.AbpIo.Commercial:030007": "This trial license has already been activated once!", "Volo.AbpIo.Commercial:030008": "Purchase date can be set only when status is Purchased!", "Volo.AbpIo.Commercial:030009": "User not found!", - "Volo.AbpIo.Commercial:030010": "To purchase the trial license, you need to activate the trial license!", - "Volo.AbpIo.Commercial:030011": "Trial license cannot be deleted if Purchased!" + "Volo.AbpIo.Commercial:030010": "To purchase the trial license, first you need to activate your trial license!", + "Volo.AbpIo.Commercial:030011": "You cannot delete a trial license when it is purchased!" } } From 66ae0b81555a517fbfdd02ebd923fe157c7cf064 Mon Sep 17 00:00:00 2001 From: albert <9526587+ebicoglu@users.noreply.github.com> Date: Sun, 10 Oct 2021 15:45:10 +0300 Subject: [PATCH 18/31] Update en.json --- .../src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json index 37c6c685ff..cdef076bb7 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json @@ -14,7 +14,7 @@ "ReIndexAllProjects": "ReIndex all projects", "ReIndexProject": "ReIndex project", "ReIndexProjectConfirmationMessage": "Are you sure to reindex for project \"{0}\"", - "SuccessfullyReIndexProject": "Successfully reindex for project \"{0}\"", + "SuccessfullyReIndexProject": "Successfully reindexed: \"{0}\"", "ReIndexAllProjectConfirmationMessage": "Are you sure to reindex all project?", "SuccessfullyReIndexAllProject": "Successfully reindex for all projects", "InThisDocument": "In this document", From f77d54247c419194bb1bf585832b91b98a7cf435 Mon Sep 17 00:00:00 2001 From: albert <9526587+ebicoglu@users.noreply.github.com> Date: Sun, 10 Oct 2021 15:45:36 +0300 Subject: [PATCH 19/31] Update en.json --- .../src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json index cdef076bb7..62913fa806 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json @@ -16,7 +16,7 @@ "ReIndexProjectConfirmationMessage": "Are you sure to reindex for project \"{0}\"", "SuccessfullyReIndexProject": "Successfully reindexed: \"{0}\"", "ReIndexAllProjectConfirmationMessage": "Are you sure to reindex all project?", - "SuccessfullyReIndexAllProject": "Successfully reindex for all projects", + "SuccessfullyReIndexAllProject": "Successfully reindexed all projects", "InThisDocument": "In this document", "GoToTop": "Go to top", "Projects": "Project(s)", From d5a01437caca9b00022db0f163599cbae48ac870 Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Sun, 10 Oct 2021 17:13:02 +0300 Subject: [PATCH 20/31] add visual studio upgrade section. --- .../en/Blog-Posts/2021-10-05 v5_0_Beta1/POST.md | 8 ++++++++ .../use-preview-visual-studio.png | Bin 0 -> 33270 bytes 2 files changed, 8 insertions(+) create mode 100644 docs/en/Blog-Posts/2021-10-05 v5_0_Beta1/use-preview-visual-studio.png diff --git a/docs/en/Blog-Posts/2021-10-05 v5_0_Beta1/POST.md b/docs/en/Blog-Posts/2021-10-05 v5_0_Beta1/POST.md index 675544fba0..14d23f61fc 100644 --- a/docs/en/Blog-Posts/2021-10-05 v5_0_Beta1/POST.md +++ b/docs/en/Blog-Posts/2021-10-05 v5_0_Beta1/POST.md @@ -32,6 +32,14 @@ See the [ABP CLI documentation](https://docs.abp.io/en/abp/latest/CLI) for all t > You can also use the *Direct Download* tab on the [Get Started](https://abp.io/get-started) page by selecting the **Preview checkbox**. +### Visual Studio Upgrade + +As .NET 6 is a preview version you need to make changes on your current Visual Studio. If you want to run a .NET 6 project on Visual Studio 2019, you need to enable the `Use Previews of the .NET SDK` option from Visual Studio 2019 options. See the screenshot: + +![Enable Using Previews of the .NET SDK ](use-preview-visual-studio.png) + +Alternatively you can download the latest Visual Studio 2022 preview to run/create the .NET 6 projects. We have tested the ABP solution with the Visual Studio 2022 `17.0.0 Preview 4.1`. Check out https://visualstudio.microsoft.com/vs/preview/ for more information. + ### Migration Notes & Breaking Changes This is a major version and there are some breaking changes and upgrade steps. Here, a list of important breaking changes in this version: diff --git a/docs/en/Blog-Posts/2021-10-05 v5_0_Beta1/use-preview-visual-studio.png b/docs/en/Blog-Posts/2021-10-05 v5_0_Beta1/use-preview-visual-studio.png new file mode 100644 index 0000000000000000000000000000000000000000..317cf1d2094de66497a82cbe5709b2b76be1052b GIT binary patch literal 33270 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 ZA~ZS8J!$)XbW%^>R@G7|P=4(He*vDXky-!% literal 0 HcmV?d00001 From 5b13ca07db4d2b73f412aa7406bff790ad974f6c Mon Sep 17 00:00:00 2001 From: albert <9526587+ebicoglu@users.noreply.github.com> Date: Mon, 11 Oct 2021 11:27:14 +0300 Subject: [PATCH 21/31] Replace PayU with Iyzico --- .../AbpIoLocalization/Commercial/Localization/Resources/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index b31c33621c..c8bfea1f13 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -230,7 +230,7 @@ "HowCanIRefundVatExplanation2": "Log in into your 2Checkout account", "HowCanIRefundVatExplanation3": "Find the appropriate order and press \"Refund Belated VAT\" (enter your VAT ID)", "HowCanIGetMyInvoice": "How can I get my invoice?", - "HowCanIGetMyInvoiceExplanation": "There are 2 payment gateways for purchasing a license: PayU and 2Checkout. If you purchase your license through 2Checkout gateway, it sends the PDF invoice to your email address, see 2Checkout invoicing. If you purchase through the PayU gateway or via bank wire transfer, we will prepare and send your invoice. You can request your invoice from the organization management page.", + "HowCanIGetMyInvoiceExplanation": "There are 2 payment gateways for purchasing a license: Iyzico and 2Checkout. If you purchase your license through the 2Checkout gateway, it sends the PDF invoice to your email address, see 2Checkout invoicing. If you purchase through the Iyzico gateway, with custom purchase link or via bank wire transfer, we will prepare and send your invoice. You can request or download your invoice from the organization management page. Before contacting us for the invoice, check your organization management page!", "Forum": "Forum", "SupportExplanation": "ABP Commercial licenses provides a premium forum support by a team consists of the ABP Framework experts.", "PrivateTicket": "Private Ticket", From b914a723643c298480c62b1d128a6bf63a7b12d1 Mon Sep 17 00:00:00 2001 From: albert <9526587+ebicoglu@users.noreply.github.com> Date: Mon, 11 Oct 2021 11:28:00 +0300 Subject: [PATCH 22/31] Update zh-Hans.json --- .../Commercial/Localization/Resources/zh-Hans.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json index ac5e247264..7dddad6580 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json @@ -230,7 +230,7 @@ "HowCanIRefundVatExplanation2": "登录到你的2Checkout账户", "HowCanIRefundVatExplanation3": "找到适当的订单,然后按\"退还已延期的增值税\"(输入你的增值税ID", "HowCanIGetMyInvoice": "我如何获得发票?", - "HowCanIGetMyInvoiceExplanation": "有两个用于购买许可的支付网关:PayU和2Checkout. 如果你通过2Checkout网关购买许可,则它将PDF发票发送到你的电子邮件地址,请参阅2Checkout发票.如果你是通过PayU网关或通过银行电汇购买的,我们将准备并发送你的发票. 你可以从组织管理页面索取发票.", + "HowCanIGetMyInvoiceExplanation": "有两个用于购买许可的支付网关:Iyzico和2Checkout. 如果你通过2Checkout网关购买许可,则它将PDF发票发送到你的电子邮件地址,请参阅2Checkout发票.如果你是通过Iyzico网关或通过银行电汇购买的,我们将准备并发送你的发票. 你可以从组织管理页面索取发票.", "Forum": "论坛", "SupportExplanation": "ABP商业版许可包含由ABP框架专家组成的团队提供的高级论坛支持.", "PrivateTicket": "私有票", From f709d0787183365b05351033968671f8be89504c Mon Sep 17 00:00:00 2001 From: albert <9526587+ebicoglu@users.noreply.github.com> Date: Mon, 11 Oct 2021 11:28:30 +0300 Subject: [PATCH 23/31] Update zh-Hant.json --- .../Commercial/Localization/Resources/zh-Hant.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hant.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hant.json index 80fed13066..137079c16a 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hant.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hant.json @@ -228,7 +228,7 @@ "HowCanIRefundVatExplanation2": "登錄到你的2Checkout賬戶", "HowCanIRefundVatExplanation3": "找到適當的訂單,然後按\"退還已延期的增值稅\"(輸入你的增值稅ID", "HowCanIGetMyInvoice": "我如何獲得發票?", - "HowCanIGetMyInvoiceExplanation": "有兩個用於購買許可的支付網關:PayU和2Checkout. 如果你通過2Checkout網關購買許可,則它將PDF發票發送到你的電子郵件地址,請參閱2Checkout發票.如果你是通過PayU網關或通過銀行電匯購買的,我們將準備並發送你的發票. 你可以從組織管理頁面索取發票.", + "HowCanIGetMyInvoiceExplanation": "有兩個用於購買許可的支付網關:Iyzico和2Checkout. 如果你通過2Checkout網關購買許可,則它將PDF發票發送到你的電子郵件地址,請參閱2Checkout發票.如果你是通過Iyzico網關或通過銀行電匯購買的,我們將準備並發送你的發票. 你可以從組織管理頁面索取發票.", "Forum": "論壇", "SupportExplanation": "ABP商業版許可包含由ABP框架專家組成的團隊提供的高級論壇支持.", "PrivateTicket": "私有票", @@ -319,4 +319,4 @@ "MongoDB": "MongoDB" } -} \ No newline at end of file +} From 393eb2d5e1c99387d9dfb1f1b2ee09d3c6468461 Mon Sep 17 00:00:00 2001 From: Necati Meral Date: Mon, 11 Oct 2021 10:33:34 +0200 Subject: [PATCH 24/31] Fixes #10281 --- .../Pages/Documents/Project/Index.cshtml.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs index 99c323bf89..88760b0a7a 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs @@ -246,16 +246,31 @@ namespace Volo.Docs.Pages.Documents.Project { var projects = await _projectAppService.GetListAsync(); - var sb = new StringBuilder(); - ProjectSelectItems = projects.Items.Select(p => new SelectListItem { Text = p.Name, - Value = p.Id != Project.Id ? sb.Append(DocumentsUrlPrefix).Append(LanguageCode).Append("/").Append(p.ShortName).Append("/").Append(DocsAppConsts.Latest).ToString() : null, + Value = CreateProjectLink(p), Selected = p.Id == Project.Id }).ToList(); } + private string CreateProjectLink(ProjectDto project) + { + if (project.Id == Project.Id) + { + return null; + } + + return new StringBuilder() + .Append(DocumentsUrlPrefix) + .Append(LanguageCode) + .Append('/') + .Append(project.ShortName) + .Append('/') + .Append(DocsAppConsts.Latest) + .ToString(); + } + private async Task SetVersionAsync() { //TODO: Needs refactoring From e0049ef0e44c47fefda034313558412b98708875 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Mon, 11 Oct 2021 17:37:08 +0800 Subject: [PATCH 25/31] Fix ServiceProxyScript parameters name --- .../ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs index 46eb2fca13..d40e92df0e 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs @@ -161,7 +161,7 @@ namespace Volo.Abp.Http.ProxyScripting.Generators public static string GenerateJsFuncParameterList(ActionApiDescriptionModel action, string ajaxParametersName) { - var methodParamNames = action.ParametersOnMethod.Select(p => p.Name).Distinct().ToList(); + var methodParamNames = action.Parameters.GroupBy(p => p.NameOnMethod).Select(x => x.Key).ToList(); methodParamNames.Add(ajaxParametersName); return methodParamNames.Select(prmName => NormalizeJsVariableName(prmName.ToCamelCase())).JoinAsString(", "); } From c39ee0b6a29e814ff3c1125319f4d5d9ce50108e Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Tue, 12 Oct 2021 13:25:17 +0800 Subject: [PATCH 26/31] Update AzureDistributedEventBus.cs --- .../Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index f4fbca13b4..91f20072a2 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -77,6 +77,11 @@ namespace Volo.Abp.EventBus.Azure return; } + if (await AddToInboxAsync(message.MessageId, eventName, eventType, message.Body.ToArray())) + { + return; + } + var eventData = _serializer.Deserialize(message.Body.ToArray(), eventType); await TriggerHandlersAsync(eventType, eventData); From 8415b969faf2a6334e8f5866f05b742b2400b1d3 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Tue, 12 Oct 2021 13:34:45 +0800 Subject: [PATCH 27/31] Update AzureDistributedEventBus.cs --- .../Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index 91f20072a2..a719e0a8d5 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -19,7 +19,7 @@ namespace Volo.Abp.EventBus.Azure { [Dependency(ReplaceServices = true)] [ExposeServices(typeof(IDistributedEventBus), typeof(AzureDistributedEventBus))] - public class AzureDistributedEventBus : DistributedEventBusBase, IDistributedEventBus, ISingletonDependency + public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDependency { private readonly AbpAzureEventBusOptions _options; private readonly IAzureServiceBusMessageConsumerFactory _messageConsumerFactory; @@ -33,7 +33,6 @@ namespace Volo.Abp.EventBus.Azure IServiceScopeFactory serviceScopeFactory, ICurrentTenant currentTenant, IUnitOfWorkManager unitOfWorkManager, - IEventErrorHandler errorHandler, IOptions abpDistributedEventBusOptions, IGuidGenerator guidGenerator, IClock clock, @@ -44,7 +43,6 @@ namespace Volo.Abp.EventBus.Azure : base(serviceScopeFactory, currentTenant, unitOfWorkManager, - errorHandler, abpDistributedEventBusOptions, guidGenerator, clock) @@ -232,6 +230,5 @@ namespace Volo.Abp.EventBus.Azure } ); } - } } From d981c337ebf99d5da39991bd84795a303f1e4f49 Mon Sep 17 00:00:00 2001 From: bnymncoskuner Date: Tue, 12 Oct 2021 16:59:56 +0300 Subject: [PATCH 28/31] feat: add element options to content strategy --- .../src/lib/strategies/content.strategy.ts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/strategies/content.strategy.ts b/npm/ng-packs/packages/core/src/lib/strategies/content.strategy.ts index 05261bc5e4..839e867f41 100644 --- a/npm/ng-packs/packages/core/src/lib/strategies/content.strategy.ts +++ b/npm/ng-packs/packages/core/src/lib/strategies/content.strategy.ts @@ -1,11 +1,16 @@ import { ContentSecurityStrategy, CONTENT_SECURITY_STRATEGY } from './content-security.strategy'; import { DomStrategy, DOM_STRATEGY } from './dom.strategy'; +export type ElementOptions = Partial<{ + [key in keyof T]: T[key]; +}>; + export abstract class ContentStrategy { constructor( public content: string, protected domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(), protected contentSecurityStrategy: ContentSecurityStrategy = CONTENT_SECURITY_STRATEGY.None(), + protected options: ElementOptions = {}, ) {} abstract createElement(): T; @@ -13,6 +18,10 @@ export abstract class ContentStrategy 0) { + Object.keys(this.options).forEach(key => (element[key] = this.options[key])); + } + this.contentSecurityStrategy.applyCSP(element); this.domStrategy.insertElement(element); @@ -39,16 +48,16 @@ export class ScriptContentStrategy extends ContentStrategy { } export const CONTENT_STRATEGY = { - AppendScriptToBody(content: string) { - return new ScriptContentStrategy(content, DOM_STRATEGY.AppendToBody()); + AppendScriptToBody(content: string, options?: ElementOptions) { + return new ScriptContentStrategy(content, DOM_STRATEGY.AppendToBody(), undefined, options); }, - AppendScriptToHead(content: string) { - return new ScriptContentStrategy(content, DOM_STRATEGY.AppendToHead()); + AppendScriptToHead(content: string, options?: ElementOptions) { + return new ScriptContentStrategy(content, DOM_STRATEGY.AppendToHead(), undefined, options); }, - AppendStyleToHead(content: string) { - return new StyleContentStrategy(content, DOM_STRATEGY.AppendToHead()); + AppendStyleToHead(content: string, options?: ElementOptions) { + return new StyleContentStrategy(content, DOM_STRATEGY.AppendToHead(), undefined, options); }, - PrependStyleToHead(content: string) { - return new StyleContentStrategy(content, DOM_STRATEGY.PrependToHead()); + PrependStyleToHead(content: string, options?: ElementOptions) { + return new StyleContentStrategy(content, DOM_STRATEGY.PrependToHead(), undefined, options); }, }; From 57e9f3856449ba7992654a0493ee0c5fed6ba4a0 Mon Sep 17 00:00:00 2001 From: bnymncoskuner Date: Tue, 12 Oct 2021 17:00:55 +0300 Subject: [PATCH 29/31] docs: add new option in Content Strategy --- docs/en/UI/Angular/Content-Strategy.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/en/UI/Angular/Content-Strategy.md b/docs/en/UI/Angular/Content-Strategy.md index 8544042c32..cb06ecf044 100644 --- a/docs/en/UI/Angular/Content-Strategy.md +++ b/docs/en/UI/Angular/Content-Strategy.md @@ -11,13 +11,15 @@ constructor( public content: string, protected domStrategy?: DomStrategy, - protected contentSecurityStrategy?: ContentSecurityStrategy + protected contentSecurityStrategy?: ContentSecurityStrategy, + protected options?: ElementOptions = {}, ) ``` - `content` is set to `