From 22ae6ca99346e4e81957563e4b387e64c45bb989 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Wed, 12 Aug 2020 20:30:09 +0800 Subject: [PATCH 001/147] Add Kafka event bus integration --- framework/Volo.Abp.sln | 14 ++ .../Volo.Abp.EventBus.Kafka/FodyWeavers.xml | 3 + .../Volo.Abp.EventBus.Kafka/FodyWeavers.xsd | 30 +++ .../Volo.Abp.EventBus.Kafka.csproj | 16 ++ .../EventBus/Kafka/AbpEventBusKafkaModule.cs | 27 +++ .../EventBus/Kafka/AbpKafkaEventBusOptions.cs | 12 + .../Kafka/KafkaDistributedEventBus.cs | 208 ++++++++++++++++++ framework/src/Volo.Abp.Kafka/FodyWeavers.xml | 3 + framework/src/Volo.Abp.Kafka/FodyWeavers.xsd | 30 +++ .../src/Volo.Abp.Kafka/Volo.Abp.Kafka.csproj | 17 ++ .../Volo/Abp/Kafka/AbpKafkaModule.cs | 31 +++ .../Volo/Abp/Kafka/AbpKafkaOptions.cs | 22 ++ .../Volo/Abp/Kafka/ConsumerPool.cs | 105 +++++++++ .../Volo/Abp/Kafka/IConsumerPool.cs | 10 + .../Volo/Abp/Kafka/IKafkaMessageConsumer.cs | 11 + .../Abp/Kafka/IKafkaMessageConsumerFactory.cs | 19 ++ .../Volo/Abp/Kafka/IKafkaSerializer.cs | 11 + .../Volo/Abp/Kafka/IProducerPool.cs | 10 + .../Volo/Abp/Kafka/KafkaConnections.cs | 35 +++ .../Volo/Abp/Kafka/KafkaMessageConsumer.cs | 156 +++++++++++++ .../Abp/Kafka/KafkaMessageConsumerFactory.cs | 32 +++ .../Volo/Abp/Kafka/ProducerPool.cs | 97 ++++++++ .../Volo/Abp/Kafka/Utf8JsonKafkaSerializer.cs | 27 +++ nupkg/common.ps1 | 2 + 24 files changed, 928 insertions(+) create mode 100644 framework/src/Volo.Abp.EventBus.Kafka/FodyWeavers.xml create mode 100644 framework/src/Volo.Abp.EventBus.Kafka/FodyWeavers.xsd create mode 100644 framework/src/Volo.Abp.EventBus.Kafka/Volo.Abp.EventBus.Kafka.csproj create mode 100644 framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/AbpEventBusKafkaModule.cs create mode 100644 framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/AbpKafkaEventBusOptions.cs create mode 100644 framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs create mode 100644 framework/src/Volo.Abp.Kafka/FodyWeavers.xml create mode 100644 framework/src/Volo.Abp.Kafka/FodyWeavers.xsd create mode 100644 framework/src/Volo.Abp.Kafka/Volo.Abp.Kafka.csproj create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/AbpKafkaModule.cs create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/AbpKafkaOptions.cs create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IConsumerPool.cs create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaMessageConsumer.cs create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaMessageConsumerFactory.cs create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaSerializer.cs create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IProducerPool.cs create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaConnections.cs create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumer.cs create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumerFactory.cs create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs create mode 100644 framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/Utf8JsonKafkaSerializer.cs diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 5778b054ed..2b381f4315 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -323,6 +323,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Aws", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Aws.Tests", "test\Volo.Abp.BlobStoring.Aws.Tests\Volo.Abp.BlobStoring.Aws.Tests.csproj", "{2CD3B26A-CA81-4279-8D5D-6A594517BB3F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Kafka", "src\Volo.Abp.Kafka\Volo.Abp.Kafka.csproj", "{2A864049-9CD5-4493-8CDB-C408474D43D4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EventBus.Kafka", "src\Volo.Abp.EventBus.Kafka\Volo.Abp.EventBus.Kafka.csproj", "{C1D891B0-AE83-42CB-987D-425A2787DE78}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -961,6 +965,14 @@ Global {2CD3B26A-CA81-4279-8D5D-6A594517BB3F}.Debug|Any CPU.Build.0 = Debug|Any CPU {2CD3B26A-CA81-4279-8D5D-6A594517BB3F}.Release|Any CPU.ActiveCfg = Release|Any CPU {2CD3B26A-CA81-4279-8D5D-6A594517BB3F}.Release|Any CPU.Build.0 = Release|Any CPU + {2A864049-9CD5-4493-8CDB-C408474D43D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A864049-9CD5-4493-8CDB-C408474D43D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A864049-9CD5-4493-8CDB-C408474D43D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A864049-9CD5-4493-8CDB-C408474D43D4}.Release|Any CPU.Build.0 = Release|Any CPU + {C1D891B0-AE83-42CB-987D-425A2787DE78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1D891B0-AE83-42CB-987D-425A2787DE78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1D891B0-AE83-42CB-987D-425A2787DE78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1D891B0-AE83-42CB-987D-425A2787DE78}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1124,6 +1136,8 @@ Global {8E49687A-E69F-49F2-8DB0-428D0883A937} = {447C8A77-E5F0-4538-8687-7383196D04EA} {50968CDE-1029-4051-B2E5-B69D0ECF2A18} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {2CD3B26A-CA81-4279-8D5D-6A594517BB3F} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {2A864049-9CD5-4493-8CDB-C408474D43D4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {C1D891B0-AE83-42CB-987D-425A2787DE78} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.EventBus.Kafka/FodyWeavers.xml b/framework/src/Volo.Abp.EventBus.Kafka/FodyWeavers.xml new file mode 100644 index 0000000000..bc5a74a236 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Kafka/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + diff --git a/framework/src/Volo.Abp.EventBus.Kafka/FodyWeavers.xsd b/framework/src/Volo.Abp.EventBus.Kafka/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Kafka/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.Kafka/Volo.Abp.EventBus.Kafka.csproj b/framework/src/Volo.Abp.EventBus.Kafka/Volo.Abp.EventBus.Kafka.csproj new file mode 100644 index 0000000000..42b0293781 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo.Abp.EventBus.Kafka.csproj @@ -0,0 +1,16 @@ + + + + + + + netstandard2.0 + + + + + + + + + diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/AbpEventBusKafkaModule.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/AbpEventBusKafkaModule.cs new file mode 100644 index 0000000000..e0e408cb6b --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/AbpEventBusKafkaModule.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Kafka; +using Volo.Abp.Modularity; + +namespace Volo.Abp.EventBus.Kafka +{ + [DependsOn( + typeof(AbpEventBusModule), + typeof(AbpKafkaModule))] + public class AbpEventBusKafkaModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + Configure(configuration.GetSection("Kafka:EventBus")); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + context + .ServiceProvider + .GetRequiredService() + .Initialize(); + } + } +} diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/AbpKafkaEventBusOptions.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/AbpKafkaEventBusOptions.cs new file mode 100644 index 0000000000..3aff9699fb --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/AbpKafkaEventBusOptions.cs @@ -0,0 +1,12 @@ +namespace Volo.Abp.EventBus.Kafka +{ + public class AbpKafkaEventBusOptions + { + + public string ConnectionName { get; set; } + + public string TopicName { get; set; } + + public string GroupId { get; set; } + } +} diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs new file mode 100644 index 0000000000..fd6d773952 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Confluent.Kafka; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Kafka; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Threading; + +namespace Volo.Abp.EventBus.Kafka +{ + [Dependency(ReplaceServices = true)] + [ExposeServices(typeof(IDistributedEventBus), typeof(KafkaDistributedEventBus))] + public class KafkaDistributedEventBus : EventBusBase, IDistributedEventBus, ISingletonDependency + { + protected AbpKafkaEventBusOptions AbpKafkaEventBusOptions { get; } + protected AbpDistributedEventBusOptions AbpDistributedEventBusOptions { get; } + protected IKafkaMessageConsumerFactory MessageConsumerFactory { get; } + protected IKafkaSerializer Serializer { get; } + protected IProducerPool ProducerPool { get; } + protected ConcurrentDictionary> HandlerFactories { get; } + protected ConcurrentDictionary EventTypes { get; } + protected IKafkaMessageConsumer Consumer { get; private set; } + + public KafkaDistributedEventBus( + IServiceScopeFactory serviceScopeFactory, + ICurrentTenant currentTenant, + IOptions abpKafkaEventBusOptions, + IKafkaMessageConsumerFactory messageConsumerFactory, + IOptions abpDistributedEventBusOptions, + IKafkaSerializer serializer, + IProducerPool producerPool) + : base(serviceScopeFactory, currentTenant) + { + AbpKafkaEventBusOptions = abpKafkaEventBusOptions.Value; + AbpDistributedEventBusOptions = abpDistributedEventBusOptions.Value; + MessageConsumerFactory = messageConsumerFactory; + Serializer = serializer; + ProducerPool = producerPool; + + HandlerFactories = new ConcurrentDictionary>(); + EventTypes = new ConcurrentDictionary(); + } + + public void Initialize() + { + Consumer = MessageConsumerFactory.Create( + AbpKafkaEventBusOptions.TopicName, + AbpKafkaEventBusOptions.GroupId, + AbpKafkaEventBusOptions.ConnectionName); + + Consumer.OnMessageReceived(ProcessEventAsync); + + SubscribeHandlers(AbpDistributedEventBusOptions.Handlers); + } + + private async Task ProcessEventAsync(Message message) + { + var eventName = message.Key; + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + return; + } + + var eventData = Serializer.Deserialize(message.Value, 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 producer = ProducerPool.Get(AbpKafkaEventBusOptions.ConnectionName); + + await producer.ProduceAsync( + AbpKafkaEventBusOptions.TopicName, + new Message + { + Key = eventName, Value = body + }); + } + + private List GetOrCreateHandlerFactories(Type eventType) + { + return HandlerFactories.GetOrAdd( + eventType, + type => + { + var eventName = EventNameAttribute.GetNameOrDefault(type); + EventTypes[eventName] = type; + return new List(); + } + ); + } + + protected override IEnumerable GetHandlerFactories(Type eventType) + { + var handlerFactoryList = new List(); + + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key)) + ) + { + handlerFactoryList.Add( + new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + } + + return handlerFactoryList.ToArray(); + } + + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) + { + //Should trigger same type + if (handlerEventType == targetEventType) + { + return true; + } + + //Should trigger for inherited types + if (handlerEventType.IsAssignableFrom(targetEventType)) + { + return true; + } + + return false; + } + } +} diff --git a/framework/src/Volo.Abp.Kafka/FodyWeavers.xml b/framework/src/Volo.Abp.Kafka/FodyWeavers.xml new file mode 100644 index 0000000000..be0de3a908 --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.Kafka/FodyWeavers.xsd b/framework/src/Volo.Abp.Kafka/FodyWeavers.xsd new file mode 100644 index 0000000000..3f3946e282 --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/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.Kafka/Volo.Abp.Kafka.csproj b/framework/src/Volo.Abp.Kafka/Volo.Abp.Kafka.csproj new file mode 100644 index 0000000000..e91f42d7b2 --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo.Abp.Kafka.csproj @@ -0,0 +1,17 @@ + + + + + + + netstandard2.0 + + + + + + + + + + diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/AbpKafkaModule.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/AbpKafkaModule.cs new file mode 100644 index 0000000000..d69e924349 --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/AbpKafkaModule.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Json; +using Volo.Abp.Modularity; +using Volo.Abp.Threading; + +namespace Volo.Abp.Kafka +{ + [DependsOn( + typeof(AbpJsonModule), + typeof(AbpThreadingModule) + )] + public class AbpKafkaModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + Configure(configuration.GetSection("Kafka")); + } + + public override void OnApplicationShutdown(ApplicationShutdownContext context) + { + context.ServiceProvider + .GetRequiredService() + .Dispose(); + + context.ServiceProvider + .GetRequiredService() + .Dispose(); + } + } +} diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/AbpKafkaOptions.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/AbpKafkaOptions.cs new file mode 100644 index 0000000000..1769d8a076 --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/AbpKafkaOptions.cs @@ -0,0 +1,22 @@ +using System; +using Confluent.Kafka; +using Confluent.Kafka.Admin; + +namespace Volo.Abp.Kafka +{ + public class AbpKafkaOptions + { + public KafkaConnections Connections { get; } + + public Action ConfigureProducer { get; set; } + + public Action ConfigureConsumer { get; set; } + + public Action ConfigureTopic { get; set; } + + public AbpKafkaOptions() + { + Connections = new KafkaConnections(); + } + } +} diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs new file mode 100644 index 0000000000..07af447e1a --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Confluent.Kafka; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Kafka +{ + public class ConsumerPool : IConsumerPool, ISingletonDependency + { + protected AbpKafkaOptions Options { get; } + + protected ConcurrentDictionary> Consumers { get; } + + protected TimeSpan TotalDisposeWaitDuration { get; set; } = TimeSpan.FromSeconds(10); + + public ILogger Logger { get; set; } + + private bool _isDisposed; + + public ConsumerPool(IOptions options) + { + Options = options.Value; + + Consumers = new ConcurrentDictionary>(); + Logger = new NullLogger(); + } + + public virtual IConsumer Get(string groupId, string connectionName = null) + { + connectionName ??= KafkaConnections.DefaultConnectionName; + var config = new ConsumerConfig(Options.Connections.GetOrDefault(connectionName)) + { + GroupId = groupId, + EnableAutoCommit = false + }; + + Options.ConfigureConsumer?.Invoke(config); + + return Consumers.GetOrAdd( + connectionName, + new ConsumerBuilder(config).Build()); + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!Consumers.Any()) + { + Logger.LogDebug($"Disposed consumer pool with no consumers in the pool."); + return; + } + + var poolDisposeStopwatch = Stopwatch.StartNew(); + + Logger.LogInformation($"Disposing consumer pool ({Consumers.Count} consumers)."); + + var remainingWaitDuration = TotalDisposeWaitDuration; + + foreach (var consumer in Consumers.Values) + { + var poolItemDisposeStopwatch = Stopwatch.StartNew(); + + try + { + consumer.Close(); + consumer.Dispose(); + } + catch + { + } + + poolItemDisposeStopwatch.Stop(); + + remainingWaitDuration = remainingWaitDuration > poolItemDisposeStopwatch.Elapsed + ? remainingWaitDuration.Subtract(poolItemDisposeStopwatch.Elapsed) + : TimeSpan.Zero; + } + + poolDisposeStopwatch.Stop(); + + Logger.LogInformation( + $"Disposed Kafka Consumer Pool ({Consumers.Count} consumers in {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms)."); + + if (poolDisposeStopwatch.Elapsed.TotalSeconds > 5.0) + { + Logger.LogWarning( + $"Disposing Kafka Consumer Pool got time greather than expected: {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms."); + } + + Consumers.Clear(); + } + } +} diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IConsumerPool.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IConsumerPool.cs new file mode 100644 index 0000000000..94c8a551e2 --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IConsumerPool.cs @@ -0,0 +1,10 @@ +using System; +using Confluent.Kafka; + +namespace Volo.Abp.Kafka +{ + public interface IConsumerPool : IDisposable + { + IConsumer Get(string groupId, string connectionName = null); + } +} diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaMessageConsumer.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaMessageConsumer.cs new file mode 100644 index 0000000000..87872b31a2 --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaMessageConsumer.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Confluent.Kafka; + +namespace Volo.Abp.Kafka +{ + public interface IKafkaMessageConsumer + { + void OnMessageReceived(Func, Task> callback); + } +} diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaMessageConsumerFactory.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaMessageConsumerFactory.cs new file mode 100644 index 0000000000..2b01b5a935 --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaMessageConsumerFactory.cs @@ -0,0 +1,19 @@ +namespace Volo.Abp.Kafka +{ + public interface IKafkaMessageConsumerFactory + { + /// + /// Creates a new . + /// Avoid to create too many consumers since they are + /// not disposed until end of the application. + /// + /// + /// + /// + /// + IKafkaMessageConsumer Create( + string topicName, + string groupId, + string connectionName = null); + } +} diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaSerializer.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaSerializer.cs new file mode 100644 index 0000000000..58e718831c --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IKafkaSerializer.cs @@ -0,0 +1,11 @@ +using System; + +namespace Volo.Abp.Kafka +{ + public interface IKafkaSerializer + { + byte[] Serialize(object obj); + + object Deserialize(byte[] value, Type type); + } +} diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IProducerPool.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IProducerPool.cs new file mode 100644 index 0000000000..2f94f23e2a --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/IProducerPool.cs @@ -0,0 +1,10 @@ +using System; +using Confluent.Kafka; + +namespace Volo.Abp.Kafka +{ + public interface IProducerPool : IDisposable + { + IProducer Get(string connectionName = null); + } +} diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaConnections.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaConnections.cs new file mode 100644 index 0000000000..159c798691 --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaConnections.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Confluent.Kafka; +using JetBrains.Annotations; + +namespace Volo.Abp.Kafka +{ + [Serializable] + public class KafkaConnections : Dictionary + { + public const string DefaultConnectionName = "Default"; + + [NotNull] + public ClientConfig Default + { + get => this[DefaultConnectionName]; + set => this[DefaultConnectionName] = Check.NotNull(value, nameof(value)); + } + + public KafkaConnections() + { + Default = new ClientConfig(); + } + + public ClientConfig GetOrDefault(string connectionName) + { + if (TryGetValue(connectionName, out var connectionFactory)) + { + return connectionFactory; + } + + return Default; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumer.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumer.cs new file mode 100644 index 0000000000..c22ca6756b --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumer.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; +using Confluent.Kafka; +using Confluent.Kafka.Admin; +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.Kafka +{ + public class KafkaMessageConsumer : IKafkaMessageConsumer, ITransientDependency, IDisposable + { + public ILogger Logger { get; set; } + + protected IConsumerPool ConsumerPool { get; } + + protected IExceptionNotifier ExceptionNotifier { get; } + + protected AbpKafkaOptions Options { get; } + + protected ConcurrentBag, Task>> Callbacks { get; } + + protected IConsumer Consumer { get; private set; } + + protected string ConnectionName { get; private set; } + + protected string GroupId { get; private set; } + + protected string TopicName { get; private set; } + + public KafkaMessageConsumer( + IConsumerPool consumerPool, + IExceptionNotifier exceptionNotifier, + IOptions options) + { + ConsumerPool = consumerPool; + ExceptionNotifier = exceptionNotifier; + Options = options.Value; + Logger = NullLogger.Instance; + + Callbacks = new ConcurrentBag, Task>>(); + } + + public virtual void Initialize( + [NotNull] string topicName, + [NotNull] string groupId, + string connectionName = null) + { + Check.NotNull(topicName, nameof(topicName)); + Check.NotNull(groupId, nameof(groupId)); + TopicName = topicName; + ConnectionName = connectionName ?? KafkaConnections.DefaultConnectionName; + GroupId = groupId; + + AsyncHelper.RunSync(CreateTopicAsync); + Consume(); + } + + public virtual void OnMessageReceived(Func, Task> callback) + { + Callbacks.Add(callback); + } + + protected virtual async Task CreateTopicAsync() + { + using (var adminClient = new AdminClientBuilder(Options.Connections.GetOrDefault(ConnectionName)).Build()) + { + var topic = new TopicSpecification + { + Name = TopicName, + NumPartitions = 1, + ReplicationFactor = 1 + }; + + Options.ConfigureTopic?.Invoke(topic); + + try + { + await adminClient.CreateTopicsAsync(new[] {topic}); + } + catch (CreateTopicsException e) + { + if (!e.Error.Reason.Contains($"Topic '{TopicName}' already exists")) + { + throw; + } + } + } + } + + protected virtual void Consume() + { + Consumer = ConsumerPool.Get(GroupId, ConnectionName); + + Task.Factory.StartNew(async () => + { + Consumer.Subscribe(TopicName); + + while (true) + { + try + { + var consumeResult = Consumer.Consume(); + + if (consumeResult.IsPartitionEOF) + { + continue; + } + + await HandleIncomingMessage(consumeResult); + } + catch (ConsumeException ex) + { + Logger.LogException(ex, LogLevel.Warning); + AsyncHelper.RunSync(() => ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning)); + } + } + }); + } + + protected virtual async Task HandleIncomingMessage(ConsumeResult consumeResult) + { + try + { + foreach (var callback in Callbacks) + { + await callback(consumeResult.Message); + } + + Consumer.Commit(consumeResult); + } + catch (Exception ex) + { + Logger.LogException(ex); + await ExceptionNotifier.NotifyAsync(ex); + } + } + + public virtual void Dispose() + { + if (Consumer == null) + { + return; + } + + Consumer.Close(); + Consumer.Dispose(); + } + } +} diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumerFactory.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumerFactory.cs new file mode 100644 index 0000000000..fb08aa4144 --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/KafkaMessageConsumerFactory.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Kafka +{ + public class KafkaMessageConsumerFactory : IKafkaMessageConsumerFactory, ISingletonDependency, IDisposable + { + protected IServiceScope ServiceScope { get; } + + public KafkaMessageConsumerFactory(IServiceScopeFactory serviceScopeFactory) + { + ServiceScope = serviceScopeFactory.CreateScope(); + } + + public IKafkaMessageConsumer Create( + string topicName, + string groupId, + string connectionName = null) + { + var consumer = ServiceScope.ServiceProvider.GetRequiredService(); + consumer.Initialize(topicName, groupId, connectionName); + return consumer; + } + + public void Dispose() + { + ServiceScope?.Dispose(); + } + } +} diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs new file mode 100644 index 0000000000..77c8898d42 --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; +using Confluent.Kafka; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Kafka +{ + public class ProducerPool : IProducerPool, ISingletonDependency + { + protected AbpKafkaOptions Options { get; } + + protected ConcurrentDictionary> Producers { get; } + + protected TimeSpan TotalDisposeWaitDuration { get; set; } = TimeSpan.FromSeconds(10); + + public ILogger Logger { get; set; } + + private bool _isDisposed; + + public ProducerPool(IOptions options) + { + Options = options.Value; + + Producers = new ConcurrentDictionary>(); + Logger = new NullLogger(); + } + + public virtual IProducer Get(string connectionName = null) + { + connectionName ??= KafkaConnections.DefaultConnectionName; + var config = Options.Connections.GetOrDefault(connectionName); + + Options.ConfigureProducer?.Invoke(new ProducerConfig(config)); + + return Producers.GetOrAdd( + connectionName, + new ProducerBuilder(config).Build()); + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (!Producers.Any()) + { + Logger.LogDebug($"Disposed producer pool with no producers in the pool."); + return; + } + + var poolDisposeStopwatch = Stopwatch.StartNew(); + + Logger.LogInformation($"Disposing producer pool ({Producers.Count} producers)."); + + var remainingWaitDuration = TotalDisposeWaitDuration; + + foreach (var producer in Producers.Values) + { + var poolItemDisposeStopwatch = Stopwatch.StartNew(); + + try + { + producer.Dispose(); + } + catch + { + } + + poolItemDisposeStopwatch.Stop(); + + remainingWaitDuration = remainingWaitDuration > poolItemDisposeStopwatch.Elapsed + ? remainingWaitDuration.Subtract(poolItemDisposeStopwatch.Elapsed) + : TimeSpan.Zero; + } + + poolDisposeStopwatch.Stop(); + + Logger.LogInformation($"Disposed Kafka Producer Pool ({Producers.Count} producers in {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms)."); + + if(poolDisposeStopwatch.Elapsed.TotalSeconds > 5.0) + { + Logger.LogWarning($"Disposing Kafka Producer Pool got time greather than expected: {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms."); + } + + Producers.Clear(); + } + } +} diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/Utf8JsonKafkaSerializer.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/Utf8JsonKafkaSerializer.cs new file mode 100644 index 0000000000..a04125f8a6 --- /dev/null +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/Utf8JsonKafkaSerializer.cs @@ -0,0 +1,27 @@ +using System; +using System.Text; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; + +namespace Volo.Abp.Kafka +{ + public class Utf8JsonKafkaSerializer : IKafkaSerializer, ITransientDependency + { + private readonly IJsonSerializer _jsonSerializer; + + public Utf8JsonKafkaSerializer(IJsonSerializer jsonSerializer) + { + _jsonSerializer = jsonSerializer; + } + + public byte[] Serialize(object obj) + { + return Encoding.UTF8.GetBytes(_jsonSerializer.Serialize(obj)); + } + + public object Deserialize(byte[] value, Type type) + { + return _jsonSerializer.Deserialize(type, Encoding.UTF8.GetString(value)); + } + } +} diff --git a/nupkg/common.ps1 b/nupkg/common.ps1 index 99244d5d6c..39d2700411 100644 --- a/nupkg/common.ps1 +++ b/nupkg/common.ps1 @@ -84,6 +84,7 @@ $projects = ( "framework/src/Volo.Abp.EntityFrameworkCore.SqlServer", "framework/src/Volo.Abp.EventBus", "framework/src/Volo.Abp.EventBus.RabbitMQ", + "framework/src/Volo.Abp.EventBus.Kafka", "framework/src/Volo.Abp.Features", "framework/src/Volo.Abp.FluentValidation", "framework/src/Volo.Abp.Guids", @@ -122,6 +123,7 @@ $projects = ( "framework/src/Volo.Abp.Validation.Abstractions", "framework/src/Volo.Abp.Validation", "framework/src/Volo.Abp.VirtualFileSystem", + "framework/src/Volo.Abp.Kafka" # modules/account "modules/account/src/Volo.Abp.Account.Application.Contracts", From 0e25b18aad27aded5468e693d56a94d5c23287ab Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Fri, 14 Aug 2020 15:12:05 +0800 Subject: [PATCH 002/147] Update kafka connection pool --- .../Volo/Abp/Kafka/ConsumerPool.cs | 22 ++++++++++-------- .../Volo/Abp/Kafka/ProducerPool.cs | 23 +++++++++++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs index 07af447e1a..b2995d1079 100644 --- a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ConsumerPool.cs @@ -34,17 +34,21 @@ namespace Volo.Abp.Kafka public virtual IConsumer Get(string groupId, string connectionName = null) { connectionName ??= KafkaConnections.DefaultConnectionName; - var config = new ConsumerConfig(Options.Connections.GetOrDefault(connectionName)) - { - GroupId = groupId, - EnableAutoCommit = false - }; - - Options.ConfigureConsumer?.Invoke(config); return Consumers.GetOrAdd( - connectionName, - new ConsumerBuilder(config).Build()); + connectionName, connection => + { + var config = new ConsumerConfig(Options.Connections.GetOrDefault(connection)) + { + GroupId = groupId, + EnableAutoCommit = false + }; + + Options.ConfigureConsumer?.Invoke(config); + + return new ConsumerBuilder(config).Build(); + } + ); } public void Dispose() diff --git a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs index 77c8898d42..c4b4b9ead0 100644 --- a/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs +++ b/framework/src/Volo.Abp.Kafka/Volo/Abp/Kafka/ProducerPool.cs @@ -33,13 +33,16 @@ namespace Volo.Abp.Kafka public virtual IProducer Get(string connectionName = null) { connectionName ??= KafkaConnections.DefaultConnectionName; - var config = Options.Connections.GetOrDefault(connectionName); - - Options.ConfigureProducer?.Invoke(new ProducerConfig(config)); - + return Producers.GetOrAdd( - connectionName, - new ProducerBuilder(config).Build()); + connectionName, connection => + { + var config = Options.Connections.GetOrDefault(connection); + + Options.ConfigureProducer?.Invoke(new ProducerConfig(config)); + + return new ProducerBuilder(config).Build(); + }); } public void Dispose() @@ -84,11 +87,13 @@ namespace Volo.Abp.Kafka poolDisposeStopwatch.Stop(); - Logger.LogInformation($"Disposed Kafka Producer Pool ({Producers.Count} producers in {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms)."); + Logger.LogInformation( + $"Disposed Kafka Producer Pool ({Producers.Count} producers in {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms)."); - if(poolDisposeStopwatch.Elapsed.TotalSeconds > 5.0) + if (poolDisposeStopwatch.Elapsed.TotalSeconds > 5.0) { - Logger.LogWarning($"Disposing Kafka Producer Pool got time greather than expected: {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms."); + Logger.LogWarning( + $"Disposing Kafka Producer Pool got time greather than expected: {poolDisposeStopwatch.Elapsed.TotalMilliseconds:0.00} ms."); } Producers.Clear(); From a3744f812ea75b03e3f8f453bd6242b6455081af Mon Sep 17 00:00:00 2001 From: Yunus Emre Kalkan Date: Thu, 20 Aug 2020 15:25:24 +0300 Subject: [PATCH 003/147] CmsKit: Should focus on the comment area after login and return back https://github.com/abpframework/abp/issues/5073 --- .../CmsKit/Shared/Components/Commenting/Default.cshtml | 2 +- .../CmsKit/Shared/Components/Commenting/default.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml index 5246f9a0f1..b8118e99ca 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml @@ -87,7 +87,7 @@ ; } -
+

diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js index 2950f8239b..8cce108ee5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js @@ -135,6 +135,14 @@ }); } + function focusOnHash($container) { + if (location.hash === ''){ + return; + } + + $('html, body').animate({ scrollTop: $(location.hash).offset().top + $(this).height() / 2 }, 'slow'); + } + function init() { registerReplyLinks($widget); registerEditLinks($widget); @@ -142,6 +150,8 @@ registerUpdateOfNewComment($widget); registerSubmissionOfNewComment($widget); + + focusOnHash($widget); } return { From 7fd9c6ab554cd7a0499960b52310b990e2bd62f2 Mon Sep 17 00:00:00 2001 From: bnymncoskuner Date: Thu, 20 Aug 2020 17:06:52 +0300 Subject: [PATCH 004/147] docs: create environment docs for Angular --- docs/en/UI/Angular/Environment.md | 99 +++++++++++++++++++++++++++++ docs/en/UI/Angular/Multi-Tenancy.md | 2 +- docs/en/docs-nav.json | 8 ++- 3 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 docs/en/UI/Angular/Environment.md diff --git a/docs/en/UI/Angular/Environment.md b/docs/en/UI/Angular/Environment.md new file mode 100644 index 0000000000..b1f47ac864 --- /dev/null +++ b/docs/en/UI/Angular/Environment.md @@ -0,0 +1,99 @@ +# Environment + +Current `Environment` Configuration holds sub config classes. + +```typescript +export interface Environment { + apis: Apis; + application: Application; + hmr?: boolean; + localization?: { defaultResourceName?: string }; + oAuthConfig: AuthConfig; + production: boolean; + remoteEnv?: RemoteEnv; +} +``` + +## Apis + +```typescript +export interface Apis { + [key: string]: ApiConfig; + default: ApiConfig; +} + +export interface ApiConfig { + [key: string]: string; + url: string; +} +``` + +Api config has to have a default config and it may have some additional ones for different modules. +I.e. you may want to connect to different Apis for different modules. + +Take a look at example + +```json +{ + ... + "apis": { + "default": { + "url": "https://localhost:8080", + }, + "AbpIdentity": { + "url": "https://localhost:9090", + } + } +} +``` + +When an api from `AbpIdentity` is called, the request will be sent to `"https://localhost:9090"`. +Everything else will be sent to `"https://localhost:8080"` + +## Application + +```typescript + export interface Application { + name: string; + baseUrl?: string; + logoUrl?: string; +} +``` + +* `name`: Name of the backend Application. It is also used by `logo.component` if `logoUrl` is not provided. +* `logoUrl`: Url of application logo. It is used by `logo.component` +* `baseUrl`: [For detailed information](./Multi-Tenancy.md#domain-tenant-resolver) + +## Localization + +You can read about `Localization` [here in detail](./Localization.md) + +## OAuthConfig + +..... + +## RemoteEnvironment + +To integrate an existing config json into environment, you need to set `remoteEnv` + +```typescript +export type customMergeFn = ( + localEnv: Partial, + remoteEnv: any, +) => Config.Environment; + +export interface RemoteEnv { + url: string; + mergeStrategy: 'deepmerge' | 'overwrite' | customMergeFn; + method?: string; + headers?: ABP.Dictionary; +} +``` + +* `url` *: Required. The url to be used to retrieve environment config +* `mergeStrategy`: Defines how local and remote environment json will be merged + * `deepmerge`: Both local and remote environment json will be merged recursively. If both config has same nested path, remote environment will be prioritized. + * `overwrite`: Remote environment will be used and local environment will be ignored. + * `customMergeFn`: You can also provide your own merge function as shown in the example. It will take two parameters, `localEnv: Partial` and `remoteEnv` and it needs to return a `Config.Environment` object. +* `method`: HTTP method to be used when retrieving environment config. Default: `GET` +* `headers`: If extra headers are needed for the request, it can be set through this field. \ No newline at end of file diff --git a/docs/en/UI/Angular/Multi-Tenancy.md b/docs/en/UI/Angular/Multi-Tenancy.md index fb9fb54f76..2f768de32a 100644 --- a/docs/en/UI/Angular/Multi-Tenancy.md +++ b/docs/en/UI/Angular/Multi-Tenancy.md @@ -20,7 +20,7 @@ On the page above, you can; You can switch between existing tenants by using the tenant switching component in the child pages of the `AccountLayoutComponent` (like Login page). Angular UI sends the selected tenant id to the backend as `__tenant` header on each request. -## Domain Tenant Resolver +

Domain Tenant Resolver

Angular UI can get the tenant name from the app running URL. You can determine the current tenant by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). To do this, you need to set the `application.baseUrl` property in the environment: diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 660f2b1b60..411cced68d 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -219,11 +219,11 @@ "items": [ { "text": "Email Sending System", - "path": "Emailing.md", + "path": "Emailing.md" }, { "text": "MailKit Integration", - "path": "MailKit.md", + "path": "MailKit.md" } ] }, @@ -423,6 +423,10 @@ "text": "Migration Guide v2.x to v3", "path": "UI/Angular/Migration-Guide-v3.md" }, + { + "text": "Environment", + "path": "UI/Angular/Environment.md" + }, { "text": "Service Proxies", "path": "UI/Angular/Service-Proxies.md" From 1bb107bde4443854773c8509cd4d2cd005fdb80c Mon Sep 17 00:00:00 2001 From: bnymncoskuner Date: Fri, 21 Aug 2020 09:02:33 +0300 Subject: [PATCH 005/147] docs: improve environment.md --- docs/en/UI/Angular/Environment.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/en/UI/Angular/Environment.md b/docs/en/UI/Angular/Environment.md index b1f47ac864..22acc369d9 100644 --- a/docs/en/UI/Angular/Environment.md +++ b/docs/en/UI/Angular/Environment.md @@ -35,7 +35,7 @@ Take a look at example ```json { - ... + // ... "apis": { "default": { "url": "https://localhost:8080", @@ -43,7 +43,8 @@ Take a look at example "AbpIdentity": { "url": "https://localhost:9090", } - } + }, + // ... } ``` @@ -68,9 +69,9 @@ Everything else will be sent to `"https://localhost:8080"` You can read about `Localization` [here in detail](./Localization.md) -## OAuthConfig +## AuthConfig -..... +For authentication, we use [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) ## RemoteEnvironment From 45458342e4fd46e6d51c8761000241bd00d57702 Mon Sep 17 00:00:00 2001 From: bnymncoskuner Date: Fri, 21 Aug 2020 12:17:00 +0300 Subject: [PATCH 006/147] docs: improve environment md --- docs/en/UI/Angular/Environment.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/en/UI/Angular/Environment.md b/docs/en/UI/Angular/Environment.md index 22acc369d9..34b7a38370 100644 --- a/docs/en/UI/Angular/Environment.md +++ b/docs/en/UI/Angular/Environment.md @@ -1,6 +1,8 @@ # Environment -Current `Environment` Configuration holds sub config classes. +Every application needs some ** environment ** variables. In Angular world, this is usually managed by `environment.ts`, `environment.prod.ts` and so on. It is the same for Abp as well. + +Current `Environment` configuration holds sub config classes as follows: ```typescript export interface Environment { @@ -31,7 +33,7 @@ export interface ApiConfig { Api config has to have a default config and it may have some additional ones for different modules. I.e. you may want to connect to different Apis for different modules. -Take a look at example +Take a look at following example ```json { @@ -62,7 +64,7 @@ Everything else will be sent to `"https://localhost:8080"` ``` * `name`: Name of the backend Application. It is also used by `logo.component` if `logoUrl` is not provided. -* `logoUrl`: Url of application logo. It is used by `logo.component` +* `logoUrl`: Url of the application logo. It is used by `logo.component` * `baseUrl`: [For detailed information](./Multi-Tenancy.md#domain-tenant-resolver) ## Localization @@ -71,11 +73,14 @@ You can read about `Localization` [here in detail](./Localization.md) ## AuthConfig -For authentication, we use [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) +For authentication, we use angular-oauth2-oidc. Please check their [docs](https://github.com/manfredsteyer/angular-oauth2-oidc) out ## RemoteEnvironment -To integrate an existing config json into environment, you need to set `remoteEnv` +Some applications need to integrate an existing config into the `environment` used throughout the application. +Abp Framework supports this out of box. + +To integrate an existing config json into the `environment`, you need to set `remoteEnv` ```typescript export type customMergeFn = ( @@ -92,9 +97,9 @@ export interface RemoteEnv { ``` * `url` *: Required. The url to be used to retrieve environment config -* `mergeStrategy`: Defines how local and remote environment json will be merged - * `deepmerge`: Both local and remote environment json will be merged recursively. If both config has same nested path, remote environment will be prioritized. - * `overwrite`: Remote environment will be used and local environment will be ignored. +* `mergeStrategy` *: Required. Defines how the local and the remote `environment` json will be merged + * `deepmerge`: Both local and remote `environment` json will be merged recursively. If both configs have same nested path, the remote `environment` will be prioritized. + * `overwrite`: Remote `environment` will be used and local environment will be ignored. * `customMergeFn`: You can also provide your own merge function as shown in the example. It will take two parameters, `localEnv: Partial` and `remoteEnv` and it needs to return a `Config.Environment` object. * `method`: HTTP method to be used when retrieving environment config. Default: `GET` * `headers`: If extra headers are needed for the request, it can be set through this field. \ No newline at end of file From d410e8059ced47fbaadc04536bb2595b03c04562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20=C3=87otur?= Date: Fri, 21 Aug 2020 23:31:59 +0300 Subject: [PATCH 007/147] Introduce @abp/cropperjs npm package CropperJs script and style contributor are added --- .../CropperJs/CropperJsScriptContributor.cs | 13 +++++++++++++ .../Packages/CropperJs/CropperJsStyleContributor.cs | 13 +++++++++++++ npm/packs/cropperjs/abp.resourcemapping.js | 6 ++++++ npm/packs/cropperjs/package.json | 11 +++++++++++ 4 files changed, 43 insertions(+) create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/CropperJs/CropperJsScriptContributor.cs create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/CropperJs/CropperJsStyleContributor.cs create mode 100644 npm/packs/cropperjs/abp.resourcemapping.js create mode 100644 npm/packs/cropperjs/package.json diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/CropperJs/CropperJsScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/CropperJs/CropperJsScriptContributor.cs new file mode 100644 index 0000000000..fbfd19f765 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/CropperJs/CropperJsScriptContributor.cs @@ -0,0 +1,13 @@ +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.CropperJs +{ + public class CropperJsScriptContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/libs/cropperjs/js/cropper.min.js"); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/CropperJs/CropperJsStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/CropperJs/CropperJsStyleContributor.cs new file mode 100644 index 0000000000..1e6f78ff3f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/CropperJs/CropperJsStyleContributor.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.CropperJs +{ + public class CropperJsStyleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/libs/cropperjs/css/cropper.min.css"); + } + } +} \ No newline at end of file diff --git a/npm/packs/cropperjs/abp.resourcemapping.js b/npm/packs/cropperjs/abp.resourcemapping.js new file mode 100644 index 0000000000..68cc55b64e --- /dev/null +++ b/npm/packs/cropperjs/abp.resourcemapping.js @@ -0,0 +1,6 @@ +module.exports = { + mappings: { + "@node_modules/cropperjs/dist/cropper.min.js": "@libs/cropperjs/js/", + "@node_modules/cropperjs/dist/cropper.min.css": "@libs/cropperjs/css/" + } +} \ No newline at end of file diff --git a/npm/packs/cropperjs/package.json b/npm/packs/cropperjs/package.json new file mode 100644 index 0000000000..9ce511e71b --- /dev/null +++ b/npm/packs/cropperjs/package.json @@ -0,0 +1,11 @@ +{ + "version": "3.0.5", + "name": "@abp/cropperjs", + "publishConfig": { + "access": "public" + }, + "dependencies": { + "cropperjs": "^1.5.7" + }, + "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431" +} From d6173997caaa056eaf6d1af16249a16ff8036aa0 Mon Sep 17 00:00:00 2001 From: Erol Arkat Date: Mon, 24 Aug 2020 10:03:49 +0300 Subject: [PATCH 008/147] version update --- common.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.props b/common.props index eea8755d42..1d2d986131 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 3.1.0 + 3.1.0-rc.1 $(NoWarn);CS1591 https://abp.io/assets/abp_nupkg.png https://abp.io From d59dd4a28821c018e11ba993e9650dc3cc137158 Mon Sep 17 00:00:00 2001 From: Yunus Emre Kalkan Date: Mon, 24 Aug 2020 10:19:35 +0300 Subject: [PATCH 009/147] pull/5134 requested changes --- .../Pages/CmsKit/Shared/Components/Commenting/default.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js index 8cce108ee5..f7bf554638 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js @@ -136,11 +136,15 @@ } function focusOnHash($container) { - if (location.hash === ''){ + if (!location.hash.toLowerCase().startsWith('#cms-comment')){ return; } - $('html, body').animate({ scrollTop: $(location.hash).offset().top + $(this).height() / 2 }, 'slow'); + $('html, body').animate( + { + scrollTop: $(location.hash).offset().top + $(this).height() / 2 + }, + 'slow'); } function init() { From 6db86830a9c1636b170d939011d82b6662a43928 Mon Sep 17 00:00:00 2001 From: Bunyamin Coskuner Date: Mon, 24 Aug 2020 10:37:34 +0300 Subject: [PATCH 010/147] docs: capitalize ABP Co-authored-by: Mehmet Erim <34455572+mehmet-erim@users.noreply.github.com> --- docs/en/UI/Angular/Environment.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/UI/Angular/Environment.md b/docs/en/UI/Angular/Environment.md index 34b7a38370..cfdfb4cce1 100644 --- a/docs/en/UI/Angular/Environment.md +++ b/docs/en/UI/Angular/Environment.md @@ -1,6 +1,6 @@ # Environment -Every application needs some ** environment ** variables. In Angular world, this is usually managed by `environment.ts`, `environment.prod.ts` and so on. It is the same for Abp as well. +Every application needs some ** environment ** variables. In Angular world, this is usually managed by `environment.ts`, `environment.prod.ts` and so on. It is the same for ABP as well. Current `Environment` configuration holds sub config classes as follows: @@ -102,4 +102,4 @@ export interface RemoteEnv { * `overwrite`: Remote `environment` will be used and local environment will be ignored. * `customMergeFn`: You can also provide your own merge function as shown in the example. It will take two parameters, `localEnv: Partial` and `remoteEnv` and it needs to return a `Config.Environment` object. * `method`: HTTP method to be used when retrieving environment config. Default: `GET` -* `headers`: If extra headers are needed for the request, it can be set through this field. \ No newline at end of file +* `headers`: If extra headers are needed for the request, it can be set through this field. From b0dbd4e8f1a34e10942f92fa3ea89cec67ca2492 Mon Sep 17 00:00:00 2001 From: Bunyamin Coskuner Date: Mon, 24 Aug 2020 10:38:51 +0300 Subject: [PATCH 011/147] docs: remove hmr and localization fields Co-authored-by: Mehmet Erim <34455572+mehmet-erim@users.noreply.github.com> --- docs/en/UI/Angular/Environment.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/en/UI/Angular/Environment.md b/docs/en/UI/Angular/Environment.md index cfdfb4cce1..5ffe7b1ba5 100644 --- a/docs/en/UI/Angular/Environment.md +++ b/docs/en/UI/Angular/Environment.md @@ -8,8 +8,6 @@ Current `Environment` configuration holds sub config classes as follows: export interface Environment { apis: Apis; application: Application; - hmr?: boolean; - localization?: { defaultResourceName?: string }; oAuthConfig: AuthConfig; production: boolean; remoteEnv?: RemoteEnv; From 9e4b2a7335259bf3cc20c795c687be0031adc15d Mon Sep 17 00:00:00 2001 From: Bunyamin Coskuner Date: Mon, 24 Aug 2020 10:39:16 +0300 Subject: [PATCH 012/147] docs: remove localization part Co-authored-by: Mehmet Erim <34455572+mehmet-erim@users.noreply.github.com> --- docs/en/UI/Angular/Environment.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/en/UI/Angular/Environment.md b/docs/en/UI/Angular/Environment.md index 5ffe7b1ba5..af50e34155 100644 --- a/docs/en/UI/Angular/Environment.md +++ b/docs/en/UI/Angular/Environment.md @@ -65,9 +65,6 @@ Everything else will be sent to `"https://localhost:8080"` * `logoUrl`: Url of the application logo. It is used by `logo.component` * `baseUrl`: [For detailed information](./Multi-Tenancy.md#domain-tenant-resolver) -## Localization - -You can read about `Localization` [here in detail](./Localization.md) ## AuthConfig From ae9f593b5e990a27f40b67eadcfb15f24d54d5cb Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Mon, 24 Aug 2020 15:42:57 +0800 Subject: [PATCH 013/147] Add abp.luxon.js --- .../Packages/Luxon/LuxonScriptContributor.cs | 1 + npm/packs/luxon/abp.resourcemapping.js | 1 + npm/packs/luxon/package.json | 1 + npm/packs/luxon/src/abp.luxon.js | 48 +++++++++++++++++++ .../wwwroot/libs/abp/luxon/abp.luxon.js | 48 +++++++++++++++++++ .../wwwroot/libs/abp/luxon/abp.luxon.js | 48 +++++++++++++++++++ .../wwwroot/libs/abp/luxon/abp.luxon.js | 48 +++++++++++++++++++ .../wwwroot/libs/abp/luxon/abp.luxon.js | 48 +++++++++++++++++++ .../wwwroot/libs/abp/luxon/abp.luxon.js | 48 +++++++++++++++++++ .../wwwroot/libs/abp/luxon/abp.luxon.js | 48 +++++++++++++++++++ .../wwwroot/libs/abp/luxon/abp.luxon.js | 48 +++++++++++++++++++ 11 files changed, 387 insertions(+) create mode 100644 npm/packs/luxon/src/abp.luxon.js create mode 100644 templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/wwwroot/libs/abp/luxon/abp.luxon.js create mode 100644 templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/wwwroot/libs/abp/luxon/abp.luxon.js create mode 100644 templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/wwwroot/libs/abp/luxon/abp.luxon.js create mode 100644 templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/wwwroot/libs/abp/luxon/abp.luxon.js create mode 100644 templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/wwwroot/libs/abp/luxon/abp.luxon.js create mode 100644 templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/wwwroot/libs/abp/luxon/abp.luxon.js create mode 100644 templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/wwwroot/libs/abp/luxon/abp.luxon.js diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Luxon/LuxonScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Luxon/LuxonScriptContributor.cs index 3d2a1dc774..afa3ed3994 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Luxon/LuxonScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Luxon/LuxonScriptContributor.cs @@ -8,6 +8,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Luxon public override void ConfigureBundle(BundleConfigurationContext context) { context.Files.AddIfNotContains("/libs/luxon/luxon.min.js"); + context.Files.AddIfNotContains("/libs/abp/luxon/abp.luxon.js"); } } } diff --git a/npm/packs/luxon/abp.resourcemapping.js b/npm/packs/luxon/abp.resourcemapping.js index a037c5bf64..0afc74f204 100644 --- a/npm/packs/luxon/abp.resourcemapping.js +++ b/npm/packs/luxon/abp.resourcemapping.js @@ -1,5 +1,6 @@ module.exports = { mappings: { "@node_modules/luxon/build/global/*.*": "@libs/luxon/" + "@node_modules/@abp/luxon/src/*.*": "@libs/abp/luxon/" } } \ No newline at end of file diff --git a/npm/packs/luxon/package.json b/npm/packs/luxon/package.json index 57a6053319..4fd02d0e18 100644 --- a/npm/packs/luxon/package.json +++ b/npm/packs/luxon/package.json @@ -5,6 +5,7 @@ "access": "public" }, "dependencies": { + "@abp/core": "~3.0.5", "luxon": "^1.24.1" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431" diff --git a/npm/packs/luxon/src/abp.luxon.js b/npm/packs/luxon/src/abp.luxon.js new file mode 100644 index 0000000000..15f8cd57be --- /dev/null +++ b/npm/packs/luxon/src/abp.luxon.js @@ -0,0 +1,48 @@ +var abp = abp || {}; +(function () { + + if (!luxon) { + throw "abp/luxon library requires the luxon library included to the page!"; + } + + /* TIMING *************************************************/ + + abp.timing = abp.timing || {}; + + /* NOTIFICATION *********************************************/ + + var setObjectValue = function (obj, property, value) { + if (typeof property === "string") { + property = property.split('.'); + } + + if (property.length > 1) { + var p = property.shift(); + setObjectValue(obj[p], property, value); + } else { + obj[property[0]] = value; + } + } + + var getObjectValue = function (obj, property) { + return property.split('.').reduce((a, v) => a[v], obj) + } + + abp.timing.convertFieldsToIsoDate = function (form, fields) { + for (var field of fields) { + var dateTime = luxon.DateTime + .fromFormat( + getObjectValue(form, field), + abp.localization.currentCulture.dateTimeFormat.shortDatePattern, + {locale: abp.localization.currentCulture.cultureName} + ); + + if (!dateTime.invalid) { + setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss")) + } + } + + return form; + } + +})(jQuery); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/wwwroot/libs/abp/luxon/abp.luxon.js b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/wwwroot/libs/abp/luxon/abp.luxon.js new file mode 100644 index 0000000000..15f8cd57be --- /dev/null +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/wwwroot/libs/abp/luxon/abp.luxon.js @@ -0,0 +1,48 @@ +var abp = abp || {}; +(function () { + + if (!luxon) { + throw "abp/luxon library requires the luxon library included to the page!"; + } + + /* TIMING *************************************************/ + + abp.timing = abp.timing || {}; + + /* NOTIFICATION *********************************************/ + + var setObjectValue = function (obj, property, value) { + if (typeof property === "string") { + property = property.split('.'); + } + + if (property.length > 1) { + var p = property.shift(); + setObjectValue(obj[p], property, value); + } else { + obj[property[0]] = value; + } + } + + var getObjectValue = function (obj, property) { + return property.split('.').reduce((a, v) => a[v], obj) + } + + abp.timing.convertFieldsToIsoDate = function (form, fields) { + for (var field of fields) { + var dateTime = luxon.DateTime + .fromFormat( + getObjectValue(form, field), + abp.localization.currentCulture.dateTimeFormat.shortDatePattern, + {locale: abp.localization.currentCulture.cultureName} + ); + + if (!dateTime.invalid) { + setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss")) + } + } + + return form; + } + +})(jQuery); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/wwwroot/libs/abp/luxon/abp.luxon.js b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/wwwroot/libs/abp/luxon/abp.luxon.js new file mode 100644 index 0000000000..15f8cd57be --- /dev/null +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/wwwroot/libs/abp/luxon/abp.luxon.js @@ -0,0 +1,48 @@ +var abp = abp || {}; +(function () { + + if (!luxon) { + throw "abp/luxon library requires the luxon library included to the page!"; + } + + /* TIMING *************************************************/ + + abp.timing = abp.timing || {}; + + /* NOTIFICATION *********************************************/ + + var setObjectValue = function (obj, property, value) { + if (typeof property === "string") { + property = property.split('.'); + } + + if (property.length > 1) { + var p = property.shift(); + setObjectValue(obj[p], property, value); + } else { + obj[property[0]] = value; + } + } + + var getObjectValue = function (obj, property) { + return property.split('.').reduce((a, v) => a[v], obj) + } + + abp.timing.convertFieldsToIsoDate = function (form, fields) { + for (var field of fields) { + var dateTime = luxon.DateTime + .fromFormat( + getObjectValue(form, field), + abp.localization.currentCulture.dateTimeFormat.shortDatePattern, + {locale: abp.localization.currentCulture.cultureName} + ); + + if (!dateTime.invalid) { + setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss")) + } + } + + return form; + } + +})(jQuery); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/wwwroot/libs/abp/luxon/abp.luxon.js b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/wwwroot/libs/abp/luxon/abp.luxon.js new file mode 100644 index 0000000000..15f8cd57be --- /dev/null +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/wwwroot/libs/abp/luxon/abp.luxon.js @@ -0,0 +1,48 @@ +var abp = abp || {}; +(function () { + + if (!luxon) { + throw "abp/luxon library requires the luxon library included to the page!"; + } + + /* TIMING *************************************************/ + + abp.timing = abp.timing || {}; + + /* NOTIFICATION *********************************************/ + + var setObjectValue = function (obj, property, value) { + if (typeof property === "string") { + property = property.split('.'); + } + + if (property.length > 1) { + var p = property.shift(); + setObjectValue(obj[p], property, value); + } else { + obj[property[0]] = value; + } + } + + var getObjectValue = function (obj, property) { + return property.split('.').reduce((a, v) => a[v], obj) + } + + abp.timing.convertFieldsToIsoDate = function (form, fields) { + for (var field of fields) { + var dateTime = luxon.DateTime + .fromFormat( + getObjectValue(form, field), + abp.localization.currentCulture.dateTimeFormat.shortDatePattern, + {locale: abp.localization.currentCulture.cultureName} + ); + + if (!dateTime.invalid) { + setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss")) + } + } + + return form; + } + +})(jQuery); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/wwwroot/libs/abp/luxon/abp.luxon.js b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/wwwroot/libs/abp/luxon/abp.luxon.js new file mode 100644 index 0000000000..15f8cd57be --- /dev/null +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/wwwroot/libs/abp/luxon/abp.luxon.js @@ -0,0 +1,48 @@ +var abp = abp || {}; +(function () { + + if (!luxon) { + throw "abp/luxon library requires the luxon library included to the page!"; + } + + /* TIMING *************************************************/ + + abp.timing = abp.timing || {}; + + /* NOTIFICATION *********************************************/ + + var setObjectValue = function (obj, property, value) { + if (typeof property === "string") { + property = property.split('.'); + } + + if (property.length > 1) { + var p = property.shift(); + setObjectValue(obj[p], property, value); + } else { + obj[property[0]] = value; + } + } + + var getObjectValue = function (obj, property) { + return property.split('.').reduce((a, v) => a[v], obj) + } + + abp.timing.convertFieldsToIsoDate = function (form, fields) { + for (var field of fields) { + var dateTime = luxon.DateTime + .fromFormat( + getObjectValue(form, field), + abp.localization.currentCulture.dateTimeFormat.shortDatePattern, + {locale: abp.localization.currentCulture.cultureName} + ); + + if (!dateTime.invalid) { + setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss")) + } + } + + return form; + } + +})(jQuery); diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/wwwroot/libs/abp/luxon/abp.luxon.js b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/wwwroot/libs/abp/luxon/abp.luxon.js new file mode 100644 index 0000000000..15f8cd57be --- /dev/null +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.IdentityServer/wwwroot/libs/abp/luxon/abp.luxon.js @@ -0,0 +1,48 @@ +var abp = abp || {}; +(function () { + + if (!luxon) { + throw "abp/luxon library requires the luxon library included to the page!"; + } + + /* TIMING *************************************************/ + + abp.timing = abp.timing || {}; + + /* NOTIFICATION *********************************************/ + + var setObjectValue = function (obj, property, value) { + if (typeof property === "string") { + property = property.split('.'); + } + + if (property.length > 1) { + var p = property.shift(); + setObjectValue(obj[p], property, value); + } else { + obj[property[0]] = value; + } + } + + var getObjectValue = function (obj, property) { + return property.split('.').reduce((a, v) => a[v], obj) + } + + abp.timing.convertFieldsToIsoDate = function (form, fields) { + for (var field of fields) { + var dateTime = luxon.DateTime + .fromFormat( + getObjectValue(form, field), + abp.localization.currentCulture.dateTimeFormat.shortDatePattern, + {locale: abp.localization.currentCulture.cultureName} + ); + + if (!dateTime.invalid) { + setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss")) + } + } + + return form; + } + +})(jQuery); diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/wwwroot/libs/abp/luxon/abp.luxon.js b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/wwwroot/libs/abp/luxon/abp.luxon.js new file mode 100644 index 0000000000..15f8cd57be --- /dev/null +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/wwwroot/libs/abp/luxon/abp.luxon.js @@ -0,0 +1,48 @@ +var abp = abp || {}; +(function () { + + if (!luxon) { + throw "abp/luxon library requires the luxon library included to the page!"; + } + + /* TIMING *************************************************/ + + abp.timing = abp.timing || {}; + + /* NOTIFICATION *********************************************/ + + var setObjectValue = function (obj, property, value) { + if (typeof property === "string") { + property = property.split('.'); + } + + if (property.length > 1) { + var p = property.shift(); + setObjectValue(obj[p], property, value); + } else { + obj[property[0]] = value; + } + } + + var getObjectValue = function (obj, property) { + return property.split('.').reduce((a, v) => a[v], obj) + } + + abp.timing.convertFieldsToIsoDate = function (form, fields) { + for (var field of fields) { + var dateTime = luxon.DateTime + .fromFormat( + getObjectValue(form, field), + abp.localization.currentCulture.dateTimeFormat.shortDatePattern, + {locale: abp.localization.currentCulture.cultureName} + ); + + if (!dateTime.invalid) { + setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss")) + } + } + + return form; + } + +})(jQuery); diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/wwwroot/libs/abp/luxon/abp.luxon.js b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/wwwroot/libs/abp/luxon/abp.luxon.js new file mode 100644 index 0000000000..15f8cd57be --- /dev/null +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/wwwroot/libs/abp/luxon/abp.luxon.js @@ -0,0 +1,48 @@ +var abp = abp || {}; +(function () { + + if (!luxon) { + throw "abp/luxon library requires the luxon library included to the page!"; + } + + /* TIMING *************************************************/ + + abp.timing = abp.timing || {}; + + /* NOTIFICATION *********************************************/ + + var setObjectValue = function (obj, property, value) { + if (typeof property === "string") { + property = property.split('.'); + } + + if (property.length > 1) { + var p = property.shift(); + setObjectValue(obj[p], property, value); + } else { + obj[property[0]] = value; + } + } + + var getObjectValue = function (obj, property) { + return property.split('.').reduce((a, v) => a[v], obj) + } + + abp.timing.convertFieldsToIsoDate = function (form, fields) { + for (var field of fields) { + var dateTime = luxon.DateTime + .fromFormat( + getObjectValue(form, field), + abp.localization.currentCulture.dateTimeFormat.shortDatePattern, + {locale: abp.localization.currentCulture.cultureName} + ); + + if (!dateTime.invalid) { + setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss")) + } + } + + return form; + } + +})(jQuery); From 155f72f2f3d765a60578e978a769238c5797631e Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Mon, 24 Aug 2020 16:07:45 +0800 Subject: [PATCH 014/147] Add abp.luxon.js to modules --- .../wwwroot/libs/abp/luxon/abp.luxon.js | 48 +++++++++++++++++++ .../wwwroot/libs/abp/luxon/abp.luxon.js | 48 +++++++++++++++++++ .../wwwroot/libs/abp/luxon/abp.luxon.js | 48 +++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 modules/blogging/app/Volo.BloggingTestApp/wwwroot/libs/abp/luxon/abp.luxon.js create mode 100644 modules/docs/app/VoloDocs.Web/wwwroot/libs/abp/luxon/abp.luxon.js create mode 100644 modules/virtual-file-explorer/app/Volo.Abp.VirtualFileExplorer.DemoApp/wwwroot/libs/abp/luxon/abp.luxon.js diff --git a/modules/blogging/app/Volo.BloggingTestApp/wwwroot/libs/abp/luxon/abp.luxon.js b/modules/blogging/app/Volo.BloggingTestApp/wwwroot/libs/abp/luxon/abp.luxon.js new file mode 100644 index 0000000000..15f8cd57be --- /dev/null +++ b/modules/blogging/app/Volo.BloggingTestApp/wwwroot/libs/abp/luxon/abp.luxon.js @@ -0,0 +1,48 @@ +var abp = abp || {}; +(function () { + + if (!luxon) { + throw "abp/luxon library requires the luxon library included to the page!"; + } + + /* TIMING *************************************************/ + + abp.timing = abp.timing || {}; + + /* NOTIFICATION *********************************************/ + + var setObjectValue = function (obj, property, value) { + if (typeof property === "string") { + property = property.split('.'); + } + + if (property.length > 1) { + var p = property.shift(); + setObjectValue(obj[p], property, value); + } else { + obj[property[0]] = value; + } + } + + var getObjectValue = function (obj, property) { + return property.split('.').reduce((a, v) => a[v], obj) + } + + abp.timing.convertFieldsToIsoDate = function (form, fields) { + for (var field of fields) { + var dateTime = luxon.DateTime + .fromFormat( + getObjectValue(form, field), + abp.localization.currentCulture.dateTimeFormat.shortDatePattern, + {locale: abp.localization.currentCulture.cultureName} + ); + + if (!dateTime.invalid) { + setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss")) + } + } + + return form; + } + +})(jQuery); diff --git a/modules/docs/app/VoloDocs.Web/wwwroot/libs/abp/luxon/abp.luxon.js b/modules/docs/app/VoloDocs.Web/wwwroot/libs/abp/luxon/abp.luxon.js new file mode 100644 index 0000000000..15f8cd57be --- /dev/null +++ b/modules/docs/app/VoloDocs.Web/wwwroot/libs/abp/luxon/abp.luxon.js @@ -0,0 +1,48 @@ +var abp = abp || {}; +(function () { + + if (!luxon) { + throw "abp/luxon library requires the luxon library included to the page!"; + } + + /* TIMING *************************************************/ + + abp.timing = abp.timing || {}; + + /* NOTIFICATION *********************************************/ + + var setObjectValue = function (obj, property, value) { + if (typeof property === "string") { + property = property.split('.'); + } + + if (property.length > 1) { + var p = property.shift(); + setObjectValue(obj[p], property, value); + } else { + obj[property[0]] = value; + } + } + + var getObjectValue = function (obj, property) { + return property.split('.').reduce((a, v) => a[v], obj) + } + + abp.timing.convertFieldsToIsoDate = function (form, fields) { + for (var field of fields) { + var dateTime = luxon.DateTime + .fromFormat( + getObjectValue(form, field), + abp.localization.currentCulture.dateTimeFormat.shortDatePattern, + {locale: abp.localization.currentCulture.cultureName} + ); + + if (!dateTime.invalid) { + setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss")) + } + } + + return form; + } + +})(jQuery); diff --git a/modules/virtual-file-explorer/app/Volo.Abp.VirtualFileExplorer.DemoApp/wwwroot/libs/abp/luxon/abp.luxon.js b/modules/virtual-file-explorer/app/Volo.Abp.VirtualFileExplorer.DemoApp/wwwroot/libs/abp/luxon/abp.luxon.js new file mode 100644 index 0000000000..15f8cd57be --- /dev/null +++ b/modules/virtual-file-explorer/app/Volo.Abp.VirtualFileExplorer.DemoApp/wwwroot/libs/abp/luxon/abp.luxon.js @@ -0,0 +1,48 @@ +var abp = abp || {}; +(function () { + + if (!luxon) { + throw "abp/luxon library requires the luxon library included to the page!"; + } + + /* TIMING *************************************************/ + + abp.timing = abp.timing || {}; + + /* NOTIFICATION *********************************************/ + + var setObjectValue = function (obj, property, value) { + if (typeof property === "string") { + property = property.split('.'); + } + + if (property.length > 1) { + var p = property.shift(); + setObjectValue(obj[p], property, value); + } else { + obj[property[0]] = value; + } + } + + var getObjectValue = function (obj, property) { + return property.split('.').reduce((a, v) => a[v], obj) + } + + abp.timing.convertFieldsToIsoDate = function (form, fields) { + for (var field of fields) { + var dateTime = luxon.DateTime + .fromFormat( + getObjectValue(form, field), + abp.localization.currentCulture.dateTimeFormat.shortDatePattern, + {locale: abp.localization.currentCulture.cultureName} + ); + + if (!dateTime.invalid) { + setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss")) + } + } + + return form; + } + +})(jQuery); From 571c08fc6d9bc8e32ec8984d08b01bd94465a584 Mon Sep 17 00:00:00 2001 From: bnymncoskuner Date: Mon, 24 Aug 2020 13:33:44 +0300 Subject: [PATCH 015/147] build: ts-toolbelt version is fixed at 6.15.4 --- npm/ng-packs/package.json | 2 +- npm/ng-packs/yarn.lock | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/npm/ng-packs/package.json b/npm/ng-packs/package.json index 9ec24387d0..552af1182a 100644 --- a/npm/ng-packs/package.json +++ b/npm/ng-packs/package.json @@ -88,7 +88,7 @@ "snq": "^1.0.3", "symlink-manager": "^1.5.0", "ts-node": "~7.0.0", - "ts-toolbelt": "^6.9.9", + "ts-toolbelt": "6.15.4", "tsickle": "^0.38.1", "tslint": "~6.1.0", "typescript": "~3.9.5", diff --git a/npm/ng-packs/yarn.lock b/npm/ng-packs/yarn.lock index ffaf8fcb65..cd90d0ffb9 100644 --- a/npm/ng-packs/yarn.lock +++ b/npm/ng-packs/yarn.lock @@ -13241,6 +13241,11 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== +ts-toolbelt@6.15.4: + version "6.15.4" + resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-6.15.4.tgz#f7fad584e197d0f495f77b5e3ee75a8f4c4dd3da" + integrity sha512-Ifp2yNo4I8q5UwNARUBMnBiFpv5DEtTbCtS8RCjjLOz+PNcThbjUsPJCK3hRnz0dTygM1Fi3Mgvnt/DoKUTU2g== + ts-toolbelt@^6.9.9: version "6.15.5" resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz#cb3b43ed725cb63644782c64fbcad7d8f28c0a83" From a221fa0738e8b5c46215f8b54c71b99974064743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 24 Aug 2020 14:14:05 +0300 Subject: [PATCH 016/147] Update Post.md --- docs/en/Blog-Posts/2020-08-20 v3_1_Release/Post.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Blog-Posts/2020-08-20 v3_1_Release/Post.md b/docs/en/Blog-Posts/2020-08-20 v3_1_Release/Post.md index b61d77e3c8..0dcfb183b7 100644 --- a/docs/en/Blog-Posts/2020-08-20 v3_1_Release/Post.md +++ b/docs/en/Blog-Posts/2020-08-20 v3_1_Release/Post.md @@ -66,7 +66,7 @@ If you already have a solution and want to use/test the latest RC/Preview versio ### ABP Commercial Only -* We've **moved favicons** into `/wwwroot/images/favicon/` folder for the ASP.NET Core **MVC / Razor Page UI** applications. There are 10 favicon related files (including the `favicon.ico`) under this directory to better work with different browser and cases. You can create a new application to check this folder and copy the files into your own application. Then you can customize the icons for your own brand. +* We've **moved favicons** into `/wwwroot/images/favicon/` folder for the ASP.NET Core **MVC / Razor Page UI** applications. There are 10 favicon related files (including the `favicon.ico`) under this directory to better work with different browser and cases. You can create a new application to check this folder and copy the files into your own application. Then you can customize the icons for your own brand (hint: you can use a tool [like that](https://realfavicongenerator.net/) to create the favicons with various formats). * Removed direct **Twitter & Facebook social login integrations** from the [account module](https://commercial.abp.io/modules/Volo.Account.Pro), for **MVC / Razor Pages UI**. Follow [this documentation](https://github.com/abpframework/abp/blob/dev/docs/en/Authentication/Social-External-Logins.md) to easily add social logins to your applications if you need. The account module provides all the infrastructure to handle social/external logins, you just need to configure it. ## What's New with the ABP Framework 3.1 RC.1 From 12b4f79559d314bcddcf4ec17fce6a33db2b5281 Mon Sep 17 00:00:00 2001 From: Yunus Emre Kalkan Date: Mon, 24 Aug 2020 15:36:35 +0300 Subject: [PATCH 017/147] cmskit: focus after login improvements --- .../Components/Commenting/Default.cshtml | 10 +++-- .../Shared/Components/Commenting/default.js | 38 ++++++++++--------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml index c9fee7e7d8..790a66fcd4 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml @@ -60,7 +60,7 @@ { @if (CurrentUser.IsAuthenticated) { - + @L["Reply"] } @@ -87,7 +87,7 @@ }

-