Browse Source

Refactoring for mongo services.

pull/336/head
Sebastian Stehle 8 years ago
parent
commit
0235760ad3
  1. 40
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsEntity.cs
  2. 101
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs
  3. 72
      src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs
  4. 74
      src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs
  5. 34
      src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  6. 2
      src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs
  7. 16
      src/Squidex.Domain.Apps.Entities/Assets/AssetStats.cs
  8. 67
      src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetStatsRepository.cs
  9. 41
      src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetStatsRepository_EventHandling.cs
  10. 2
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetStatsRepository.cs
  11. 2
      src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs
  12. 54
      src/Squidex.Domain.Apps.Entities/History/HistoryEvent.cs
  13. 43
      src/Squidex.Domain.Apps.Entities/History/HistoryEventToStore.cs
  14. 10
      src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs
  15. 89
      src/Squidex.Domain.Apps.Entities/History/HistoryService.cs
  16. 2
      src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs
  17. 15
      src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs
  18. 35
      src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs
  19. 6
      src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs
  20. 4
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs
  21. 6
      src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsage.cs
  22. 96
      src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs
  23. 58
      src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs
  24. 58
      src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs
  25. 26
      src/Squidex.Infrastructure/UsageTracking/Counters.cs
  26. 4
      src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs
  27. 12
      src/Squidex.Infrastructure/UsageTracking/StoredUsage.cs
  28. 30
      src/Squidex.Infrastructure/UsageTracking/UsageUpdate.cs
  29. 10
      src/Squidex/Areas/Api/Controllers/History/HistoryController.cs
  30. 10
      src/Squidex/Areas/Api/Controllers/History/Models/HistoryEventDto.cs
  31. 2
      src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs
  32. 12
      src/Squidex/Config/Domain/AssetServices.cs
  33. 7
      src/Squidex/Config/Domain/EntitiesServices.cs
  34. 3
      src/Squidex/Config/Domain/EventPublishersServices.cs
  35. 1
      src/Squidex/Config/Domain/EventStoreServices.cs
  36. 39
      src/Squidex/Config/Domain/StoreServices.cs
  37. 14
      src/Squidex/Config/ServiceExtensions.cs
  38. 51
      tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs

40
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsEntity.cs

@ -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; }
}
}

101
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs

@ -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<MongoAssetStatsEntity>, IAssetStatsRepository, IEventConsumer
{
public MongoAssetStatsRepository(IMongoDatabase database)
: base(database)
{
}
protected override string CollectionName()
{
return "Projections_AssetStats";
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoAssetStatsEntity> collection, CancellationToken ct = default(CancellationToken))
{
return collection.Indexes.CreateManyAsync(
new[]
{
new CreateIndexModel<MongoAssetStatsEntity>(Index.Ascending(x => x.AssetId).Ascending(x => x.Date)),
new CreateIndexModel<MongoAssetStatsEntity>(Index.Ascending(x => x.AssetId).Descending(x => x.Date))
}, ct);
}
public async Task<IReadOnlyList<IAssetStatsEntity>> 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<MongoAssetStatsEntity>();
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<long> GetTotalSizeAsync(Guid appId)
{
var totalSizeEntity =
await Collection.Find(x => x.AssetId == appId).SortByDescending(x => x.Date)
.FirstOrDefaultAsync();
return totalSizeEntity?.TotalSize ?? 0;
}
}
}

72
src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs

@ -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<string, string> Parameters { get; set; }
RefToken IUpdateableEntityWithCreatedBy.CreatedBy
{
get
{
return Actor;
}
set
{
Actor = value;
}
}
public MongoHistoryEventEntity()
{
Parameters = new Dictionary<string, string>();
}
public MongoHistoryEventEntity AddParameter(string key, string value)
{
Parameters.Add(key, value);
return this;
}
}
}

74
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<MongoHistoryEventEntity>, IHistoryEventRepository, IEventConsumer
public class MongoHistoryEventRepository : MongoRepositoryBase<HistoryEvent>, IHistoryEventRepository
{
private readonly List<IHistoryEventsCreator> creators;
private readonly Dictionary<string, string> texts = new Dictionary<string, string>();
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return ".*"; }
}
public MongoHistoryEventRepository(IMongoDatabase database, IEnumerable<IHistoryEventsCreator> 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<MongoHistoryEventEntity> collection, CancellationToken ct = default(CancellationToken))
protected override Task SetupCollectionAsync(IMongoCollection<HistoryEvent> collection, CancellationToken ct = default(CancellationToken))
{
return collection.Indexes.CreateManyAsync(
new[]
{
new CreateIndexModel<MongoHistoryEventEntity>(
new CreateIndexModel<HistoryEvent>(
Index
.Ascending(x => x.AppId)
.Ascending(x => x.Channel)
.Descending(x => x.Created)
.Descending(x => x.Version)),
new CreateIndexModel<MongoHistoryEventEntity>(Index.Ascending(x => x.Created),
new CreateIndexModel<HistoryEvent>(Index.Ascending(x => x.Created),
new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(365) })
}, ct);
}
public async Task<IReadOnlyList<IHistoryEventEntity>> QueryByChannelAsync(Guid appId, string channelPrefix, int count)
public async Task<IReadOnlyList<HistoryEvent>> QueryByChannelAsync(Guid appId, string channelPrefix, int count)
{
List<MongoHistoryEventEntity> 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<IEvent> @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)

34
src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs

@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
"updated role {[Name]}");
}
protected Task<HistoryEventToStore> On(AppContributorRemoved @event)
protected Task<HistoryEvent> On(AppContributorRemoved @event)
{
const string channel = "settings.contributors";
@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("Contributor", @event.ContributorId));
}
protected Task<HistoryEventToStore> On(AppContributorAssigned @event)
protected Task<HistoryEvent> 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<HistoryEventToStore> On(AppClientAttached @event)
protected Task<HistoryEvent> On(AppClientAttached @event)
{
const string channel = "settings.clients";
@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("Id", @event.Id));
}
protected Task<HistoryEventToStore> On(AppClientRevoked @event)
protected Task<HistoryEvent> On(AppClientRevoked @event)
{
const string channel = "settings.clients";
@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("Id", @event.Id));
}
protected Task<HistoryEventToStore> On(AppClientRenamed @event)
protected Task<HistoryEvent> 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<HistoryEventToStore> On(AppLanguageAdded @event)
protected Task<HistoryEvent> On(AppLanguageAdded @event)
{
const string channel = "settings.languages";
@ -128,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("Language", @event.Language));
}
protected Task<HistoryEventToStore> On(AppLanguageRemoved @event)
protected Task<HistoryEvent> On(AppLanguageRemoved @event)
{
const string channel = "settings.languages";
@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("Language", @event.Language));
}
protected Task<HistoryEventToStore> On(AppLanguageUpdated @event)
protected Task<HistoryEvent> On(AppLanguageUpdated @event)
{
const string channel = "settings.languages";
@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("Language", @event.Language));
}
protected Task<HistoryEventToStore> On(AppMasterLanguageSet @event)
protected Task<HistoryEvent> On(AppMasterLanguageSet @event)
{
const string channel = "settings.languages";
@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("Language", @event.Language));
}
protected Task<HistoryEventToStore> On(AppPatternAdded @event)
protected Task<HistoryEvent> On(AppPatternAdded @event)
{
const string channel = "settings.patterns";
@ -164,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("Name", @event.Name));
}
protected Task<HistoryEventToStore> On(AppPatternUpdated @event)
protected Task<HistoryEvent> On(AppPatternUpdated @event)
{
const string channel = "settings.patterns";
@ -173,7 +173,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("Name", @event.Name));
}
protected Task<HistoryEventToStore> On(AppPatternDeleted @event)
protected Task<HistoryEvent> On(AppPatternDeleted @event)
{
const string channel = "settings.patterns";
@ -182,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("PatternId", @event.PatternId));
}
protected Task<HistoryEventToStore> On(AppRoleAdded @event)
protected Task<HistoryEvent> On(AppRoleAdded @event)
{
const string channel = "settings.roles";
@ -191,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("Name", @event.Name));
}
protected Task<HistoryEventToStore> On(AppRoleUpdated @event)
protected Task<HistoryEvent> On(AppRoleUpdated @event)
{
const string channel = "settings.roles";
@ -200,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("Name", @event.Name));
}
protected Task<HistoryEventToStore> On(AppRoleDeleted @event)
protected Task<HistoryEvent> 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<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event)
protected override Task<HistoryEvent> CreateEventCoreAsync(Envelope<IEvent> @event)
{
return this.DispatchFuncAsync(@event.Payload, (HistoryEventToStore)null);
return this.DispatchFuncAsync(@event.Payload, (HistoryEvent)null);
}
private static string ClientName(AppClientRenamed @event)

2
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<string>(); ;
var schemaNames = new List<string>();
schemaNames.Add(Permission.Any);
schemaNames.AddRange(schemas.Select(x => x.Name));

16
src/Squidex.Domain.Apps.Entities/Assets/IAssetStatsEntity.cs → 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;
}
}
}

67
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<long> 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<IReadOnlyList<AssetStats>> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate)
{
var enriched = new List<AssetStats>();
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;
}
}
}

41
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs → 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)));
}
}
}

2
src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetStatsRepository.cs

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
public interface IAssetStatsRepository
{
Task<IReadOnlyList<IAssetStatsEntity>> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate);
Task<IReadOnlyList<AssetStats>> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate);
Task<long> GetTotalSizeAsync(Guid appId);
}

2
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<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event)
protected override Task<HistoryEvent> CreateEventCoreAsync(Envelope<IEvent> @event)
{
var channel = $"contents.{@event.Headers.AggregateId()}";

54
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<string, string> Parameters { get; set; } = new Dictionary<string, string>();
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;
}
}
}

43
src/Squidex.Domain.Apps.Entities/History/HistoryEventToStore.cs

@ -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<string, string> parameters = new Dictionary<string, string>();
public string Channel { get; }
public string Message { get; }
public IReadOnlyDictionary<string, string> 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;
}
}
}

10
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<HistoryEventToStore> CreateEventAsync(Envelope<IEvent> @event)
public Task<HistoryEvent> CreateEventAsync(Envelope<IEvent> @event)
{
if (HasEventText(@event.Payload))
{
return CreateEventCoreAsync(@event);
}
return Task.FromResult<HistoryEventToStore>(null);
return Task.FromResult<HistoryEvent>(null);
}
protected abstract Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event);
protected abstract Task<HistoryEvent> CreateEventCoreAsync(Envelope<IEvent> @event);
}
}

89
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<string, string> texts = new Dictionary<string, string>();
private readonly List<IHistoryEventsCreator> 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<IHistoryEventsCreator> 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<IEvent> @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<IReadOnlyList<ParsedHistoryEvent>> QueryByChannelAsync(Guid appId, string channelPrefix, int count)
{
var items = await repository.QueryByChannelAsync(appId, channelPrefix, count);
return items.Select(x => new ParsedHistoryEvent(x, texts)).ToList();
}
}
}

2
src/Squidex.Domain.Apps.Entities/History/IHistoryEventsCreator.cs

@ -15,6 +15,6 @@ namespace Squidex.Domain.Apps.Entities.History
{
IReadOnlyDictionary<string, string> Texts { get; }
Task<HistoryEventToStore> CreateEventAsync(Envelope<IEvent> @event);
Task<HistoryEvent> CreateEventAsync(Envelope<IEvent> @event);
}
}

15
src/Squidex.Domain.Apps.Entities/History/IHistoryEventEntity.cs → 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<IReadOnlyList<ParsedHistoryEvent>> QueryByChannelAsync(Guid appId, string channelPrefix, int count);
}
}

35
src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs → 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<string> 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<string, string> texts)
public ParsedHistoryEvent(HistoryEvent item, IReadOnlyDictionary<string, string> texts)
{
this.inner = inner;
this.item = item;
message = new Lazy<string>(() =>
{
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);
}

6
src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs

@ -13,8 +13,10 @@ namespace Squidex.Domain.Apps.Entities.History.Repositories
{
public interface IHistoryEventRepository
{
Task<IReadOnlyList<IHistoryEventEntity>> QueryByChannelAsync(Guid appId, string channelPrefix, int count);
Task<IReadOnlyList<HistoryEvent>> QueryByChannelAsync(Guid appId, string channelPrefix, int count);
Task RemoveAsync(Guid appId);
Task InsertAsync(HistoryEvent item);
Task ClearAsync();
}
}

4
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<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event)
protected override Task<HistoryEvent> CreateEventCoreAsync(Envelope<IEvent> @event)
{
if (@event.Payload is SchemaEvent schemaEvent)
{
@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas
return Task.FromResult(result);
}
return Task.FromResult<HistoryEventToStore>(null);
return Task.FromResult<HistoryEvent>(null);
}
}
}

6
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();
}
}

