diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsEntity.cs deleted file mode 100644 index 00be2d8ec..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsEntity.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Entities.Assets; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets -{ - public sealed class MongoAssetStatsEntity : IAssetStatsEntity - { - [BsonId] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public string Id { get; set; } - - [BsonRequired] - [BsonElement] - [BsonRepresentation(BsonType.String)] - public Guid AssetId { get; set; } - - [BsonRequired] - [BsonElement] - [BsonDateTimeOptions(DateOnly = true)] - public DateTime Date { get; set; } - - [BsonRequired] - [BsonElement] - public long TotalSize { get; set; } - - [BsonRequired] - [BsonElement] - public long TotalCount { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs deleted file mode 100644 index a78ad5987..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets -{ - public partial class MongoAssetStatsRepository : MongoRepositoryBase, IAssetStatsRepository, IEventConsumer - { - public MongoAssetStatsRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "Projections_AssetStats"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default(CancellationToken)) - { - return collection.Indexes.CreateManyAsync( - new[] - { - new CreateIndexModel(Index.Ascending(x => x.AssetId).Ascending(x => x.Date)), - new CreateIndexModel(Index.Ascending(x => x.AssetId).Descending(x => x.Date)) - }, ct); - } - - public async Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate) - { - var originalSizesEntities = - await Collection.Find(x => x.AssetId == appId && x.Date >= fromDate && x.Date <= toDate).SortBy(x => x.Date) - .ToListAsync(); - - var enrichedSizes = new List(); - - var sizesDictionary = originalSizesEntities.ToDictionary(x => x.Date); - - var previousSize = long.MinValue; - var previousCount = long.MinValue; - - for (var date = fromDate; date <= toDate; date = date.AddDays(1)) - { - var size = sizesDictionary.GetOrDefault(date); - - if (size != null) - { - previousSize = size.TotalSize; - previousCount = size.TotalCount; - } - else - { - if (previousSize < 0) - { - var firstBeforeRangeEntity = - await Collection.Find(x => x.AssetId == appId && x.Date < fromDate).SortByDescending(x => x.Date) - .FirstOrDefaultAsync(); - - previousSize = firstBeforeRangeEntity?.TotalSize ?? 0L; - previousCount = firstBeforeRangeEntity?.TotalCount ?? 0L; - } - - size = new MongoAssetStatsEntity - { - Date = date, - TotalSize = previousSize, - TotalCount = previousCount - }; - } - - enrichedSizes.Add(size); - } - - return enrichedSizes; - } - - public async Task GetTotalSizeAsync(Guid appId) - { - var totalSizeEntity = - await Collection.Find(x => x.AssetId == appId).SortByDescending(x => x.Date) - .FirstOrDefaultAsync(); - - return totalSizeEntity?.TotalSize ?? 0; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs deleted file mode 100644 index 5e9d9d50d..000000000 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Entities.MongoDb.History -{ - public sealed class MongoHistoryEventEntity : MongoEntity, - IEntity, - IUpdateableEntity, - IUpdateableEntityWithVersion, - IUpdateableEntityWithCreatedBy - { - [BsonElement] - [BsonRequired] - [BsonRepresentation(BsonType.String)] - public Guid AppId { get; set; } - - [BsonRequired] - [BsonElement] - public long Version { get; set; } - - [BsonRequired] - [BsonElement] - public string Channel { get; set; } - - [BsonRequired] - [BsonElement] - public string Message { get; set; } - - [BsonRequired] - [BsonElement] - public RefToken Actor { get; set; } - - [BsonRequired] - [BsonElement] - public Dictionary Parameters { get; set; } - - RefToken IUpdateableEntityWithCreatedBy.CreatedBy - { - get - { - return Actor; - } - set - { - Actor = value; - } - } - - public MongoHistoryEventEntity() - { - Parameters = new Dictionary(); - } - - public MongoHistoryEventEntity AddParameter(string key, string value) - { - Parameters.Add(key, value); - - return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs index 95e9c6e74..6d7e27bf7 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs @@ -13,39 +13,15 @@ using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History.Repositories; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Entities.MongoDb.History { - public class MongoHistoryEventRepository : MongoRepositoryBase, IHistoryEventRepository, IEventConsumer + public class MongoHistoryEventRepository : MongoRepositoryBase, IHistoryEventRepository { - private readonly List creators; - private readonly Dictionary texts = new Dictionary(); - - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return ".*"; } - } - - public MongoHistoryEventRepository(IMongoDatabase database, IEnumerable creators) + public MongoHistoryEventRepository(IMongoDatabase database) : base(database) { - this.creators = creators.ToList(); - - foreach (var creator in this.creators) - { - foreach (var text in creator.Texts) - { - texts[text.Key] = text.Value; - } - } } protected override string CollectionName() @@ -53,67 +29,37 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History return "Projections_History"; } - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default(CancellationToken)) + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default(CancellationToken)) { return collection.Indexes.CreateManyAsync( new[] { - new CreateIndexModel( + new CreateIndexModel( Index .Ascending(x => x.AppId) .Ascending(x => x.Channel) .Descending(x => x.Created) .Descending(x => x.Version)), - new CreateIndexModel(Index.Ascending(x => x.Created), + new CreateIndexModel(Index.Ascending(x => x.Created), new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(365) }) }, ct); } - public async Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count) + public async Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count) { - List historyEventEntities; - if (!string.IsNullOrWhiteSpace(channelPrefix)) { - historyEventEntities = - await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count) - .ToListAsync(); + return await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count).ToListAsync(); } else { - historyEventEntities = - await Collection.Find(x => x.AppId == appId).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count) - .ToListAsync(); + return await Collection.Find(x => x.AppId == appId).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count).ToListAsync(); } - - return historyEventEntities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList(); } - public async Task On(Envelope @event) + public Task InsertAsync(HistoryEvent item) { - foreach (var creator in creators) - { - var message = await creator.CreateEventAsync(@event); - - if (message != null) - { - var appEvent = (AppEvent)@event.Payload; - - await Collection.CreateAsync(appEvent, @event.Headers, entity => - { - entity.Id = Guid.NewGuid(); - - entity.AppId = appEvent.AppId.Id; - - entity.Version = @event.Headers.EventStreamNumber(); - - entity.Channel = message.Channel; - entity.Message = message.Message; - - entity.Parameters = message.Parameters.ToDictionary(p => p.Key, p => p.Value); - }); - } - } + return Collection.ReplaceOneAsync(x => x.Id == item.Id, item, Upsert); } public Task RemoveAsync(Guid appId) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs index 9a5e52a01..78a9ecd9b 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps "updated role {[Name]}"); } - protected Task On(AppContributorRemoved @event) + protected Task On(AppContributorRemoved @event) { const string channel = "settings.contributors"; @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Contributor", @event.ContributorId)); } - protected Task On(AppContributorAssigned @event) + protected Task On(AppContributorAssigned @event) { const string channel = "settings.contributors"; @@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Contributor", @event.ContributorId).AddParameter("Role", @event.Role)); } - protected Task On(AppClientAttached @event) + protected Task On(AppClientAttached @event) { const string channel = "settings.clients"; @@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Id", @event.Id)); } - protected Task On(AppClientRevoked @event) + protected Task On(AppClientRevoked @event) { const string channel = "settings.clients"; @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Id", @event.Id)); } - protected Task On(AppClientRenamed @event) + protected Task On(AppClientRenamed @event) { const string channel = "settings.clients"; @@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Id", @event.Id).AddParameter("Name", ClientName(@event))); } - protected Task On(AppLanguageAdded @event) + protected Task On(AppLanguageAdded @event) { const string channel = "settings.languages"; @@ -128,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Language", @event.Language)); } - protected Task On(AppLanguageRemoved @event) + protected Task On(AppLanguageRemoved @event) { const string channel = "settings.languages"; @@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Language", @event.Language)); } - protected Task On(AppLanguageUpdated @event) + protected Task On(AppLanguageUpdated @event) { const string channel = "settings.languages"; @@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Language", @event.Language)); } - protected Task On(AppMasterLanguageSet @event) + protected Task On(AppMasterLanguageSet @event) { const string channel = "settings.languages"; @@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Language", @event.Language)); } - protected Task On(AppPatternAdded @event) + protected Task On(AppPatternAdded @event) { const string channel = "settings.patterns"; @@ -164,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Name", @event.Name)); } - protected Task On(AppPatternUpdated @event) + protected Task On(AppPatternUpdated @event) { const string channel = "settings.patterns"; @@ -173,7 +173,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Name", @event.Name)); } - protected Task On(AppPatternDeleted @event) + protected Task On(AppPatternDeleted @event) { const string channel = "settings.patterns"; @@ -182,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("PatternId", @event.PatternId)); } - protected Task On(AppRoleAdded @event) + protected Task On(AppRoleAdded @event) { const string channel = "settings.roles"; @@ -191,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Name", @event.Name)); } - protected Task On(AppRoleUpdated @event) + protected Task On(AppRoleUpdated @event) { const string channel = "settings.roles"; @@ -200,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Name", @event.Name)); } - protected Task On(AppRoleDeleted @event) + protected Task On(AppRoleDeleted @event) { const string channel = "settings.roles"; @@ -209,9 +209,9 @@ namespace Squidex.Domain.Apps.Entities.Apps .AddParameter("Name", @event.Name)); } - protected override Task CreateEventCoreAsync(Envelope @event) + protected override Task CreateEventCoreAsync(Envelope @event) { - return this.DispatchFuncAsync(@event.Payload, (HistoryEventToStore)null); + return this.DispatchFuncAsync(@event.Payload, (HistoryEvent)null); } private static string ClientName(AppClientRenamed @event) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs b/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs index 191aa2347..05b447fc9 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs @@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Apps { var schemas = await appProvider.GetSchemasAsync(app.Id); - var schemaNames = new List(); ; + var schemaNames = new List(); schemaNames.Add(Permission.Any); schemaNames.AddRange(schemas.Select(x => x.Name)); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetStatsEntity.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs similarity index 56% rename from src/Squidex.Domain.Apps.Entities/Assets/IAssetStatsEntity.cs rename to src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs index 0ab6c4078..72ce523e6 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetStatsEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs @@ -9,12 +9,20 @@ using System; namespace Squidex.Domain.Apps.Entities.Assets { - public interface IAssetStatsEntity + public sealed class AssetStats { - DateTime Date { get; } + public DateTime Date { get; } - long TotalSize { get; } + public long TotalCount { get; } - long TotalCount { get; } + public long TotalSize { get; } + + public AssetStats(DateTime date, long totalCount, long totalSize) + { + Date = date; + + TotalCount = totalCount; + TotalSize = totalSize; + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetStatsRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetStatsRepository.cs new file mode 100644 index 000000000..0089370e9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetStatsRepository.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.UsageTracking; + +#pragma warning disable CS0649 + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public partial class DefaultAssetStatsRepository : IAssetStatsRepository + { + private const string Category = "Default"; + private const string CounterTotalCount = "TotalAssets"; + private const string CounterTotalSize = "TotalSize"; + private static readonly DateTime SummaryDate; + private readonly IUsageRepository usageStore; + + public DefaultAssetStatsRepository(IUsageRepository usageStore) + { + Guard.NotNull(usageStore, nameof(usageStore)); + + this.usageStore = usageStore; + } + + public async Task GetTotalSizeAsync(Guid appId) + { + var entries = await usageStore.QueryAsync(appId.ToString(), SummaryDate, SummaryDate); + + return (long)entries.Select(x => x.Counters.Get(CounterTotalSize)).FirstOrDefault(); + } + + public async Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate) + { + var enriched = new List(); + + var usagesFlat = await usageStore.QueryAsync(appId.ToString(), fromDate, toDate); + + for (var date = fromDate; date <= toDate; date = date.AddDays(1)) + { + var stored = usagesFlat.FirstOrDefault(x => x.Date == date && x.Category == Category); + + var totalCount = 0L; + var totalSize = 0L; + + if (stored != null) + { + totalCount = (long)stored.Counters.Get(CounterTotalCount); + totalSize = (long)stored.Counters.Get(CounterTotalSize); + } + + enriched.Add(new AssetStats(date, totalCount, totalSize)); + } + + return enriched; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetStatsRepository_EventHandling.cs similarity index 55% rename from src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs rename to src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetStatsRepository_EventHandling.cs index b3605d444..79db13d42 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetStatsRepository_EventHandling.cs @@ -1,20 +1,20 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Threading.Tasks; -using MongoDB.Driver; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.UsageTracking; -namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.Assets { - public partial class MongoAssetStatsRepository + public partial class DefaultAssetStatsRepository { public string Name { @@ -46,34 +46,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets return UpdateSizeAsync(@event.AppId.Id, headers.Timestamp().ToDateTimeUtc().Date, -@event.DeletedSize, -1); } - private async Task UpdateSizeAsync(Guid appId, DateTime date, long size, long count) + private Task UpdateSizeAsync(Guid appId, DateTime date, long size, long count) { - var id = $"{appId}_{date:yyyy-MM-dd}"; - - var assetStatsEntity = - await Collection.Find(x => x.Id == id) - .FirstOrDefaultAsync(); - - if (assetStatsEntity == null) + var counters = new Counters { - var lastEntity = - await Collection.Find(x => x.AssetId == appId).SortByDescending(x => x.Date) - .FirstOrDefaultAsync(); - - assetStatsEntity = new MongoAssetStatsEntity - { - Id = id, - Date = date, - AssetId = appId, - TotalSize = lastEntity?.TotalSize ?? 0, - TotalCount = lastEntity?.TotalCount ?? 0 - }; - } + [CounterTotalSize] = size, + [CounterTotalCount] = count + }; - assetStatsEntity.TotalSize += size; - assetStatsEntity.TotalCount += count; + var key = appId.ToString(); - await Collection.ReplaceOneAsync(x => x.Id == id, assetStatsEntity, Upsert); + return Task.WhenAll( + usageStore.TrackUsagesAsync(new UsageUpdate(date, key, Category, counters)), + usageStore.TrackUsagesAsync(new UsageUpdate(SummaryDate, key, Category, counters))); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetStatsRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetStatsRepository.cs index c41fd9197..5e4697b33 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetStatsRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetStatsRepository.cs @@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories { public interface IAssetStatsRepository { - Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate); + Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate); Task GetTotalSizeAsync(Guid appId); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs index 3b7260f3f..02bf171d3 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Contents "scheduled to change status of {[Schema]} content to {[Status]}."); } - protected override Task CreateEventCoreAsync(Envelope @event) + protected override Task CreateEventCoreAsync(Envelope @event) { var channel = $"contents.{@event.Headers.AggregateId()}"; diff --git a/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs b/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs new file mode 100644 index 000000000..f7f4ea7e9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.History +{ + public sealed class HistoryEvent + { + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid AppId { get; set; } + + public RefToken Actor { get; set; } + + public Instant Created { get; set; } + + public long Version { get; set; } + + public string Channel { get; set; } + + public string Message { get; set; } + + public Dictionary Parameters { get; set; } = new Dictionary(); + + public HistoryEvent() + { + } + + public HistoryEvent(string channel, string message) + { + Guard.NotNullOrEmpty(channel, nameof(channel)); + Guard.NotNullOrEmpty(message, nameof(message)); + + Channel = channel; + + Message = message; + } + + public HistoryEvent AddParameter(string key, object value) + { + Parameters[key] = value.ToString(); + + return this; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/HistoryEventToStore.cs b/src/Squidex.Domain.Apps.Entities/History/HistoryEventToStore.cs deleted file mode 100644 index 671987597..000000000 --- a/src/Squidex.Domain.Apps.Entities/History/HistoryEventToStore.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.History -{ - public sealed class HistoryEventToStore - { - private readonly Dictionary parameters = new Dictionary(); - - public string Channel { get; } - - public string Message { get; } - - public IReadOnlyDictionary Parameters - { - get { return parameters; } - } - - public HistoryEventToStore(string channel, string message) - { - Guard.NotNullOrEmpty(channel, nameof(channel)); - Guard.NotNullOrEmpty(message, nameof(message)); - - Channel = channel; - - Message = message; - } - - public HistoryEventToStore AddParameter(string key, object value) - { - parameters[key] = value.ToString(); - - return this; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs b/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs index 498b748f6..fd238e450 100644 --- a/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs +++ b/src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs @@ -50,23 +50,23 @@ namespace Squidex.Domain.Apps.Entities.History return texts.ContainsKey(message); } - protected HistoryEventToStore ForEvent(IEvent @event, string channel) + protected HistoryEvent ForEvent(IEvent @event, string channel) { var message = typeNameRegistry.GetName(@event.GetType()); - return new HistoryEventToStore(channel, message); + return new HistoryEvent(channel, message); } - public Task CreateEventAsync(Envelope @event) + public Task CreateEventAsync(Envelope @event) { if (HasEventText(@event.Payload)) { return CreateEventCoreAsync(@event); } - return Task.FromResult(null); + return Task.FromResult(null); } - protected abstract Task CreateEventCoreAsync(Envelope @event); + protected abstract Task CreateEventCoreAsync(Envelope @event); } } diff --git a/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs b/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs new file mode 100644 index 000000000..2de39253f --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NodaTime; +using Squidex.Domain.Apps.Entities.History.Repositories; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.History +{ + public sealed class HistoryService : IHistoryService, IEventConsumer + { + private readonly Dictionary texts = new Dictionary(); + private readonly List creators; + private readonly IHistoryEventRepository repository; + private readonly IClock clock; + + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return ".*"; } + } + + public HistoryService(IHistoryEventRepository repository, IEnumerable creators, IClock clock) + { + Guard.NotNull(repository, nameof(repository)); + Guard.NotNull(clock, nameof(clock)); + Guard.NotNull(creators, nameof(creators)); + + this.clock = clock; + this.creators = creators.ToList(); + + foreach (var creator in this.creators) + { + foreach (var text in creator.Texts) + { + texts[text.Key] = text.Value; + } + } + + this.repository = repository; + } + + public async Task On(Envelope @event) + { + foreach (var creator in creators) + { + var historyEvent = await creator.CreateEventAsync(@event); + + if (historyEvent != null) + { + var appEvent = (AppEvent)@event.Payload; + + historyEvent.Actor = appEvent.Actor; + historyEvent.AppId = appEvent.AppId.Id; + historyEvent.Created = clock.GetCurrentInstant(); + historyEvent.Version = @event.Headers.EventStreamNumber(); + + await repository.InsertAsync(historyEvent); + } + } + } + + public Task ClearAsync() + { + return repository.ClearAsync(); + } + + public async Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count) + { + var items = await repository.QueryByChannelAsync(appId, channelPrefix, count); + + return items.Select(x => new ParsedHistoryEvent(x, texts)).ToList(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs index 718c87e58..5b15f92d9 100644 --- a/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs @@ -15,6 +15,6 @@ namespace Squidex.Domain.Apps.Entities.History { IReadOnlyDictionary Texts { get; } - Task CreateEventAsync(Envelope @event); + Task CreateEventAsync(Envelope @event); } } diff --git a/src/Squidex.Domain.Apps.Entities/History/IHistoryEventEntity.cs b/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs similarity index 59% rename from src/Squidex.Domain.Apps.Entities/History/IHistoryEventEntity.cs rename to src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs index badecd0e0..71a307a37 100644 --- a/src/Squidex.Domain.Apps.Entities/History/IHistoryEventEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs @@ -1,23 +1,18 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; -using Squidex.Infrastructure; +using System.Collections.Generic; +using System.Threading.Tasks; namespace Squidex.Domain.Apps.Entities.History { - public interface IHistoryEventEntity : IEntity + public interface IHistoryService { - Guid EventId { get; } - - RefToken Actor { get; } - - string Message { get; } - - long Version { get; } + Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count); } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs b/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs similarity index 56% rename from src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs rename to src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs index 156a32822..92f18d5a2 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs +++ b/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs @@ -8,49 +8,38 @@ using System; using System.Collections.Generic; using NodaTime; -using Squidex.Domain.Apps.Entities.History; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.MongoDb.History +namespace Squidex.Domain.Apps.Entities.History { - internal sealed class ParsedHistoryEvent : IHistoryEventEntity + public sealed class ParsedHistoryEvent { - private readonly MongoHistoryEventEntity inner; + private readonly HistoryEvent item; private readonly Lazy message; public Guid Id { - get { return inner.Id; } - } - - public Guid EventId - { - get { return inner.Id; } + get { return item.Id; } } public Instant Created { - get { return inner.Created; } - } - - public Instant LastModified - { - get { return inner.LastModified; } + get { return item.Created; } } public RefToken Actor { - get { return inner.Actor; } + get { return item.Actor; } } public long Version { - get { return inner.Version; } + get { return item.Version; } } public string Channel { - get { return inner.Channel; } + get { return item.Channel; } } public string Message @@ -58,15 +47,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History get { return message.Value; } } - public ParsedHistoryEvent(MongoHistoryEventEntity inner, IReadOnlyDictionary texts) + public ParsedHistoryEvent(HistoryEvent item, IReadOnlyDictionary texts) { - this.inner = inner; + this.item = item; message = new Lazy(() => { - var result = texts[inner.Message]; + var result = texts[item.Message]; - foreach (var kvp in inner.Parameters) + foreach (var kvp in item.Parameters) { result = result.Replace("[" + kvp.Key + "]", kvp.Value); } diff --git a/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs index 02a8d6e5a..d9fb04e2b 100644 --- a/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs @@ -13,8 +13,10 @@ namespace Squidex.Domain.Apps.Entities.History.Repositories { public interface IHistoryEventRepository { - Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count); + Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count); - Task RemoveAsync(Guid appId); + Task InsertAsync(HistoryEvent item); + + Task ClearAsync(); } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs index 3990c07b1..45fbd966f 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas "deleted field {[Field]} of schema {[Name]}."); } - protected override Task CreateEventCoreAsync(Envelope @event) + protected override Task CreateEventCoreAsync(Envelope @event) { if (@event.Payload is SchemaEvent schemaEvent) { @@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas return Task.FromResult(result); } - return Task.FromResult(null); + return Task.FromResult(null); } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs b/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs index 9d64092e6..a32d7dc70 100644 --- a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs +++ b/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs @@ -33,10 +33,6 @@ namespace Squidex.Infrastructure.UsageTracking [BsonRequired] [BsonElement] - public double TotalCount { get; set; } - - [BsonRequired] - [BsonElement] - public double TotalElapsedMs { get; set; } + public Counters Counters { get; set; } = new Counters(); } } diff --git a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs b/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs new file mode 100644 index 000000000..57ad2dd2e --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Infrastructure.UsageTracking +{ + public sealed class MongoUsageRepository : MongoRepositoryBase, IUsageRepository + { + private static readonly BulkWriteOptions Unordered = new BulkWriteOptions { IsOrdered = false }; + + public MongoUsageRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "UsagesV2"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default(CancellationToken)) + { + return collection.Indexes.CreateOneAsync( + new CreateIndexModel(Index.Ascending(x => x.Key).Ascending(x => x.Category).Ascending(x => x.Date)), cancellationToken: ct); + } + + public async Task TrackUsagesAsync(params UsageUpdate[] updates) + { + if (updates.Length == 1) + { + var value = updates[0]; + + if (value.Counters.Count > 0) + { + var (filter, update) = CreateOperation(value); + + await Collection.UpdateOneAsync(filter, update, Upsert); + } + } + else if (updates.Length > 0) + { + var writes = new List>(); + + foreach (var value in updates) + { + if (value.Counters.Count > 0) + { + var (filter, update) = CreateOperation(value); + + writes.Add(new UpdateOneModel(filter, update) { IsUpsert = true }); + } + } + + await Collection.BulkWriteAsync(writes, Unordered); + } + } + + private static (FilterDefinition, UpdateDefinition) CreateOperation(UsageUpdate usageUpdate) + { + var id = $"{usageUpdate.Key}_{usageUpdate.Date:yyyy-MM-dd}_{usageUpdate.Category}"; + + var update = Update + .SetOnInsert(x => x.Id, id) + .SetOnInsert(x => x.Key, usageUpdate.Key) + .SetOnInsert(x => x.Date, usageUpdate.Date) + .SetOnInsert(x => x.Category, usageUpdate.Category); + + foreach (var counter in usageUpdate.Counters) + { + update = update.Inc($"Counters.{counter.Key}", counter.Value); + } + + var filter = Filter.Eq(x => x.Id, id); + + return (filter, update); + } + + public async Task> QueryAsync(string key, DateTime fromDate, DateTime toDate) + { + var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync(); + + return entities.Select(x => new StoredUsage(x.Category, x.Date, x.Counters)).ToList(); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs b/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs deleted file mode 100644 index 63d6d32bc..000000000 --- a/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Infrastructure.UsageTracking -{ - public sealed class MongoUsageStore : MongoRepositoryBase, IUsageStore - { - public MongoUsageStore(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "Usages"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default(CancellationToken)) - { - return collection.Indexes.CreateOneAsync( - new CreateIndexModel(Index.Ascending(x => x.Key).Ascending(x => x.Category).Ascending(x => x.Date)), cancellationToken: ct); - } - - public Task TrackUsagesAsync(DateTime date, string key, string category, double count, double elapsedMs) - { - var id = $"{key}_{date:yyyy-MM-dd}_{category}"; - - return Collection.UpdateOneAsync(x => x.Id == id && x.Category == category, - Update - .Inc(x => x.TotalCount, count) - .Inc(x => x.TotalElapsedMs, elapsedMs) - .SetOnInsert(x => x.Id, id) - .SetOnInsert(x => x.Key, key) - .SetOnInsert(x => x.Date, date) - .SetOnInsert(x => x.Category, category), - Upsert); - } - - public async Task> QueryAsync(string key, DateTime fromDate, DateTime toDate) - { - var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync(); - - return entities.Select(x => new StoredUsage(x.Category, x.Date, (long)x.TotalCount, (long)x.TotalElapsedMs)).ToList(); - } - } -} diff --git a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs index a02192fae..3dcc82b19 100644 --- a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -19,19 +19,22 @@ namespace Squidex.Infrastructure.UsageTracking { public sealed class BackgroundUsageTracker : DisposableObjectBase, IUsageTracker { + public const string CounterTotalCalls = "TotalCalls"; + public const string CounterTotalElapsedMs = "TotalElapsedMs"; + private const string FallbackCategory = "*"; private const int Intervall = 60 * 1000; - private readonly IUsageStore usageStore; + private readonly IUsageRepository usageRepository; private readonly ISemanticLog log; private readonly CompletionTimer timer; private ConcurrentDictionary<(string Key, string Category), Usage> usages = new ConcurrentDictionary<(string Key, string Category), Usage>(); - public BackgroundUsageTracker(IUsageStore usageStore, ISemanticLog log) + public BackgroundUsageTracker(IUsageRepository usageRepository, ISemanticLog log) { - Guard.NotNull(usageStore, nameof(usageStore)); + Guard.NotNull(usageRepository, nameof(usageRepository)); Guard.NotNull(log, nameof(log)); - this.usageStore = usageStore; + this.usageRepository = usageRepository; this.log = log; @@ -61,13 +64,29 @@ namespace Squidex.Infrastructure.UsageTracking var localUsages = Interlocked.Exchange(ref usages, new ConcurrentDictionary<(string Key, string Category), Usage>()); - await Task.WhenAll(localUsages.Select(x => - usageStore.TrackUsagesAsync( - today, - x.Key.Key, - x.Key.Category, - x.Value.Count, - x.Value.ElapsedMs))); + if (localUsages.Count > 0) + { + var updates = new UsageUpdate[localUsages.Count]; + var updateIndex = 0; + + foreach (var kvp in localUsages) + { + var counters = new Counters + { + [CounterTotalCalls] = kvp.Value.Count, + [CounterTotalElapsedMs] = kvp.Value.ElapsedMs + }; + + updates[updateIndex].Key = kvp.Key.Key; + updates[updateIndex].Category = kvp.Key.Category; + updates[updateIndex].Counters = counters; + updates[updateIndex].Date = today; + + updateIndex++; + } + + await usageRepository.TrackUsagesAsync(updates); + } } catch (Exception ex) { @@ -99,7 +118,7 @@ namespace Squidex.Infrastructure.UsageTracking ThrowIfDisposed(); - var usagesFlat = await usageStore.QueryAsync(key, fromDate, toDate); + var usagesFlat = await usageRepository.QueryAsync(key, fromDate, toDate); var usagesByCategory = usagesFlat.GroupBy(x => CleanCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList()); var result = new Dictionary>(); @@ -129,7 +148,16 @@ namespace Squidex.Infrastructure.UsageTracking { var stored = usagesDictionary.GetOrDefault(date); - enriched.Add(new DateUsage(date, stored?.TotalCount ?? 0, stored?.TotalElapsedMs ?? 0)); + var totalCount = 0L; + var totalElapsedMs = 0L; + + if (stored != null) + { + totalCount = (long)stored.Counters.Get(CounterTotalCalls); + totalElapsedMs = (long)stored.Counters.Get(CounterTotalElapsedMs); + } + + enriched.Add(new DateUsage(date, totalCount, totalElapsedMs)); } result[category] = enriched; @@ -148,9 +176,9 @@ namespace Squidex.Infrastructure.UsageTracking var dateFrom = new DateTime(date.Year, date.Month, 1); var dateTo = dateFrom.AddMonths(1).AddDays(-1); - var originalUsages = await usageStore.QueryAsync(key, dateFrom, dateTo); + var originalUsages = await usageRepository.QueryAsync(key, dateFrom, dateTo); - return originalUsages.Sum(x => x.TotalCount); + return originalUsages.Sum(x => (long)x.Counters.Get(CounterTotalCalls)); } private static string CleanCategory(string category) diff --git a/src/Squidex.Infrastructure/UsageTracking/Counters.cs b/src/Squidex.Infrastructure/UsageTracking/Counters.cs new file mode 100644 index 000000000..c722609f2 --- /dev/null +++ b/src/Squidex.Infrastructure/UsageTracking/Counters.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Infrastructure.UsageTracking +{ + public sealed class Counters : Dictionary + { + public double Get(string name) + { + if (name == null) + { + return 0; + } + + TryGetValue(name, out var value); + + return value; + } + } +} diff --git a/src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs b/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs similarity index 81% rename from src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs rename to src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs index b5bc0871e..58a47028d 100644 --- a/src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs +++ b/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs @@ -11,9 +11,9 @@ using System.Threading.Tasks; namespace Squidex.Infrastructure.UsageTracking { - public interface IUsageStore + public interface IUsageRepository { - Task TrackUsagesAsync(DateTime date, string key, string category, double count, double elapsedMs); + Task TrackUsagesAsync(params UsageUpdate[] updates); Task> QueryAsync(string key, DateTime fromDate, DateTime toDate); } diff --git a/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs b/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs index 982498c1a..6a84b4129 100644 --- a/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs +++ b/src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs @@ -15,18 +15,16 @@ namespace Squidex.Infrastructure.UsageTracking public DateTime Date { get; } - public long TotalCount { get; } + public Counters Counters { get; } - public long TotalElapsedMs { get; } - - public StoredUsage(string category, DateTime date, long totalCount, long totalElapsedMs) + public StoredUsage(string category, DateTime date, Counters counters) { + Guard.NotNull(counters, nameof(counters)); + Category = category; + Counters = counters; Date = date; - - TotalCount = totalCount; - TotalElapsedMs = totalElapsedMs; } } } diff --git a/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs b/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs new file mode 100644 index 000000000..b23c45696 --- /dev/null +++ b/src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.UsageTracking +{ + public struct UsageUpdate + { + public DateTime Date; + + public string Key; + + public string Category; + + public Counters Counters; + + public UsageUpdate(DateTime date, string key, string category, Counters counters) + { + Key = key; + Category = category; + Counters = counters; + Date = date; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index 106a27ac4..6a7565235 100644 --- a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs +++ b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -9,7 +9,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.History.Models; -using Squidex.Domain.Apps.Entities.History.Repositories; +using Squidex.Domain.Apps.Entities.History; using Squidex.Infrastructure.Commands; using Squidex.Pipeline; using Squidex.Shared; @@ -22,12 +22,12 @@ namespace Squidex.Areas.Api.Controllers.History [ApiExplorerSettings(GroupName = nameof(History))] public sealed class HistoryController : ApiController { - private readonly IHistoryEventRepository historyEventRepository; + private readonly IHistoryService historyService; - public HistoryController(ICommandBus commandBus, IHistoryEventRepository historyEventRepository) + public HistoryController(ICommandBus commandBus, IHistoryService historyService) : base(commandBus) { - this.historyEventRepository = historyEventRepository; + this.historyService = historyService; } /// @@ -46,7 +46,7 @@ namespace Squidex.Areas.Api.Controllers.History [ApiCosts(0.1)] public async Task GetHistory(string app, string channel) { - var entities = await historyEventRepository.QueryByChannelAsync(AppId, channel, 100); + var entities = await historyService.QueryByChannelAsync(AppId, channel, 100); var response = entities.Select(HistoryEventDto.FromHistoryEvent).ToList(); diff --git a/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs b/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs index 5beb31654..fc3fe9af8 100644 --- a/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs +++ b/src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs @@ -27,12 +27,6 @@ namespace Squidex.Areas.Api.Controllers.History.Models [Required] public string Actor { get; set; } - /// - /// The type of the event. - /// - [Required] - public string EventType { get; set; } - /// /// Gets a unique id for the event. /// @@ -48,9 +42,9 @@ namespace Squidex.Areas.Api.Controllers.History.Models /// public long Version { get; set; } - public static HistoryEventDto FromHistoryEvent(IHistoryEventEntity x) + public static HistoryEventDto FromHistoryEvent(ParsedHistoryEvent historyEvent) { - return SimpleMapper.Map(x, new HistoryEventDto()); + return SimpleMapper.Map(historyEvent, new HistoryEventDto { EventId = historyEvent.Id }); } } } diff --git a/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs b/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs index dac9e6f35..fe0a55bae 100644 --- a/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs @@ -27,7 +27,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics.Models /// public long Size { get; set; } - public static StorageUsageDto FromStats(IAssetStatsEntity stats) + public static StorageUsageDto FromStats(AssetStats stats) { return new StorageUsageDto { Date = stats.Date, Count = stats.TotalCount, Size = stats.TotalSize }; } diff --git a/src/Squidex/Config/Domain/AssetServices.cs b/src/Squidex/Config/Domain/AssetServices.cs index 3a10e12ce..8ec712b17 100644 --- a/src/Squidex/Config/Domain/AssetServices.cs +++ b/src/Squidex/Config/Domain/AssetServices.cs @@ -27,16 +27,14 @@ namespace Squidex.Config.Domain var path = config.GetRequiredValue("assetStore:folder:path"); services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService())) - .As() - .As(); + .As(); }, ["GoogleCloud"] = () => { var bucketName = config.GetRequiredValue("assetStore:googleCloud:bucket"); services.AddSingletonAs(c => new GoogleCloudAssetStore(bucketName)) - .As() - .As(); + .As(); }, ["AzureBlob"] = () => { @@ -44,8 +42,7 @@ namespace Squidex.Config.Domain var containerName = config.GetRequiredValue("assetStore:azureBlob:containerName"); services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName)) - .As() - .As(); + .As(); }, ["MongoDb"] = () => { @@ -65,8 +62,7 @@ namespace Squidex.Config.Domain return new MongoGridFsAssetStore(gridFsbucket); }) - .As() - .As(); + .As(); } }); diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index d662d6560..569f75a64 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -24,6 +24,7 @@ using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Comments; using Squidex.Domain.Apps.Entities.Comments.Commands; @@ -72,12 +73,18 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/Config/Domain/EventPublishersServices.cs b/src/Squidex/Config/Domain/EventPublishersServices.cs index 3ac733d82..9116f7594 100644 --- a/src/Squidex/Config/Domain/EventPublishersServices.cs +++ b/src/Squidex/Config/Domain/EventPublishersServices.cs @@ -55,8 +55,7 @@ namespace Squidex.Config.Domain if (enabled) { services.AddSingletonAs(c => new RabbitMqEventConsumer(c.GetRequiredService(), name, publisherConfig, exchange, eventsFilter)) - .As() - .As(); + .As(); } } else diff --git a/src/Squidex/Config/Domain/EventStoreServices.cs b/src/Squidex/Config/Domain/EventStoreServices.cs index 0338487d0..85066ae53 100644 --- a/src/Squidex/Config/Domain/EventStoreServices.cs +++ b/src/Squidex/Config/Domain/EventStoreServices.cs @@ -51,7 +51,6 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs(c => new GetEventStore(connection, eventStorePrefix, eventStoreProjectionHost)) - .As() .As(); } }); diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index 479a78450..eb5c8065f 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -63,50 +63,35 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs() - .As() - .As(); + .As(); services.AddSingletonAs() - .As() - .As(); + .As(); - services.AddSingletonAs() - .As() - .As(); + services.AddSingletonAs() + .As(); services.AddSingletonAs() - .As() - .As(); - - services.AddSingletonAs() - .As>() - .As() - .As(); + .As(); services.AddSingletonAs() - .As>() - .As(); + .As>(); services.AddSingletonAs() - .As() - .As() - .As(); + .As(); - services.AddSingletonAs() - .As() - .As() - .As(); + services.AddSingletonAs() + .As>() + .As(); services.AddSingletonAs() .As() - .As>() - .As(); + .As>(); services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService())) .As() .As>() - .As() - .As(); + .As(); services.AddTransientAs() .As(); diff --git a/src/Squidex/Config/ServiceExtensions.cs b/src/Squidex/Config/ServiceExtensions.cs index 90129c887..4487dc1c6 100644 --- a/src/Squidex/Config/ServiceExtensions.cs +++ b/src/Squidex/Config/ServiceExtensions.cs @@ -58,6 +58,8 @@ namespace Squidex.Config { services.AddSingleton(typeof(T), factory); + RegisterDefaults(services); + return new InterfaceRegistrator(services); } @@ -65,6 +67,8 @@ namespace Squidex.Config { services.AddSingleton(typeof(T), instance); + RegisterDefaults(services); + return new InterfaceRegistrator(services); } @@ -72,9 +76,19 @@ namespace Squidex.Config { services.AddSingleton(); + RegisterDefaults(services); + return new InterfaceRegistrator(services); } + private static void RegisterDefaults(IServiceCollection services) where T : class + { + if (typeof(T).GetInterfaces().Contains(typeof(IInitializable))) + { + services.AddSingleton(typeof(IInitializable), c => c.GetRequiredService()); + } + } + public static T GetOptionalValue(this IConfiguration config, string path, T defaultValue = default(T)) { var value = config.GetValue(path, defaultValue); diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs index cb307815c..e3ae304e2 100644 --- a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs @@ -17,7 +17,7 @@ namespace Squidex.Infrastructure.UsageTracking { public class BackgroundUsageTrackerTests { - private readonly IUsageStore usageStore = A.Fake(); + private readonly IUsageRepository usageStore = A.Fake(); private readonly ISemanticLog log = A.Fake(); private readonly BackgroundUsageTracker sut; @@ -57,10 +57,10 @@ namespace Squidex.Infrastructure.UsageTracking IReadOnlyList originalData = new List { - new StoredUsage("category1", date.AddDays(1), 10, 15), - new StoredUsage("category1", date.AddDays(3), 13, 18), - new StoredUsage("category1", date.AddDays(5), 15, 20), - new StoredUsage("category1", date.AddDays(7), 17, 22) + new StoredUsage("category1", date.AddDays(1), Counters(10, 15)), + new StoredUsage("category1", date.AddDays(3), Counters(13, 18)), + new StoredUsage("category1", date.AddDays(5), Counters(15, 20)), + new StoredUsage("category1", date.AddDays(7), Counters(17, 22)) }; A.CallTo(() => usageStore.QueryAsync("MyKey1", new DateTime(2016, 1, 1), new DateTime(2016, 1, 31))) @@ -79,11 +79,11 @@ namespace Squidex.Infrastructure.UsageTracking var originalData = new List { - new StoredUsage("MyCategory1", f.AddDays(1), 10, 15), - new StoredUsage("MyCategory1", f.AddDays(3), 13, 18), - new StoredUsage("MyCategory1", f.AddDays(4), 15, 20), - new StoredUsage(null, f.AddDays(0), 17, 22), - new StoredUsage(null, f.AddDays(2), 11, 14) + new StoredUsage("MyCategory1", f.AddDays(1), Counters(10, 15)), + new StoredUsage("MyCategory1", f.AddDays(3), Counters(13, 18)), + new StoredUsage("MyCategory1", f.AddDays(4), Counters(15, 20)), + new StoredUsage(null, f.AddDays(0), Counters(17, 22)), + new StoredUsage(null, f.AddDays(2), Counters(11, 14)) }; A.CallTo(() => usageStore.QueryAsync("MyKey1", f, t)) @@ -149,7 +149,8 @@ namespace Squidex.Infrastructure.UsageTracking sut.Next(); sut.Dispose(); - A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored, A.Ignored)).MustNotHaveHappened(); + A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) + .MustNotHaveHappened(); } [Fact] @@ -164,17 +165,37 @@ namespace Squidex.Infrastructure.UsageTracking await sut.TrackAsync("MyKey3", "MyCategory1", 0.3, 4000); await sut.TrackAsync("MyKey3", "MyCategory1", 0.1, 5000); + await sut.TrackAsync("MyKey3", null, 0.5, 2000); await sut.TrackAsync("MyKey3", null, 0.5, 6000); + UsageUpdate[] updates = null; + + A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) + .Invokes((UsageUpdate[] u) => updates = u); + sut.Next(); sut.Dispose(); - A.CallTo(() => usageStore.TrackUsagesAsync(today, "MyKey1", "MyCategory1", 1.0, 1000)).MustHaveHappened(); - A.CallTo(() => usageStore.TrackUsagesAsync(today, "MyKey2", "MyCategory1", 1.5, 5000)).MustHaveHappened(); - A.CallTo(() => usageStore.TrackUsagesAsync(today, "MyKey3", "MyCategory1", 0.4, 9000)).MustHaveHappened(); + updates.Should().BeEquivalentTo(new[] + { + new UsageUpdate(today, "MyKey1", "MyCategory1", Counters(1.0, 1000)), + new UsageUpdate(today, "MyKey2", "MyCategory1", Counters(1.5, 5000)), + new UsageUpdate(today, "MyKey3", "MyCategory1", Counters(0.4, 9000)), + new UsageUpdate(today, "MyKey3", "*", Counters(1, 8000)) + }, o => o.ComparingByMembers()); + + A.CallTo(() => usageStore.TrackUsagesAsync(A.Ignored)) + .MustHaveHappened(); + } - A.CallTo(() => usageStore.TrackUsagesAsync(today, "MyKey3", "*", 1.0, 8000)).MustHaveHappened(); + private static Counters Counters(double count, long ms) + { + return new Counters + { + [BackgroundUsageTracker.CounterTotalCalls] = count, + [BackgroundUsageTracker.CounterTotalElapsedMs] = ms + }; } } }