diff --git a/aspnet-core/Directory.Build.props b/aspnet-core/Directory.Build.props
index 969253fd5..9f9403ebe 100644
--- a/aspnet-core/Directory.Build.props
+++ b/aspnet-core/Directory.Build.props
@@ -12,6 +12,11 @@
2.0.3
1.7.28
7.15.1
+ 1.0.0-rc8
+ 1.2.0-rc1
+ 1.0.0-rc8
+ 1.0.0-rc8
+ 1.0.0-beta2
3.3.3
2.0.593
2.10.0
diff --git a/aspnet-core/LINGYUN.MicroService.All.sln b/aspnet-core/LINGYUN.MicroService.All.sln
index 2d20d15c8..3bda2e147 100644
--- a/aspnet-core/LINGYUN.MicroService.All.sln
+++ b/aspnet-core/LINGYUN.MicroService.All.sln
@@ -394,6 +394,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Saas.HttpApi.Cl
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.MultiTenancy.Saas", "modules\tenants\LINGYUN.Abp.MultiTenancy.Saas\LINGYUN.Abp.MultiTenancy.Saas.csproj", "{F57594AA-10C2-4DFF-87F6-19F2548099EA}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "webhooks", "webhooks", "{13ACF670-F109-404E-B252-2FA34A4EA061}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WebHooks", "modules\webhooks\LINGYUN.Abp.WebHooks\LINGYUN.Abp.WebHooks.csproj", "{91AE01B1-CC82-40E2-8290-B8A84C6E90D1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1020,6 +1024,10 @@ Global
{F57594AA-10C2-4DFF-87F6-19F2548099EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F57594AA-10C2-4DFF-87F6-19F2548099EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F57594AA-10C2-4DFF-87F6-19F2548099EA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {91AE01B1-CC82-40E2-8290-B8A84C6E90D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {91AE01B1-CC82-40E2-8290-B8A84C6E90D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {91AE01B1-CC82-40E2-8290-B8A84C6E90D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {91AE01B1-CC82-40E2-8290-B8A84C6E90D1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1212,6 +1220,8 @@ Global
{8DF50094-6791-4C7C-B07D-C3E995B69C49} = {D01D859E-4B72-478A-BABD-90F0981652D5}
{96EF6CDD-CD29-4E7B-B86A-3EBEE6AC9FDC} = {D01D859E-4B72-478A-BABD-90F0981652D5}
{F57594AA-10C2-4DFF-87F6-19F2548099EA} = {A5543E56-DA53-494D-A531-DA75091D46FF}
+ {13ACF670-F109-404E-B252-2FA34A4EA061} = {C5CAD011-DF84-4914-939C-0C029DCEF26F}
+ {91AE01B1-CC82-40E2-8290-B8A84C6E90D1} = {13ACF670-F109-404E-B252-2FA34A4EA061}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C95FDF91-16F2-4A8B-A4BE-0E62D1B66718}
diff --git a/aspnet-core/LINGYUN.MicroService.Common.sln b/aspnet-core/LINGYUN.MicroService.Common.sln
index e2fdcc2f0..edeb3b22c 100644
--- a/aspnet-core/LINGYUN.MicroService.Common.sln
+++ b/aspnet-core/LINGYUN.MicroService.Common.sln
@@ -226,6 +226,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Dapr.Actors.Asp
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.MultiTenancy.Editions", "modules\tenants\LINGYUN.Abp.MultiTenancy.Editions\LINGYUN.Abp.MultiTenancy.Editions.csproj", "{3FF4CEA0-1555-4D62-AA81-B3B599253F8D}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "webhooks", "webhooks", "{BD97C98B-0B4B-443D-AB29-145A344F46D3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WebHooks", "modules\webhooks\LINGYUN.Abp.WebHooks\LINGYUN.Abp.WebHooks.csproj", "{AFE75D2B-8853-488B-B5D5-277B58C5DBB2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -580,6 +584,10 @@ Global
{3FF4CEA0-1555-4D62-AA81-B3B599253F8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3FF4CEA0-1555-4D62-AA81-B3B599253F8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3FF4CEA0-1555-4D62-AA81-B3B599253F8D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AFE75D2B-8853-488B-B5D5-277B58C5DBB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AFE75D2B-8853-488B-B5D5-277B58C5DBB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AFE75D2B-8853-488B-B5D5-277B58C5DBB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AFE75D2B-8853-488B-B5D5-277B58C5DBB2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -692,6 +700,8 @@ Global
{FF518E10-C9AB-440C-8E8D-9CFF67A926AC} = {3A0784A6-AFBF-406F-B79E-9505EB100445}
{49E0B90B-8635-43D0-B0AB-9D484CAE68B5} = {7FDFB22F-1BFF-4E05-9427-78B7A8461D50}
{3FF4CEA0-1555-4D62-AA81-B3B599253F8D} = {38E21687-5F19-42C9-9D11-4B1D2EF64EDB}
+ {BD97C98B-0B4B-443D-AB29-145A344F46D3} = {02EA4E78-5891-43BC-944F-3E52FEE032E4}
+ {AFE75D2B-8853-488B-B5D5-277B58C5DBB2} = {BD97C98B-0B4B-443D-AB29-145A344F46D3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {06C707C6-02C0-411A-AD3B-2D0E13787CB8}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks.ClientProxies/LINGYUN.Abp.WebHooks.ClientProxies.csproj b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks.ClientProxies/LINGYUN.Abp.WebHooks.ClientProxies.csproj
new file mode 100644
index 000000000..251dd53d3
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks.ClientProxies/LINGYUN.Abp.WebHooks.ClientProxies.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks.ClientProxies/LINGYUN/Abp/WebHooks/ClientProxies/AbpWebHooksClientProxiesModule.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks.ClientProxies/LINGYUN/Abp/WebHooks/ClientProxies/AbpWebHooksClientProxiesModule.cs
new file mode 100644
index 000000000..41e08a4a7
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks.ClientProxies/LINGYUN/Abp/WebHooks/ClientProxies/AbpWebHooksClientProxiesModule.cs
@@ -0,0 +1,10 @@
+using LINGYUN.Abp.WebhooksManagement;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Webhooks.ClientProxies;
+
+[DependsOn(typeof(AbpWebhooksModule))]
+[DependsOn(typeof(WebhooksManagementHttpApiClientModule))]
+public class AbpWebHooksClientProxiesModule : AbpModule
+{
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks.ClientProxies/LINGYUN/Abp/WebHooks/ClientProxies/ClientProxiesWebhookPublisher.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks.ClientProxies/LINGYUN/Abp/WebHooks/ClientProxies/ClientProxiesWebhookPublisher.cs
new file mode 100644
index 000000000..9e5ae8b35
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks.ClientProxies/LINGYUN/Abp/WebHooks/ClientProxies/ClientProxiesWebhookPublisher.cs
@@ -0,0 +1,91 @@
+using LINGYUN.Abp.WebhooksManagement;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.Webhooks.ClientProxies;
+
+[Dependency(ReplaceServices = true)]
+public class ClientProxiesWebhookPublisher : IWebhookPublisher, ITransientDependency
+{
+ protected IWebhookPublishAppService PublishAppService { get; }
+
+ public ClientProxiesWebhookPublisher(
+ IWebhookPublishAppService publishAppService)
+ {
+ PublishAppService = publishAppService;
+ }
+
+ public async virtual Task PublishAsync(string webhookName, object data, bool sendExactSameData = false, WebhookHeader headers = null)
+ {
+ var input = new WebhookPublishInput
+ {
+ WebhookName = webhookName,
+ Data = JsonConvert.SerializeObject(data),
+ SendExactSameData = sendExactSameData,
+ };
+ if (headers != null)
+ {
+ input.Header = new WebhooksHeaderInput
+ {
+ UseOnlyGivenHeaders = headers.UseOnlyGivenHeaders,
+ Headers = headers.Headers
+ };
+ }
+
+ await PublishAsync(input);
+ }
+
+ public async virtual Task PublishAsync(string webhookName, object data, Guid? tenantId, bool sendExactSameData = false, WebhookHeader headers = null)
+ {
+ var input = new WebhookPublishInput
+ {
+ WebhookName = webhookName,
+ Data = JsonConvert.SerializeObject(data),
+ SendExactSameData = sendExactSameData,
+ TenantIds = new List
+ {
+ tenantId
+ },
+ };
+ if (headers != null)
+ {
+ input.Header = new WebhooksHeaderInput
+ {
+ UseOnlyGivenHeaders = headers.UseOnlyGivenHeaders,
+ Headers = headers.Headers
+ };
+ }
+
+ await PublishAsync(input);
+ }
+
+ public async virtual Task PublishAsync(Guid?[] tenantIds, string webhookName, object data, bool sendExactSameData = false, WebhookHeader headers = null)
+ {
+ var input = new WebhookPublishInput
+ {
+ WebhookName = webhookName,
+ Data = JsonConvert.SerializeObject(data),
+ SendExactSameData = sendExactSameData,
+ TenantIds = tenantIds.ToList(),
+ };
+ if (headers != null)
+ {
+ input.Header = new WebhooksHeaderInput
+ {
+ UseOnlyGivenHeaders = headers.UseOnlyGivenHeaders,
+ Headers = headers.Headers
+ };
+ }
+
+ await PublishAsync(input);
+ }
+
+ protected virtual async Task PublishAsync(WebhookPublishInput input)
+ {
+ await PublishAppService.PublishAsync(input);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/FodyWeavers.xml b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/FodyWeavers.xml
new file mode 100644
index 000000000..17d32672d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/FodyWeavers.xsd b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/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/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN.Abp.WebHooks.csproj b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN.Abp.WebHooks.csproj
new file mode 100644
index 000000000..2cd64673f
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN.Abp.WebHooks.csproj
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/AbpWebhooksModule.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/AbpWebhooksModule.cs
new file mode 100644
index 000000000..84798d8d2
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/AbpWebhooksModule.cs
@@ -0,0 +1,54 @@
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Collections.Generic;
+using Volo.Abp.BackgroundJobs;
+using Volo.Abp.Features;
+using Volo.Abp.Guids;
+using Volo.Abp.Http.Client;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Webhooks;
+
+//[DependsOn(typeof(AbpBackgroundJobsAbstractionsModule))]
+// 防止未引用实现无法发布到后台作业
+[DependsOn(typeof(AbpBackgroundJobsModule))]
+[DependsOn(typeof(AbpFeaturesModule))]
+[DependsOn(typeof(AbpGuidsModule))]
+[DependsOn(typeof(AbpHttpClientModule))]
+public class AbpWebhooksModule : AbpModule
+{
+ internal const string WebhooksClient = "__Abp_Webhooks_HttpClient";
+
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ AutoAddDefinitionProviders(context.Services);
+ }
+
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ var options = context.Services.ExecutePreConfiguredActions();
+
+ context.Services.AddHttpClient(WebhooksClient, client =>
+ {
+ client.Timeout = options.TimeoutDuration;
+ });
+ }
+
+ private static void AutoAddDefinitionProviders(IServiceCollection services)
+ {
+ var definitionProviders = new List();
+
+ services.OnRegistred(context =>
+ {
+ if (typeof(WebhookDefinitionProvider).IsAssignableFrom(context.ImplementationType))
+ {
+ definitionProviders.Add(context.ImplementationType);
+ }
+ });
+
+ services.Configure(options =>
+ {
+ options.DefinitionProviders.AddIfNotContains(definitionProviders);
+ });
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/AbpWebhooksOptions.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/AbpWebhooksOptions.cs
new file mode 100644
index 000000000..072a76bcc
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/AbpWebhooksOptions.cs
@@ -0,0 +1,35 @@
+using System;
+using Volo.Abp.Collections;
+
+namespace LINGYUN.Abp.Webhooks;
+
+public class AbpWebhooksOptions
+{
+ ///
+ /// 默认超时时间
+ ///
+ public TimeSpan TimeoutDuration { get; set; }
+ ///
+ /// 默认最大发送次数
+ ///
+ public int MaxSendAttemptCount { get; set; }
+ ///
+ /// 是否达到最大连续失败次数时自动取消订阅
+ ///
+ public bool IsAutomaticSubscriptionDeactivationEnabled { get; set; }
+ ///
+ /// 取消订阅前最大连续失败次数
+ ///
+ public int MaxConsecutiveFailCountBeforeDeactivateSubscription { get; set; }
+
+ public ITypeList DefinitionProviders { get; }
+
+ public AbpWebhooksOptions()
+ {
+ TimeoutDuration = TimeSpan.FromSeconds(60);
+ MaxSendAttemptCount = 5;
+ MaxConsecutiveFailCountBeforeDeactivateSubscription = MaxSendAttemptCount * 3;
+
+ DefinitionProviders = new TypeList();
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/BackgroundWorker/WebhookSenderJob.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/BackgroundWorker/WebhookSenderJob.cs
new file mode 100644
index 000000000..b411e3a84
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/BackgroundWorker/WebhookSenderJob.cs
@@ -0,0 +1,131 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.BackgroundJobs;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Uow;
+
+namespace LINGYUN.Abp.Webhooks.BackgroundWorker
+{
+ public class WebhookSenderJob : AsyncBackgroundJob, ITransientDependency
+ {
+ private readonly IUnitOfWorkManager _unitOfWorkManager;
+ private readonly IWebhookDefinitionManager _webhookDefinitionManager;
+ private readonly IWebhookSubscriptionManager _webhookSubscriptionManager;
+ private readonly IWebhookSendAttemptStore _webhookSendAttemptStore;
+ private readonly IWebhookSender _webhookSender;
+
+ private readonly AbpWebhooksOptions _options;
+
+ public WebhookSenderJob(
+ IUnitOfWorkManager unitOfWorkManager,
+ IWebhookDefinitionManager webhookDefinitionManager,
+ IWebhookSubscriptionManager webhookSubscriptionManager,
+ IWebhookSendAttemptStore webhookSendAttemptStore,
+ IWebhookSender webhookSender,
+ IOptions options)
+ {
+ _unitOfWorkManager = unitOfWorkManager;
+ _webhookDefinitionManager = webhookDefinitionManager;
+ _webhookSubscriptionManager = webhookSubscriptionManager;
+ _webhookSendAttemptStore = webhookSendAttemptStore;
+ _webhookSender = webhookSender;
+ _options = options.Value;
+ }
+
+ public override async Task ExecuteAsync(WebhookSenderArgs args)
+ {
+ var webhookDefinition = _webhookDefinitionManager.Get(args.WebhookName);
+
+ if (webhookDefinition.TryOnce)
+ {
+ try
+ {
+ await SendWebhook(args, webhookDefinition);
+ }
+ catch (Exception e)
+ {
+ Logger.LogWarning("An error occured while sending webhook with try once.", e);
+ // ignored
+ }
+ }
+ else
+ {
+ await SendWebhook(args, webhookDefinition);
+ }
+ }
+
+ private async Task SendWebhook(WebhookSenderArgs args, WebhookDefinition webhookDefinition)
+ {
+ if (args.WebhookEventId == default)
+ {
+ return;
+ }
+
+ if (args.WebhookSubscriptionId == default)
+ {
+ return;
+ }
+
+ if (!webhookDefinition.TryOnce)
+ {
+ var sendAttemptCount = await _webhookSendAttemptStore.GetSendAttemptCountAsync(
+ args.TenantId,
+ args.WebhookEventId,
+ args.WebhookSubscriptionId
+ );
+
+ if ((webhookDefinition.MaxSendAttemptCount > 0 && sendAttemptCount > webhookDefinition.MaxSendAttemptCount) ||
+ sendAttemptCount > _options.MaxSendAttemptCount)
+ {
+ return;
+ }
+ }
+
+ try
+ {
+ await _webhookSender.SendWebhookAsync(args);
+ }
+ catch (Exception)
+ {
+ // no need to retry to send webhook since subscription disabled
+ if (!await TryDeactivateSubscriptionIfReachedMaxConsecutiveFailCount(
+ args.TenantId,
+ args.WebhookSubscriptionId))
+ {
+ throw; //Throw exception to re-try sending webhook
+ }
+ }
+ }
+
+ private async Task TryDeactivateSubscriptionIfReachedMaxConsecutiveFailCount(
+ Guid? tenantId,
+ Guid subscriptionId)
+ {
+ if (!_options.IsAutomaticSubscriptionDeactivationEnabled)
+ {
+ return false;
+ }
+
+ var hasXConsecutiveFail = await _webhookSendAttemptStore
+ .HasXConsecutiveFailAsync(
+ tenantId,
+ subscriptionId,
+ _options.MaxConsecutiveFailCountBeforeDeactivateSubscription
+ );
+
+ if (!hasXConsecutiveFail)
+ {
+ return false;
+ }
+
+ using (var uow = _unitOfWorkManager.Begin())
+ {
+ await _webhookSubscriptionManager.ActivateWebhookSubscriptionAsync(subscriptionId, false);
+ await uow.CompleteAsync();
+ return true;
+ }
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/DefaultWebhookPublisher.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/DefaultWebhookPublisher.cs
new file mode 100644
index 000000000..be46197ae
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/DefaultWebhookPublisher.cs
@@ -0,0 +1,140 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.BackgroundJobs;
+using Volo.Abp.Guids;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class DefaultWebhookPublisher : IWebhookPublisher, ITransientDependency
+ {
+ public IWebhookEventStore WebhookEventStore { get; set; }
+
+ private readonly ICurrentTenant _currentTenant;
+ private readonly IBackgroundJobManager _backgroundJobManager;
+ private readonly IWebhookSubscriptionManager _webhookSubscriptionManager;
+
+ public DefaultWebhookPublisher(
+ IWebhookSubscriptionManager webhookSubscriptionManager,
+ ICurrentTenant currentTenant,
+ IBackgroundJobManager backgroundJobManager)
+ {
+ _currentTenant = currentTenant;
+ _backgroundJobManager = backgroundJobManager;
+ _webhookSubscriptionManager = webhookSubscriptionManager;
+
+ WebhookEventStore = NullWebhookEventStore.Instance;
+ }
+
+ #region Async Publish Methods
+
+ public virtual async Task PublishAsync(
+ string webhookName,
+ object data,
+ bool sendExactSameData = false,
+ WebhookHeader headers = null)
+ {
+ var subscriptions = await _webhookSubscriptionManager.GetAllSubscriptionsIfFeaturesGrantedAsync(_currentTenant.Id, webhookName);
+ await PublishAsync(webhookName, data, subscriptions, sendExactSameData, headers);
+ }
+
+ public virtual async Task PublishAsync(
+ string webhookName,
+ object data,
+ Guid? tenantId,
+ bool sendExactSameData = false,
+ WebhookHeader headers = null)
+ {
+ var subscriptions = await _webhookSubscriptionManager.GetAllSubscriptionsIfFeaturesGrantedAsync(tenantId, webhookName);
+ await PublishAsync(webhookName, data, subscriptions, sendExactSameData, headers);
+ }
+
+ public virtual async Task PublishAsync(
+ Guid?[] tenantIds,
+ string webhookName,
+ object data,
+ bool sendExactSameData = false,
+ WebhookHeader headers = null)
+ {
+ var subscriptions = await _webhookSubscriptionManager.GetAllSubscriptionsOfTenantsIfFeaturesGrantedAsync(tenantIds, webhookName);
+ await PublishAsync(webhookName, data, subscriptions, sendExactSameData, headers);
+ }
+
+ protected virtual async Task PublishAsync(
+ string webhookName,
+ object data,
+ List webhookSubscriptions,
+ bool sendExactSameData = false,
+ WebhookHeader headers = null)
+ {
+ if (webhookSubscriptions.IsNullOrEmpty())
+ {
+ return;
+ }
+
+ var subscriptionsGroupedByTenant = webhookSubscriptions.GroupBy(x => x.TenantId);
+
+ foreach (var subscriptionGroupedByTenant in subscriptionsGroupedByTenant)
+ {
+ var webhookInfo = await SaveAndGetWebhookAsync(subscriptionGroupedByTenant.Key, webhookName, data);
+
+ foreach (var webhookSubscription in subscriptionGroupedByTenant)
+ {
+ var headersToSend = webhookSubscription.Headers;
+ if (headers != null)
+ {
+ if (headers.UseOnlyGivenHeaders)//do not use the headers defined in subscription
+ {
+ headersToSend = headers.Headers;
+ }
+ else
+ {
+ //use the headers defined in subscription. If additional headers has same header, use additional headers value.
+ foreach (var additionalHeader in headers.Headers)
+ {
+ headersToSend[additionalHeader.Key] = additionalHeader.Value;
+ }
+ }
+ }
+
+ await _backgroundJobManager.EnqueueAsync(new WebhookSenderArgs
+ {
+ TenantId = webhookSubscription.TenantId,
+ WebhookEventId = webhookInfo.Id,
+ Data = webhookInfo.Data,
+ WebhookName = webhookInfo.WebhookName,
+ WebhookSubscriptionId = webhookSubscription.Id,
+ Headers = headersToSend,
+ Secret = webhookSubscription.Secret,
+ WebhookUri = webhookSubscription.WebhookUri,
+ SendExactSameData = sendExactSameData
+ });
+ }
+ }
+ }
+
+ #endregion
+
+ protected virtual async Task SaveAndGetWebhookAsync(
+ Guid? tenantId,
+ string webhookName,
+ object data)
+ {
+ var webhookInfo = new WebhookEvent
+ {
+ WebhookName = webhookName,
+ Data = JsonConvert.SerializeObject(data),
+ TenantId = tenantId
+ };
+
+ var webhookId = await WebhookEventStore.InsertAndGetIdAsync(webhookInfo);
+ webhookInfo.Id = webhookId;
+
+ return webhookInfo;
+ }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/DefaultWebhookSender.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/DefaultWebhookSender.cs
new file mode 100644
index 000000000..19982a68e
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/DefaultWebhookSender.cs
@@ -0,0 +1,129 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class DefaultWebhookSender : IWebhookSender, ITransientDependency
+ {
+ public ILogger Logger { protected get; set; }
+
+ private readonly IWebhookManager _webhookManager;
+ private readonly IHttpClientFactory _httpClientFactory;
+
+ private const string FailedRequestDefaultContent = "Webhook Send Request Failed";
+
+ public DefaultWebhookSender(
+ IWebhookManager webhookManager,
+ IHttpClientFactory httpClientFactory)
+ {
+ _webhookManager = webhookManager;
+ _httpClientFactory = httpClientFactory;
+
+ Logger = NullLogger.Instance;
+ }
+
+ public async Task SendWebhookAsync(WebhookSenderArgs webhookSenderArgs)
+ {
+ if (webhookSenderArgs.WebhookEventId == default)
+ {
+ throw new ArgumentNullException(nameof(webhookSenderArgs.WebhookEventId));
+ }
+
+ if (webhookSenderArgs.WebhookSubscriptionId == default)
+ {
+ throw new ArgumentNullException(nameof(webhookSenderArgs.WebhookSubscriptionId));
+ }
+
+ var webhookSendAttemptId = await _webhookManager.InsertAndGetIdWebhookSendAttemptAsync(webhookSenderArgs);
+
+ var request = CreateWebhookRequestMessage(webhookSenderArgs);
+
+ var serializedBody = await _webhookManager.GetSerializedBodyAsync(webhookSenderArgs);
+
+ _webhookManager.SignWebhookRequest(request, serializedBody, webhookSenderArgs.Secret);
+
+ AddAdditionalHeaders(request, webhookSenderArgs);
+
+ var isSucceed = false;
+ HttpStatusCode? statusCode = null;
+ var content = FailedRequestDefaultContent;
+
+ try
+ {
+ var response = await SendHttpRequest(request);
+ isSucceed = response.isSucceed;
+ statusCode = response.statusCode;
+ content = response.content;
+ }
+ catch (TaskCanceledException)
+ {
+ statusCode = HttpStatusCode.RequestTimeout;
+ content = "Request Timeout";
+ }
+ catch (HttpRequestException e)
+ {
+ content = e.Message;
+ }
+ catch (Exception e)
+ {
+ Logger.LogError("An error occured while sending a webhook request", e);
+ }
+ finally
+ {
+ await _webhookManager.StoreResponseOnWebhookSendAttemptAsync(webhookSendAttemptId, webhookSenderArgs.TenantId, statusCode, content);
+ }
+
+ if (!isSucceed)
+ {
+ throw new Exception($"Webhook sending attempt failed. WebhookSendAttempt id: {webhookSendAttemptId}");
+ }
+
+ return webhookSendAttemptId;
+ }
+
+ ///
+ /// You can override this to change request message
+ ///
+ ///
+ protected virtual HttpRequestMessage CreateWebhookRequestMessage(WebhookSenderArgs webhookSenderArgs)
+ {
+ return new HttpRequestMessage(HttpMethod.Post, webhookSenderArgs.WebhookUri);
+ }
+
+ protected virtual void AddAdditionalHeaders(HttpRequestMessage request, WebhookSenderArgs webhookSenderArgs)
+ {
+ foreach (var header in webhookSenderArgs.Headers)
+ {
+ if (request.Headers.TryAddWithoutValidation(header.Key, header.Value))
+ {
+ continue;
+ }
+
+ if (request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value))
+ {
+ continue;
+ }
+
+ throw new Exception($"Invalid Header. SubscriptionId:{webhookSenderArgs.WebhookSubscriptionId},Header: {header.Key}:{header.Value}");
+ }
+ }
+
+ protected virtual async Task<(bool isSucceed, HttpStatusCode statusCode, string content)> SendHttpRequest(HttpRequestMessage request)
+ {
+ var client = _httpClientFactory.CreateClient(AbpWebhooksModule.WebhooksClient);
+
+ var response = await client.SendAsync(request);
+
+ var isSucceed = response.IsSuccessStatusCode;
+ var statusCode = response.StatusCode;
+ var content = await response.Content.ReadAsStringAsync();
+
+ return (isSucceed, statusCode, content);
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/Extensions/WebhookSubscriptionExtensions.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/Extensions/WebhookSubscriptionExtensions.cs
new file mode 100644
index 000000000..8cf48f520
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/Extensions/WebhookSubscriptionExtensions.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.Webhooks.Extensions
+{
+ public static class WebhookSubscriptionExtensions
+ {
+ ///
+ /// checks if subscribed to given webhook
+ ///
+ ///
+ public static bool IsSubscribed(this WebhookSubscriptionInfo webhookSubscription, string webhookName)
+ {
+ if (webhookSubscription.Webhooks.IsNullOrEmpty())
+ {
+ return false;
+ }
+
+ return webhookSubscription.Webhooks.Contains(webhookName);
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionContext.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionContext.cs
new file mode 100644
index 000000000..c170ede3d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionContext.cs
@@ -0,0 +1,16 @@
+using JetBrains.Annotations;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookDefinitionContext
+ {
+ WebhookGroupDefinition AddGroup(
+ [NotNull] string name,
+ ILocalizableString displayName = null);
+
+ WebhookGroupDefinition GetGroupOrNull(string name);
+
+ void RemoveGroup(string name);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionManager.cs
new file mode 100644
index 000000000..a284ab693
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookDefinitionManager.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookDefinitionManager
+ {
+ ///
+ /// Gets a webhook definition by name.
+ /// Returns null if there is no webhook definition with given name.
+ ///
+ WebhookDefinition GetOrNull(string name);
+
+ ///
+ /// Gets a webhook definition by name.
+ /// Throws exception if there is no webhook definition with given name.
+ ///
+ WebhookDefinition Get(string name);
+
+ ///
+ /// Gets all webhook definitions.
+ ///
+ IReadOnlyList GetAll();
+
+ ///
+ /// Gets all webhook group definitions.
+ ///
+ ///
+ IReadOnlyList GetGroups();
+
+ ///
+ /// Checks if given webhook name is available for given tenant.
+ ///
+ Task IsAvailableAsync(Guid? tenantId, string name);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookEventStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookEventStore.cs
new file mode 100644
index 000000000..1fb8f5c21
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookEventStore.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookEventStore
+ {
+ ///
+ /// Inserts to persistent store
+ ///
+ Task InsertAndGetIdAsync(WebhookEvent webhookEvent);
+
+ ///
+ /// Gets Webhook info by id
+ ///
+ Task GetAsync(Guid? tenantId, Guid id);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookManager.cs
new file mode 100644
index 000000000..1e8e5fd0c
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookManager.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookManager
+ {
+ Task GetWebhookPayloadAsync(WebhookSenderArgs webhookSenderArgs);
+
+ void SignWebhookRequest(HttpRequestMessage request, string serializedBody, string secret);
+
+ Task GetSerializedBodyAsync(WebhookSenderArgs webhookSenderArgs);
+
+ Task InsertAndGetIdWebhookSendAttemptAsync(WebhookSenderArgs webhookSenderArgs);
+
+ Task StoreResponseOnWebhookSendAttemptAsync(
+ Guid webhookSendAttemptId, Guid? tenantId,
+ HttpStatusCode? statusCode, string content);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookPublisher.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookPublisher.cs
new file mode 100644
index 000000000..3b53fec83
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookPublisher.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookPublisher
+ {
+ ///
+ /// Sends webhooks to current tenant subscriptions (). with given data, (Checks permissions)
+ ///
+ ///
+ /// data to send
+ ///
+ /// True: It sends the exact same data as the parameter to clients.
+ ///
+ /// False: It sends data in . It is recommended way.
+ ///
+ ///
+ /// Headers to send. Publisher uses subscription defined webhook by default. You can add additional headers from here. If subscription already has given header, publisher uses the one you give here.
+ Task PublishAsync(string webhookName, object data, bool sendExactSameData = false, WebhookHeader headers = null);
+
+ ///
+ /// Sends webhooks to given tenant's subscriptions
+ ///
+ ///
+ /// data to send
+ ///
+ /// Target tenant id
+ ///
+ ///
+ /// True: It sends the exact same data as the parameter to clients.
+ ///
+ /// False: It sends data in . It is recommended way.
+ ///
+ ///
+ /// Headers to send. Publisher uses subscription defined webhook by default. You can add additional headers from here. If subscription already has given header, publisher uses the one you give here.
+ Task PublishAsync(string webhookName, object data, Guid? tenantId, bool sendExactSameData = false, WebhookHeader headers = null);
+
+ ///
+ /// Sends webhooks to given tenant's subscriptions
+ ///
+ ///
+ /// data to send
+ ///
+ /// Target tenant id(s)
+ ///
+ ///
+ /// True: It sends the exact same data as the parameter to clients.
+ ///
+ /// False: It sends data in . It is recommended way.
+ ///
+ ///
+ /// Headers to send. Publisher uses subscription defined webhook by default. You can add additional headers from here. If subscription already has given header, publisher uses the one you give here.
+ Task PublishAsync(Guid?[] tenantIds, string webhookName, object data, bool sendExactSameData = false, WebhookHeader headers = null);
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookSendAttemptStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookSendAttemptStore.cs
new file mode 100644
index 000000000..2e32bc63f
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookSendAttemptStore.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookSendAttemptStore
+ {
+ Task GetAsync(Guid? tenantId, Guid id);
+
+ ///
+ /// Returns work item count by given web hook id and subscription id, (How many times publisher tried to send web hook)
+ ///
+ Task GetSendAttemptCountAsync(Guid? tenantId, Guid webhookId, Guid webhookSubscriptionId);
+
+ ///
+ /// Checks is there any successful webhook attempt in last items. Should return true if there are not X number items
+ ///
+ Task HasXConsecutiveFailAsync(Guid? tenantId, Guid subscriptionId, int searchCount);
+
+ Task<(int TotalCount, IReadOnlyCollection Webhooks)> GetAllSendAttemptsBySubscriptionAsPagedListAsync(Guid? tenantId, Guid subscriptionId, int maxResultCount, int skipCount);
+
+ Task> GetAllSendAttemptsByWebhookEventIdAsync(Guid? tenantId, Guid webhookEventId);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookSender.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookSender.cs
new file mode 100644
index 000000000..d30e110e6
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookSender.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookSender
+ {
+ ///
+ /// Tries to send webhook with given transactionId and stores process in
+ /// Should throw exception if fails or response status not succeed
+ ///
+ /// arguments
+ /// Webhook send attempt id
+ Task SendWebhookAsync(WebhookSenderArgs webhookSenderArgs);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionManager.cs
new file mode 100644
index 000000000..956b2dbd3
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionManager.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public interface IWebhookSubscriptionManager
+ {
+ ///
+ /// Returns subscription for given id.
+ ///
+ /// Unique identifier of
+ Task GetAsync(Guid id);
+
+ ///
+ /// Returns all subscriptions of tenant
+ ///
+ ///
+ /// Target tenant id.
+ ///
+ Task> GetAllSubscriptionsAsync(Guid? tenantId);
+
+ ///
+ /// Returns all subscriptions for given webhook.
+ ///
+ ///
+ ///
+ /// Target tenant id.
+ ///
+ Task> GetAllSubscriptionsIfFeaturesGrantedAsync(Guid? tenantId, string webhookName);
+
+ ///
+ /// Returns all subscriptions of tenant
+ ///
+ ///
+ Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds);
+
+ ///
+ /// Returns all subscriptions for given webhook.
+ ///
+ ///
+ ///
+ /// Target tenant id(s).
+ ///
+ Task> GetAllSubscriptionsOfTenantsIfFeaturesGrantedAsync(Guid?[] tenantIds, string webhookName);
+
+ ///
+ /// Checks if tenant subscribed for a webhook. (Checks if webhook features are granted)
+ ///
+ ///
+ /// Target tenant id(s).
+ ///
+ ///
+ Task IsSubscribedAsync(Guid? tenantId, string webhookName);
+
+ ///
+ /// If id is the default(Guid) adds new subscription, else updates current one. (Checks if webhook features are granted)
+ ///
+ Task AddOrUpdateSubscriptionAsync(WebhookSubscriptionInfo webhookSubscription);
+
+ ///
+ /// Activates/Deactivates given webhook subscription
+ ///
+ /// unique identifier of
+ /// IsActive
+ Task ActivateWebhookSubscriptionAsync(Guid id, bool active);
+
+ ///
+ /// Delete given webhook subscription.
+ ///
+ /// unique identifier of
+ Task DeleteSubscriptionAsync(Guid id);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionsStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionsStore.cs
new file mode 100644
index 000000000..23849ad4e
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/IWebhookSubscriptionsStore.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ ///
+ /// This interface should be implemented by vendors to make webhooks working.
+ ///
+ public interface IWebhookSubscriptionsStore
+ {
+ ///
+ /// returns subscription
+ ///
+ /// webhook subscription id
+ ///
+ Task GetAsync(Guid id);
+
+ ///
+ /// Saves webhook subscription to a persistent store.
+ ///
+ /// webhook subscription information
+ Task InsertAsync(WebhookSubscriptionInfo webhookSubscription);
+
+ ///
+ /// Updates webhook subscription to a persistent store.
+ ///
+ /// webhook subscription information
+ Task UpdateAsync(WebhookSubscriptionInfo webhookSubscription);
+
+ ///
+ /// Deletes subscription if exists
+ ///
+ /// primary key
+ ///
+ Task DeleteAsync(Guid id);
+
+ ///
+ /// Returns all subscriptions of given tenant including deactivated
+ ///
+ ///
+ /// Target tenant id.
+ ///
+ Task> GetAllSubscriptionsAsync(Guid? tenantId);
+
+ ///
+ /// Returns webhook subscriptions which subscribe to given webhook on tenant(s)
+ ///
+ ///
+ /// Target tenant id.
+ ///
+ ///
+ ///
+ Task> GetAllSubscriptionsAsync(Guid? tenantId, string webhookName);
+
+ ///
+ /// Returns all subscriptions of given tenant including deactivated
+ ///
+ ///
+ /// Target tenant id(s).
+ ///
+ Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds);
+
+ ///
+ /// Returns webhook subscriptions which subscribe to given webhook on tenant(s)
+ ///
+ ///
+ /// Target tenant id(s).
+ ///
+ ///
+ ///
+ Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds, string webhookName);
+
+ ///
+ /// Checks if tenant subscribed for a webhook
+ ///
+ ///
+ /// Target tenant id(s).
+ ///
+ /// Name of the webhook
+ Task IsSubscribedAsync(Guid? tenantId, string webhookName);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/NullWebhookEventStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/NullWebhookEventStore.cs
new file mode 100644
index 000000000..069726e67
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/NullWebhookEventStore.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ ///
+ /// Null pattern implementation of .
+ /// It's used if is not implemented by actual persistent store
+ ///
+ public class NullWebhookEventStore : IWebhookEventStore
+ {
+ public static NullWebhookEventStore Instance { get; } = new NullWebhookEventStore();
+
+ public Task InsertAndGetIdAsync(WebhookEvent webhookEvent)
+ {
+ return Task.FromResult(default);
+ }
+
+ public Task GetAsync(Guid? tenantId, Guid id)
+ {
+ return Task.FromResult(default);
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/NullWebhookSendAttemptStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/NullWebhookSendAttemptStore.cs
new file mode 100644
index 000000000..7254b1ae6
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/NullWebhookSendAttemptStore.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class NullWebhookSendAttemptStore : IWebhookSendAttemptStore
+ {
+ public static NullWebhookSendAttemptStore Instance = new NullWebhookSendAttemptStore();
+
+ public Task InsertAsync(WebhookSendAttempt webhookSendAttempt)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task UpdateAsync(WebhookSendAttempt webhookSendAttempt)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task GetAsync(Guid? tenantId, Guid id)
+ {
+ return Task.FromResult(default);
+ }
+
+ public Task GetSendAttemptCountAsync(Guid? tenantId, Guid webhookId, Guid webhookSubscriptionId)
+ {
+ return Task.FromResult(int.MaxValue);
+ }
+
+ public Task HasXConsecutiveFailAsync(Guid? tenantId, Guid subscriptionId, int searchCount)
+ {
+ return default;
+ }
+
+ public Task<(int TotalCount, IReadOnlyCollection Webhooks)> GetAllSendAttemptsBySubscriptionAsPagedListAsync(Guid? tenantId, Guid subscriptionId, int maxResultCount,
+ int skipCount)
+ {
+ return Task.FromResult(ValueTuple.Create(0, new List() as IReadOnlyCollection));
+ }
+
+ public Task> GetAllSendAttemptsByWebhookEventIdAsync(Guid? tenantId, Guid webhookEventId)
+ {
+ return Task.FromResult(new List());
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/NullWebhookSubscriptionsStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/NullWebhookSubscriptionsStore.cs
new file mode 100644
index 000000000..4214d1d25
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/NullWebhookSubscriptionsStore.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ ///
+ /// Null pattern implementation of .
+ /// It's used if is not implemented by actual persistent store
+ ///
+ public class NullWebhookSubscriptionsStore : IWebhookSubscriptionsStore
+ {
+ public static NullWebhookSubscriptionsStore Instance { get; } = new NullWebhookSubscriptionsStore();
+
+ public Task GetAsync(Guid id)
+ {
+ return Task.FromResult(default);
+ }
+
+ public WebhookSubscriptionInfo Get(Guid id)
+ {
+ return default;
+ }
+
+ public Task InsertAsync(WebhookSubscriptionInfo webhookSubscription)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task UpdateAsync(WebhookSubscriptionInfo webhookSubscription)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task DeleteAsync(Guid id)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task> GetAllSubscriptionsAsync(Guid? tenantId)
+ {
+ return Task.FromResult(new List());
+ }
+
+ public Task> GetAllSubscriptionsAsync(Guid? tenantId, string webhookName)
+ {
+ return Task.FromResult(new List());
+ }
+
+ public Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds)
+ {
+ return Task.FromResult(new List());
+ }
+
+ public Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds, string webhookName)
+ {
+ return Task.FromResult(new List());
+ }
+
+ public Task IsSubscribedAsync(Guid? tenantId, string webhookName)
+ {
+ return Task.FromResult(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookDefinition.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookDefinition.cs
new file mode 100644
index 000000000..1c1461e8a
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookDefinition.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookDefinition
+ {
+ ///
+ /// Unique name of the webhook.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Tries to send webhook only one time without checking to send attempt count
+ ///
+ public bool TryOnce { get; set; }
+
+ ///
+ /// Defined maximum number of sending times
+ ///
+ public int MaxSendAttemptCount { get; set; }
+
+ ///
+ /// Display name of the webhook.
+ /// Optional.
+ ///
+ public ILocalizableString DisplayName { get; set; }
+
+ ///
+ /// Description for the webhook.
+ /// Optional.
+ ///
+ public ILocalizableString Description { get; set; }
+
+ public List RequiredFeatures { get; set; }
+
+ public WebhookDefinition(string name, ILocalizableString displayName = null, ILocalizableString description = null)
+ {
+ if (name.IsNullOrWhiteSpace())
+ {
+ throw new ArgumentNullException(nameof(name), $"{nameof(name)} can not be null, empty or whitespace!");
+ }
+
+ Name = name.Trim();
+ DisplayName = displayName;
+ Description = description;
+
+ RequiredFeatures = new List();
+ }
+
+ public WebhookDefinition WithFeature(params string[] features)
+ {
+ if (!features.IsNullOrEmpty())
+ {
+ RequiredFeatures.AddRange(features);
+ }
+
+ return this;
+ }
+
+ public override string ToString()
+ {
+ return $"[{nameof(WebhookDefinition)} {Name}]";
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookDefinitionContext.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookDefinitionContext.cs
new file mode 100644
index 000000000..5e8987a8b
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookDefinitionContext.cs
@@ -0,0 +1,55 @@
+using JetBrains.Annotations;
+using System.Collections.Generic;
+using Volo.Abp;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookDefinitionContext : IWebhookDefinitionContext
+ {
+ protected Dictionary Groups { get; }
+
+ public WebhookDefinitionContext(Dictionary webhooks)
+ {
+ Groups = webhooks;
+ }
+
+ public WebhookGroupDefinition AddGroup(
+ [NotNull] string name,
+ ILocalizableString displayName = null)
+ {
+ Check.NotNull(name, nameof(name));
+
+ if (Groups.ContainsKey(name))
+ {
+ throw new AbpException($"There is already an existing webhook group with name: {name}");
+ }
+
+ return Groups[name] = new WebhookGroupDefinition(name, displayName);
+ }
+
+ public WebhookGroupDefinition GetGroupOrNull([NotNull] string name)
+ {
+ Check.NotNull(name, nameof(name));
+
+ if (!Groups.ContainsKey(name))
+ {
+ return null;
+ }
+
+ return Groups[name];
+ }
+
+ public void RemoveGroup(string name)
+ {
+ Check.NotNull(name, nameof(name));
+
+ if (!Groups.ContainsKey(name))
+ {
+ throw new AbpException($"Undefined notification webhook group: '{name}'.");
+ }
+
+ Groups.Remove(name);
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookDefinitionManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookDefinitionManager.cs
new file mode 100644
index 000000000..c0f96f20e
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookDefinitionManager.cs
@@ -0,0 +1,139 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Features;
+using Volo.Abp.MultiTenancy;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ internal class WebhookDefinitionManager : IWebhookDefinitionManager, ISingletonDependency
+ {
+ protected IDictionary WebhookGroupDefinitions => _lazyWebhookGroupDefinitions.Value;
+ private readonly Lazy> _lazyWebhookGroupDefinitions;
+
+ protected IDictionary WebhookDefinitions => _lazyWebhookDefinitions.Value;
+ private readonly Lazy> _lazyWebhookDefinitions;
+
+ private readonly IServiceProvider _serviceProvider;
+ private readonly AbpWebhooksOptions _options;
+
+ public WebhookDefinitionManager(
+ IServiceProvider serviceProvider,
+ IOptions options)
+ {
+ _serviceProvider = serviceProvider;
+ _options = options.Value;
+
+ _lazyWebhookGroupDefinitions = new Lazy>(CreateWebhookGroupDefinitions);
+ _lazyWebhookDefinitions = new Lazy>(CreateWebhookDefinitions);
+ }
+
+ public WebhookDefinition GetOrNull(string name)
+ {
+ if (!WebhookDefinitions.ContainsKey(name))
+ {
+ return null;
+ }
+
+ return WebhookDefinitions[name];
+ }
+
+ public WebhookDefinition Get(string name)
+ {
+ if (!WebhookDefinitions.ContainsKey(name))
+ {
+ throw new KeyNotFoundException($"Webhook definitions does not contain a definition with the key \"{name}\".");
+ }
+
+ return WebhookDefinitions[name];
+ }
+
+ public IReadOnlyList GetAll()
+ {
+ return WebhookDefinitions.Values.ToImmutableList();
+ }
+
+ public IReadOnlyList GetGroups()
+ {
+ return WebhookGroupDefinitions.Values.ToImmutableList();
+ }
+
+ public async Task IsAvailableAsync(Guid? tenantId, string name)
+ {
+ if (tenantId == null) // host allowed to subscribe all webhooks
+ {
+ return true;
+ }
+
+ var webhookDefinition = GetOrNull(name);
+
+ if (webhookDefinition == null)
+ {
+ return false;
+ }
+
+ if (webhookDefinition.RequiredFeatures?.Any() == false)
+ {
+ return true;
+ }
+
+ var currentTenant = _serviceProvider.GetRequiredService();
+ var featureChecker = _serviceProvider.GetRequiredService();
+ using (currentTenant.Change(tenantId))
+ {
+ if (!await featureChecker.IsEnabledAsync(true, webhookDefinition.RequiredFeatures.ToArray()))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ protected virtual Dictionary CreateWebhookDefinitions()
+ {
+ var definitions = new Dictionary();
+
+ foreach (var groupDefinition in WebhookGroupDefinitions.Values)
+ {
+ foreach (var webhook in groupDefinition.Webhooks)
+ {
+ if (definitions.ContainsKey(webhook.Name))
+ {
+ throw new AbpException("Duplicate webhook name: " + webhook.Name);
+ }
+
+ definitions[webhook.Name] = webhook;
+ }
+ }
+
+ return definitions;
+ }
+
+ protected virtual Dictionary CreateWebhookGroupDefinitions()
+ {
+ var definitions = new Dictionary();
+
+ using (var scope = _serviceProvider.CreateScope())
+ {
+ var providers = _options
+ .DefinitionProviders
+ .Select(p => scope.ServiceProvider.GetRequiredService(p) as WebhookDefinitionProvider)
+ .ToList();
+
+ foreach (var provider in providers)
+ {
+ provider.Define(new WebhookDefinitionContext(definitions));
+ }
+ }
+
+ return definitions;
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookDefinitionProvider.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookDefinitionProvider.cs
new file mode 100644
index 000000000..ead9ab679
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookDefinitionProvider.cs
@@ -0,0 +1,13 @@
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public abstract class WebhookDefinitionProvider : ITransientDependency
+ {
+ ///
+ /// Used to add/manipulate webhook definitions.
+ ///
+ /// Context,
+ public abstract void Define(IWebhookDefinitionContext context);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookEvent.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookEvent.cs
new file mode 100644
index 000000000..bd32fa76d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookEvent.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ ///
+ /// Store created web hooks. To see who get that webhook check with and you can get
+ ///
+ public class WebhookEvent
+ {
+ public Guid Id { get; set; }
+
+ ///
+ /// Webhook unique name
+ ///
+ public string WebhookName { get; set; }
+
+ ///
+ /// Webhook data as JSON string.
+ ///
+ public string Data { get; set; }
+
+ public DateTime CreationTime { get; set; }
+
+ public Guid? TenantId { get; set; }
+
+ public bool IsDeleted { get; set; }
+
+ public DateTime? DeletionTime { get; set; }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookGroupDefinition.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookGroupDefinition.cs
new file mode 100644
index 000000000..ae58049ed
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookGroupDefinition.cs
@@ -0,0 +1,101 @@
+using JetBrains.Annotations;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Volo.Abp;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Webhooks;
+
+public class WebhookGroupDefinition
+{
+ [NotNull]
+ public string Name { get; set; }
+ public Dictionary Properties { get; }
+
+ private ILocalizableString _displayName;
+ public ILocalizableString DisplayName
+ {
+ get {
+ return _displayName;
+ }
+ set {
+ _displayName = value;
+ }
+ }
+
+ public IReadOnlyList Webhooks => _webhooks.ToImmutableList();
+ private readonly List _webhooks;
+ public object this[string name] {
+ get => Properties.GetOrDefault(name);
+ set => Properties[name] = value;
+ }
+
+ protected internal WebhookGroupDefinition(
+ string name,
+ ILocalizableString displayName = null)
+ {
+ Name = name;
+ DisplayName = displayName ?? new FixedLocalizableString(Name);
+
+ Properties = new Dictionary();
+ _webhooks = new List();
+ }
+
+ public virtual WebhookDefinition AddWebhook(
+ string name,
+ ILocalizableString displayName = null,
+ ILocalizableString description = null)
+ {
+ if (Webhooks.Any(hook => hook.Name.Equals(name)))
+ {
+ throw new AbpException($"There is already an existing webhook with name: {name} in group {Name}");
+ }
+
+ var webhook = new WebhookDefinition(
+ name,
+ displayName,
+ description
+ );
+
+ _webhooks.Add(webhook);
+
+ return webhook;
+ }
+
+ public virtual void AddWebhooks(params WebhookDefinition[] webhooks)
+ {
+ foreach (var webhook in webhooks)
+ {
+ if (Webhooks.Any(hook => hook.Name.Equals(webhook.Name)))
+ {
+ throw new AbpException($"There is already an existing webhook with name: {webhook.Name} in group {Name}");
+ }
+ }
+
+ _webhooks.AddRange(webhooks);
+ }
+
+
+
+ [CanBeNull]
+ public WebhookDefinition GetWebhookOrNull([NotNull] string name)
+ {
+ Check.NotNull(name, nameof(name));
+
+ foreach (var webhook in Webhooks)
+ {
+ if (webhook.Name == name)
+ {
+ return webhook;
+ }
+ }
+
+ return null;
+ }
+
+ public override string ToString()
+ {
+ return $"[{nameof(WebhookGroupDefinition)} {Name}]";
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookHeader.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookHeader.cs
new file mode 100644
index 000000000..91442552f
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookHeader.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookHeader
+ {
+ ///
+ /// If true, webhook will only contain given headers. If false given headers will be added to predefined headers in subscription.
+ /// Default is false
+ ///
+ public bool UseOnlyGivenHeaders { get; set; }
+
+ ///
+ /// That headers will be sent with the webhook.
+ ///
+ public IDictionary Headers { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookManager.cs
new file mode 100644
index 000000000..854795786
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookManager.cs
@@ -0,0 +1,80 @@
+using Newtonsoft.Json;
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public abstract class WebhookManager : IWebhookManager
+ {
+ private const string SignatureHeaderKey = "sha256";
+ private const string SignatureHeaderValueTemplate = SignatureHeaderKey + "={0}";
+ private const string SignatureHeaderName = "abp-webhook-signature";
+ protected IWebhookSendAttemptStore WebhookSendAttemptStore { get; }
+
+ protected WebhookManager(
+ IWebhookSendAttemptStore webhookSendAttemptStore)
+ {
+ WebhookSendAttemptStore = webhookSendAttemptStore;
+ }
+
+ public virtual async Task GetWebhookPayloadAsync(WebhookSenderArgs webhookSenderArgs)
+ {
+ var data = JsonConvert.SerializeObject(webhookSenderArgs.Data);
+
+ var attemptNumber = await WebhookSendAttemptStore.GetSendAttemptCountAsync(
+ webhookSenderArgs.TenantId,
+ webhookSenderArgs.WebhookEventId,
+ webhookSenderArgs.WebhookSubscriptionId);
+
+ return new WebhookPayload(
+ webhookSenderArgs.WebhookEventId.ToString(),
+ webhookSenderArgs.WebhookName,
+ attemptNumber)
+ {
+ Data = data
+ };
+ }
+
+ public virtual void SignWebhookRequest(HttpRequestMessage request, string serializedBody, string secret)
+ {
+ if (request == null)
+ {
+ throw new ArgumentNullException(nameof(request));
+ }
+
+ if (string.IsNullOrWhiteSpace(serializedBody))
+ {
+ throw new ArgumentNullException(nameof(serializedBody));
+ }
+
+ request.Content = new StringContent(serializedBody, Encoding.UTF8, "application/json");
+
+ var secretBytes = Encoding.UTF8.GetBytes(secret);
+ var headerValue = string.Format(CultureInfo.InvariantCulture, SignatureHeaderValueTemplate, serializedBody.Sha256(secretBytes));
+
+ request.Headers.Add(SignatureHeaderName, headerValue);
+ }
+
+ public virtual async Task GetSerializedBodyAsync(WebhookSenderArgs webhookSenderArgs)
+ {
+ if (webhookSenderArgs.SendExactSameData)
+ {
+ return webhookSenderArgs.Data;
+ }
+
+ var payload = await GetWebhookPayloadAsync(webhookSenderArgs);
+
+ var serializedBody = JsonConvert.SerializeObject(payload);
+
+ return serializedBody;
+ }
+
+ public abstract Task InsertAndGetIdWebhookSendAttemptAsync(WebhookSenderArgs webhookSenderArgs);
+
+ public abstract Task StoreResponseOnWebhookSendAttemptAsync(Guid webhookSendAttemptId, Guid? tenantId, HttpStatusCode? statusCode, string content);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookPayload.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookPayload.cs
new file mode 100644
index 000000000..ff287cbd0
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookPayload.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookPayload
+ {
+ public string Id { get; set; }
+
+ public string WebhookEvent { get; set; }
+
+ public int Attempt { get; set; }
+
+ public dynamic Data { get; set; }
+
+ public DateTime CreationTimeUtc { get; set; }
+
+ public WebhookPayload(string id, string webhookEvent, int attempt)
+ {
+ if (id.IsNullOrWhiteSpace())
+ {
+ throw new ArgumentNullException(nameof(id));
+ }
+
+ if (webhookEvent.IsNullOrWhiteSpace())
+ {
+ throw new ArgumentNullException(nameof(webhookEvent));
+ }
+
+ Id = id;
+ WebhookEvent = webhookEvent;
+ Attempt = attempt;
+ CreationTimeUtc = DateTime.UtcNow;
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookSendAttempt.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookSendAttempt.cs
new file mode 100644
index 000000000..8189f8f72
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookSendAttempt.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Net;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ ///
+ /// Table for store webhook work items. Each item stores web hook send attempt of to subscribed tenants
+ ///
+ public class WebhookSendAttempt
+ {
+ public Guid Id { get; set; }
+
+ ///
+ /// foreign id
+ ///
+ public Guid WebhookEventId { get; set; }
+
+ ///
+ /// foreign id
+ ///
+ public Guid WebhookSubscriptionId { get; set; }
+
+ ///
+ /// Webhook response content that webhook endpoint send back
+ ///
+ public string Response { get; set; }
+
+ ///
+ /// Webhook response status code that webhook endpoint send back
+ ///
+ public HttpStatusCode? ResponseStatusCode { get; set; }
+
+ public DateTime CreationTime { get; set; }
+
+ public DateTime? LastModificationTime { get; set; }
+
+ public Guid? TenantId { get; set; }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookSenderArgs.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookSenderArgs.cs
new file mode 100644
index 000000000..cb30acbf9
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookSenderArgs.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookSenderArgs
+ {
+ public Guid? TenantId { get; set; }
+
+ //Webhook information
+
+ ///
+ /// foreign id
+ ///
+ public Guid WebhookEventId { get; set; }
+
+ ///
+ /// Webhook unique name
+ ///
+ public string WebhookName { get; set; }
+
+ ///
+ /// Webhook data as JSON string.
+ ///
+ public string Data { get; set; }
+
+ //Subscription information
+
+ ///
+ /// foreign id
+ ///
+ public Guid WebhookSubscriptionId { get; set; }
+
+ ///
+ /// Subscription webhook endpoint
+ ///
+ public string WebhookUri { get; set; }
+
+ ///
+ /// Webhook secret
+ ///
+ public string Secret { get; set; }
+
+ ///
+ /// Gets a set of additional HTTP headers.That headers will be sent with the webhook.
+ ///
+ public IDictionary Headers { get; set; }
+
+ ///
+ /// True: It sends the exact same data as the parameter to clients.
+ ///
+ /// False: It sends data in . It is recommended way.
+ ///
+ ///
+ public bool SendExactSameData { get; set; }
+
+ public WebhookSenderArgs()
+ {
+ Headers = new Dictionary();
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionInfo.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionInfo.cs
new file mode 100644
index 000000000..9e4e3bf2a
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionInfo.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookSubscriptionInfo
+ {
+ public Guid Id { get; set; }
+ ///
+ /// Subscribed Tenant's id .
+ ///
+ public Guid? TenantId { get; set; }
+
+ ///
+ /// Subscription webhook endpoint
+ ///
+ public string WebhookUri { get; set; }
+
+ ///
+ /// Webhook secret
+ ///
+ public string Secret { get; set; }
+
+ ///
+ /// Is subscription active
+ ///
+ public bool IsActive { get; set; }
+
+ ///
+ /// Subscribed webhook definitions unique names.It contains webhook definitions list as json
+ ///
+ /// Do not change it manually.
+ /// Use ,
+ /// ,
+ /// and
+ /// to change it.
+ ///
+ ///
+ public List Webhooks { get; set; }
+
+ ///
+ /// Gets a set of additional HTTP headers.That headers will be sent with the webhook. It contains webhook header dictionary as json
+ ///
+ /// Do not change it manually.
+ /// Use ,
+ /// ,
+ /// ,
+ /// to change it.
+ ///
+ ///
+ public IDictionary Headers { get; set; }
+
+ public WebhookSubscriptionInfo()
+ {
+ IsActive = true;
+ Headers = new Dictionary();
+ Webhooks = new List();
+ }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionManager.cs
new file mode 100644
index 000000000..fe1a9c697
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/LINGYUN/Abp/Webhooks/WebhookSubscriptionManager.cs
@@ -0,0 +1,172 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.Authorization;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Guids;
+using Volo.Abp.Uow;
+
+namespace LINGYUN.Abp.Webhooks
+{
+ public class WebhookSubscriptionManager : IWebhookSubscriptionManager, ITransientDependency
+ {
+ public IWebhookSubscriptionsStore WebhookSubscriptionsStore { get; set; }
+
+ private readonly IGuidGenerator _guidGenerator;
+ private readonly IUnitOfWorkManager _unitOfWorkManager;
+ private readonly IWebhookDefinitionManager _webhookDefinitionManager;
+
+ private const string WebhookSubscriptionSecretPrefix = "whs_";
+
+ public WebhookSubscriptionManager(
+ IGuidGenerator guidGenerator,
+ IUnitOfWorkManager unitOfWorkManager,
+ IWebhookDefinitionManager webhookDefinitionManager)
+ {
+ _guidGenerator = guidGenerator;
+ _unitOfWorkManager = unitOfWorkManager;
+ _webhookDefinitionManager = webhookDefinitionManager;
+
+ WebhookSubscriptionsStore = NullWebhookSubscriptionsStore.Instance;
+ }
+
+ public virtual async Task GetAsync(Guid id)
+ {
+ return await WebhookSubscriptionsStore.GetAsync(id);
+ }
+
+ public virtual async Task> GetAllSubscriptionsAsync(Guid? tenantId)
+ {
+ return await WebhookSubscriptionsStore.GetAllSubscriptionsAsync(tenantId);
+ }
+
+ public virtual async Task> GetAllSubscriptionsIfFeaturesGrantedAsync(Guid? tenantId, string webhookName)
+ {
+ if (!await _webhookDefinitionManager.IsAvailableAsync(tenantId, webhookName))
+ {
+ return new List();
+ }
+
+ return (await WebhookSubscriptionsStore.GetAllSubscriptionsAsync(tenantId, webhookName)).ToList();
+ }
+
+ public virtual async Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds)
+ {
+ return (await WebhookSubscriptionsStore.GetAllSubscriptionsOfTenantsAsync(tenantIds)).ToList();
+ }
+
+ public virtual async Task> GetAllSubscriptionsOfTenantsIfFeaturesGrantedAsync(Guid?[] tenantIds, string webhookName)
+ {
+ var featureGrantedTenants = new List();
+ foreach (var tenantId in tenantIds)
+ {
+ if (await _webhookDefinitionManager.IsAvailableAsync(tenantId, webhookName))
+ {
+ featureGrantedTenants.Add(tenantId);
+ }
+ }
+
+ return (await WebhookSubscriptionsStore.GetAllSubscriptionsOfTenantsAsync(featureGrantedTenants.ToArray(), webhookName)).ToList();
+ }
+
+ public virtual async Task IsSubscribedAsync(Guid? tenantId, string webhookName)
+ {
+ if (!await _webhookDefinitionManager.IsAvailableAsync(tenantId, webhookName))
+ {
+ return false;
+ }
+
+ return await WebhookSubscriptionsStore.IsSubscribedAsync(tenantId, webhookName);
+ }
+
+ public virtual async Task AddOrUpdateSubscriptionAsync(WebhookSubscriptionInfo webhookSubscription)
+ {
+ using (var uow = _unitOfWorkManager.Begin())
+ {
+ await CheckIfPermissionsGrantedAsync(webhookSubscription);
+
+ if (webhookSubscription.Id == default)
+ {
+ webhookSubscription.Id = _guidGenerator.Create();
+ webhookSubscription.Secret = WebhookSubscriptionSecretPrefix + Guid.NewGuid().ToString("N");
+ await WebhookSubscriptionsStore.InsertAsync(webhookSubscription);
+ }
+ else
+ {
+ await WebhookSubscriptionsStore.UpdateAsync(webhookSubscription);
+ }
+
+ await uow.SaveChangesAsync();
+ }
+ }
+
+ public virtual async Task ActivateWebhookSubscriptionAsync(Guid id, bool active)
+ {
+ using (var uow = _unitOfWorkManager.Begin())
+ {
+ var webhookSubscription = await WebhookSubscriptionsStore.GetAsync(id);
+ webhookSubscription.IsActive = active;
+
+ await uow.SaveChangesAsync();
+ }
+ }
+
+ public virtual async Task DeleteSubscriptionAsync(Guid id)
+ {
+ using (var uow = _unitOfWorkManager.Begin())
+ {
+ await WebhookSubscriptionsStore.DeleteAsync(id);
+
+ await uow.SaveChangesAsync();
+ }
+ }
+
+ public virtual async Task AddWebhookAsync(WebhookSubscriptionInfo subscription, string webhookName)
+ {
+ using (var uow = _unitOfWorkManager.Begin())
+ {
+ await CheckPermissionsAsync(subscription.TenantId, webhookName);
+ webhookName = webhookName.Trim();
+ if (webhookName.IsNullOrWhiteSpace())
+ {
+ throw new ArgumentNullException(nameof(webhookName), $"{nameof(webhookName)} can not be null, empty or whitespace!");
+ }
+
+ if (!subscription.Webhooks.Contains(webhookName))
+ {
+ subscription.Webhooks.Add(webhookName);
+
+ await WebhookSubscriptionsStore.UpdateAsync(subscription);
+ }
+
+ await uow.SaveChangesAsync();
+ }
+ }
+
+ #region PermissionCheck
+
+ protected virtual async Task CheckIfPermissionsGrantedAsync(WebhookSubscriptionInfo webhookSubscription)
+ {
+ if (webhookSubscription.Webhooks.IsNullOrEmpty())
+ {
+ return;
+ }
+
+ foreach (var webhookDefinition in webhookSubscription.Webhooks)
+ {
+ await CheckPermissionsAsync(webhookSubscription.TenantId, webhookDefinition);
+ }
+ }
+
+ protected virtual async Task CheckPermissionsAsync(Guid? tenantId, string webhookName)
+ {
+ if (!await _webhookDefinitionManager.IsAvailableAsync(tenantId, webhookName))
+ {
+ throw new AbpAuthorizationException($"Tenant \"{tenantId}\" must have necessary feature(s) to use webhook \"{webhookName}\"");
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/System/AbpStringCryptographyExtensions.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/System/AbpStringCryptographyExtensions.cs
new file mode 100644
index 000000000..f4202f6e2
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebHooks/System/AbpStringCryptographyExtensions.cs
@@ -0,0 +1,13 @@
+using System.Security.Cryptography;
+
+namespace System;
+
+internal static class AbpStringCryptographyExtensions
+{
+ public static string Sha256(this string planText, byte[] salt)
+ {
+ var data = planText.GetBytes();
+ using var hmacsha256 = new HMACSHA256(salt);
+ return BitConverter.ToString(hmacsha256.ComputeHash(data));
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/FodyWeavers.xml b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/FodyWeavers.xml
new file mode 100644
index 000000000..c485a4548
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/FodyWeavers.xsd b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/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/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN.Abp.Webhooks.Identity.csproj b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN.Abp.Webhooks.Identity.csproj
new file mode 100644
index 000000000..32c987515
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN.Abp.Webhooks.Identity.csproj
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/AbpWebhooksIdentityModule.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/AbpWebhooksIdentityModule.cs
new file mode 100644
index 000000000..0ca523251
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/AbpWebhooksIdentityModule.cs
@@ -0,0 +1,32 @@
+using Volo.Abp.Domain;
+using Volo.Abp.EventBus;
+using Volo.Abp.Identity;
+using Volo.Abp.Identity.Localization;
+using Volo.Abp.Localization;
+using Volo.Abp.Modularity;
+using Volo.Abp.Users;
+using Volo.Abp.VirtualFileSystem;
+
+namespace LINGYUN.Abp.Webhooks.Identity;
+
+[DependsOn(typeof(AbpDddDomainModule))]
+[DependsOn(typeof(AbpEventBusModule))]
+[DependsOn(typeof(AbpUsersAbstractionModule))]
+[DependsOn(typeof(AbpIdentityDomainSharedModule))]
+public class AbpWebhooksIdentityModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ Configure(options =>
+ {
+ options.Resources
+ .Get()
+ .AddVirtualJson("/LINGYUN/Abp/Webhooks/Identity/Localization/Resources");
+ });
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityRoleNameChangedWto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityRoleNameChangedWto.cs
new file mode 100644
index 000000000..c91615749
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityRoleNameChangedWto.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace LINGYUN.Abp.Webhooks.Identity;
+
+[Serializable]
+public class IdentityRoleNameChangedWto
+{
+ public Guid Id { get; set; }
+
+ public string Name { get; set; }
+
+ public string OldName { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityRoleWebHooker.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityRoleWebHooker.cs
new file mode 100644
index 000000000..5d60012c0
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityRoleWebHooker.cs
@@ -0,0 +1,63 @@
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Entities.Events.Distributed;
+using Volo.Abp.EventBus.Distributed;
+using Volo.Abp.Identity;
+
+namespace LINGYUN.Abp.Webhooks.Identity;
+
+public class IdentityRoleWebhooker :
+ IDistributedEventHandler>,
+ IDistributedEventHandler>,
+ IDistributedEventHandler>,
+ IDistributedEventHandler,
+ ITransientDependency
+{
+ private readonly IWebhookPublisher _webhookPublisher;
+
+ public IdentityRoleWebhooker(
+ IWebhookPublisher webhookPublisher)
+ {
+ _webhookPublisher = webhookPublisher;
+ }
+
+ public async virtual Task HandleEventAsync(EntityCreatedEto eventData)
+ {
+ await PublishAsync(IdentityWebhookNames.IdentityRole.Create, eventData.Entity);
+ }
+
+ public async virtual Task HandleEventAsync(EntityUpdatedEto eventData)
+ {
+ await PublishAsync(IdentityWebhookNames.IdentityRole.Update, eventData.Entity);
+ }
+
+ public async virtual Task HandleEventAsync(EntityDeletedEto eventData)
+ {
+ await PublishAsync(IdentityWebhookNames.IdentityRole.Delete, eventData.Entity);
+ }
+
+ public async virtual Task HandleEventAsync(IdentityRoleNameChangedEto eventData)
+ {
+ await _webhookPublisher.PublishAsync(
+ IdentityWebhookNames.IdentityRole.ChangeName,
+ new IdentityRoleNameChangedWto
+ {
+ Id = eventData.Id,
+ Name = eventData.Name,
+ OldName = eventData.OldName,
+ },
+ eventData.TenantId);
+ }
+
+ protected async virtual Task PublishAsync(string webhookName, IdentityRoleEto eto)
+ {
+ await _webhookPublisher.PublishAsync(
+ webhookName,
+ new IdentityRoleWto
+ {
+ Id = eto.Id,
+ Name = eto.Name
+ },
+ eto.TenantId);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityRoleWto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityRoleWto.cs
new file mode 100644
index 000000000..1271a5877
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityRoleWto.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace LINGYUN.Abp.Webhooks.Identity;
+
+[Serializable]
+public class IdentityRoleWto
+{
+ public Guid Id { get; set; }
+
+ public string Name { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityUserWebHooker.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityUserWebHooker.cs
new file mode 100644
index 000000000..7dc188cf5
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityUserWebHooker.cs
@@ -0,0 +1,55 @@
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Entities.Events.Distributed;
+using Volo.Abp.EventBus.Distributed;
+using Volo.Abp.Users;
+
+namespace LINGYUN.Abp.Webhooks.Identity;
+
+public class IdentityUserWebhooker :
+ IDistributedEventHandler>,
+ IDistributedEventHandler>,
+ IDistributedEventHandler>,
+ ITransientDependency
+{
+ private readonly IWebhookPublisher _webhookPublisher;
+
+ public IdentityUserWebhooker(
+ IWebhookPublisher webhookPublisher)
+ {
+ _webhookPublisher = webhookPublisher;
+ }
+
+ public async virtual Task HandleEventAsync(EntityCreatedEto eventData)
+ {
+ await PublishAsync(IdentityWebhookNames.IdentityUser.Create, eventData.Entity);
+ }
+
+ public async virtual Task HandleEventAsync(EntityUpdatedEto eventData)
+ {
+ await PublishAsync(IdentityWebhookNames.IdentityUser.Update, eventData.Entity);
+ }
+
+ public async virtual Task HandleEventAsync(EntityDeletedEto eventData)
+ {
+ await PublishAsync(IdentityWebhookNames.IdentityUser.Delete, eventData.Entity);
+ }
+
+ protected async virtual Task PublishAsync(string webhookName, UserEto eto)
+ {
+ await _webhookPublisher.PublishAsync(
+ webhookName,
+ new IdentityUserWto
+ {
+ Id = eto.Id,
+ Name = eto.Name,
+ Email = eto.Email,
+ EmailConfirmed = eto.EmailConfirmed,
+ UserName = eto.UserName,
+ PhoneNumber = eto.PhoneNumber,
+ PhoneNumberConfirmed = eto.PhoneNumberConfirmed,
+ Surname = eto.Surname,
+ },
+ eto.TenantId);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityUserWto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityUserWto.cs
new file mode 100644
index 000000000..29da00874
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityUserWto.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace LINGYUN.Abp.Webhooks.Identity;
+
+[Serializable]
+public class IdentityUserWto
+{
+ public Guid Id { get; set; }
+
+ public string UserName { get; set; }
+
+ public string Name { get; set; }
+
+ public string Surname { get; set; }
+
+ public string Email { get; set; }
+
+ public bool EmailConfirmed { get; set; }
+
+ public string PhoneNumber { get; set; }
+
+ public bool PhoneNumberConfirmed { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityWebhookDefinitionProvider.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityWebhookDefinitionProvider.cs
new file mode 100644
index 000000000..6c243b942
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityWebhookDefinitionProvider.cs
@@ -0,0 +1,64 @@
+using Volo.Abp.Identity.Localization;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Webhooks.Identity;
+
+public class IdentityWebhookDefinitionProvider : WebhookDefinitionProvider
+{
+ public override void Define(IWebhookDefinitionContext context)
+ {
+ var identityGroup = context.AddGroup(
+ IdentityWebhookNames.GroupName,
+ L("Webhooks:Identity"));
+
+ identityGroup.AddWebhooks(CreateIdentityRoleWebhooks());
+ identityGroup.AddWebhooks(CreateIdentityUserWebhooks());
+ }
+
+ protected virtual WebhookDefinition[] CreateIdentityRoleWebhooks()
+ {
+ return new[]
+ {
+ new WebhookDefinition(
+ IdentityWebhookNames.IdentityRole.Create,
+ L("Webhooks:CreateRole"),
+ L("Webhooks:CreateRoleDesc")),
+ new WebhookDefinition(
+ IdentityWebhookNames.IdentityRole.Update,
+ L("Webhooks:UpdateRole"),
+ L("Webhooks:UpdateRoleDesc")),
+ new WebhookDefinition(
+ IdentityWebhookNames.IdentityRole.Delete,
+ L("Webhooks:DeleteRole"),
+ L("Webhooks:DeleteRoleDesc")),
+ new WebhookDefinition(
+ IdentityWebhookNames.IdentityRole.ChangeName,
+ L("Webhooks:ChangeRoleName"),
+ L("Webhooks:ChangeRoleNameDesc")),
+ };
+ }
+
+ protected virtual WebhookDefinition[] CreateIdentityUserWebhooks()
+ {
+ return new[]
+ {
+ new WebhookDefinition(
+ IdentityWebhookNames.IdentityUser.Create,
+ L("Webhooks:CreateUser"),
+ L("Webhooks:CreateUserDesc")),
+ new WebhookDefinition(
+ IdentityWebhookNames.IdentityUser.Update,
+ L("Webhooks:UpdateUser"),
+ L("Webhooks:UpdateUserDesc")),
+ new WebhookDefinition(
+ IdentityWebhookNames.IdentityUser.Delete,
+ L("Webhooks:DeleteUser"),
+ L("Webhooks:DeleteUserDesc")),
+ };
+ }
+
+ private static ILocalizableString L(string name)
+ {
+ return LocalizableString.Create(name);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityWebhookNames.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityWebhookNames.cs
new file mode 100644
index 000000000..be7fa0232
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/IdentityWebhookNames.cs
@@ -0,0 +1,22 @@
+namespace LINGYUN.Abp.Webhooks.Identity;
+
+public static class IdentityWebhookNames
+{
+ public const string GroupName = "abp.webhooks.identity";
+ public static class IdentityRole
+ {
+ public const string Prefix = GroupName + ".roles";
+ public const string Create = Prefix + ".create";
+ public const string Update = Prefix + ".update";
+ public const string Delete = Prefix + ".delete";
+ public const string ChangeName = Prefix + ".change_name";
+ }
+
+ public static class IdentityUser
+ {
+ public const string Prefix = GroupName + ".users";
+ public const string Create = Prefix + ".create";
+ public const string Update = Prefix + ".update";
+ public const string Delete = Prefix + ".delete";
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/Localization/Resources/en.json b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/Localization/Resources/en.json
new file mode 100644
index 000000000..05df4003d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/Localization/Resources/en.json
@@ -0,0 +1,20 @@
+{
+ "culture": "en",
+ "texts": {
+ "Webhooks:Identity": "Identity",
+ "Webhooks:CreateRole": "Create Role",
+ "Webhooks:CreateRoleDesc": "A new role has been created",
+ "Webhooks:UpdateRole": "Update Role",
+ "Webhooks:UpdateRoleDesc": "A role has changed",
+ "Webhooks:DeleteRole": "Delete Role",
+ "Webhooks:DeleteRoleDesc": "Deleted Role",
+ "Webhooks:ChangeRoleName": "Change Role Name",
+ "Webhooks:ChangeRoleNameDesc": "The name of a role was changed",
+ "Webhooks:CreateUser": "Create User",
+ "Webhooks:CreateUserDesc": "A new user has been created",
+ "Webhooks:UpdateUser": "Update User",
+ "Webhooks:UpdateUserDesc": "A user has been changed",
+ "Webhooks:DeleteUser": "Delete User",
+ "Webhooks:DeleteUserDesc": "Deleted User"
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/Localization/Resources/zh-Hans.json b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/Localization/Resources/zh-Hans.json
new file mode 100644
index 000000000..eb06d0489
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/Localization/Resources/zh-Hans.json
@@ -0,0 +1,20 @@
+{
+ "culture": "zh-Hans",
+ "texts": {
+ "Webhooks:Identity": "身份认证",
+ "Webhooks:CreateRole": "创建角色",
+ "Webhooks:CreateRoleDesc": "一个新角色已创建",
+ "Webhooks:UpdateRole": "编辑角色",
+ "Webhooks:UpdateRoleDesc": "一个角色属性已变更",
+ "Webhooks:DeleteRole": "删除角色",
+ "Webhooks:DeleteRoleDesc": "已删除角色",
+ "Webhooks:ChangeRoleName": "改变角色名称",
+ "Webhooks:ChangeRoleNameDesc": "一个角色的名称被更改",
+ "Webhooks:CreateUser": "创建用户",
+ "Webhooks:CreateUserDesc": "一个新用户已创建",
+ "Webhooks:UpdateUser": "编辑用户",
+ "Webhooks:UpdateUserDesc": "一个用户属性已变更",
+ "Webhooks:DeleteUser": "删除用户",
+ "Webhooks:DeleteUserDesc": "已删除用户"
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/OrganizationUnitWebHooker.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/OrganizationUnitWebHooker.cs
new file mode 100644
index 000000000..a259bc16b
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/OrganizationUnitWebHooker.cs
@@ -0,0 +1,49 @@
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Entities.Events.Distributed;
+using Volo.Abp.EventBus.Distributed;
+using Volo.Abp.Identity;
+
+namespace LINGYUN.Abp.Webhooks.Identity;
+
+public class OrganizationUnitWebhooker :
+ IDistributedEventHandler>,
+ IDistributedEventHandler>,
+ IDistributedEventHandler>,
+ ITransientDependency
+{
+ private readonly IWebhookPublisher _webhookPublisher;
+
+ public OrganizationUnitWebhooker(
+ IWebhookPublisher webhookPublisher)
+ {
+ _webhookPublisher = webhookPublisher;
+ }
+
+ public async virtual Task HandleEventAsync(EntityCreatedEto eventData)
+ {
+ await PublishAsync(IdentityWebhookNames.IdentityRole.Create, eventData.Entity);
+ }
+
+ public async virtual Task HandleEventAsync(EntityUpdatedEto eventData)
+ {
+ await PublishAsync(IdentityWebhookNames.IdentityRole.Update, eventData.Entity);
+ }
+
+ public async virtual Task HandleEventAsync(EntityDeletedEto eventData)
+ {
+ await PublishAsync(IdentityWebhookNames.IdentityRole.Delete, eventData.Entity);
+ }
+
+ protected async virtual Task PublishAsync(string webhookName, OrganizationUnitEto eto)
+ {
+ await _webhookPublisher.PublishAsync(
+ webhookName,
+ new OrganizationUnitWto
+ {
+ Id = eto.Id,
+ DisplayName = eto.DisplayName
+ },
+ eto.TenantId);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/OrganizationUnitWto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/OrganizationUnitWto.cs
new file mode 100644
index 000000000..3dd758890
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Identity/LINGYUN/Abp/Webhooks/Identity/OrganizationUnitWto.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace LINGYUN.Abp.Webhooks.Identity;
+
+[Serializable]
+public class OrganizationUnitWto
+{
+ public Guid Id { get; set; }
+
+ public string Code { get; set; }
+
+ public string DisplayName { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/FodyWeavers.xml b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/FodyWeavers.xml
new file mode 100644
index 000000000..c485a4548
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/FodyWeavers.xsd b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/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/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN.Abp.Webhooks.Saas.csproj b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN.Abp.Webhooks.Saas.csproj
new file mode 100644
index 000000000..66d9fa1b8
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN.Abp.Webhooks.Saas.csproj
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/AbpWebhooksSaasModule.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/AbpWebhooksSaasModule.cs
new file mode 100644
index 000000000..93567e45a
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/AbpWebhooksSaasModule.cs
@@ -0,0 +1,30 @@
+using LINGYUN.Abp.Saas;
+using LINGYUN.Abp.Saas.Localization;
+using Volo.Abp.Domain;
+using Volo.Abp.EventBus;
+using Volo.Abp.Localization;
+using Volo.Abp.Modularity;
+using Volo.Abp.VirtualFileSystem;
+
+namespace LINGYUN.Abp.Webhooks.Saas;
+
+[DependsOn(typeof(AbpDddDomainModule))]
+[DependsOn(typeof(AbpEventBusModule))]
+[DependsOn(typeof(AbpSaasDomainSharedModule))]
+public class AbpWebhooksSaasModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ Configure(options =>
+ {
+ options.Resources
+ .Get()
+ .AddVirtualJson("/LINGYUN/Abp/Webhooks/Saas/Localization/Resources");
+ });
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/EditionWebhooker.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/EditionWebhooker.cs
new file mode 100644
index 000000000..fffdd0328
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/EditionWebhooker.cs
@@ -0,0 +1,48 @@
+using LINGYUN.Abp.Saas.Editions;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Entities.Events.Distributed;
+using Volo.Abp.EventBus.Distributed;
+
+namespace LINGYUN.Abp.Webhooks.Saas;
+
+public class EditionWebhooker :
+ IDistributedEventHandler>,
+ IDistributedEventHandler>,
+ IDistributedEventHandler>,
+ ITransientDependency
+{
+ private readonly IWebhookPublisher _webhookPublisher;
+
+ public EditionWebhooker(
+ IWebhookPublisher webhookPublisher)
+ {
+ _webhookPublisher = webhookPublisher;
+ }
+
+ public async virtual Task HandleEventAsync(EntityCreatedEto eventData)
+ {
+ await PublishAsync(SaasWebhookNames.Edition.Create, eventData.Entity);
+ }
+
+ public async virtual Task HandleEventAsync(EntityUpdatedEto eventData)
+ {
+ await PublishAsync(SaasWebhookNames.Edition.Update, eventData.Entity);
+ }
+
+ public async virtual Task HandleEventAsync(EntityDeletedEto eventData)
+ {
+ await PublishAsync(SaasWebhookNames.Edition.Delete, eventData.Entity);
+ }
+
+ protected async virtual Task PublishAsync(string webhookName, EditionEto eto)
+ {
+ await _webhookPublisher.PublishAsync(
+ webhookName,
+ new EditionWto
+ {
+ Id = eto.Id,
+ DisplayName = eto.DisplayName
+ });
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/EditionWto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/EditionWto.cs
new file mode 100644
index 000000000..8a3a1208b
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/EditionWto.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace LINGYUN.Abp.Webhooks.Saas;
+
+[Serializable]
+public class EditionWto
+{
+ public Guid Id { get; set; }
+
+ public string DisplayName { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/Localization/Resources/en.json b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/Localization/Resources/en.json
new file mode 100644
index 000000000..fdafcd2d1
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/Localization/Resources/en.json
@@ -0,0 +1,18 @@
+{
+ "culture": "en",
+ "texts": {
+ "Webhooks:Saas": "Saas",
+ "Webhooks:CreateEdition": "Create Edition",
+ "Webhooks:CreateEditionDesc": "A new Edition has been created",
+ "Webhooks:UpdateEdition": "Update Edition",
+ "Webhooks:UpdateEditionDesc": "A Edition has changed",
+ "Webhooks:DeleteEdition": "Delete Edition",
+ "Webhooks:DeleteEditionDesc": "Deleted Edition",
+ "Webhooks:CreateTenant": "Create Tenant",
+ "Webhooks:CreateTenantDesc": "A new Tenant has been created",
+ "Webhooks:UpdateTenant": "Update Tenant",
+ "Webhooks:UpdateTenantDesc": "A Tenant has been changed",
+ "Webhooks:DeleteTenant": "Delete Tenant",
+ "Webhooks:DeleteTenantDesc": "Deleted Tenant"
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/Localization/Resources/zh-Hans.json b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/Localization/Resources/zh-Hans.json
new file mode 100644
index 000000000..fd5ddbf30
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/Localization/Resources/zh-Hans.json
@@ -0,0 +1,18 @@
+{
+ "culture": "zh-Hans",
+ "texts": {
+ "Webhooks:Saas": "Saas",
+ "Webhooks:CreateEdition": "创建版本",
+ "Webhooks:CreateEditionDesc": "一个新版本已创建",
+ "Webhooks:UpdateEdition": "编辑版本",
+ "Webhooks:UpdateEditionDesc": "一个版本属性已变更",
+ "Webhooks:DeleteEdition": "删除版本",
+ "Webhooks:DeleteEditionDesc": "已删除版本",
+ "Webhooks:CreateTenant": "创建租户",
+ "Webhooks:CreateTenantDesc": "一个新租户已创建",
+ "Webhooks:UpdateTenant": "编辑租户",
+ "Webhooks:UpdateTenantDesc": "一个租户属性已变更",
+ "Webhooks:DeleteTenant": "删除租户",
+ "Webhooks:DeleteTenantDesc": "已删除租户"
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/SaasWebhookDefinitionProvider.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/SaasWebhookDefinitionProvider.cs
new file mode 100644
index 000000000..b2624f179
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/SaasWebhookDefinitionProvider.cs
@@ -0,0 +1,60 @@
+using LINGYUN.Abp.Saas.Localization;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Webhooks.Saas;
+
+public class SaasWebhookDefinitionProvider : WebhookDefinitionProvider
+{
+ public override void Define(IWebhookDefinitionContext context)
+ {
+ var saasGroup = context.AddGroup(
+ SaasWebhookNames.GroupName,
+ L("Webhooks:Saas"));
+
+ saasGroup.AddWebhooks(CreateEditionWebhooks());
+ saasGroup.AddWebhooks(CreateTenantWebhooks());
+ }
+
+ protected virtual WebhookDefinition[] CreateEditionWebhooks()
+ {
+ return new[]
+ {
+ new WebhookDefinition(
+ SaasWebhookNames.Edition.Create,
+ L("Webhooks:CreateEdition"),
+ L("Webhooks:CreateEditionDesc")),
+ new WebhookDefinition(
+ SaasWebhookNames.Edition.Update,
+ L("Webhooks:UpdateEdition"),
+ L("Webhooks:UpdateEditionDesc")),
+ new WebhookDefinition(
+ SaasWebhookNames.Edition.Delete,
+ L("Webhooks:DeleteEdition"),
+ L("Webhooks:DeleteEditionDesc")),
+ };
+ }
+
+ protected virtual WebhookDefinition[] CreateTenantWebhooks()
+ {
+ return new[]
+ {
+ new WebhookDefinition(
+ SaasWebhookNames.Tenant.Create,
+ L("Webhooks:CreateTenant"),
+ L("Webhooks:CreateTenantDesc")),
+ new WebhookDefinition(
+ SaasWebhookNames.Tenant.Update,
+ L("Webhooks:UpdateTenant"),
+ L("Webhooks:UpdateTenantDesc")),
+ new WebhookDefinition(
+ SaasWebhookNames.Tenant.Delete,
+ L("Webhooks:DeleteTenant"),
+ L("Webhooks:DeleteTenantDesc")),
+ };
+ }
+
+ private static ILocalizableString L(string name)
+ {
+ return LocalizableString.Create(name);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/SaasWebhookNames.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/SaasWebhookNames.cs
new file mode 100644
index 000000000..399903fc6
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/SaasWebhookNames.cs
@@ -0,0 +1,21 @@
+namespace LINGYUN.Abp.Webhooks.Saas;
+
+public static class SaasWebhookNames
+{
+ public const string GroupName = "abp.webhooks.saas";
+ public static class Edition
+ {
+ public const string Prefix = GroupName + ".editions";
+ public const string Create = Prefix + ".create";
+ public const string Update = Prefix + ".update";
+ public const string Delete = Prefix + ".delete";
+ }
+
+ public static class Tenant
+ {
+ public const string Prefix = GroupName + ".tenants";
+ public const string Create = Prefix + ".create";
+ public const string Update = Prefix + ".update";
+ public const string Delete = Prefix + ".delete";
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/TenantWebhooker.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/TenantWebhooker.cs
new file mode 100644
index 000000000..916fd8f3b
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/TenantWebhooker.cs
@@ -0,0 +1,48 @@
+using LINGYUN.Abp.Saas.Tenants;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Entities.Events.Distributed;
+using Volo.Abp.EventBus.Distributed;
+
+namespace LINGYUN.Abp.Webhooks.Saas;
+
+public class TenantWebhooker :
+ IDistributedEventHandler>,
+ IDistributedEventHandler>,
+ IDistributedEventHandler>,
+ ITransientDependency
+{
+ private readonly IWebhookPublisher _webhookPublisher;
+
+ public TenantWebhooker(
+ IWebhookPublisher webhookPublisher)
+ {
+ _webhookPublisher = webhookPublisher;
+ }
+
+ public async virtual Task HandleEventAsync(EntityCreatedEto eventData)
+ {
+ await PublishAsync(SaasWebhookNames.Tenant.Create, eventData.Entity);
+ }
+
+ public async virtual Task HandleEventAsync(EntityUpdatedEto eventData)
+ {
+ await PublishAsync(SaasWebhookNames.Tenant.Update, eventData.Entity);
+ }
+
+ public async virtual Task HandleEventAsync(EntityDeletedEto eventData)
+ {
+ await PublishAsync(SaasWebhookNames.Tenant.Delete, eventData.Entity);
+ }
+
+ protected async virtual Task PublishAsync(string webhookName, TenantEto eto)
+ {
+ await _webhookPublisher.PublishAsync(
+ webhookName,
+ new TenantWto
+ {
+ Id = eto.Id,
+ Name = eto.Name
+ });
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/TenantWto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/TenantWto.cs
new file mode 100644
index 000000000..ba2d4ef0d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Saas/LINGYUN/Abp/Webhooks/Saas/TenantWto.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace LINGYUN.Abp.Webhooks.Saas;
+
+[Serializable]
+public class TenantWto
+{
+ public Guid Id { get; set; }
+
+ public string Name { get; set; }
+}
+
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/FodyWeavers.xml b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/FodyWeavers.xml
new file mode 100644
index 000000000..be0de3a90
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/FodyWeavers.xsd b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/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/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN.Abp.WebhooksManagement.Application.Contracts.csproj b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN.Abp.WebhooksManagement.Application.Contracts.csproj
new file mode 100644
index 000000000..70d317b54
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN.Abp.WebhooksManagement.Application.Contracts.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/Authorization/WebhooksManagementPermissionDefinitionProvider.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/Authorization/WebhooksManagementPermissionDefinitionProvider.cs
new file mode 100644
index 000000000..4c9513f02
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/Authorization/WebhooksManagementPermissionDefinitionProvider.cs
@@ -0,0 +1,62 @@
+using LINGYUN.Abp.WebhooksManagement.Localization;
+using Volo.Abp.Authorization.Permissions;
+using Volo.Abp.Localization;
+using Volo.Abp.MultiTenancy;
+
+namespace LINGYUN.Abp.WebhooksManagement.Authorization;
+
+public class WebhooksManagementPermissionDefinitionProvider : PermissionDefinitionProvider
+{
+ public override void Define(IPermissionDefinitionContext context)
+ {
+ var group = context.AddGroup(
+ WebhooksManagementPermissions.GroupName,
+ L("Permission:WebhooksManagement"),
+ MultiTenancySides.Host);
+
+ var subscription = group.AddPermission(
+ WebhooksManagementPermissions.WebhookSubscription.Default,
+ L("Permission:Subscriptions"),
+ MultiTenancySides.Host)
+ .WithProviders(ClientPermissionValueProvider.ProviderName);
+ subscription.AddChild(
+ WebhooksManagementPermissions.WebhookSubscription.Create,
+ L("Permission:Create"),
+ MultiTenancySides.Host)
+ .WithProviders(ClientPermissionValueProvider.ProviderName);
+ subscription.AddChild(
+ WebhooksManagementPermissions.WebhookSubscription.Update,
+ L("Permission:Update"),
+ MultiTenancySides.Host)
+ .WithProviders(ClientPermissionValueProvider.ProviderName);
+ subscription.AddChild(
+ WebhooksManagementPermissions.WebhookSubscription.Delete,
+ L("Permission:Delete"),
+ MultiTenancySides.Host)
+ .WithProviders(ClientPermissionValueProvider.ProviderName);
+
+ var sendAttempts = group.AddPermission(
+ WebhooksManagementPermissions.WebhooksSendAttempts.Default,
+ L("Permission:SendAttempts"),
+ MultiTenancySides.Host);
+ sendAttempts.AddChild(
+ WebhooksManagementPermissions.WebhooksSendAttempts.Resend,
+ L("Permission:Resend"),
+ MultiTenancySides.Host);
+
+ group.AddPermission(
+ WebhooksManagementPermissions.Publish,
+ L("Permission:Publish"))
+ .WithProviders(ClientPermissionValueProvider.ProviderName);
+
+ group.AddPermission(
+ WebhooksManagementPermissions.ManageSettings,
+ L("Permission:ManageSettings"),
+ MultiTenancySides.Host);
+ }
+
+ private static LocalizableString L(string name)
+ {
+ return LocalizableString.Create(name);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/Authorization/WebhooksManagementPermissions.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/Authorization/WebhooksManagementPermissions.cs
new file mode 100644
index 000000000..6f6ab9c47
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/Authorization/WebhooksManagementPermissions.cs
@@ -0,0 +1,27 @@
+namespace LINGYUN.Abp.WebhooksManagement.Authorization;
+
+public static class WebhooksManagementPermissions
+{
+ public const string GroupName = "AbpWebhooks";
+
+ ///
+ /// 授权允许发布Webhooks事件, 建议客户端授权
+ ///
+ public const string Publish = GroupName + ".Publish";
+
+ public const string ManageSettings = GroupName + ".ManageSettings";
+
+ public static class WebhookSubscription
+ {
+ public const string Default = GroupName + ".Subscriptions";
+ public const string Create = Default + ".Create";
+ public const string Update = Default + ".Update";
+ public const string Delete = Default + ".Delete";
+ }
+
+ public static class WebhooksSendAttempts
+ {
+ public const string Default = GroupName + ".SendAttempts";
+ public const string Resend = Default + ".Resend";
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/Features/WebhooksManagementFeatureDefinitionProvider.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/Features/WebhooksManagementFeatureDefinitionProvider.cs
new file mode 100644
index 000000000..d124abe45
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/Features/WebhooksManagementFeatureDefinitionProvider.cs
@@ -0,0 +1,18 @@
+using LINGYUN.Abp.WebhooksManagement.Localization;
+using Volo.Abp.Features;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.WebhooksManagement.Features;
+
+public class WebhooksManagementFeatureDefinitionProvider : FeatureDefinitionProvider
+{
+ public override void Define(IFeatureDefinitionContext context)
+ {
+ //var group = context.AddGroup(WebhooksManagementFeatureNames.GroupName, L("Features:WebhooksManagement"));
+ }
+
+ private static ILocalizableString L(string name)
+ {
+ return LocalizableString.Create(name);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/Features/WebhooksManagementFeatureNames.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/Features/WebhooksManagementFeatureNames.cs
new file mode 100644
index 000000000..6cb71bf6d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/Features/WebhooksManagementFeatureNames.cs
@@ -0,0 +1,6 @@
+namespace LINGYUN.Abp.WebhooksManagement.Features;
+
+public static class WebhooksManagementFeatureNames
+{
+ public const string GroupName = "WebhooksManagement";
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/IWebhookPublishAppService.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/IWebhookPublishAppService.cs
new file mode 100644
index 000000000..1e375d98f
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/IWebhookPublishAppService.cs
@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+using Volo.Abp.Application.Services;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public interface IWebhookPublishAppService : IApplicationService
+{
+ Task PublishAsync(WebhookPublishInput input);
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/IWebhookSendRecordAppService.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/IWebhookSendRecordAppService.cs
new file mode 100644
index 000000000..da24b710b
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/IWebhookSendRecordAppService.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Application.Services;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public interface IWebhookSendRecordAppService : IApplicationService
+{
+ Task GetAsync(Guid id);
+
+ Task ResendAsync(Guid id);
+
+ Task> GetListAsync(WebhookSendRecordGetListInput input);
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/IWebhookSubscriptionAppService.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/IWebhookSubscriptionAppService.cs
new file mode 100644
index 000000000..132fcbd73
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/IWebhookSubscriptionAppService.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Application.Services;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public interface IWebhookSubscriptionAppService :
+ ICrudAppService<
+ WebhookSubscriptionDto,
+ Guid,
+ WebhookSubscriptionGetListInput,
+ WebhookSubscriptionCreateInput,
+ WebhookSubscriptionUpdateInput>
+{
+ Task> GetAllAvailableWebhooksAsync();
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookAvailableDto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookAvailableDto.cs
new file mode 100644
index 000000000..0e4c5ed6d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookAvailableDto.cs
@@ -0,0 +1,8 @@
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookAvailableDto
+{
+ public string Name { get; set; }
+ public string DisplayName { get; set; }
+ public string Description { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookAvailableGroupDto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookAvailableGroupDto.cs
new file mode 100644
index 000000000..37a3c8fe1
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookAvailableGroupDto.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookAvailableGroupDto
+{
+ public string Name { get; set; }
+ public string DisplayName { get; set; }
+ public List Webhooks { get; set; } = new List();
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookEventRecordDto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookEventRecordDto.cs
new file mode 100644
index 000000000..a282777a6
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookEventRecordDto.cs
@@ -0,0 +1,12 @@
+using System;
+using Volo.Abp.Application.Dtos;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookEventRecordDto : EntityDto
+{
+ public Guid? TenantId { get; set; }
+ public string WebhookName { get; set; }
+ public string Data { get; set; }
+ public DateTime CreationTime { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookPublishInput.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookPublishInput.cs
new file mode 100644
index 000000000..e27013228
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookPublishInput.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Volo.Abp.Validation;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookPublishInput
+{
+ [Required]
+ [DynamicStringLength(typeof(WebhookEventRecordConsts), nameof(WebhookEventRecordConsts.MaxWebhookNameLength))]
+ public string WebhookName { get; set; }
+
+ [Required]
+ [DynamicStringLength(typeof(WebhookEventRecordConsts), nameof(WebhookEventRecordConsts.MaxDataLength))]
+ public string Data { get; set; }
+
+ public bool SendExactSameData { get; set; }
+
+ public WebhooksHeaderInput Header { get; set; } = new WebhooksHeaderInput();
+
+ public List TenantIds { get; set; } = new List();
+}
+
+public class WebhooksHeaderInput
+{
+ public bool UseOnlyGivenHeaders { get; set; }
+
+ public IDictionary Headers { get; set; } = new Dictionary();
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordDto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordDto.cs
new file mode 100644
index 000000000..9c0dcd85e
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordDto.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Net;
+using Volo.Abp.Application.Dtos;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookSendRecordDto : EntityDto
+{
+ public Guid? TenantId { get; set; }
+
+ public Guid WebhookEventId { get; set; }
+
+ public Guid WebhookSubscriptionId { get; set; }
+
+ public string Response { get; set; }
+
+ public HttpStatusCode? ResponseStatusCode { get; set; }
+
+ public DateTime CreationTime { get; set; }
+
+ public DateTime? LastModificationTime { get; set; }
+
+ public WebhookEventRecordDto WebhookEvent { get; set; } = new WebhookEventRecordDto();
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordGetListInput.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordGetListInput.cs
new file mode 100644
index 000000000..018cec849
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordGetListInput.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Net;
+using Volo.Abp.Application.Dtos;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookSendRecordGetListInput : PagedAndSortedResultRequestDto
+{
+ public string Filter { get; set; }
+
+ public Guid? WebhookEventId { get; set; }
+
+ public Guid? SubscriptionId { get; set; }
+
+ public HttpStatusCode? ResponseStatusCode { get; set; }
+
+ public DateTime? BeginCreationTime { get; set; }
+
+ public DateTime? EndCreationTime { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionCreateOrUpdateInput.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionCreateOrUpdateInput.cs
new file mode 100644
index 000000000..94897417d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionCreateOrUpdateInput.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Volo.Abp.Validation;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookSubscriptionCreateInput : WebhookSubscriptionCreateOrUpdateInput
+{
+
+}
+
+public class WebhookSubscriptionUpdateInput : WebhookSubscriptionCreateOrUpdateInput
+{
+
+}
+
+public abstract class WebhookSubscriptionCreateOrUpdateInput
+{
+ [Required]
+ [DynamicStringLength(typeof(WebhookSubscriptionConsts), nameof(WebhookSubscriptionConsts.MaxWebhookUriLength))]
+ public string WebhookUri { get; set; }
+
+ [Required]
+ [DynamicStringLength(typeof(WebhookSubscriptionConsts), nameof(WebhookSubscriptionConsts.MaxSecretLength))]
+ public string Secret { get; set; }
+
+ public bool IsActive { get; set; }
+
+ public List Webhooks { get; set; } = new List();
+
+ public Dictionary Headers { get; set; } = new Dictionary();
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionDto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionDto.cs
new file mode 100644
index 000000000..204ffd2f9
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionDto.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using Volo.Abp.Application.Dtos;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookSubscriptionDto : CreationAuditedEntityDto
+{
+ public Guid? TenantId { get; set; }
+ public string WebhookUri { get; set; }
+ public string Secret { get; set; }
+ public bool IsActive { get; set; }
+ public List Webhooks { get; set; } = new List();
+ public IDictionary Headers { get; set; } = new Dictionary();
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionGetListInput.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionGetListInput.cs
new file mode 100644
index 000000000..9e9060184
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionGetListInput.cs
@@ -0,0 +1,26 @@
+using System;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Validation;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookSubscriptionGetListInput : PagedAndSortedResultRequestDto
+{
+ public string Filter { get; set; }
+
+ public Guid? TenantId { get; set; }
+
+ [DynamicStringLength(typeof(WebhookSubscriptionConsts), nameof(WebhookSubscriptionConsts.MaxWebhookUriLength))]
+ public string WebhookUri { get; set; }
+
+ [DynamicStringLength(typeof(WebhookSubscriptionConsts), nameof(WebhookSubscriptionConsts.MaxSecretLength))]
+ public string Secret { get; set; }
+
+ public bool? IsActive { get; set; }
+
+ public string Webhooks { get; set; }
+
+ public DateTime? BeginCreationTime { get; set; }
+
+ public DateTime? EndCreationTime { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhooksManagementApplicationContractsModule.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhooksManagementApplicationContractsModule.cs
new file mode 100644
index 000000000..977589b9c
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhooksManagementApplicationContractsModule.cs
@@ -0,0 +1,15 @@
+using Volo.Abp.Application;
+using Volo.Abp.Authorization;
+using Volo.Abp.Features;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+[DependsOn(
+ typeof(AbpFeaturesModule),
+ typeof(AbpAuthorizationModule),
+ typeof(AbpDddApplicationContractsModule),
+ typeof(WebhooksManagementDomainSharedModule))]
+public class WebhooksManagementApplicationContractsModule : AbpModule
+{
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhooksManagementRemoteServiceConsts.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhooksManagementRemoteServiceConsts.cs
new file mode 100644
index 000000000..3d5a2b9f6
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application.Contracts/LINGYUN/Abp/WebhooksManagement/WebhooksManagementRemoteServiceConsts.cs
@@ -0,0 +1,7 @@
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public static class WebhooksManagementRemoteServiceConsts
+{
+ public const string RemoteServiceName = "WebhooksManagement";
+ public const string ModuleName = "webhooks-management";
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/FodyWeavers.xml b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/FodyWeavers.xml
new file mode 100644
index 000000000..be0de3a90
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/FodyWeavers.xsd b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/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/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN.Abp.WebhooksManagement.Application.csproj b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN.Abp.WebhooksManagement.Application.csproj
new file mode 100644
index 000000000..24d0a91e7
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN.Abp.WebhooksManagement.Application.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/Extensions/WebhookSubscriptionExtensions.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/Extensions/WebhookSubscriptionExtensions.cs
new file mode 100644
index 000000000..49c190f0a
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/Extensions/WebhookSubscriptionExtensions.cs
@@ -0,0 +1,44 @@
+using Newtonsoft.Json;
+using System.Linq;
+
+namespace LINGYUN.Abp.WebhooksManagement.Extensions
+{
+ public static class WebhookSubscriptionExtensions
+ {
+ public static WebhookSubscriptionDto ToWebhookSubscriptionDto(this WebhookSubscription webhookSubscription)
+ {
+ return new WebhookSubscriptionDto
+ {
+ Id = webhookSubscription.Id,
+ TenantId = webhookSubscription.TenantId,
+ IsActive = webhookSubscription.IsActive,
+ Secret = webhookSubscription.Secret,
+ WebhookUri = webhookSubscription.WebhookUri,
+ Webhooks = webhookSubscription.GetSubscribedWebhooks(),
+ Headers = webhookSubscription.GetWebhookHeaders(),
+ CreationTime = webhookSubscription.CreationTime,
+ CreatorId = webhookSubscription.CreatorId
+ };
+ }
+
+ public static string ToSubscribedWebhooksString(this WebhookSubscriptionUpdateInput webhookSubscription)
+ {
+ if (webhookSubscription.Webhooks.Any())
+ {
+ return JsonConvert.SerializeObject(webhookSubscription.Webhooks);
+ }
+
+ return null;
+ }
+
+ public static string ToWebhookHeadersString(this WebhookSubscriptionUpdateInput webhookSubscription)
+ {
+ if (webhookSubscription.Headers.Any())
+ {
+ return JsonConvert.SerializeObject(webhookSubscription.Headers);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookPublishAppService.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookPublishAppService.cs
new file mode 100644
index 000000000..c6d637e77
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookPublishAppService.cs
@@ -0,0 +1,45 @@
+using LINGYUN.Abp.Webhooks;
+using LINGYUN.Abp.WebhooksManagement.Authorization;
+using Microsoft.AspNetCore.Authorization;
+using Newtonsoft.Json;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+[Authorize(WebhooksManagementPermissions.Publish)]
+public class WebhookPublishAppService : WebhooksManagementAppServiceBase, IWebhookPublishAppService
+{
+ protected IWebhookPublisher InnerPublisher { get; }
+
+ public WebhookPublishAppService(IWebhookPublisher innerPublisher)
+ {
+ InnerPublisher = innerPublisher;
+ }
+
+ public async virtual Task PublishAsync(WebhookPublishInput input)
+ {
+ var webhookHeader = new WebhookHeader
+ {
+ UseOnlyGivenHeaders = input.Header.UseOnlyGivenHeaders,
+ Headers = input.Header.Headers,
+ };
+ var inputData = JsonConvert.DeserializeObject(input.Data);
+
+ if (input.TenantIds.Any())
+ {
+ await InnerPublisher.PublishAsync(
+ input.TenantIds.ToArray(),
+ input.WebhookName,
+ inputData,
+ input.SendExactSameData,
+ webhookHeader);
+ return;
+ }
+ await InnerPublisher.PublishAsync(
+ input.WebhookName,
+ inputData,
+ input.SendExactSameData,
+ webhookHeader);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordAppService.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordAppService.cs
new file mode 100644
index 000000000..77683b17c
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordAppService.cs
@@ -0,0 +1,74 @@
+using LINGYUN.Abp.Webhooks;
+using LINGYUN.Abp.WebhooksManagement.Authorization;
+using LINGYUN.Abp.WebhooksManagement.Extensions;
+using Microsoft.AspNetCore.Authorization;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.BackgroundJobs;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+[Authorize(WebhooksManagementPermissions.WebhooksSendAttempts.Default)]
+public class WebhookSendRecordAppService : WebhooksManagementAppServiceBase, IWebhookSendRecordAppService
+{
+ protected IBackgroundJobManager BackgroundJobManager => LazyServiceProvider.LazyGetRequiredService();
+ protected IWebhookEventRecordRepository EventRepository => LazyServiceProvider.LazyGetRequiredService();
+ protected IWebhookSubscriptionRepository SubscriptionRepository => LazyServiceProvider.LazyGetRequiredService();
+
+
+ protected IWebhookSendRecordRepository RecordRepository { get; }
+
+ public WebhookSendRecordAppService(
+ IWebhookSendRecordRepository recordRepository)
+ {
+ RecordRepository = recordRepository;
+ }
+
+ public async virtual Task GetAsync(Guid id)
+ {
+ var sendRecord = await RecordRepository.GetAsync(id);
+
+ return ObjectMapper.Map(sendRecord);
+ }
+
+ public async virtual Task> GetListAsync(WebhookSendRecordGetListInput input)
+ {
+ var filter = new WebhookSendRecordFilter
+ {
+ SubscriptionId = input.SubscriptionId,
+ ResponseStatusCode = input.ResponseStatusCode,
+ BeginCreationTime = input.BeginCreationTime,
+ EndCreationTime = input.EndCreationTime,
+ WebhookEventId = input.WebhookEventId,
+ Filter = input.Filter
+ };
+ var totalCount = await RecordRepository.GetCountAsync(filter);
+ var sendRecords = await RecordRepository.GetListAsync(filter,
+ input.Sorting, input.MaxResultCount, input.SkipCount);
+
+ return new PagedResultDto(totalCount,
+ ObjectMapper.Map, List>(sendRecords));
+ }
+
+ [Authorize(WebhooksManagementPermissions.WebhooksSendAttempts.Resend)]
+ public async virtual Task ResendAsync(Guid id)
+ {
+ var sendRecord = await RecordRepository.GetAsync(id);
+ var sendEvent = await EventRepository.GetAsync(sendRecord.WebhookEventId);
+ var subscription = await SubscriptionRepository.GetAsync(sendRecord.WebhookSubscriptionId);
+
+ await BackgroundJobManager.EnqueueAsync(new WebhookSenderArgs
+ {
+ TenantId = CurrentTenant.Id,
+ WebhookSubscriptionId = sendRecord.WebhookSubscriptionId,
+ WebhookEventId = sendRecord.WebhookEventId,
+ WebhookName = sendEvent.WebhookName,
+ WebhookUri = subscription.WebhookUri,
+ Data = sendEvent.Data,
+ Headers = subscription.GetWebhookHeaders(),
+ Secret = subscription.Secret,
+ });
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionAppService.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionAppService.cs
new file mode 100644
index 000000000..3b140c91b
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionAppService.cs
@@ -0,0 +1,150 @@
+using LINGYUN.Abp.Webhooks;
+using LINGYUN.Abp.WebhooksManagement.Authorization;
+using LINGYUN.Abp.WebhooksManagement.Extensions;
+using Microsoft.AspNetCore.Authorization;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.Application.Dtos;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+[Authorize(WebhooksManagementPermissions.WebhookSubscription.Default)]
+public class WebhookSubscriptionAppService : WebhooksManagementAppServiceBase, IWebhookSubscriptionAppService
+{
+ protected IWebhookDefinitionManager WebhookDefinitionManager { get; }
+ protected IWebhookSubscriptionRepository SubscriptionRepository { get; }
+
+ public WebhookSubscriptionAppService(
+ IWebhookDefinitionManager webhookDefinitionManager,
+ IWebhookSubscriptionRepository subscriptionRepository)
+ {
+ WebhookDefinitionManager = webhookDefinitionManager;
+ SubscriptionRepository = subscriptionRepository;
+ }
+
+ [Authorize(WebhooksManagementPermissions.WebhookSubscription.Create)]
+ public async virtual Task CreateAsync(WebhookSubscriptionCreateInput input)
+ {
+ await CheckSubscribedAsync(input);
+
+ var subscription = new WebhookSubscription(
+ GuidGenerator.Create(),
+ input.WebhookUri,
+ input.Secret,
+ JsonConvert.SerializeObject(input.Webhooks),
+ JsonConvert.SerializeObject(input.Headers),
+ CurrentTenant.Id);
+
+ await SubscriptionRepository.InsertAsync(subscription);
+
+ await CurrentUnitOfWork.SaveChangesAsync();
+
+ return subscription.ToWebhookSubscriptionDto();
+ }
+
+ [Authorize(WebhooksManagementPermissions.WebhookSubscription.Delete)]
+ public virtual Task DeleteAsync(Guid id)
+ {
+ return SubscriptionRepository.DeleteAsync(id);
+ }
+
+ public async virtual Task GetAsync(Guid id)
+ {
+ var subscription = await SubscriptionRepository.GetAsync(id);
+
+ return subscription.ToWebhookSubscriptionDto();
+ }
+
+ public async virtual Task> GetListAsync(WebhookSubscriptionGetListInput input)
+ {
+ var filter = new WebhookSubscriptionFilter
+ {
+ Filter = input.Filter,
+ BeginCreationTime = input.BeginCreationTime,
+ EndCreationTime = input.EndCreationTime,
+ IsActive = input.IsActive,
+ Secret = input.Secret,
+ TenantId = input.TenantId,
+ Webhooks = input.Webhooks,
+ WebhookUri = input.WebhookUri
+ };
+
+ var totalCount = await SubscriptionRepository.GetCountAsync(filter);
+ var subscriptions = await SubscriptionRepository.GetListAsync(filter,
+ input.Sorting, input.MaxResultCount, input.SkipCount);
+
+ return new PagedResultDto(totalCount,
+ subscriptions.Select(subscription => subscription.ToWebhookSubscriptionDto()).ToList());
+ }
+
+ [Authorize(WebhooksManagementPermissions.WebhookSubscription.Update)]
+ public async virtual Task UpdateAsync(Guid id, WebhookSubscriptionUpdateInput input)
+ {
+ var subscription = await SubscriptionRepository.GetAsync(id);
+ if (!string.Equals(subscription.WebhookUri, input.WebhookUri))
+ {
+ await CheckSubscribedAsync(input);
+ }
+
+ subscription.SetSecret(input.Secret);
+ subscription.SetWebhookUri(input.WebhookUri);
+ subscription.SetWebhooks(input.ToSubscribedWebhooksString());
+ subscription.SetHeaders(input.ToWebhookHeadersString());
+ subscription.IsActive = input.IsActive;
+
+ await SubscriptionRepository.UpdateAsync(subscription);
+
+ await CurrentUnitOfWork.SaveChangesAsync();
+
+ return subscription.ToWebhookSubscriptionDto();
+ }
+
+ public async virtual Task> GetAllAvailableWebhooksAsync()
+ {
+ var groups = WebhookDefinitionManager.GetGroups();
+ var definitions = new List();
+
+ foreach (var groupDefinition in groups)
+ {
+ var group = new WebhookAvailableGroupDto
+ {
+ Name = groupDefinition.Name,
+ DisplayName = groupDefinition.DisplayName?.Localize(StringLocalizerFactory),
+ };
+
+ foreach (var webhookDefinition in groupDefinition.Webhooks.OrderBy(d => d.Name))
+ {
+ if (await WebhookDefinitionManager.IsAvailableAsync(CurrentTenant.Id, webhookDefinition.Name))
+ {
+ group.Webhooks.Add(new WebhookAvailableDto
+ {
+ Name = webhookDefinition.Name,
+ Description = webhookDefinition.Description?.Localize(StringLocalizerFactory),
+ DisplayName = webhookDefinition.DisplayName?.Localize(StringLocalizerFactory)
+ });
+ }
+ }
+
+ definitions.Add(group);
+ }
+
+ return new ListResultDto(definitions.OrderBy(d => d.Name).ToList());
+ }
+
+ protected async virtual Task CheckSubscribedAsync(WebhookSubscriptionCreateOrUpdateInput input)
+ {
+ foreach (var webhookName in input.Webhooks)
+ {
+ if (await SubscriptionRepository.IsSubscribedAsync(CurrentTenant.Id, input.WebhookUri, webhookName))
+ {
+ throw new BusinessException(WebhooksManagementErrorCodes.WebhookSubscription.DuplicateSubscribed)
+ .WithData(nameof(WebhookSubscription.WebhookUri), input.WebhookUri)
+ .WithData(nameof(WebhookSubscription.Webhooks), webhookName);
+ }
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhooksManagementAppServiceBase.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhooksManagementAppServiceBase.cs
new file mode 100644
index 000000000..3953ab3e0
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhooksManagementAppServiceBase.cs
@@ -0,0 +1,13 @@
+using LINGYUN.Abp.WebhooksManagement.Localization;
+using Volo.Abp.Application.Services;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public abstract class WebhooksManagementAppServiceBase : ApplicationService
+{
+ protected WebhooksManagementAppServiceBase()
+ {
+ LocalizationResource = typeof(WebhooksManagementResource);
+ ObjectMapperContext = typeof(WebhooksManagementApplicationModule);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhooksManagementApplicationMapperProfile.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhooksManagementApplicationMapperProfile.cs
new file mode 100644
index 000000000..11ab195a0
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhooksManagementApplicationMapperProfile.cs
@@ -0,0 +1,12 @@
+using AutoMapper;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhooksManagementApplicationMapperProfile : Profile
+{
+ public WebhooksManagementApplicationMapperProfile()
+ {
+ CreateMap();
+ CreateMap();
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhooksManagementApplicationModule.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhooksManagementApplicationModule.cs
new file mode 100644
index 000000000..eef27929b
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Application/LINGYUN/Abp/WebhooksManagement/WebhooksManagementApplicationModule.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Application;
+using Volo.Abp.Authorization;
+using Volo.Abp.AutoMapper;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+[DependsOn(
+ typeof(AbpAuthorizationModule),
+ typeof(AbpDddApplicationModule),
+ typeof(WebhooksManagementDomainModule))]
+public class WebhooksManagementApplicationModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddAutoMapperObjectMapper();
+
+ Configure(options =>
+ {
+ options.AddProfile(validate: true);
+ });
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Dapr.Client/FodyWeavers.xml b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Dapr.Client/FodyWeavers.xml
new file mode 100644
index 000000000..be0de3a90
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Dapr.Client/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Dapr.Client/FodyWeavers.xsd b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Dapr.Client/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Dapr.Client/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/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Dapr.Client/LINGYUN.Abp.WebhooksManagement.Dapr.Client.csproj b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Dapr.Client/LINGYUN.Abp.WebhooksManagement.Dapr.Client.csproj
new file mode 100644
index 000000000..e07d4c79d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Dapr.Client/LINGYUN.Abp.WebhooksManagement.Dapr.Client.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Dapr.Client/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDaprClientModule.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Dapr.Client/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDaprClientModule.cs
new file mode 100644
index 000000000..bb89de573
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Dapr.Client/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDaprClientModule.cs
@@ -0,0 +1,18 @@
+using LINGYUN.Abp.Dapr.Client;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+[DependsOn(
+ typeof(AbpDaprClientModule),
+ typeof(WebhooksManagementApplicationContractsModule))]
+public class WebhooksManagementDaprClientModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddDaprClientProxies(
+ typeof(WebhooksManagementApplicationContractsModule).Assembly,
+ WebhooksManagementRemoteServiceConsts.RemoteServiceName);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/FodyWeavers.xml b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/FodyWeavers.xml
new file mode 100644
index 000000000..be0de3a90
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/FodyWeavers.xsd b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/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/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN.Abp.WebhooksManagement.Domain.Shared.csproj b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN.Abp.WebhooksManagement.Domain.Shared.csproj
new file mode 100644
index 000000000..682b8eff5
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN.Abp.WebhooksManagement.Domain.Shared.csproj
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/Resources/en.json b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/Resources/en.json
new file mode 100644
index 000000000..a64f32f2a
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/Resources/en.json
@@ -0,0 +1,19 @@
+{
+ "culture": "en",
+ "texts": {
+ "Features:WebhooksManagement": "Webhooks",
+ "Permission:WebhooksManagement": "Webhooks",
+ "Permission:Subscriptions": "Subscriptions",
+ "Permission:SendAttempts": "Attempts",
+ "Permission:Create": "Create",
+ "Permission:Update": "Update",
+ "Permission:Delete": "Delete",
+ "Permission:Resend": "Resend",
+ "Permission:Publish": "Publish",
+ "Permission:ManageSettings": "Manage Settings",
+ "Webhooks:010001": "Payload address {WebhookUri} has been mounted event {Webhooks}!",
+ "Webhooks:Tests": "Tests",
+ "Webhooks:CheckConnect": "Check Connect",
+ "Webhooks:CheckConnectDesc": "When a third-party service is connected, it is used to check whether the communication is normal."
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/Resources/zh-Hans.json b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/Resources/zh-Hans.json
new file mode 100644
index 000000000..efb4975cd
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/Resources/zh-Hans.json
@@ -0,0 +1,19 @@
+{
+ "culture": "zh-Hans",
+ "texts": {
+ "Features:WebhooksManagement": "Webhooks",
+ "Permission:WebhooksManagement": "Webhooks",
+ "Permission:Subscriptions": "管理订阅",
+ "Permission:SendAttempts": "管理发送",
+ "Permission:Create": "创建",
+ "Permission:Update": "编辑",
+ "Permission:Delete": "删除",
+ "Permission:Resend": "重新发送",
+ "Permission:Publish": "发布事件",
+ "Permission:ManageSettings": "管理设置",
+ "Webhooks:010001": "载荷地址 {WebhookUri} 已经挂载事件 {Webhooks}!",
+ "Webhooks:Tests": "测试",
+ "Webhooks:CheckConnect": "检查连接",
+ "Webhooks:CheckConnectDesc": "第三方服务接入时,用于检查是否通讯正常."
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/WebhooksManagementResource.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/WebhooksManagementResource.cs
new file mode 100644
index 000000000..c95515cad
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/Localization/WebhooksManagementResource.cs
@@ -0,0 +1,8 @@
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.WebhooksManagement.Localization;
+
+[LocalizationResourceName("WebhooksManagement")]
+public class WebhooksManagementResource
+{
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/ObjectExtending/WebhooksManagementModuleExtensionConfiguration.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/ObjectExtending/WebhooksManagementModuleExtensionConfiguration.cs
new file mode 100644
index 000000000..bf7e418ce
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/ObjectExtending/WebhooksManagementModuleExtensionConfiguration.cs
@@ -0,0 +1,16 @@
+using System;
+using Volo.Abp.ObjectExtending.Modularity;
+
+namespace LINGYUN.Abp.WebhooksManagement.ObjectExtending;
+
+public class WebhooksManagementModuleExtensionConfiguration : ModuleExtensionConfiguration
+{
+ public WebhooksManagementModuleExtensionConfiguration ConfigureWebhooksManagement(
+ Action configureAction)
+ {
+ return this
+ .ConfigureEntity(WebhooksManagementModuleExtensionConsts.EntityNames.WebhookEvent, configureAction)
+ .ConfigureEntity(WebhooksManagementModuleExtensionConsts.EntityNames.WebhookSendAttempt, configureAction)
+ .ConfigureEntity(WebhooksManagementModuleExtensionConsts.EntityNames.WebhookSubscription, configureAction);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/ObjectExtending/WebhooksManagementModuleExtensionConfigurationDictionaryExtensions.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/ObjectExtending/WebhooksManagementModuleExtensionConfigurationDictionaryExtensions.cs
new file mode 100644
index 000000000..fe0bff94b
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/ObjectExtending/WebhooksManagementModuleExtensionConfigurationDictionaryExtensions.cs
@@ -0,0 +1,17 @@
+using System;
+using Volo.Abp.ObjectExtending.Modularity;
+
+namespace LINGYUN.Abp.WebhooksManagement.ObjectExtending;
+
+public static class WebhooksManagementModuleExtensionConfigurationDictionaryExtensions
+{
+ public static ModuleExtensionConfigurationDictionary ConfigureWebhooksManagement(
+ this ModuleExtensionConfigurationDictionary modules,
+ Action configureAction)
+ {
+ return modules.ConfigureModule(
+ WebhooksManagementModuleExtensionConsts.ModuleName,
+ configureAction
+ );
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/ObjectExtending/WebhooksManagementModuleExtensionConsts.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/ObjectExtending/WebhooksManagementModuleExtensionConsts.cs
new file mode 100644
index 000000000..a51d51035
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/ObjectExtending/WebhooksManagementModuleExtensionConsts.cs
@@ -0,0 +1,13 @@
+namespace LINGYUN.Abp.WebhooksManagement.ObjectExtending;
+
+public static class WebhooksManagementModuleExtensionConsts
+{
+ public const string ModuleName = "WebhooksManagement";
+
+ public static class EntityNames
+ {
+ public const string WebhookEvent = "WebhookEvent";
+ public const string WebhookSendAttempt = "WebhookSendAttempt";
+ public const string WebhookSubscription = "WebhookSubscription";
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookEventEto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookEventEto.cs
new file mode 100644
index 000000000..f6625a851
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookEventEto.cs
@@ -0,0 +1,12 @@
+using System;
+using Volo.Abp.MultiTenancy;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+[Serializable]
+public class WebhookEventEto : IMultiTenant
+{
+ public Guid Id { get; set; }
+ public Guid? TenantId { get; set; }
+ public string WebhookName { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookEventRecordConsts.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookEventRecordConsts.cs
new file mode 100644
index 000000000..31f4cf431
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookEventRecordConsts.cs
@@ -0,0 +1,7 @@
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public static class WebhookEventRecordConsts
+{
+ public static int MaxWebhookNameLength { get; set; } = 100;
+ public static int MaxDataLength { get; set; } = int.MaxValue;
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookSendAttemptEto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookSendAttemptEto.cs
new file mode 100644
index 000000000..12f1cb812
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookSendAttemptEto.cs
@@ -0,0 +1,16 @@
+using System;
+using Volo.Abp.MultiTenancy;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+[Serializable]
+public class WebhookSendAttemptEto : IMultiTenant
+{
+ public Guid Id { get; set; }
+
+ public Guid? TenantId { get; set; }
+
+ public Guid WebhookEventId { get; set; }
+
+ public Guid WebhookSubscriptionId { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordConsts.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordConsts.cs
new file mode 100644
index 000000000..464658f04
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordConsts.cs
@@ -0,0 +1,6 @@
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public static class WebhookSendRecordConsts
+{
+ public static int MaxResponseLength { get; set; } = int.MaxValue;
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionConsts.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionConsts.cs
new file mode 100644
index 000000000..70a51fc17
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionConsts.cs
@@ -0,0 +1,9 @@
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public static class WebhookSubscriptionConsts
+{
+ public static int MaxWebhookUriLength { get; set; } = 255;
+ public static int MaxSecretLength { get; set; } = 128;
+ public static int MaxWebhooksLength { get; set; } = int.MaxValue;
+ public static int MaxHeadersLength { get; set; } = int.MaxValue;
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionEto.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionEto.cs
new file mode 100644
index 000000000..157d6bc04
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionEto.cs
@@ -0,0 +1,14 @@
+using System;
+using Volo.Abp.MultiTenancy;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+[Serializable]
+public class WebhookSubscriptionEto : IMultiTenant
+{
+ public Guid Id { get; set; }
+
+ public Guid? TenantId { get; set; }
+
+ public string WebhookUri { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDomainSharedModule.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDomainSharedModule.cs
new file mode 100644
index 000000000..1e9234e7a
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDomainSharedModule.cs
@@ -0,0 +1,32 @@
+using LINGYUN.Abp.WebhooksManagement.Localization;
+using Volo.Abp.Localization;
+using Volo.Abp.Localization.ExceptionHandling;
+using Volo.Abp.Modularity;
+using Volo.Abp.VirtualFileSystem;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+[DependsOn(
+ typeof(AbpLocalizationModule))]
+public class WebhooksManagementDomainSharedModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ Configure(options =>
+ {
+ options.Resources
+ .Add()
+ .AddVirtualJson("/LINGYUN/Abp/WebhooksManagement/Localization/Resources");
+ });
+
+ Configure(options =>
+ {
+ options.MapCodeNamespace(WebhooksManagementErrorCodes.Namespace, typeof(WebhooksManagementResource));
+ });
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhooksManagementErrorCodes.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhooksManagementErrorCodes.cs
new file mode 100644
index 000000000..5c13db31c
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain.Shared/LINGYUN/Abp/WebhooksManagement/WebhooksManagementErrorCodes.cs
@@ -0,0 +1,13 @@
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public static class WebhooksManagementErrorCodes
+{
+ public const string Namespace = "Webhooks";
+
+ public static class WebhookSubscription
+ {
+ public const string Prefix = Namespace + ":010";
+
+ public const string DuplicateSubscribed = Prefix + "001";
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/FodyWeavers.xml b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/FodyWeavers.xml
new file mode 100644
index 000000000..be0de3a90
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/FodyWeavers.xsd b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/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/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN.Abp.WebhooksManagement.Domain.csproj b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN.Abp.WebhooksManagement.Domain.csproj
new file mode 100644
index 000000000..5234b20c6
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN.Abp.WebhooksManagement.Domain.csproj
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/DefaultWebhookManager.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/DefaultWebhookManager.cs
new file mode 100644
index 000000000..c0ab03de1
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/DefaultWebhookManager.cs
@@ -0,0 +1,60 @@
+using LINGYUN.Abp.Webhooks;
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Guids;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.Uow;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class DefaultWebhookManager : WebhookManager, ITransientDependency
+{
+ protected ICurrentTenant CurrentTenant { get; }
+ protected IGuidGenerator GuidGenerator { get; }
+ protected IUnitOfWorkManager UnitOfWorkManager { get; }
+ protected IWebhookSendRecordRepository WebhookSendAttemptRepository { get; }
+ public DefaultWebhookManager(
+ ICurrentTenant currentTenant,
+ IGuidGenerator guidGenerator,
+ IWebhookSendAttemptStore webhookSendAttemptStore,
+ IUnitOfWorkManager unitOfWorkManager,
+ IWebhookSendRecordRepository webhookSendAttemptRepository)
+ : base(webhookSendAttemptStore)
+ {
+ CurrentTenant = currentTenant;
+ GuidGenerator = guidGenerator;
+ UnitOfWorkManager = unitOfWorkManager;
+ WebhookSendAttemptRepository = webhookSendAttemptRepository;
+ }
+
+ [UnitOfWork]
+ public async override Task InsertAndGetIdWebhookSendAttemptAsync(WebhookSenderArgs webhookSenderArgs)
+ {
+ using (CurrentTenant.Change(webhookSenderArgs.TenantId))
+ {
+ var record = new WebhookSendRecord(
+ GuidGenerator.Create(),
+ webhookSenderArgs.WebhookEventId,
+ webhookSenderArgs.WebhookSubscriptionId,
+ webhookSenderArgs.TenantId);
+
+ await WebhookSendAttemptRepository.InsertAsync(record);
+
+ return record.Id;
+ }
+ }
+
+ [UnitOfWork]
+ public async override Task StoreResponseOnWebhookSendAttemptAsync(Guid webhookSendAttemptId, Guid? tenantId, HttpStatusCode? statusCode, string content)
+ {
+ using (CurrentTenant.Change(tenantId))
+ {
+ var record = await WebhookSendAttemptRepository.GetAsync(webhookSendAttemptId);
+ record.SetResponse(content, statusCode);
+
+ await WebhookSendAttemptRepository.UpdateAsync(record);
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Extensions/WebhookSubscriptionExtensions.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Extensions/WebhookSubscriptionExtensions.cs
new file mode 100644
index 000000000..3630b3760
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Extensions/WebhookSubscriptionExtensions.cs
@@ -0,0 +1,29 @@
+using LINGYUN.Abp.Webhooks;
+using Newtonsoft.Json;
+using System.Linq;
+
+namespace LINGYUN.Abp.WebhooksManagement.Extensions
+{
+ public static class WebhookSubscriptionExtensions
+ {
+ public static string ToSubscribedWebhooksString(this WebhookSubscriptionInfo webhookSubscription)
+ {
+ if (webhookSubscription.Webhooks.Any())
+ {
+ return JsonConvert.SerializeObject(webhookSubscription.Webhooks);
+ }
+
+ return null;
+ }
+
+ public static string ToWebhookHeadersString(this WebhookSubscriptionInfo webhookSubscription)
+ {
+ if (webhookSubscription.Headers.Any())
+ {
+ return JsonConvert.SerializeObject(webhookSubscription.Headers);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Extensions/WebhookSubscriptionInfoExtensions.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Extensions/WebhookSubscriptionInfoExtensions.cs
new file mode 100644
index 000000000..8068dfa10
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Extensions/WebhookSubscriptionInfoExtensions.cs
@@ -0,0 +1,175 @@
+using LINGYUN.Abp.Webhooks;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.WebhooksManagement.Extensions
+{
+ public static class WebhookSubscriptionInfoExtensions
+ {
+ ///
+ /// Return List of subscribed webhooks definitions
+ ///
+ ///
+ public static List GetSubscribedWebhooks(this WebhookSubscription webhookSubscription)
+ {
+ if (webhookSubscription.Webhooks.IsNullOrWhiteSpace())
+ {
+ return new List();
+ }
+
+ return JsonConvert.DeserializeObject>(webhookSubscription.Webhooks);
+ }
+
+ ///
+ /// Adds webhook subscription to if not exists
+ ///
+ ///
+ /// webhook unique name
+ public static void SubscribeWebhook(this WebhookSubscription webhookSubscription, string name)
+ {
+ name = name.Trim();
+ if (name.IsNullOrWhiteSpace())
+ {
+ throw new ArgumentNullException(nameof(name), $"{nameof(name)} can not be null, empty or whitespace!");
+ }
+
+ var webhookDefinitions = webhookSubscription.GetSubscribedWebhooks();
+ if (webhookDefinitions.Contains(name))
+ {
+ return;
+ }
+
+ webhookDefinitions.Add(name);
+ webhookSubscription.SetWebhooks(JsonConvert.SerializeObject(webhookDefinitions));
+ }
+
+ ///
+ /// Removes webhook subscription from if exists
+ ///
+ ///
+ /// webhook unique name
+ public static void UnsubscribeWebhook(this WebhookSubscription webhookSubscription, string name)
+ {
+ name = name.Trim();
+ if (name.IsNullOrWhiteSpace())
+ {
+ throw new ArgumentNullException(nameof(name), $"{nameof(name)} can not be null, empty or whitespace!");
+ }
+
+ var webhookDefinitions = webhookSubscription.GetSubscribedWebhooks();
+ if (!webhookDefinitions.Contains(name))
+ {
+ return;
+ }
+
+ webhookDefinitions.Remove(name);
+ webhookSubscription.SetWebhooks(JsonConvert.SerializeObject(webhookDefinitions));
+ }
+
+ ///
+ /// Clears all
+ ///
+ ///
+ public static void RemoveAllSubscribedWebhooks(this WebhookSubscription webhookSubscription)
+ {
+ webhookSubscription.SetWebhooks(null);
+ }
+
+ ///
+ /// if subscribed to given webhook
+ ///
+ ///
+ public static bool IsSubscribed(this WebhookSubscription webhookSubscription, string webhookName)
+ {
+ if (webhookSubscription.Webhooks.IsNullOrWhiteSpace())
+ {
+ return false;
+ }
+
+ return webhookSubscription.GetSubscribedWebhooks().Contains(webhookName);
+ }
+
+ ///
+ /// Returns additional webhook headers
+ ///
+ ///
+ public static IDictionary GetWebhookHeaders(this WebhookSubscription webhookSubscription)
+ {
+ if (webhookSubscription.Headers.IsNullOrWhiteSpace())
+ {
+ return new Dictionary();
+ }
+
+ return JsonConvert.DeserializeObject>(webhookSubscription.Headers);
+ }
+
+ ///
+ /// Adds webhook subscription to if not exists
+ ///
+ public static void AddWebhookHeader(this WebhookSubscription webhookSubscription, string key, string value)
+ {
+ if (key.IsNullOrWhiteSpace() )
+ {
+ throw new ArgumentNullException(nameof(key), $"{nameof(key)} can not be null, empty or whitespace!");
+ }
+
+ if (value.IsNullOrWhiteSpace())
+ {
+ throw new ArgumentNullException(nameof(value), $"{nameof(value)} can not be null, empty or whitespace!");
+ }
+
+ var headers = webhookSubscription.GetWebhookHeaders();
+ headers[key] = value;
+
+ webhookSubscription.SetHeaders(JsonConvert.SerializeObject(headers));
+ }
+
+ ///
+ /// Adds webhook subscription to if not exists
+ ///
+ ///
+ /// Key of header
+ public static void RemoveWebhookHeader(this WebhookSubscription webhookSubscription, string header)
+ {
+ if (header.IsNullOrWhiteSpace())
+ {
+ throw new ArgumentNullException(nameof(header), $"{nameof(header)} can not be null, empty or whitespace!");
+ }
+
+ var headers = webhookSubscription.GetWebhookHeaders();
+
+ if (!headers.ContainsKey(header))
+ {
+ return;
+ }
+
+ headers.Remove(header);
+
+ webhookSubscription.SetHeaders(JsonConvert.SerializeObject(headers));
+ }
+
+ ///
+ /// Clears all
+ ///
+ ///
+ public static void RemoveAllWebhookHeaders(this WebhookSubscription webhookSubscription)
+ {
+ webhookSubscription.SetHeaders(null);
+ }
+
+ public static WebhookSubscriptionInfo ToWebhookSubscriptionInfo(this WebhookSubscription webhookSubscription)
+ {
+ return new WebhookSubscriptionInfo
+ {
+ Id = webhookSubscription.Id,
+ TenantId = webhookSubscription.TenantId,
+ IsActive = webhookSubscription.IsActive,
+ Secret = webhookSubscription.Secret,
+ WebhookUri = webhookSubscription.WebhookUri,
+ Webhooks = webhookSubscription.GetSubscribedWebhooks(),
+ Headers = webhookSubscription.GetWebhookHeaders()
+ };
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/IWebhookEventRecordRepository.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/IWebhookEventRecordRepository.cs
new file mode 100644
index 000000000..45a69d4f2
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/IWebhookEventRecordRepository.cs
@@ -0,0 +1,8 @@
+using System;
+using Volo.Abp.Domain.Repositories;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public interface IWebhookEventRecordRepository : IRepository
+{
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/IWebhookSendRecordRepository.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/IWebhookSendRecordRepository.cs
new file mode 100644
index 000000000..a94eb35ee
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/IWebhookSendRecordRepository.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.Domain.Repositories;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public interface IWebhookSendRecordRepository : IRepository
+{
+ Task GetCountAsync(
+ WebhookSendRecordFilter filter,
+ CancellationToken cancellationToken = default);
+
+ Task> GetListAsync(
+ WebhookSendRecordFilter filter,
+ string sorting = nameof(WebhookSendRecord.CreationTime),
+ int maxResultCount = 10,
+ int skipCount = 10,
+ bool includeDetails = false,
+ CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/IWebhookSubscriptionRepository.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/IWebhookSubscriptionRepository.cs
new file mode 100644
index 000000000..069e8375c
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/IWebhookSubscriptionRepository.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.Domain.Repositories;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public interface IWebhookSubscriptionRepository : IRepository
+{
+ Task IsSubscribedAsync(
+ Guid? tenantId,
+ string webhookUri,
+ string webhookName,
+ CancellationToken cancellationToken = default);
+
+ Task GetCountAsync(
+ WebhookSubscriptionFilter filter,
+ CancellationToken cancellationToken = default);
+
+ Task> GetListAsync(
+ WebhookSubscriptionFilter filter,
+ string sorting = $"{nameof(WebhookSubscription.CreationTime)} DESC",
+ int maxResultCount = 10,
+ int skipCount = 0,
+ CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Settings/WebhooksManagementSettingDefinitionProvider.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Settings/WebhooksManagementSettingDefinitionProvider.cs
new file mode 100644
index 000000000..e3aa21264
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Settings/WebhooksManagementSettingDefinitionProvider.cs
@@ -0,0 +1,11 @@
+using Volo.Abp.Settings;
+
+namespace LINGYUN.Abp.WebhooksManagement.Settings
+{
+ public class WebhooksManagementSettingDefinitionProvider : SettingDefinitionProvider
+ {
+ public override void Define(ISettingDefinitionContext context)
+ {
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Settings/WebhooksManagementSettings.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Settings/WebhooksManagementSettings.cs
new file mode 100644
index 000000000..1de9fcda9
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Settings/WebhooksManagementSettings.cs
@@ -0,0 +1,7 @@
+namespace LINGYUN.Abp.WebhooksManagement.Settings
+{
+ public static class WebhooksManagementSettings
+ {
+ public const string GroupName = "WebhooksManagement";
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookEventRecord.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookEventRecord.cs
new file mode 100644
index 000000000..82fc7fb50
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookEventRecord.cs
@@ -0,0 +1,31 @@
+using System;
+using Volo.Abp;
+using Volo.Abp.Auditing;
+using Volo.Abp.Domain.Entities;
+using Volo.Abp.MultiTenancy;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookEventRecord : Entity, IMultiTenant, IHasCreationTime, IHasDeletionTime
+{
+ public virtual Guid? TenantId { get; protected set; }
+ public virtual string WebhookName { get; protected set; }
+ public virtual string Data { get; protected set; }
+ public virtual DateTime CreationTime { get; set; }
+ public virtual DateTime? DeletionTime { get; set; }
+ public virtual bool IsDeleted { get; set; }
+ protected WebhookEventRecord()
+ {
+ }
+
+ public WebhookEventRecord(
+ Guid id,
+ string webhookName,
+ string data,
+ Guid? tenantId = null) : base(id)
+ {
+ WebhookName = Check.NotNullOrWhiteSpace(webhookName, nameof(webhookName), WebhookEventRecordConsts.MaxWebhookNameLength);
+ Data = Check.Length(data, nameof(data), WebhookEventRecordConsts.MaxDataLength);
+ TenantId = tenantId;
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookEventStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookEventStore.cs
new file mode 100644
index 000000000..ff14e571a
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookEventStore.cs
@@ -0,0 +1,52 @@
+using LINGYUN.Abp.Webhooks;
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Domain.Services;
+using Volo.Abp.ObjectMapping;
+using Volo.Abp.Uow;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookEventStore : DomainService, IWebhookEventStore
+{
+ protected IObjectMapper ObjectMapper => LazyServiceProvider.LazyGetRequiredService>();
+
+ protected IWebhookEventRecordRepository WebhookEventRepository { get; }
+
+ public WebhookEventStore(
+ IWebhookEventRecordRepository webhookEventRepository)
+ {
+ WebhookEventRepository = webhookEventRepository;
+ }
+
+ [UnitOfWork]
+ public async virtual Task GetAsync(Guid? tenantId, Guid id)
+ {
+ using (CurrentTenant.Change(tenantId))
+ {
+ var record = await WebhookEventRepository.GetAsync(id);
+
+ return ObjectMapper.Map(record);
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task InsertAndGetIdAsync(WebhookEvent webhookEvent)
+ {
+ using (CurrentTenant.Change(webhookEvent.TenantId))
+ {
+ var record = new WebhookEventRecord(
+ GuidGenerator.Create(),
+ webhookEvent.WebhookName,
+ webhookEvent.Data,
+ webhookEvent.TenantId)
+ {
+ CreationTime = Clock.Now,
+ };
+
+ await WebhookEventRepository.InsertAsync(record);
+
+ return record.Id;
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSendAttemptStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSendAttemptStore.cs
new file mode 100644
index 000000000..1b469c105
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSendAttemptStore.cs
@@ -0,0 +1,122 @@
+using LINGYUN.Abp.Webhooks;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Volo.Abp.Domain.Repositories;
+using Volo.Abp.Domain.Services;
+using Volo.Abp.ObjectMapping;
+using Volo.Abp.Uow;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookSendAttemptStore : DomainService, IWebhookSendAttemptStore
+{
+ protected IObjectMapper ObjectMapper => LazyServiceProvider.LazyGetRequiredService>();
+
+ protected IWebhookSendRecordRepository WebhookSendAttemptRepository { get; }
+
+ public WebhookSendAttemptStore(
+ IWebhookSendRecordRepository webhookSendAttemptRepository)
+ {
+ WebhookSendAttemptRepository = webhookSendAttemptRepository;
+ }
+
+ [UnitOfWork]
+ public async virtual Task<(int TotalCount, IReadOnlyCollection Webhooks)> GetAllSendAttemptsBySubscriptionAsPagedListAsync(
+ Guid? tenantId,
+ Guid subscriptionId,
+ int maxResultCount,
+ int skipCount)
+ {
+ using (CurrentTenant.Change(tenantId))
+ {
+ var filter = new WebhookSendRecordFilter
+ {
+ SubscriptionId = subscriptionId,
+ };
+ var totalCount = await WebhookSendAttemptRepository.GetCountAsync(filter);
+
+ var list = await WebhookSendAttemptRepository.GetListAsync(
+ filter,
+ maxResultCount: maxResultCount,
+ skipCount: skipCount);
+
+ var webHooks = ObjectMapper.Map, List>(list);
+
+ return ValueTuple.Create(totalCount, webHooks.ToImmutableList());
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task> GetAllSendAttemptsByWebhookEventIdAsync(
+ Guid? tenantId,
+ Guid webhookEventId)
+ {
+ using (CurrentTenant.Change(tenantId))
+ {
+ var queryable = await WebhookSendAttemptRepository.GetQueryableAsync();
+
+ var list = await AsyncExecuter.ToListAsync(queryable
+ .Where(attempt => attempt.WebhookEventId == webhookEventId)
+ .OrderByDescending(attempt => attempt.CreationTime)
+ );
+
+ return ObjectMapper.Map, List>(list);
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task GetAsync(
+ Guid? tenantId,
+ Guid id)
+ {
+ using (CurrentTenant.Change(tenantId))
+ {
+ var sendAttempt = await WebhookSendAttemptRepository.GetAsync(id);
+
+ return ObjectMapper.Map(sendAttempt);
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task GetSendAttemptCountAsync(
+ Guid? tenantId,
+ Guid webhookEventId,
+ Guid webhookSubscriptionId)
+ {
+ using (CurrentTenant.Change(tenantId))
+ {
+ return await WebhookSendAttemptRepository.CountAsync(attempt =>
+ attempt.WebhookEventId == webhookEventId &&
+ attempt.WebhookSubscriptionId == webhookSubscriptionId);
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task HasXConsecutiveFailAsync(
+ Guid? tenantId,
+ Guid subscriptionId,
+ int failCount)
+ {
+ using (CurrentTenant.Change(tenantId))
+ {
+ if (await WebhookSendAttemptRepository.CountAsync(x => x.WebhookSubscriptionId == subscriptionId) < failCount)
+ {
+ return false;
+ }
+ else
+ {
+ var queryable = await WebhookSendAttemptRepository.GetQueryableAsync();
+
+ return !await AsyncExecuter.AnyAsync(queryable
+ .OrderByDescending(attempt => attempt.CreationTime)
+ .Take(failCount)
+ .Where(attempt => attempt.ResponseStatusCode == HttpStatusCode.OK)
+ );
+ }
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSendRecord.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSendRecord.cs
new file mode 100644
index 000000000..0d0d0115a
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSendRecord.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Net;
+using Volo.Abp;
+using Volo.Abp.Auditing;
+using Volo.Abp.Domain.Entities;
+using Volo.Abp.MultiTenancy;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookSendRecord : Entity, IHasCreationTime, IHasModificationTime, IMultiTenant
+{
+ public virtual Guid? TenantId { get; protected set; }
+
+ public virtual Guid WebhookEventId { get; protected set; }
+
+ public virtual Guid WebhookSubscriptionId { get; protected set; }
+
+ public virtual string Response { get; protected set; }
+
+ public virtual HttpStatusCode? ResponseStatusCode { get; set; }
+
+ public virtual DateTime CreationTime { get; set; }
+
+ public virtual DateTime? LastModificationTime { get; set; }
+
+ public virtual WebhookEventRecord WebhookEvent { get; protected set; }
+
+ protected WebhookSendRecord()
+ {
+ }
+
+ public WebhookSendRecord(
+ Guid id,
+ Guid eventId,
+ Guid subscriptionId,
+ Guid? tenantId = null) : base(id)
+ {
+ WebhookEventId = eventId;
+ WebhookSubscriptionId = subscriptionId;
+
+ TenantId = tenantId;
+ }
+
+ public void SetResponse(
+ string response,
+ HttpStatusCode? statusCode = null)
+ {
+ Response = Check.Length(response, nameof(response), WebhookSendRecordConsts.MaxResponseLength);
+ ResponseStatusCode = statusCode;
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordFilter.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordFilter.cs
new file mode 100644
index 000000000..44f034c30
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSendRecordFilter.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Net;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookSendRecordFilter
+{
+ public string Filter { get; set; }
+
+ public Guid? WebhookEventId { get; set; }
+
+ public Guid? SubscriptionId { get; set; }
+
+ public HttpStatusCode? ResponseStatusCode { get; set; }
+
+ public DateTime? BeginCreationTime { get; set; }
+
+ public DateTime? EndCreationTime { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSubscription.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSubscription.cs
new file mode 100644
index 000000000..50476fce4
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSubscription.cs
@@ -0,0 +1,54 @@
+using System;
+using Volo.Abp;
+using Volo.Abp.Domain.Entities.Auditing;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookSubscription : CreationAuditedEntity
+{
+ public virtual Guid? TenantId { get; protected set; }
+ public virtual string WebhookUri { get; protected set; }
+ public virtual string Secret { get; protected set; }
+ public virtual bool IsActive { get; set; }
+ public virtual string Webhooks { get; protected set; }
+ public virtual string Headers { get; protected set; }
+ protected WebhookSubscription()
+ {
+ }
+ public WebhookSubscription(
+ Guid id,
+ string webhookUri,
+ string secret,
+ string webhooks,
+ string headers,
+ Guid? tenantId = null) : base(id)
+ {
+ SetSecret(secret);
+ SetWebhookUri(webhookUri);
+ SetWebhooks(webhooks);
+ SetHeaders(headers);
+ TenantId = tenantId;
+
+ IsActive = true;
+ }
+
+ public void SetSecret(string secret)
+ {
+ Secret = Check.NotNullOrWhiteSpace(secret, nameof(secret), WebhookSubscriptionConsts.MaxSecretLength);
+ }
+
+ public void SetWebhookUri(string webhookUri)
+ {
+ WebhookUri = Check.NotNullOrWhiteSpace(webhookUri, nameof(webhookUri), WebhookSubscriptionConsts.MaxWebhookUriLength);
+ }
+
+ public void SetWebhooks(string webhooks)
+ {
+ Webhooks = Check.Length(webhooks, nameof(webhooks), WebhookSubscriptionConsts.MaxWebhooksLength);
+ }
+
+ public void SetHeaders(string headers)
+ {
+ Headers = Check.Length(headers, nameof(headers), WebhookSubscriptionConsts.MaxHeadersLength);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionFilter.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionFilter.cs
new file mode 100644
index 000000000..adbd92c7d
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionFilter.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookSubscriptionFilter
+{
+ public string Filter { get; set; }
+
+ public Guid? TenantId { get; set; }
+
+ public string WebhookUri { get; set; }
+
+ public string Secret { get; set; }
+
+ public bool? IsActive { get; set; }
+
+ public string Webhooks { get; set; }
+
+ public DateTime? BeginCreationTime { get; set; }
+
+ public DateTime? EndCreationTime { get; set; }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionsStore.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionsStore.cs
new file mode 100644
index 000000000..fb14c7bae
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhookSubscriptionsStore.cs
@@ -0,0 +1,153 @@
+using LINGYUN.Abp.Webhooks;
+using LINGYUN.Abp.WebhooksManagement.Extensions;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.Domain.Services;
+using Volo.Abp.Uow;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhookSubscriptionsStore : DomainService, IWebhookSubscriptionsStore
+{
+ protected IWebhookSubscriptionRepository SubscriptionRepository { get; }
+
+ public WebhookSubscriptionsStore(
+ IWebhookSubscriptionRepository subscriptionRepository)
+ {
+ SubscriptionRepository = subscriptionRepository;
+ }
+
+ [UnitOfWork]
+ public async virtual Task DeleteAsync(Guid id)
+ {
+ using (CurrentTenant.Change(null))
+ {
+ await SubscriptionRepository.DeleteAsync(id);
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task> GetAllSubscriptionsAsync(Guid? tenantId)
+ {
+ using (CurrentTenant.Change(null))
+ {
+ var queryable = await SubscriptionRepository.GetQueryableAsync();
+
+ queryable = queryable.Where(x => x.TenantId == tenantId);
+
+ var subscriptions = await AsyncExecuter.ToListAsync(queryable);
+
+ return subscriptions.Select(subscription => subscription.ToWebhookSubscriptionInfo()).ToList();
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task> GetAllSubscriptionsAsync(Guid? tenantId, string webhookName)
+ {
+ using (CurrentTenant.Change(null))
+ {
+ var queryable = await SubscriptionRepository.GetQueryableAsync();
+
+ queryable = queryable.Where(x =>
+ x.TenantId == tenantId &&
+ x.IsActive &&
+ x.Webhooks.Contains("\"" + webhookName + "\""));
+
+ var subscriptions = await AsyncExecuter.ToListAsync(queryable);
+
+ return subscriptions.Select(subscription => subscription.ToWebhookSubscriptionInfo()).ToList();
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds)
+ {
+ using (CurrentTenant.Change(null))
+ {
+ var queryable = await SubscriptionRepository.GetQueryableAsync();
+
+ var subscriptions = await AsyncExecuter.ToListAsync(queryable.Where(x => tenantIds.Contains(x.TenantId)));
+
+ return subscriptions.Select(subscription => subscription.ToWebhookSubscriptionInfo()).ToList();
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task> GetAllSubscriptionsOfTenantsAsync(Guid?[] tenantIds, string webhookName)
+ {
+ using (CurrentTenant.Change(null))
+ {
+ var queryable = await SubscriptionRepository.GetQueryableAsync();
+
+ queryable = queryable.Where(x =>
+ x.IsActive &&
+ tenantIds.Contains(x.TenantId) &&
+ x.Webhooks.Contains("\"" + webhookName + "\""));
+
+ var subscriptions = await AsyncExecuter.ToListAsync(queryable);
+
+ return subscriptions.Select(subscription => subscription.ToWebhookSubscriptionInfo()).ToList();
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task GetAsync(Guid id)
+ {
+ using (CurrentTenant.Change(null))
+ {
+ var subscription = await SubscriptionRepository.GetAsync(id);
+
+ return subscription.ToWebhookSubscriptionInfo();
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task InsertAsync(WebhookSubscriptionInfo webhookSubscription)
+ {
+ using (CurrentTenant.Change(null))
+ {
+ var subscription = new WebhookSubscription(
+ webhookSubscription.Id,
+ webhookSubscription.WebhookUri,
+ webhookSubscription.Secret,
+ JsonConvert.SerializeObject(webhookSubscription.Webhooks),
+ JsonConvert.SerializeObject(webhookSubscription.Headers),
+ webhookSubscription.TenantId);
+
+ await SubscriptionRepository.InsertAsync(subscription);
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task IsSubscribedAsync(Guid? tenantId, string webhookName)
+ {
+ using (CurrentTenant.Change(null))
+ {
+ var queryable = await SubscriptionRepository.GetQueryableAsync();
+
+ queryable = queryable.Where(x =>
+ x.TenantId == tenantId &&
+ x.IsActive &&
+ x.Webhooks.Contains("\"" + webhookName + "\""));
+
+ return await AsyncExecuter.AnyAsync(queryable);
+ }
+ }
+
+ [UnitOfWork]
+ public async virtual Task UpdateAsync(WebhookSubscriptionInfo webhookSubscription)
+ {
+ using (CurrentTenant.Change(webhookSubscription.TenantId))
+ {
+ var subscription = await SubscriptionRepository.GetAsync(webhookSubscription.Id);
+ subscription.SetWebhookUri(webhookSubscription.WebhookUri);
+ subscription.SetWebhooks(webhookSubscription.ToSubscribedWebhooksString());
+ subscription.SetHeaders(webhookSubscription.ToWebhookHeadersString());
+
+ await SubscriptionRepository.UpdateAsync(subscription);
+ }
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Webhooks/WebhooksDefinitionProvider.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Webhooks/WebhooksDefinitionProvider.cs
new file mode 100644
index 000000000..0389c1573
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Webhooks/WebhooksDefinitionProvider.cs
@@ -0,0 +1,26 @@
+using LINGYUN.Abp.Webhooks;
+using LINGYUN.Abp.WebhooksManagement.Localization;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.WebhooksManagement.Webhooks;
+
+public class WebhooksDefinitionProvider : WebhookDefinitionProvider
+{
+ public override void Define(IWebhookDefinitionContext context)
+ {
+ var testsGroup = context.AddGroup(
+ WebhooksNames.GroupName,
+ L("Webhooks:Tests"));
+
+ testsGroup.AddWebhooks(
+ new WebhookDefinition(
+ WebhooksNames.CheckConnect,
+ L("Webhooks:CheckConnect"),
+ L("Webhooks:CheckConnectDesc")));
+ }
+
+ private static ILocalizableString L(string name)
+ {
+ return LocalizableString.Create(name);
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Webhooks/WebhooksNames.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Webhooks/WebhooksNames.cs
new file mode 100644
index 000000000..f650eec4b
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/Webhooks/WebhooksNames.cs
@@ -0,0 +1,8 @@
+namespace LINGYUN.Abp.WebhooksManagement.Webhooks;
+
+public static class WebhooksNames
+{
+ public const string GroupName = "abp.webhooks.tests";
+
+ public const string CheckConnect = GroupName + ".check_connect";
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDbProperties.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDbProperties.cs
new file mode 100644
index 000000000..33ddda08e
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDbProperties.cs
@@ -0,0 +1,11 @@
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public static class WebhooksManagementDbProperties
+{
+ public static string DbTablePrefix { get; set; } = "AbpWebhooks";
+
+ public static string DbSchema { get; set; } = null;
+
+
+ public const string ConnectionStringName = "WebhooksManagement";
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDomainMapperProfile.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDomainMapperProfile.cs
new file mode 100644
index 000000000..231739e73
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDomainMapperProfile.cs
@@ -0,0 +1,20 @@
+using AutoMapper;
+using LINGYUN.Abp.Webhooks;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+public class WebhooksManagementDomainMapperProfile : Profile
+{
+ public WebhooksManagementDomainMapperProfile()
+ {
+ CreateMap();
+ CreateMap();
+ CreateMap();
+
+ CreateMap();
+ CreateMap();
+ }
+}
diff --git a/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDomainModule.cs b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDomainModule.cs
new file mode 100644
index 000000000..18903b4b3
--- /dev/null
+++ b/aspnet-core/modules/webhooks/LINGYUN.Abp.WebhooksManagement.Domain/LINGYUN/Abp/WebhooksManagement/WebhooksManagementDomainModule.cs
@@ -0,0 +1,62 @@
+using LINGYUN.Abp.Webhooks;
+using LINGYUN.Abp.WebhooksManagement.ObjectExtending;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.AutoMapper;
+using Volo.Abp.Domain.Entities.Events.Distributed;
+using Volo.Abp.Modularity;
+using Volo.Abp.ObjectExtending.Modularity;
+using Volo.Abp.Threading;
+
+namespace LINGYUN.Abp.WebhooksManagement;
+
+[DependsOn(
+ typeof(AbpAutoMapperModule),
+ typeof(AbpWebhooksModule),
+ typeof(WebhooksManagementDomainSharedModule))]
+public class WebhooksManagementDomainModule : AbpModule
+{
+ private static readonly OneTimeRunner OneTimeRunner = new();
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddAutoMapperObjectMapper();
+
+ Configure(options =>
+ {
+ options.AddProfile(validate: true);
+ });
+
+ Configure(options =>
+ {
+ options.EtoMappings.Add();
+ options.EtoMappings.Add();
+ options.EtoMappings.Add();
+
+ options.AutoEventSelectors.Add();
+ options.AutoEventSelectors.Add