96
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<MongoUsage>, 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<MongoUsage> collection, CancellationToken ct = default(CancellationToken))
{
return collection.Indexes.CreateOneAsync(
new CreateIndexModel<MongoUsage>(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<WriteModel<MongoUsage>>();
foreach (var value in updates)
{
if (value.Counters.Count > 0)
{
var (filter, update) = CreateOperation(value);
writes.Add(new UpdateOneModel<MongoUsage>(filter, update) { IsUpsert = true });
}
}
await Collection.BulkWriteAsync(writes, Unordered);
}
}
private static (FilterDefinition<MongoUsage>, UpdateDefinition<MongoUsage>) 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<IReadOnlyList<StoredUsage>> 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();
}
}
}

58
src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageStore.cs

@ -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<MongoUsage>, IUsageStore
{
public MongoUsageStore(IMongoDatabase database)
: base(database)
{
}
protected override string CollectionName()
{
return "Usages";
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoUsage> collection, CancellationToken ct = default(CancellationToken))
{
return collection.Indexes.CreateOneAsync(
new CreateIndexModel<MongoUsage>(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<IReadOnlyList<StoredUsage>> 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();
}
}
}

58
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<string, IReadOnlyList<DateUsage>>();
@ -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)

26
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<string, double>
{
public double Get(string name)
{
if (name == null)
{
return 0;
}
TryGetValue(name, out var value);
return value;
}
}
}

4
src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs → 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<IReadOnlyList<StoredUsage>> QueryAsync(string key, DateTime fromDate, DateTime toDate);
}

12
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;
}
}
}

30
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;
}
}
}

10
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;
}
/// <summary>
@ -46,7 +46,7 @@ namespace Squidex.Areas.Api.Controllers.History
[ApiCosts(0.1)]
public async Task<IActionResult> 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();

10
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; }
/// <summary>
/// The type of the event.
/// </summary>
[Required]
public string EventType { get; set; }
/// <summary>
/// Gets a unique id for the event.
/// </summary>
@ -48,9 +42,9 @@ namespace Squidex.Areas.Api.Controllers.History.Models
/// </summary>
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 });
}
}
}

2
src/Squidex/Areas/Api/Controllers/Statistics/Models/StorageUsageDto.cs

@ -27,7 +27,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics.Models
/// </summary>
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 };
}

12
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<ISemanticLog>()))
.As<IAssetStore>()
.As<IInitializable>();
.As<IAssetStore>();
},
["GoogleCloud"] = () =>
{
var bucketName = config.GetRequiredValue("assetStore:googleCloud:bucket");
services.AddSingletonAs(c => new GoogleCloudAssetStore(bucketName))
.As<IAssetStore>()
.As<IInitializable>();
.As<IAssetStore>();
},
["AzureBlob"] = () =>
{
@ -44,8 +42,7 @@ namespace Squidex.Config.Domain
var containerName = config.GetRequiredValue("assetStore:azureBlob:containerName");
services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName))
.As<IAssetStore>()
.As<IInitializable>();
.As<IAssetStore>();
},
["MongoDb"] = () =>
{
@ -65,8 +62,7 @@ namespace Squidex.Config.Domain
return new MongoGridFsAssetStore(gridFsbucket);
})
.As<IAssetStore>()
.As<IInitializable>();
.As<IAssetStore>();
}
});

7
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<AssetQueryService>()
.As<IAssetQueryService>();
services.AddSingletonAs<DefaultAssetStatsRepository>()
.As<IAssetStatsRepository>();
services.AddSingletonAs<ContentQueryService>()
.As<IContentQueryService>();
services.AddSingletonAs<ContentVersionLoader>()
.As<IContentVersionLoader>();
services.AddSingletonAs<HistoryService>()
.As<IHistoryService>();
services.AddSingletonAs<AppHistoryEventsCreator>()
.As<IHistoryEventsCreator>();

3
src/Squidex/Config/Domain/EventPublishersServices.cs

@ -55,8 +55,7 @@ namespace Squidex.Config.Domain
if (enabled)
{
services.AddSingletonAs(c => new RabbitMqEventConsumer(c.GetRequiredService<JsonSerializerSettings>(), name, publisherConfig, exchange, eventsFilter))
.As<IEventConsumer>()
.As<IInitializable>();
.As<IEventConsumer>();
}
}
else

1
src/Squidex/Config/Domain/EventStoreServices.cs

@ -51,7 +51,6 @@ namespace Squidex.Config.Domain
.As<IHealthCheck>();
services.AddSingletonAs(c => new GetEventStore(connection, eventStorePrefix, eventStoreProjectionHost))
.As<IInitializable>()
.As<IEventStore>();
}
});

