10 changed files with 445 additions and 18 deletions
@ -0,0 +1,172 @@ |
|||||
|
using LINGYUN.Abp.Webhooks; |
||||
|
using Microsoft.Extensions.Caching.Distributed; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.Immutable; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp; |
||||
|
using Volo.Abp.Caching; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.DistributedLocking; |
||||
|
using Volo.Abp.Threading; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WebhooksManagement; |
||||
|
|
||||
|
[Dependency(ReplaceServices = true)] |
||||
|
public class DynamicWebhookDefinitionStore : IDynamicWebhookDefinitionStore, ITransientDependency |
||||
|
{ |
||||
|
protected IWebhookGroupDefinitionRecordRepository WebhookGroupRepository { get; } |
||||
|
protected IWebhookDefinitionRecordRepository WebhookRepository { get; } |
||||
|
protected IWebhookDefinitionSerializer WebhookDefinitionSerializer { get; } |
||||
|
protected IDynamicWebhookDefinitionStoreCache StoreCache { get; } |
||||
|
protected IDistributedCache DistributedCache { get; } |
||||
|
protected IAbpDistributedLock DistributedLock { get; } |
||||
|
public WebhookManagementOptions WebhookManagementOptions { get; } |
||||
|
protected AbpDistributedCacheOptions CacheOptions { get; } |
||||
|
|
||||
|
public DynamicWebhookDefinitionStore( |
||||
|
IWebhookGroupDefinitionRecordRepository webhookGroupRepository, |
||||
|
IWebhookDefinitionRecordRepository webhookRepository, |
||||
|
IWebhookDefinitionSerializer webhookDefinitionSerializer, |
||||
|
IDynamicWebhookDefinitionStoreCache storeCache, |
||||
|
IDistributedCache distributedCache, |
||||
|
IOptions<AbpDistributedCacheOptions> cacheOptions, |
||||
|
IOptions<WebhookManagementOptions> webhookManagementOptions, |
||||
|
IAbpDistributedLock distributedLock) |
||||
|
{ |
||||
|
WebhookGroupRepository = webhookGroupRepository; |
||||
|
WebhookRepository = webhookRepository; |
||||
|
WebhookDefinitionSerializer = webhookDefinitionSerializer; |
||||
|
StoreCache = storeCache; |
||||
|
DistributedCache = distributedCache; |
||||
|
DistributedLock = distributedLock; |
||||
|
WebhookManagementOptions = webhookManagementOptions.Value; |
||||
|
CacheOptions = cacheOptions.Value; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<WebhookDefinition> GetOrNullAsync(string name) |
||||
|
{ |
||||
|
if (!WebhookManagementOptions.IsDynamicWebhookStoreEnabled) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
using (await StoreCache.SyncSemaphore.LockAsync()) |
||||
|
{ |
||||
|
await EnsureCacheIsUptoDateAsync(); |
||||
|
return StoreCache.GetWebhookOrNull(name); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<IReadOnlyList<WebhookDefinition>> GetWebhooksAsync() |
||||
|
{ |
||||
|
if (!WebhookManagementOptions.IsDynamicWebhookStoreEnabled) |
||||
|
{ |
||||
|
return Array.Empty<WebhookDefinition>(); |
||||
|
} |
||||
|
|
||||
|
using (await StoreCache.SyncSemaphore.LockAsync()) |
||||
|
{ |
||||
|
await EnsureCacheIsUptoDateAsync(); |
||||
|
return StoreCache.GetWebhooks().ToImmutableList(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<IReadOnlyList<WebhookGroupDefinition>> GetGroupsAsync() |
||||
|
{ |
||||
|
if (!WebhookManagementOptions.IsDynamicWebhookStoreEnabled) |
||||
|
{ |
||||
|
return Array.Empty<WebhookGroupDefinition>(); |
||||
|
} |
||||
|
|
||||
|
using (await StoreCache.SyncSemaphore.LockAsync()) |
||||
|
{ |
||||
|
await EnsureCacheIsUptoDateAsync(); |
||||
|
return StoreCache.GetGroups().ToImmutableList(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task EnsureCacheIsUptoDateAsync() |
||||
|
{ |
||||
|
if (StoreCache.LastCheckTime.HasValue && |
||||
|
DateTime.Now.Subtract(StoreCache.LastCheckTime.Value).TotalSeconds < 30) |
||||
|
{ |
||||
|
/* We get the latest webhook with a small delay for optimization */ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var stampInDistributedCache = await GetOrSetStampInDistributedCache(); |
||||
|
|
||||
|
if (stampInDistributedCache == StoreCache.CacheStamp) |
||||
|
{ |
||||
|
StoreCache.LastCheckTime = DateTime.Now; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await UpdateInMemoryStoreCache(); |
||||
|
|
||||
|
StoreCache.CacheStamp = stampInDistributedCache; |
||||
|
StoreCache.LastCheckTime = DateTime.Now; |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task UpdateInMemoryStoreCache() |
||||
|
{ |
||||
|
var webhookGroupRecords = await WebhookGroupRepository.GetListAsync(); |
||||
|
var webhookRecords = await WebhookRepository.GetListAsync(); |
||||
|
|
||||
|
await StoreCache.FillAsync(webhookGroupRecords, webhookRecords); |
||||
|
} |
||||
|
|
||||
|
protected virtual async Task<string> GetOrSetStampInDistributedCache() |
||||
|
{ |
||||
|
var cacheKey = GetCommonStampCacheKey(); |
||||
|
|
||||
|
var stampInDistributedCache = await DistributedCache.GetStringAsync(cacheKey); |
||||
|
if (stampInDistributedCache != null) |
||||
|
{ |
||||
|
return stampInDistributedCache; |
||||
|
} |
||||
|
|
||||
|
await using (var commonLockHandle = await DistributedLock |
||||
|
.TryAcquireAsync(GetCommonDistributedLockKey(), TimeSpan.FromMinutes(2))) |
||||
|
{ |
||||
|
if (commonLockHandle == null) |
||||
|
{ |
||||
|
/* This request will fail */ |
||||
|
throw new AbpException( |
||||
|
"Could not acquire distributed lock for webhook definition common stamp check!" |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
stampInDistributedCache = await DistributedCache.GetStringAsync(cacheKey); |
||||
|
if (stampInDistributedCache != null) |
||||
|
{ |
||||
|
return stampInDistributedCache; |
||||
|
} |
||||
|
|
||||
|
stampInDistributedCache = Guid.NewGuid().ToString(); |
||||
|
|
||||
|
await DistributedCache.SetStringAsync( |
||||
|
cacheKey, |
||||
|
stampInDistributedCache, |
||||
|
new DistributedCacheEntryOptions |
||||
|
{ |
||||
|
SlidingExpiration = TimeSpan.FromDays(30) //TODO: Make it configurable?
|
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return stampInDistributedCache; |
||||
|
} |
||||
|
|
||||
|
protected virtual string GetCommonStampCacheKey() |
||||
|
{ |
||||
|
return $"{CacheOptions.KeyPrefix}_AbpInMemoryWebhookCacheStamp"; |
||||
|
} |
||||
|
|
||||
|
protected virtual string GetCommonDistributedLockKey() |
||||
|
{ |
||||
|
return $"{CacheOptions.KeyPrefix}_Common_AbpWebhookUpdateLock"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,114 @@ |
|||||
|
using LINGYUN.Abp.Webhooks; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.SimpleStateChecking; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WebhooksManagement; |
||||
|
|
||||
|
[ExposeServices( |
||||
|
typeof(IDynamicWebhookDefinitionStoreCache), |
||||
|
typeof(DynamicWebhookDefinitionStoreInMemoryCache))] |
||||
|
public class DynamicWebhookDefinitionStoreInMemoryCache : |
||||
|
IDynamicWebhookDefinitionStoreCache, |
||||
|
ISingletonDependency |
||||
|
{ |
||||
|
public string CacheStamp { get; set; } |
||||
|
|
||||
|
protected IDictionary<string, WebhookGroupDefinition> WebhookGroupDefinitions { get; } |
||||
|
protected IDictionary<string, WebhookDefinition> WebhookDefinitions { get; } |
||||
|
protected ISimpleStateCheckerSerializer StateCheckerSerializer { get; } |
||||
|
protected ILocalizableStringSerializer LocalizableStringSerializer { get; } |
||||
|
|
||||
|
public SemaphoreSlim SyncSemaphore { get; } = new(1, 1); |
||||
|
|
||||
|
public DateTime? LastCheckTime { get; set; } |
||||
|
|
||||
|
public DynamicWebhookDefinitionStoreInMemoryCache( |
||||
|
ISimpleStateCheckerSerializer stateCheckerSerializer, |
||||
|
ILocalizableStringSerializer localizableStringSerializer) |
||||
|
{ |
||||
|
StateCheckerSerializer = stateCheckerSerializer; |
||||
|
LocalizableStringSerializer = localizableStringSerializer; |
||||
|
|
||||
|
WebhookGroupDefinitions = new Dictionary<string, WebhookGroupDefinition>(); |
||||
|
WebhookDefinitions = new Dictionary<string, WebhookDefinition>(); |
||||
|
} |
||||
|
|
||||
|
public Task FillAsync( |
||||
|
List<WebhookGroupDefinitionRecord> webhookGroupRecords, |
||||
|
List<WebhookDefinitionRecord> webhookRecords) |
||||
|
{ |
||||
|
WebhookGroupDefinitions.Clear(); |
||||
|
WebhookDefinitions.Clear(); |
||||
|
|
||||
|
var context = new WebhookDefinitionContext(null); |
||||
|
|
||||
|
foreach (var webhookGroupRecord in webhookGroupRecords) |
||||
|
{ |
||||
|
var webhookGroup = context.AddGroup( |
||||
|
webhookGroupRecord.Name, |
||||
|
LocalizableStringSerializer.Deserialize(webhookGroupRecord.DisplayName) |
||||
|
); |
||||
|
|
||||
|
WebhookGroupDefinitions[webhookGroup.Name] = webhookGroup; |
||||
|
|
||||
|
foreach (var property in webhookGroupRecord.ExtraProperties) |
||||
|
{ |
||||
|
webhookGroup[property.Key] = property.Value; |
||||
|
} |
||||
|
|
||||
|
var webhookRecordsInThisGroup = webhookRecords |
||||
|
.Where(p => p.GroupName == webhookGroup.Name); |
||||
|
|
||||
|
foreach (var webhookRecord in webhookRecordsInThisGroup) |
||||
|
{ |
||||
|
AddWebhook(webhookGroup, webhookRecord); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
public WebhookDefinition GetWebhookOrNull(string name) |
||||
|
{ |
||||
|
return WebhookDefinitions.GetOrDefault(name); |
||||
|
} |
||||
|
|
||||
|
public IReadOnlyList<WebhookDefinition> GetWebhooks() |
||||
|
{ |
||||
|
return WebhookDefinitions.Values.ToList(); |
||||
|
} |
||||
|
|
||||
|
public IReadOnlyList<WebhookGroupDefinition> GetGroups() |
||||
|
{ |
||||
|
return WebhookGroupDefinitions.Values.ToList(); |
||||
|
} |
||||
|
|
||||
|
private void AddWebhook( |
||||
|
WebhookGroupDefinition webhookGroup, |
||||
|
WebhookDefinitionRecord webhookRecord) |
||||
|
{ |
||||
|
var webhook = webhookGroup.AddWebhook( |
||||
|
webhookRecord.Name, |
||||
|
LocalizableStringSerializer.Deserialize(webhookRecord.DisplayName), |
||||
|
LocalizableStringSerializer.Deserialize(webhookRecord.Description) |
||||
|
); |
||||
|
|
||||
|
WebhookDefinitions[webhook.Name] = webhook; |
||||
|
|
||||
|
if (!webhookRecord.RequiredFeatures.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
webhook.RequiredFeatures.AddRange(webhookRecord.RequiredFeatures.Split(',')); |
||||
|
} |
||||
|
|
||||
|
foreach (var property in webhookRecord.ExtraProperties) |
||||
|
{ |
||||
|
webhook[property.Key] = property.Value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
using LINGYUN.Abp.Webhooks; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WebhooksManagement; |
||||
|
|
||||
|
public interface IDynamicWebhookDefinitionStoreCache |
||||
|
{ |
||||
|
string CacheStamp { get; set; } |
||||
|
|
||||
|
SemaphoreSlim SyncSemaphore { get; } |
||||
|
|
||||
|
DateTime? LastCheckTime { get; set; } |
||||
|
|
||||
|
Task FillAsync( |
||||
|
List<WebhookGroupDefinitionRecord> webhookGroupRecords, |
||||
|
List<WebhookDefinitionRecord> webhookRecords); |
||||
|
|
||||
|
WebhookDefinition GetWebhookOrNull(string name); |
||||
|
|
||||
|
IReadOnlyList<WebhookDefinition> GetWebhooks(); |
||||
|
|
||||
|
IReadOnlyList<WebhookGroupDefinition> GetGroups(); |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using JetBrains.Annotations; |
||||
|
using LINGYUN.Abp.Webhooks; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WebhooksManagement; |
||||
|
|
||||
|
public interface IWebhookDefinitionSerializer |
||||
|
{ |
||||
|
Task<(WebhookGroupDefinitionRecord[], WebhookDefinitionRecord[])> |
||||
|
SerializeAsync(IEnumerable<WebhookGroupDefinition> WebhookGroups); |
||||
|
|
||||
|
Task<WebhookGroupDefinitionRecord> SerializeAsync( |
||||
|
WebhookGroupDefinition WebhookGroup); |
||||
|
|
||||
|
Task<WebhookDefinitionRecord> SerializeAsync( |
||||
|
WebhookDefinition Webhook, |
||||
|
[CanBeNull] WebhookGroupDefinition WebhookGroup); |
||||
|
} |
||||
@ -0,0 +1,99 @@ |
|||||
|
using LINGYUN.Abp.Webhooks; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Globalization; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Guids; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.SimpleStateChecking; |
||||
|
|
||||
|
namespace LINGYUN.Abp.WebhooksManagement; |
||||
|
|
||||
|
public class WebhookDefinitionSerializer : IWebhookDefinitionSerializer, ITransientDependency |
||||
|
{ |
||||
|
protected ISimpleStateCheckerSerializer StateCheckerSerializer { get; } |
||||
|
protected IGuidGenerator GuidGenerator { get; } |
||||
|
protected ILocalizableStringSerializer LocalizableStringSerializer { get; } |
||||
|
|
||||
|
public WebhookDefinitionSerializer( |
||||
|
IGuidGenerator guidGenerator, |
||||
|
ISimpleStateCheckerSerializer stateCheckerSerializer, |
||||
|
ILocalizableStringSerializer localizableStringSerializer) |
||||
|
{ |
||||
|
StateCheckerSerializer = stateCheckerSerializer; |
||||
|
LocalizableStringSerializer = localizableStringSerializer; |
||||
|
GuidGenerator = guidGenerator; |
||||
|
} |
||||
|
|
||||
|
public async Task<(WebhookGroupDefinitionRecord[], WebhookDefinitionRecord[])> |
||||
|
SerializeAsync(IEnumerable<WebhookGroupDefinition> webhookGroups) |
||||
|
{ |
||||
|
var webhookGroupRecords = new List<WebhookGroupDefinitionRecord>(); |
||||
|
var webhookRecords = new List<WebhookDefinitionRecord>(); |
||||
|
|
||||
|
foreach (var webhookGroup in webhookGroups) |
||||
|
{ |
||||
|
webhookGroupRecords.Add(await SerializeAsync(webhookGroup)); |
||||
|
|
||||
|
foreach (var webhook in webhookGroup.Webhooks) |
||||
|
{ |
||||
|
webhookRecords.Add(await SerializeAsync(webhook, webhookGroup)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return (webhookGroupRecords.ToArray(), webhookRecords.ToArray()); |
||||
|
} |
||||
|
|
||||
|
public Task<WebhookGroupDefinitionRecord> SerializeAsync(WebhookGroupDefinition webhookGroup) |
||||
|
{ |
||||
|
using (CultureHelper.Use(CultureInfo.InvariantCulture)) |
||||
|
{ |
||||
|
var webhookGroupRecord = new WebhookGroupDefinitionRecord( |
||||
|
GuidGenerator.Create(), |
||||
|
webhookGroup.Name, |
||||
|
LocalizableStringSerializer.Serialize(webhookGroup.DisplayName) |
||||
|
); |
||||
|
|
||||
|
foreach (var property in webhookGroup.Properties) |
||||
|
{ |
||||
|
webhookGroupRecord.SetProperty(property.Key, property.Value); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(webhookGroupRecord); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Task<WebhookDefinitionRecord> SerializeAsync( |
||||
|
WebhookDefinition webhook, |
||||
|
WebhookGroupDefinition webhookGroup) |
||||
|
{ |
||||
|
using (CultureHelper.Use(CultureInfo.InvariantCulture)) |
||||
|
{ |
||||
|
var webhookRecord = new WebhookDefinitionRecord( |
||||
|
GuidGenerator.Create(), |
||||
|
webhookGroup?.Name, |
||||
|
webhook.Name, |
||||
|
LocalizableStringSerializer.Serialize(webhook.DisplayName), |
||||
|
LocalizableStringSerializer.Serialize(webhook.Description), |
||||
|
true, |
||||
|
SerializeRequiredFeatures(webhook.RequiredFeatures) |
||||
|
); |
||||
|
|
||||
|
foreach (var property in webhook.Properties) |
||||
|
{ |
||||
|
webhookRecord.SetProperty(property.Key, property.Value); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(webhookRecord); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual string SerializeRequiredFeatures(List<string> requiredFeatures) |
||||
|
{ |
||||
|
return requiredFeatures.Any() |
||||
|
? requiredFeatures.JoinAsString(",") |
||||
|
: null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
namespace LINGYUN.Abp.WebhooksManagement; |
||||
|
public class WebhookManagementOptions |
||||
|
{ |
||||
|
public bool IsDynamicWebhookStoreEnabled { get; set; } |
||||
|
|
||||
|
public WebhookManagementOptions() |
||||
|
{ |
||||
|
IsDynamicWebhookStoreEnabled = true; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue