diff --git a/Squidex.sln b/Squidex.sln index fb24873ad..4d49d682e 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -69,6 +69,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entitie EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entities.Tests", "tests\Squidex.Domain.Apps.Entities.Tests\Squidex.Domain.Apps.Entities.Tests.csproj", "{AA003372-CD8D-4DBC-962C-F61E0C93CF05}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entities.MongoDb", "src\Squidex.Domain.Apps.Entities.MongoDb\Squidex.Domain.Apps.Entities.MongoDb.csproj", "{7DA5B308-D950-4496-93D5-21D6C4D91644}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -343,6 +345,18 @@ Global {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x64.Build.0 = Release|Any CPU {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x86.ActiveCfg = Release|Any CPU {AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x86.Build.0 = Release|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Debug|x64.ActiveCfg = Debug|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Debug|x64.Build.0 = Debug|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Debug|x86.ActiveCfg = Debug|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Debug|x86.Build.0 = Debug|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|Any CPU.Build.0 = Release|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|x64.ActiveCfg = Release|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|x64.Build.0 = Release|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|x86.ActiveCfg = Release|Any CPU + {7DA5B308-D950-4496-93D5-21D6C4D91644}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -374,6 +388,7 @@ Global {6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {C9809D59-6665-471E-AD87-5AC624C65892} {79FEF326-CA5E-4698-B2BA-C16A4580B4D5} = {C9809D59-6665-471E-AD87-5AC624C65892} {AA003372-CD8D-4DBC-962C-F61E0C93CF05} = {C9809D59-6665-471E-AD87-5AC624C65892} + {7DA5B308-D950-4496-93D5-21D6C4D91644} = {C9809D59-6665-471E-AD87-5AC624C65892} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs new file mode 100644 index 000000000..64f694162 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// MongoAppEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Entities.Apps.State; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Apps +{ + public sealed class MongoAppEntity + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + + [BsonElement] + [BsonRequired] + public AppState State { get; set; } + + [BsonElement] + [BsonRequired] + public int Version { get; set; } + + [BsonElement] + [BsonRequired] + public string[] UserIds { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs new file mode 100644 index 000000000..675ccddda --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// MongoAppRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Apps.Repositories; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Apps +{ + public sealed class MongoAppRepository : MongoRepositoryBase, IAppRepository, ISnapshotStore + { + public MongoAppRepository(IMongoDatabase database) + : base(database) + { + } + + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.UserIds)); + } + + public async Task<(AppState Value, long Version)> ReadAsync(string key) + { + var existing = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (existing.State, existing.Version); + } + + return (null, -1); + } + + public async Task> QueryUserAppNamesAsync(string userId) + { + var appEntities = + await Collection.Find(x => x.UserIds.Contains(userId)).Project(Projection.Include(x => x.Id)).ToListAsync(); + + return appEntities.Select(x => x.Id).ToList(); + } + + public async Task WriteAsync(string key, AppState value, long oldVersion, long newVersion) + { + try + { + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, + Update + .Set(x => x.UserIds, value.Contributors.Keys.ToArray()) + .Set(x => x.State, value) + .Set(x => x.Version, newVersion), + Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == key) + .Project(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + } + } + else + { + throw; + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs new file mode 100644 index 000000000..fb6af615b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// MongoAssetEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Entities.Assets.State; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +{ + public sealed class MongoAssetEntity + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + + [BsonElement] + [BsonRequired] + public AssetState State { get; set; } + + [BsonElement] + [BsonRequired] + public int Version { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs new file mode 100644 index 000000000..32b27a99f --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -0,0 +1,140 @@ +// ========================================================================== +// MongoAssetRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets +{ + public sealed class MongoAssetRepository : MongoRepositoryBase, IAssetRepository, ISnapshotStore + { + public MongoAssetRepository(IMongoDatabase database) + : base(database) + { + } + + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return collection.Indexes.CreateOneAsync( + Index + .Ascending(x => x.State.AppId) + .Ascending(x => x.State.FileName) + .Ascending(x => x.State.MimeType) + .Descending(x => x.State.LastModified)); + } + + public async Task<(AssetState Value, long Version)> ReadAsync(string key) + { + var existing = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (existing.State, existing.Version); + } + + return (null, -1); + } + + public async Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0) + { + var filter = CreateFilter(appId, mimeTypes, ids, query); + + var assetEntities = + await Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.State.LastModified) + .ToListAsync(); + + return assetEntities.OfType().ToList(); + } + + public async Task CountAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null) + { + var filter = CreateFilter(appId, mimeTypes, ids, query); + + var assetsCount = + await Collection.Find(filter) + .CountAsync(); + + return assetsCount; + } + + public async Task FindAssetAsync(Guid id) + { + var (state, etag) = await ReadAsync(id.ToString()); + + return state; + } + + private static FilterDefinition CreateFilter(Guid appId, ICollection mimeTypes, ICollection ids, string query) + { + var filters = new List> + { + Filter.Eq(x => x.State.AppId, appId) + }; + + if (ids != null && ids.Count > 0) + { + filters.Add(Filter.In(x => x.Id, ids.Select(x => x.ToString()))); + } + + if (mimeTypes != null && mimeTypes.Count > 0) + { + filters.Add(Filter.In(x => x.State.MimeType, mimeTypes)); + } + + if (!string.IsNullOrWhiteSpace(query)) + { + filters.Add(Filter.Regex(x => x.State.FileName, new BsonRegularExpression(query, "i"))); + } + + var filter = Filter.And(filters); + + return filter; + } + + public async Task WriteAsync(string key, AssetState value, long oldVersion, long newVersion) + { + try + { + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, + Update + .Set(x => x.State, value) + .Set(x => x.Version, newVersion), + Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == key) + .Project(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + } + } + else + { + throw; + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsEntity.cs similarity index 87% rename from src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsEntity.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsEntity.cs index 9fe49a6b0..cc7ddf110 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsEntity.cs @@ -9,9 +9,9 @@ using System; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Read.Assets; +using Squidex.Domain.Apps.Entities.Assets; -namespace Squidex.Domain.Apps.Read.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { public sealed class MongoAssetStatsEntity : IAssetStatsEntity { @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets [BsonRequired] [BsonElement] - public Guid AppId { get; set; } + public Guid AssetId { get; set; } [BsonRequired] [BsonElement] diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs similarity index 76% rename from src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs index 290c754e7..a8fc50d26 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository.cs @@ -11,12 +11,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using MongoDB.Driver; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { public partial class MongoAssetStatsRepository : MongoRepositoryBase, IAssetStatsRepository, IAssetEventConsumer { @@ -30,17 +30,16 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets return "Projections_AssetStats"; } - protected override Task SetupCollectionAsync(IMongoCollection collection) + protected override async Task SetupCollectionAsync(IMongoCollection collection) { - return Task.WhenAll( - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Ascending(x => x.Date)), - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Descending(x => x.Date))); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AssetId).Ascending(x => x.Date)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AssetId).Descending(x => x.Date)); } public async Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate) { var originalSizesEntities = - await Collection.Find(x => x.AppId == appId && x.Date >= fromDate && x.Date <= toDate).SortBy(x => x.Date) + await Collection.Find(x => x.AssetId == appId && x.Date >= fromDate && x.Date <= toDate).SortBy(x => x.Date) .ToListAsync(); var enrichedSizes = new List(); @@ -64,7 +63,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets if (previousSize < 0) { var firstBeforeRangeEntity = - await Collection.Find(x => x.AppId == appId && x.Date < fromDate).SortByDescending(x => x.Date) + await Collection.Find(x => x.AssetId == appId && x.Date < fromDate).SortByDescending(x => x.Date) .FirstOrDefaultAsync(); previousSize = firstBeforeRangeEntity?.TotalSize ?? 0L; @@ -88,7 +87,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets public async Task GetTotalSizeAsync(Guid appId) { var totalSizeEntity = - await Collection.Find(x => x.AppId == appId).SortByDescending(x => x.Date) + await Collection.Find(x => x.AssetId == appId).SortByDescending(x => x.Date) .FirstOrDefaultAsync(); return totalSizeEntity?.TotalSize ?? 0; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs similarity index 90% rename from src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs index b46374dd7..16e153aa3 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs @@ -13,12 +13,10 @@ using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Read.MongoDb.Assets +namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { public partial class MongoAssetStatsRepository { - private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; - public string Name { get { return GetType().Name; } @@ -60,14 +58,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Assets if (assetStatsEntity == null) { var lastEntity = - await Collection.Find(x => x.AppId == appId).SortByDescending(x => x.Date) + await Collection.Find(x => x.AssetId == appId).SortByDescending(x => x.Date) .FirstOrDefaultAsync(); assetStatsEntity = new MongoAssetStatsEntity { Id = id, Date = date, - AppId = appId, + AssetId = appId, TotalSize = lastEntity?.TotalSize ?? 0, TotalCount = lastEntity?.TotalCount ?? 0 }; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs similarity index 97% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs index be7dcb916..cb68d1c52 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Extensions.cs @@ -16,7 +16,7 @@ using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { public static class Extensions { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs similarity index 87% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index f2f19251e..bb8ac758d 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -13,18 +13,13 @@ using MongoDB.Bson.Serialization.Attributes; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Read.Contents; +using Squidex.Domain.Apps.Entities.Contents; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { - public sealed class MongoContentEntity : - IContentEntity, - IUpdateableEntityWithVersion, - IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithLastModifiedBy, - IUpdateableEntityWithAppRef + public sealed class MongoContentEntity : IContentEntity { private NamedContentData data; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs similarity index 88% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 9f1b3b9fb..29be5d217 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -14,17 +14,16 @@ using Microsoft.OData.UriParser; using MongoDB.Bson; using MongoDB.Driver; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Contents; -using Squidex.Domain.Apps.Read.Contents.Repositories; -using Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors; -using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { - public partial class MongoContentRepository : IContentRepository, IEventConsumer + public class MongoContentRepository : IContentRepository { private const string Prefix = "Projections_Content_"; private readonly IMongoDatabase database; @@ -189,5 +188,19 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents await action(collection, schema); } + + private Task ForAppIdAsync(Guid appId, Func, Task> action) + { + var collection = GetCollection(appId); + + return action(collection); + } + + private IMongoCollection GetCollection(Guid appId) + { + var name = $"{Prefix}{appId}"; + + return database.GetCollection(name); + } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/ConstantVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/ConstantVisitor.cs similarity index 96% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/ConstantVisitor.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/ConstantVisitor.cs index 9a21a44c4..58e116b23 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/ConstantVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/ConstantVisitor.cs @@ -12,7 +12,7 @@ using Microsoft.OData.UriParser; using NodaTime; using NodaTime.Text; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public sealed class ConstantVisitor : QueryNodeVisitor { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterBuilder.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterBuilder.cs similarity index 96% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterBuilder.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterBuilder.cs index 203ca26c7..895e5d79b 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterBuilder.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterBuilder.cs @@ -12,7 +12,7 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public static class FilterBuilder { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterVisitor.cs similarity index 98% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterVisitor.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterVisitor.cs index 250b4d513..f4d69dea9 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterVisitor.cs @@ -13,7 +13,7 @@ using MongoDB.Bson; using MongoDB.Driver; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public class FilterVisitor : QueryNodeVisitor> { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs similarity index 97% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs index 1abdb16a0..547107029 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FindExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs @@ -14,7 +14,7 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public static class FindExtensions { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/PropertyVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/PropertyVisitor.cs similarity index 97% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/PropertyVisitor.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/PropertyVisitor.cs index 36ddad63c..0052167fc 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/PropertyVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/PropertyVisitor.cs @@ -14,7 +14,7 @@ using MongoDB.Driver; using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public sealed class PropertyVisitor : QueryNodeVisitor> { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/SearchTermVisitor.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SearchTermVisitor.cs similarity index 94% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/SearchTermVisitor.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SearchTermVisitor.cs index a99f5af0e..3e106af60 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/SearchTermVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SearchTermVisitor.cs @@ -9,7 +9,7 @@ using System; using Microsoft.OData.UriParser; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public class SearchTermVisitor : QueryNodeVisitor { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/SortBuilder.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SortBuilder.cs similarity index 96% rename from src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/SortBuilder.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SortBuilder.cs index 6b851c528..627095331 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/SortBuilder.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/SortBuilder.cs @@ -11,7 +11,7 @@ using Microsoft.OData.UriParser; using MongoDB.Driver; using Squidex.Domain.Apps.Core.Schemas; -namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors +namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors { public static class SortBuilder { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs similarity index 95% rename from src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs index 4990fd528..a3223f276 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs @@ -12,11 +12,12 @@ using MongoDB.Bson.Serialization.Attributes; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.History +namespace Squidex.Domain.Apps.Entities.MongoDb.History { public sealed class MongoHistoryEventEntity : MongoEntity, IEntity, IEntityWithAppRef, + IUpdateableEntity, IUpdateableEntityWithVersion, IUpdateableEntityWithCreatedBy, IUpdateableEntityWithAppRef diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs similarity index 77% rename from src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs index 3404a64a7..169ed0df3 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs @@ -11,13 +11,13 @@ using System.Collections.Generic; using System.Linq; 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.Domain.Apps.Read.History; -using Squidex.Domain.Apps.Read.History.Repositories; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.History +namespace Squidex.Domain.Apps.Entities.MongoDb.History { public class MongoHistoryEventRepository : MongoRepositoryBase, IHistoryEventRepository, IEventConsumer { @@ -53,23 +53,22 @@ namespace Squidex.Domain.Apps.Read.MongoDb.History return "Projections_History"; } - protected override Task SetupCollectionAsync(IMongoCollection collection) + protected override async Task SetupCollectionAsync(IMongoCollection collection) { - return Task.WhenAll( - collection.Indexes.CreateOneAsync( - Index - .Ascending(x => x.AppId) - .Ascending(x => x.Channel) - .Descending(x => x.Created) - .Descending(x => x.Version)), - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Created), new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(365) })); + await collection.Indexes.CreateOneAsync( + Index + .Ascending(x => x.AppId) + .Ascending(x => x.Channel) + .Descending(x => x.Created) + .Descending(x => x.Version)); + + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Created), new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(365) }); } public async Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count) { var historyEventEntities = - await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix) - .SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count) + await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count) .ToListAsync(); return historyEventEntities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList(); diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs similarity index 95% rename from src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs index e3520bd43..c3aea8c1b 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/History/ParsedHistoryEvent.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/ParsedHistoryEvent.cs @@ -9,12 +9,12 @@ using System; using System.Collections.Generic; using NodaTime; -using Squidex.Domain.Apps.Read.History; +using Squidex.Domain.Apps.Entities.History; using Squidex.Infrastructure; #pragma warning disable RECS0029 // Warns about property or indexer setters and event adders or removers that do not use the value parameter -namespace Squidex.Domain.Apps.Read.MongoDb.History +namespace Squidex.Domain.Apps.Entities.MongoDb.History { internal sealed class ParsedHistoryEvent : IHistoryEventEntity { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCollectionExtensions.cs similarity index 98% rename from src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/MongoCollectionExtensions.cs index 3e840b8b1..2893111cc 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/MongoCollectionExtensions.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/MongoCollectionExtensions.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb +namespace Squidex.Domain.Apps.Entities.MongoDb { public static class MongoCollectionExtensions { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs new file mode 100644 index 000000000..304e5d01a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// MongoRuleEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Entities.Rules.State; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +{ + public sealed class MongoRuleEntity + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + + [BsonElement] + [BsonRequired] + public RuleState State { get; set; } + + [BsonElement] + [BsonRequired] + public int Version { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs similarity index 90% rename from src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs index e48aa1543..f57382d6e 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs @@ -11,16 +11,16 @@ using MongoDB.Bson.Serialization.Attributes; using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Read.Rules; +using Squidex.Domain.Apps.Entities.Rules; using Squidex.Infrastructure.MongoDb; -namespace Squidex.Domain.Apps.Read.MongoDb.Rules +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { public sealed class MongoRuleEventEntity : MongoEntity, IRuleEventEntity { [BsonRequired] [BsonElement] - public Guid AppId { get; set; } + public Guid AssetId { get; set; } [BsonRequired] [BsonElement] diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs similarity index 70% rename from src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs rename to src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs index 99b22d125..73c5655f2 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Rules/MongoRuleEventRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs @@ -14,12 +14,12 @@ using MongoDB.Driver; using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Read.Rules; -using Squidex.Domain.Apps.Read.Rules.Repositories; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Reflection; -namespace Squidex.Domain.Apps.Read.MongoDb.Rules +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository { @@ -33,12 +33,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules return "RuleEvents"; } - protected override Task SetupCollectionAsync(IMongoCollection collection) + protected override async Task SetupCollectionAsync(IMongoCollection collection) { - return Task.WhenAll( - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NextAttempt)), - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AppId).Descending(x => x.Created)), - collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero })); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NextAttempt)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.AssetId).Descending(x => x.Created)); + await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Expires), new CreateIndexOptions { ExpireAfter = TimeSpan.Zero }); } public Task QueryPendingAsync(Instant now, Func callback, CancellationToken cancellationToken = default(CancellationToken)) @@ -49,7 +48,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules public async Task> QueryByAppAsync(Guid appId, int skip = 0, int take = 20) { var ruleEventEntities = - await Collection.Find(x => x.AppId == appId).Skip(skip).Limit(take).SortByDescending(x => x.Created) + await Collection.Find(x => x.AssetId == appId).Skip(skip).Limit(take).SortByDescending(x => x.Created) .ToListAsync(); return ruleEventEntities; @@ -66,7 +65,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules public async Task CountByAppAsync(Guid appId) { - return (int)await Collection.CountAsync(x => x.AppId == appId); + return (int)await Collection.CountAsync(x => x.AssetId == appId); } public Task EnqueueAsync(Guid id, Instant nextAttempt) @@ -84,11 +83,12 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Rules public Task MarkSentAsync(Guid jobId, string dump, RuleResult result, RuleJobResult jobResult, TimeSpan elapsed, Instant? nextAttempt) { return Collection.UpdateOneAsync(x => x.Id == jobId, - Update.Set(x => x.Result, result) - .Set(x => x.LastDump, dump) - .Set(x => x.JobResult, jobResult) - .Set(x => x.NextAttempt, nextAttempt) - .Inc(x => x.NumCalls, 1)); + Update + .Set(x => x.Result, result) + .Set(x => x.LastDump, dump) + .Set(x => x.JobResult, jobResult) + .Set(x => x.NextAttempt, nextAttempt) + .Inc(x => x.NumCalls, 1)); } } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs new file mode 100644 index 000000000..c65f2820a --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// MongoRuleRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +{ + public sealed class MongoRuleRepository : MongoRepositoryBase, IRuleRepository, ISnapshotStore + { + public MongoRuleRepository(IMongoDatabase database) + : base(database) + { + } + + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId)); + } + + public async Task<(RuleState Value, long Version)> ReadAsync(string key) + { + var existing = + await Collection.Find(x => x.Id == key) + .FirstOrDefaultAsync(); + + if (existing != null) + { + return (existing.State, existing.Version); + } + + return (null, -1); + } + + public async Task> QueryRuleIdsAsync(Guid appId) + { + var ruleEntities = + await Collection.Find(x => x.State.AppId == appId).Project(Projection.Include(x => x.Id)).ToListAsync(); + + return ruleEntities.Select(x => x.Id).ToList(); + } + + public async Task WriteAsync(string key, RuleState value, long oldVersion, long newVersion) + { + try + { + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, + Update + .Set(x => x.State, value) + .Set(x => x.Version, newVersion), + Upsert); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + var existingVersion = + await Collection.Find(x => x.Id == key) + .Project(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + + if (existingVersion != null) + { + throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); + } + } + else + { + throw; + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj similarity index 92% rename from src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj rename to src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj index b8e390b69..2fc0d6ace 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs new file mode 100644 index 000000000..d862fe9a8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// AppProvider.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities +{ + public sealed class AppProvider : IAppProvider + { + private readonly IStateFactory factory; + + public AppProvider(IStateFactory factory) + { + Guard.NotNull(factory, nameof(factory)); + + this.factory = factory; + } + + public Task GetAppAsync(string appName) + { + return null; + } + + public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id) + { + return null; + } + + public Task> GetRulesAsync(string appName) + { + return null; + } + + public Task GetSchemaAsync(string appName, Guid id, bool provideDeleted = false) + { + return null; + } + + public Task GetSchemaAsync(string appName, string name, bool provideDeleted = false) + { + return null; + } + + public Task> GetSchemasAsync(string appName) + { + return null; + } + + public Task> GetUserApps(string userId) + { + return null; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs b/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs new file mode 100644 index 000000000..2e567dc45 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// IAppRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Apps.Repositories +{ + public interface IAppRepository + { + Task> QueryUserAppNamesAsync(string userId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs new file mode 100644 index 000000000..bee8e3061 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// ContentState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Entities.Contents.State +{ + public sealed class ContentState : DomainObjectState + { + [JsonProperty] + public IdContentData Data { get; set; } + + [JsonProperty] + public string Status { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs new file mode 100644 index 000000000..3a3f309ab --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// IRuleRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Rules.Repositories +{ + public interface IRuleRepository + { + Task> QueryRuleIdsAsync(Guid appId); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs new file mode 100644 index 000000000..04df68b68 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// ISchemaRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Schemas.Repositories +{ + public interface ISchemaRepository + { + Task FindSchemaNameAsync(Guid schemaId); + + Task> QuerySchemaNamesAsync(Guid appId); + } +} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs deleted file mode 100644 index dae9ddcfc..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetEntity.cs +++ /dev/null @@ -1,75 +0,0 @@ -// ========================================================================== -// MongoAssetEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Read.MongoDb.Assets -{ - public sealed class MongoAssetEntity : - MongoEntity, - IAssetEntity, - IUpdateableEntityWithVersion, - IUpdateableEntityWithCreatedBy, - IUpdateableEntityWithLastModifiedBy, - IUpdateableEntityWithAppRef - { - [BsonRequired] - [BsonElement] - public string MimeType { get; set; } - - [BsonRequired] - [BsonElement] - public string FileName { get; set; } - - [BsonRequired] - [BsonElement] - public long FileSize { get; set; } - - [BsonRequired] - [BsonElement] - public long FileVersion { get; set; } - - [BsonRequired] - [BsonElement] - public bool IsImage { get; set; } - - [BsonRequired] - [BsonElement] - public long Version { get; set; } - - [BsonRequired] - [BsonElement] - public int? PixelWidth { get; set; } - - [BsonRequired] - [BsonElement] - public int? PixelHeight { get; set; } - - [BsonRequired] - [BsonElement] - public Guid AppId { get; set; } - - [BsonRequired] - [BsonElement] - public RefToken CreatedBy { get; set; } - - [BsonRequired] - [BsonElement] - public RefToken LastModifiedBy { get; set; } - - Guid IAssetInfo.AssetId - { - get { return Id; } - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs deleted file mode 100644 index 73bb48ae8..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ========================================================================== -// MongoAssetRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Driver; -using Squidex.Domain.Apps.Read.Assets; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Infrastructure.MongoDb; - -namespace Squidex.Domain.Apps.Read.MongoDb.Assets -{ - public partial class MongoAssetRepository : MongoRepositoryBase, IAssetRepository, IAssetEventConsumer - { - public MongoAssetRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "Projections_Assets"; - } - - protected override Task SetupCollectionAsync(IMongoCollection collection) - { - return collection.Indexes.CreateOneAsync( - Index.Ascending(x => x.AppId) - .Ascending(x => x.FileName) - .Ascending(x => x.MimeType) - .Descending(x => x.LastModified)); - } - - public async Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0) - { - var filter = CreateFilter(appId, mimeTypes, ids, query); - - var assetEntities = - await Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.LastModified) - .ToListAsync(); - - return assetEntities.OfType().ToList(); - } - - public async Task CountAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null) - { - var filter = CreateFilter(appId, mimeTypes, ids, query); - - var assetsCount = - await Collection.Find(filter) - .CountAsync(); - - return assetsCount; - } - - public async Task FindAssetAsync(Guid id) - { - var assetEntity = - await Collection.Find(s => s.Id == id) - .FirstOrDefaultAsync(); - - return assetEntity; - } - - private static FilterDefinition CreateFilter(Guid appId, ICollection mimeTypes, ICollection ids, string query) - { - var filters = new List> - { - Filter.Eq(x => x.AppId, appId) - }; - - if (ids != null && ids.Count > 0) - { - filters.Add(Filter.In(x => x.Id, ids)); - } - - if (mimeTypes != null && mimeTypes.Count > 0) - { - filters.Add(Filter.In(x => x.MimeType, mimeTypes)); - } - - if (!string.IsNullOrWhiteSpace(query)) - { - filters.Add(Filter.Regex(x => x.FileName, new BsonRegularExpression(query, "i"))); - } - - var filter = Filter.And(filters); - - return filter; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs deleted file mode 100644 index b05fe5d41..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository_EventHandling.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ========================================================================== -// MongoAssetRepository_EventHandling.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Read.MongoDb.Assets -{ - public partial class MongoAssetRepository - { - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return "^asset-"; } - } - - public Task On(Envelope @event) - { - return this.DispatchActionAsync(@event.Payload, @event.Headers); - } - - protected Task On(AssetCreated @event, EnvelopeHeaders headers) - { - return Collection.CreateAsync(@event, headers, a => - { - SimpleMapper.Map(@event, a); - }); - } - - protected Task On(AssetUpdated @event, EnvelopeHeaders headers) - { - return Collection.UpdateAsync(@event, headers, a => - { - SimpleMapper.Map(@event, a); - }); - } - - protected Task On(AssetRenamed @event, EnvelopeHeaders headers) - { - return Collection.UpdateAsync(@event, headers, a => - { - SimpleMapper.Map(@event, a); - }); - } - - protected Task On(AssetDeleted @event, EnvelopeHeaders headers) - { - return Collection.DeleteOneAsync(x => x.Id == @event.AssetId); - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs deleted file mode 100644 index 85acec317..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ /dev/null @@ -1,160 +0,0 @@ -// ========================================================================== -// MongoContentRepository_EventHandling.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure.Dispatching; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Read.MongoDb.Contents -{ - public partial class MongoContentRepository - { - public string Name - { - get { return GetType().Name; } - } - - public string EventsFilter - { - get { return "^(content-)|(app-)|(asset-)"; } - } - - public async Task ClearAsync() - { - using (var collections = await database.ListCollectionsAsync()) - { - while (await collections.MoveNextAsync()) - { - foreach (var collection in collections.Current) - { - var name = collection["name"].ToString(); - - if (name.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) - { - await database.DropCollectionAsync(name); - } - } - } - } - } - - public Task On(Envelope @event) - { - return this.DispatchActionAsync(@event.Payload, @event.Headers); - } - - protected Task On(AppCreated @event, EnvelopeHeaders headers) - { - return ForAppIdAsync(@event.AppId.Id, async collection => - { - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaId).Descending(x => x.LastModified)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds)); - await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Status)); - await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText)); - }); - } - - protected Task On(ContentCreated @event, EnvelopeHeaders headers) - { - return ForSchemaAsync(@event.AppId, @event.SchemaId.Id, (collection, schema) => - { - return collection.CreateAsync(@event, headers, content => - { - content.SchemaId = @event.SchemaId.Id; - - SimpleMapper.Map(@event, content); - - var idData = @event.Data?.ToIdModel(schema.SchemaDef, true); - - content.DataText = idData?.ToFullText(); - content.IdData = idData; - content.ReferencedIds = idData?.ToReferencedIds(schema.SchemaDef); - }); - }); - } - - protected Task On(ContentUpdated @event, EnvelopeHeaders headers) - { - return ForSchemaAsync(@event.AppId, @event.SchemaId.Id, (collection, schema) => - { - var idData = @event.Data?.ToIdModel(schema.SchemaDef, true); - - return collection.UpdateOneAsync( - Filter.Eq(x => x.Id, @event.ContentId), - Update - .Set(x => x.DataText, idData.ToFullText()) - .Set(x => x.IdData, idData) - .Set(x => x.ReferencedIds, idData.ToReferencedIds(schema.SchemaDef)) - .Set(x => x.LastModified, headers.Timestamp()) - .Set(x => x.LastModifiedBy, @event.Actor) - .Set(x => x.Version, headers.EventStreamNumber())); - }); - } - - protected Task On(ContentStatusChanged @event, EnvelopeHeaders headers) - { - return ForAppIdAsync(@event.AppId.Id, collection => - { - return collection.UpdateOneAsync( - Filter.Eq(x => x.Id, @event.ContentId), - Update - .Set(x => x.Status, @event.Status) - .Set(x => x.LastModified, headers.Timestamp()) - .Set(x => x.LastModifiedBy, @event.Actor) - .Set(x => x.Version, headers.EventStreamNumber())); - }); - } - - protected Task On(AssetDeleted @event, EnvelopeHeaders headers) - { - return ForAppIdAsync(@event.AppId.Id, collection => - { - return collection.UpdateManyAsync( - Filter.And( - Filter.AnyEq(x => x.ReferencedIds, @event.AssetId), - Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.AssetId)), - Update.AddToSet(x => x.ReferencedIdsDeleted, @event.AssetId)); - }); - } - - protected Task On(ContentDeleted @event, EnvelopeHeaders headers) - { - return ForAppIdAsync(@event.AppId.Id, async collection => - { - await collection.UpdateManyAsync( - Filter.And( - Filter.AnyEq(x => x.ReferencedIds, @event.ContentId), - Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)), - Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId)); - - await collection.DeleteOneAsync(x => x.Id == @event.ContentId); - }); - } - - private Task ForAppIdAsync(Guid appId, Func, Task> action) - { - var collection = GetCollection(appId); - - return action(collection); - } - - private IMongoCollection GetCollection(Guid appId) - { - var name = $"{Prefix}{appId}"; - - return database.GetCollection(name); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 0eaa6c56e..f29c431a0 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -48,11 +48,7 @@ namespace Squidex.Infrastructure.States { try { - await Collection.UpdateOneAsync( - Filter.And( - Filter.Eq(x => x.Id, key), - Filter.Eq(x => x.Version, oldVersion) - ), + await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.Doc, value) .Set(x => x.Version, newVersion), diff --git a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs index 10f17b85b..ba32106a4 100644 --- a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs @@ -10,11 +10,13 @@ using System; using System.Threading.Tasks; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.Commands { public sealed class AggregateHandler : IAggregateHandler { + private readonly AsyncLockPool lockPool = new AsyncLockPool(10000); private readonly IStateFactory stateFactory; private readonly ISemanticLog log; private readonly IServiceProvider serviceProvider; @@ -45,13 +47,27 @@ namespace Squidex.Infrastructure.Commands return InvokeAsync(context, updater, true); } + public Task CreateSyncedAsync(CommandContext context, Func creator) where T : class, IDomainObject + { + Guard.NotNull(creator, nameof(creator)); + + return InvokeSyncedAsync(context, creator, false); + } + + public Task UpdateSyncedAsync(CommandContext context, Func updater) where T : class, IDomainObject + { + Guard.NotNull(updater, nameof(updater)); + + return InvokeSyncedAsync(context, updater, true); + } + private async Task InvokeAsync(CommandContext context, Func handler, bool isUpdate) where T : class, IDomainObject { Guard.NotNull(context, nameof(context)); var domainObjectCommand = GetCommand(context); var domainObjectId = domainObjectCommand.AggregateId; - var domainObject = await stateFactory.GetDetachedAsync(domainObjectId.ToString()); + var domainObject = await stateFactory.CreateAsync(domainObjectId.ToString()); await domainObject.WriteAsync(log); @@ -70,6 +86,35 @@ namespace Squidex.Infrastructure.Commands return domainObject; } + private async Task InvokeSyncedAsync(CommandContext context, Func handler, bool isUpdate) where T : class, IDomainObject + { + Guard.NotNull(context, nameof(context)); + + var domainObjectCommand = GetCommand(context); + var domainObjectId = domainObjectCommand.AggregateId; + + using (await lockPool.LockAsync(Tuple.Create(typeof(T), domainObjectId))) + { + var domainObject = await stateFactory.GetSingleAsync(domainObjectId.ToString()); + + await domainObject.WriteAsync(log); + + if (!context.IsCompleted) + { + if (isUpdate) + { + context.Complete(new EntitySavedResult(domainObject.Version)); + } + else + { + context.Complete(EntityCreatedResult.Create(domainObjectId, domainObject.Version)); + } + } + + return domainObject; + } + } + private static IAggregateCommand GetCommand(CommandContext context) { if (!(context.Command is IAggregateCommand command)) diff --git a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs index 5eb115cd6..2c5d721e5 100644 --- a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs +++ b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs @@ -34,6 +34,26 @@ namespace Squidex.Infrastructure.Commands }); } + public static Task CreateSyncedAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IDomainObject + { + return handler.CreateSyncedAsync(context, x => + { + creator(x); + + return TaskHelper.Done; + }); + } + + public static Task UpdateSyncedAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IDomainObject + { + return handler.UpdateSyncedAsync(context, x => + { + updater(x); + + return TaskHelper.Done; + }); + } + public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context) { return commandMiddleware.HandleAsync(context, () => TaskHelper.Done); diff --git a/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs b/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs index c3a01f38d..d018ec4dd 100644 --- a/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs +++ b/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs @@ -15,6 +15,10 @@ namespace Squidex.Infrastructure.Commands { Task CreateAsync(CommandContext context, Func creator) where T : class, IDomainObject; + Task CreateSyncedAsync(CommandContext context, Func creator) where T : class, IDomainObject; + Task UpdateAsync(CommandContext context, Func updater) where T : class, IDomainObject; + + Task UpdateSyncedAsync(CommandContext context, Func updater) where T : class, IDomainObject; } } diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs index 80e0686a7..70ef98c40 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs @@ -39,7 +39,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains foreach (var consumer in consumers) { - var actor = factory.GetDetachedAsync(consumer.Name).Result; + var actor = factory.CreateAsync(consumer.Name).Result; actors[consumer.Name] = actor; actor.Activate(consumer); diff --git a/src/Squidex.Infrastructure/States/IStateFactory.cs b/src/Squidex.Infrastructure/States/IStateFactory.cs index ff5ab7018..b76180150 100644 --- a/src/Squidex.Infrastructure/States/IStateFactory.cs +++ b/src/Squidex.Infrastructure/States/IStateFactory.cs @@ -12,8 +12,8 @@ namespace Squidex.Infrastructure.States { public interface IStateFactory { - Task GetSynchronizedAsync(string key) where T : IStatefulObject; + Task GetSingleAsync(string key) where T : IStatefulObject; - Task GetDetachedAsync(string key) where T : IStatefulObject; + Task CreateAsync(string key) where T : IStatefulObject; } } diff --git a/src/Squidex.Infrastructure/States/StateFactory.cs b/src/Squidex.Infrastructure/States/StateFactory.cs index 4394b8f06..a2a9ce661 100644 --- a/src/Squidex.Infrastructure/States/StateFactory.cs +++ b/src/Squidex.Infrastructure/States/StateFactory.cs @@ -81,7 +81,7 @@ namespace Squidex.Infrastructure.States }); } - public async Task GetDetachedAsync(string key) where T : IStatefulObject + public async Task CreateAsync(string key) where T : IStatefulObject { Guard.NotNull(key, nameof(key)); @@ -93,7 +93,7 @@ namespace Squidex.Infrastructure.States return state; } - public Task GetSynchronizedAsync(string key) where T : IStatefulObject + public Task GetSingleAsync(string key) where T : IStatefulObject { Guard.NotNull(key, nameof(key)); diff --git a/src/Squidex.Infrastructure/Tasks/AsyncLock.cs b/src/Squidex.Infrastructure/Tasks/AsyncLock.cs new file mode 100644 index 000000000..ad5468ef7 --- /dev/null +++ b/src/Squidex.Infrastructure/Tasks/AsyncLock.cs @@ -0,0 +1,74 @@ +// ========================================================================== +// AsyncLock.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; + +#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLock + { + private readonly SemaphoreSlim semaphore; + + public AsyncLock() + { + semaphore = new SemaphoreSlim(1); + } + + public Task LockAsync() + { + Task wait = semaphore.WaitAsync(); + + if (wait.IsCompleted) + { + return Task.FromResult((IDisposable)new LockReleaser(this)); + } + else + { + return wait.ContinueWith(x => (IDisposable)new LockReleaser(this), + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + } + + private class LockReleaser : IDisposable + { + private AsyncLock target; + + internal LockReleaser(AsyncLock target) + { + this.target = target; + } + + public void Dispose() + { + AsyncLock current = target; + + if (current == null) + { + return; + } + + target = null; + + try + { + current.semaphore.Release(); + } + catch + { + // just ignore the Exception + } + } + } + } +} diff --git a/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs b/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs new file mode 100644 index 000000000..6701782b2 --- /dev/null +++ b/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// AsyncLockPool.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLockPool + { + private readonly AsyncLock[] locks; + + public AsyncLockPool(int poolSize) + { + Guard.GreaterThan(poolSize, 0, nameof(poolSize)); + + locks = new AsyncLock[poolSize]; + + for (var i = 0; i < poolSize; i++) + { + locks[i] = new AsyncLock(); + } + } + + public Task LockAsync(object target) + { + Guard.NotNull(target, nameof(target)); + + return locks[Math.Abs(target.GetHashCode() % locks.Length)].LockAsync(); + } + } +}