39
src/Squidex/Config/Domain/StoreServices.cs

@ -63,50 +63,35 @@ namespace Squidex.Config.Domain
.As<IHealthCheck>();
services.AddSingletonAs<MongoMigrationStatus>()
.As<IMigrationStatus>()
.As<IInitializable>();
.As<IMigrationStatus>();
services.AddSingletonAs<MongoPersistedGrantStore>()
.As<IPersistedGrantStore>()
.As<IInitializable>();
.As<IPersistedGrantStore>();
services.AddSingletonAs<MongoUsageStore>()
.As<IUsageStore>()
.As<IInitializable>();
services.AddSingletonAs<MongoUsageRepository>()
.As<IUsageRepository>();
services.AddSingletonAs<MongoRuleEventRepository>()
.As<IRuleEventRepository>()
.As<IInitializable>();
services.AddSingletonAs<MongoUserStore>()
.As<IUserStore<IdentityUser>>()
.As<IUserFactory>()
.As<IInitializable>();
.As<IRuleEventRepository>();
services.AddSingletonAs<MongoRoleStore>()
.As<IRoleStore<IdentityRole>>()
.As<IInitializable>();
.As<IRoleStore<IdentityRole>>();
services.AddSingletonAs<MongoHistoryEventRepository>()
.As<IHistoryEventRepository>()
.As<IEventConsumer>()
.As<IInitializable>();
.As<IHistoryEventRepository>();
services.AddSingletonAs<MongoAssetStatsRepository>()
.As<IAssetStatsRepository>()
.As<IEventConsumer>()
.As<IInitializable>();
services.AddSingletonAs<MongoUserStore>()
.As<IUserStore<IdentityUser>>()
.As<IUserFactory>();
services.AddSingletonAs<MongoAssetRepository>()
.As<IAssetRepository>()
.As<ISnapshotStore<AssetState, Guid>>()
.As<IInitializable>();
.As<ISnapshotStore<AssetState, Guid>>();
services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService<IAppProvider>()))
.As<IContentRepository>()
.As<ISnapshotStore<ContentState, Guid>>()
.As<IEventConsumer>()
.As<IInitializable>();
.As<IEventConsumer>();
services.AddTransientAs<ConvertOldSnapshotStores>()
.As<IMigration>();

14
src/Squidex/Config/ServiceExtensions.cs

@ -58,6 +58,8 @@ namespace Squidex.Config
{
services.AddSingleton(typeof(T), factory);
RegisterDefaults<T>(services);
return new InterfaceRegistrator<T>(services);
}
@ -65,6 +67,8 @@ namespace Squidex.Config
{
services.AddSingleton(typeof(T), instance);
RegisterDefaults<T>(services);
return new InterfaceRegistrator<T>(services);
}
@ -72,9 +76,19 @@ namespace Squidex.Config
{
services.AddSingleton<T, T>();
RegisterDefaults<T>(services);
return new InterfaceRegistrator<T>(services);
}
private static void RegisterDefaults<T>(IServiceCollection services) where T : class
{
if (typeof(T).GetInterfaces().Contains(typeof(IInitializable)))
{
services.AddSingleton(typeof(IInitializable), c => c.GetRequiredService<T>());
}
}
public static T GetOptionalValue<T>(this IConfiguration config, string path, T defaultValue = default(T))
{
var value = config.GetValue(path, defaultValue);

51
tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs

@ -17,7 +17,7 @@ namespace Squidex.Infrastructure.UsageTracking
{
public class BackgroundUsageTrackerTests
{
private readonly IUsageStore usageStore = A.Fake<IUsageStore>();
private readonly IUsageRepository usageStore = A.Fake<IUsageRepository>();
private readonly ISemanticLog log = A.Fake<ISemanticLog>();
private readonly BackgroundUsageTracker sut;
@ -57,10 +57,10 @@ namespace Squidex.Infrastructure.UsageTracking
IReadOnlyList<StoredUsage> originalData = new List<StoredUsage>
{
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<StoredUsage>
{
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<DateTime>.Ignored, A<string>.Ignored, A<string>.Ignored, A<double>.Ignored, A<double>.Ignored)).MustNotHaveHappened();
A.CallTo(() => usageStore.TrackUsagesAsync(A<UsageUpdate[]>.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<UsageUpdate[]>.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<UsageUpdate>());
A.CallTo(() => usageStore.TrackUsagesAsync(A<UsageUpdate[]>.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
};
}
}
}

Loading…
Cancel
Save