From 9c15648fd37ddd1885235abae56927a9af0ac485 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 22:30:37 +0100 Subject: [PATCH] Generic methods improved. --- .../Apps/MongoAppEntity.cs | 3 +- .../Apps/MongoAppRepository.cs | 15 +- .../Assets/MongoAssetEntity.cs | 3 +- .../Assets/MongoAssetRepository.cs | 10 +- .../Contents/MongoContentRepository.cs | 12 +- .../Rules/MongoRuleEntity.cs | 2 +- .../Rules/MongoRuleRepository.cs | 6 +- .../Schemas/MongoSchemaEntity.cs | 2 +- .../Schemas/MongoSchemaRepository.cs | 6 +- .../AppProvider.cs | 16 +- .../Apps/AppDomainObject.cs | 2 +- .../Assets/AssetDomainObject.cs | 2 +- .../Contents/ContentDomainObject.cs | 2 +- .../Rules/RuleDomainObject.cs | 2 +- .../Schemas/SchemaDomainObject.cs | 2 +- .../States/MongoSnapshotStore.cs | 12 +- .../States/MongoState.cs | 4 +- .../Commands/AggregateHandler.cs | 4 +- .../Commands/CommandExtensions.cs | 28 +-- .../Commands/DomainObjectBase.cs | 18 +- .../Commands/IDomainObject.cs | 3 +- .../Grains/EventConsumerGrain.cs | 6 +- .../States/IPersistence.cs | 4 + .../States/ISnapshotStore.cs | 6 +- .../States/IStateFactory.cs | 13 +- .../States/IStatefulObject.cs | 4 +- src/Squidex.Infrastructure/States/IStore.cs | 8 +- .../States/Persistence.cs | 210 +--------------- .../States/Persistence{TOwner,TState,TKey}.cs | 232 ++++++++++++++++++ .../States/StateFactory.cs | 38 ++- src/Squidex.Infrastructure/States/Store.cs | 26 +- .../States/StoreExtensions.cs | 32 +-- .../Tasks/TaskExtensions.cs | 11 + src/Squidex/Config/Domain/StoreServices.cs | 15 +- .../TestHelpers/HandlerTestBase.cs | 4 +- .../Commands/AggregateHandlerTests.cs | 10 +- .../Commands/DomainObjectBaseTests.cs | 8 +- .../Grains/EventConsumerGrainTests.cs | 4 +- .../States/StateEventSourcingTests.cs | 18 +- .../States/StateSnapshotTests.cs | 38 +-- .../TestHelpers/MyDomainObject.cs | 2 +- 41 files changed, 437 insertions(+), 406 deletions(-) create mode 100644 src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs index bee8118bb..e5b359397 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using Squidex.Domain.Apps.Entities.Apps.State; @@ -18,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps [BsonId] [BsonElement] [BsonRepresentation(BsonType.String)] - public string Id { get; set; } + public Guid Id { get; set; } [BsonElement] [BsonRequired] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index 1c65bb489..77fec730e 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Apps { - public sealed class MongoAppRepository : MongoRepositoryBase, IAppRepository, ISnapshotStore + public sealed class MongoAppRepository : MongoRepositoryBase, IAppRepository, ISnapshotStore { public MongoAppRepository(IMongoDatabase database) : base(database) @@ -55,16 +55,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return appEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); } - 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<(AppState Value, long Version)> ReadAsync(string key) + public async Task<(AppState Value, long Version)> ReadAsync(Guid key) { var existing = await Collection.Find(x => x.Id == key) @@ -78,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return (null, EtagVersion.NotFound); } - public async Task WriteAsync(string key, AppState value, long oldVersion, long newVersion) + public async Task WriteAsync(Guid key, AppState value, long oldVersion, long newVersion) { try { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs index fb6af615b..923669eb0 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using Squidex.Domain.Apps.Entities.Assets.State; @@ -17,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets [BsonId] [BsonElement] [BsonRepresentation(BsonType.String)] - public string Id { get; set; } + public Guid Id { get; set; } [BsonElement] [BsonRequired] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index a470fe74b..440d82e3f 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -21,7 +21,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { - public sealed class MongoAssetRepository : MongoRepositoryBase, IAssetRepository, ISnapshotStore + public sealed class MongoAssetRepository : MongoRepositoryBase, IAssetRepository, ISnapshotStore { public MongoAssetRepository(IMongoDatabase database) : base(database) @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets .Descending(x => x.State.LastModified)); } - public async Task<(AssetState Value, long Version)> ReadAsync(string key) + public async Task<(AssetState Value, long Version)> ReadAsync(Guid key) { var existing = await Collection.Find(x => x.Id == key) @@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets public async Task FindAssetAsync(Guid id) { - var (state, etag) = await ReadAsync(id.ToString()); + var (state, etag) = await ReadAsync(id); return state; } @@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets if (ids != null && ids.Count > 0) { - filters.Add(Filter.In(x => x.Id, ids.Select(x => x.ToString()))); + filters.Add(Filter.In(x => x.Id, ids)); } if (mimeTypes != null && mimeTypes.Count > 0) @@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets return filter; } - public async Task WriteAsync(string key, AssetState value, long oldVersion, long newVersion) + public async Task WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion) { try { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index b2e894f4f..ca185a3c0 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents public partial class MongoContentRepository : MongoRepositoryBase, IEventConsumer, IContentRepository, - ISnapshotStore + ISnapshotStore { private readonly IAppProvider appProvider; @@ -71,9 +71,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText)); } - public async Task WriteAsync(string key, ContentState value, long oldVersion, long newVersion) + public async Task WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion) { - var documentId = $"{key}_{oldVersion}"; + var documentId = $"{key}_{newVersion}"; var schema = await appProvider.GetSchemaAsync(value.AppId, value.SchemaId); @@ -119,12 +119,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public async Task<(ContentState Value, long Version)> ReadAsync(string key) + public async Task<(ContentState Value, long Version)> ReadAsync(Guid key) { - var id = Guid.Parse(key); - var contentEntity = - await Collection.Find(x => x.Id == id && x.IsLatest) + await Collection.Find(x => x.Id == key && x.IsLatest) .FirstOrDefaultAsync(); if (contentEntity != null) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs index c1ba83cbe..1eeba5a13 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules [BsonId] [BsonElement] [BsonRepresentation(BsonType.String)] - public string Id { get; set; } + public Guid Id { get; set; } [BsonElement] [BsonRequired] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs index fe6a69c05..4ad5b8bd8 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { - public sealed class MongoRuleRepository : MongoRepositoryBase, IRuleRepository, ISnapshotStore + public sealed class MongoRuleRepository : MongoRepositoryBase, IRuleRepository, ISnapshotStore { public MongoRuleRepository(IMongoDatabase database) : base(database) @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsDeleted)); } - public async Task<(RuleState Value, long Version)> ReadAsync(string key) + public async Task<(RuleState Value, long Version)> ReadAsync(Guid key) { var existing = await Collection.Find(x => x.Id == key) @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules return ruleEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); } - public async Task WriteAsync(string key, RuleState value, long oldVersion, long newVersion) + public async Task WriteAsync(Guid key, RuleState value, long oldVersion, long newVersion) { try { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs index 3efb1a83f..a6b75820c 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas [BsonId] [BsonElement] [BsonRepresentation(BsonType.String)] - public string Id { get; set; } + public Guid Id { get; set; } [BsonElement] [BsonRequired] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs index 23ccea311..5f373c18c 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas { - public sealed class MongoSchemaRepository : MongoRepositoryBase, ISchemaRepository, ISnapshotStore + public sealed class MongoSchemaRepository : MongoRepositoryBase, ISchemaRepository, ISnapshotStore { public MongoSchemaRepository(IMongoDatabase database) : base(database) @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Name)); } - public async Task<(SchemaState Value, long Version)> ReadAsync(string key) + public async Task<(SchemaState Value, long Version)> ReadAsync(Guid key) { var existing = await Collection.Find(x => x.Id == key) @@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas return schemaEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); } - public async Task WriteAsync(string key, SchemaState value, long oldVersion, long newVersion) + public async Task WriteAsync(Guid key, SchemaState value, long oldVersion, long newVersion) { try { diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index dd9fc35ee..0f35e958f 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -48,14 +48,14 @@ namespace Squidex.Domain.Apps.Entities public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) { - var app = await stateFactory.GetSingleAsync(appId.ToString()); + var app = await stateFactory.GetSingleAsync(appId); if (IsNotFound(app)) { return (null, null); } - var schema = await stateFactory.GetSingleAsync(id.ToString()); + var schema = await stateFactory.GetSingleAsync(id); return IsNotFound(false, schema) ? (null, null) : (app.State, schema.State); } @@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities { var appId = await GetAppIdAsync(appName); - var app = await stateFactory.GetSingleAsync(appId.ToString()); + var app = await stateFactory.GetSingleAsync(appId); return IsNotFound(app) ? null : app.State; } @@ -73,14 +73,14 @@ namespace Squidex.Domain.Apps.Entities { var schemaId = await GetSchemaIdAsync(appId, name); - var schema = await stateFactory.GetSingleAsync(schemaId.ToString()); + var schema = await stateFactory.GetSingleAsync(schemaId); return IsNotFound(provideDeleted, schema) ? null : schema.State; } public async Task GetSchemaAsync(Guid appId, Guid id, bool provideDeleted = false) { - var schema = await stateFactory.GetSingleAsync(id.ToString()); + var schema = await stateFactory.GetSingleAsync(id); return IsNotFound(provideDeleted, schema) ? null : schema.State; } @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities var schemas = await Task.WhenAll( - ids.Select(id => stateFactory.GetSingleAsync(id.ToString()))); + ids.Select(id => stateFactory.GetSingleAsync(id))); return schemas.Select(a => (ISchemaEntity)a.State).ToList(); } @@ -102,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities var rules = await Task.WhenAll( - ids.Select(id => stateFactory.GetSingleAsync(id.ToString()))); + ids.Select(id => stateFactory.GetSingleAsync(id))); return rules.Select(a => (IRuleEntity)a.State).ToList(); } @@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities var apps = await Task.WhenAll( - ids.Select(id => stateFactory.GetSingleAsync(id.ToString()))); + ids.Select(id => stateFactory.GetSingleAsync(id))); return apps.Select(a => (IAppEntity)a.State).ToList(); } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index d39102795..fc8ddec2e 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Apps { - public class AppDomainObject : DomainObjectBase + public class AppDomainObject : DomainObjectBase { public AppDomainObject Create(CreateApp command) { diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs index 52677b5c4..b9bd87fb4 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs @@ -16,7 +16,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetDomainObject : DomainObjectBase + public class AssetDomainObject : DomainObjectBase { public AssetDomainObject Create(CreateAsset command) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index 6284d11a7..fe4114a0e 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs @@ -17,7 +17,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Contents { - public class ContentDomainObject : DomainObjectBase + public class ContentDomainObject : DomainObjectBase { public ContentDomainObject Create(CreateContent command) { diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs index 6aa105077..7ce3be03d 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs @@ -16,7 +16,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Rules { - public class RuleDomainObject : DomainObjectBase + public class RuleDomainObject : DomainObjectBase { public void Create(CreateRule command) { diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs index 73becbd1b..15aa08588 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Schemas { - public class SchemaDomainObject : DomainObjectBase + public class SchemaDomainObject : DomainObjectBase { private readonly FieldRegistry registry; diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index c6f6cee6b..8628d75ec 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -13,7 +13,7 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.States { - public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore, IExternalSystem + public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore, IExternalSystem { private readonly JsonSerializer serializer; @@ -30,10 +30,10 @@ namespace Squidex.Infrastructure.States return $"States_{typeof(T).Name}"; } - public async Task<(T Value, long Version)> ReadAsync(string key) + public async Task<(T Value, long Version)> ReadAsync(TKey key) { var existing = - await Collection.Find(x => x.Id == key) + await Collection.Find(x => Equals(x.Id, key)) .FirstOrDefaultAsync(); if (existing != null) @@ -44,11 +44,11 @@ namespace Squidex.Infrastructure.States return (default(T), EtagVersion.NotFound); } - public async Task WriteAsync(string key, T value, long oldVersion, long newVersion) + public async Task WriteAsync(TKey key, T value, long oldVersion, long newVersion) { try { - await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, + await Collection.UpdateOneAsync(x => Equals(x.Id, key) && x.Version == oldVersion, Update .Set(x => x.Doc, value) .Set(x => x.Version, newVersion), @@ -59,7 +59,7 @@ namespace Squidex.Infrastructure.States if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = - await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version) + await Collection.Find(x => Equals(x.Id, key)).Only(x => x.Id, x => x.Version) .FirstOrDefaultAsync(); if (existingVersion != null) diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs index 448bb45f6..6f73b1043 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs @@ -12,12 +12,12 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.States { - public sealed class MongoState + public sealed class MongoState { [BsonId] [BsonElement] [BsonRepresentation(BsonType.String)] - public string Id { get; set; } + public TKey Id { get; set; } [BsonRequired] [BsonElement] diff --git a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs index c51c651bf..cdfb05cd6 100644 --- a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs @@ -67,7 +67,7 @@ namespace Squidex.Infrastructure.Commands var domainCommand = GetCommand(context); var domainObjectId = domainCommand.AggregateId; - var domainObject = await stateFactory.CreateAsync(domainObjectId.ToString()); + var domainObject = await stateFactory.CreateAsync(domainObjectId); if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version) { @@ -102,7 +102,7 @@ namespace Squidex.Infrastructure.Commands using (await lockPool.LockAsync(Tuple.Create(typeof(T), domainObjectId))) { - var domainObject = await stateFactory.GetSingleAsync(domainObjectId.ToString()); + var domainObject = await stateFactory.GetSingleAsync(domainObjectId); if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version) { diff --git a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs index 2c5d721e5..f6d174866 100644 --- a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs +++ b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs @@ -16,42 +16,22 @@ namespace Squidex.Infrastructure.Commands { public static Task CreateAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IDomainObject { - return handler.CreateAsync(context, x => - { - creator(x); - - return TaskHelper.Done; - }); + return handler.CreateAsync(context, creator.ToAsync()); } public static Task UpdateAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IDomainObject { - return handler.UpdateAsync(context, x => - { - updater(x); - - return TaskHelper.Done; - }); + return handler.UpdateAsync(context, updater.ToAsync()); } 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; - }); + return handler.CreateSyncedAsync(context, creator.ToAsync()); } 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; - }); + return handler.UpdateSyncedAsync(context, updater.ToAsync()); } public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context) diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index c5800711f..b8351396c 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -15,26 +15,26 @@ using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObjectBase : IDomainObject where TState : IDomainState, new() + public abstract class DomainObjectBase : IDomainObject where T : IDomainState, new() { private readonly List> uncomittedEvents = new List>(); private Guid id; - private TState state; - private IPersistence persistence; + private T state; + private IPersistence persistence; public long Version { get { return state.Version; } } - public TState State + public T State { get { return state; } } protected DomainObjectBase() { - state = new TState(); + state = new T(); state.Version = EtagVersion.Empty; } @@ -48,11 +48,11 @@ namespace Squidex.Infrastructure.Commands uncomittedEvents.Clear(); } - public Task ActivateAsync(string key, IStore store) + public Task ActivateAsync(Guid key, IStore store) { - id = Guid.Parse(key); + id = key; - persistence = store.WithSnapshots(key, s => state = s); + persistence = store.WithSnapshots(key, s => state = s); return persistence.ReadAsync(); } @@ -73,7 +73,7 @@ namespace Squidex.Infrastructure.Commands uncomittedEvents.Add(@event.To()); } - public void UpdateState(TState newState) + public void UpdateState(T newState) { state = newState; } diff --git a/src/Squidex.Infrastructure/Commands/IDomainObject.cs b/src/Squidex.Infrastructure/Commands/IDomainObject.cs index f1df5c41e..5877dd2e1 100644 --- a/src/Squidex.Infrastructure/Commands/IDomainObject.cs +++ b/src/Squidex.Infrastructure/Commands/IDomainObject.cs @@ -6,13 +6,14 @@ // All rights reserved. // ========================================================================== +using System; using System.Threading.Tasks; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands { - public interface IDomainObject : IStatefulObject + public interface IDomainObject : IStatefulObject { long Version { get; } diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs index eff3b2f72..ef4cd9978 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.EventSourcing.Grains { - public class EventConsumerGrain : DisposableObjectBase, IStatefulObject, IEventSubscriber + public class EventConsumerGrain : DisposableObjectBase, IStatefulObject, IEventSubscriber { private readonly IEventDataFormatter eventDataFormatter; private readonly IEventStore eventStore; @@ -49,9 +49,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } } - public Task ActivateAsync(string key, IStore store) + public Task ActivateAsync(string key, IStore store) { - persistence = store.WithSnapshots(key, s => state = s); + persistence = store.WithSnapshots(key, s => state = s); return persistence.ReadAsync(); } diff --git a/src/Squidex.Infrastructure/States/IPersistence.cs b/src/Squidex.Infrastructure/States/IPersistence.cs index 462cf5cc3..8e9e24ba2 100644 --- a/src/Squidex.Infrastructure/States/IPersistence.cs +++ b/src/Squidex.Infrastructure/States/IPersistence.cs @@ -12,6 +12,10 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { + public interface IPersistence : IPersistence + { + } + public interface IPersistence { long Version { get; } diff --git a/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/src/Squidex.Infrastructure/States/ISnapshotStore.cs index 38d62d737..094b375ad 100644 --- a/src/Squidex.Infrastructure/States/ISnapshotStore.cs +++ b/src/Squidex.Infrastructure/States/ISnapshotStore.cs @@ -10,10 +10,10 @@ using System.Threading.Tasks; namespace Squidex.Infrastructure.States { - public interface ISnapshotStore + public interface ISnapshotStore { - Task WriteAsync(string key, T value, long oldVersion, long newVersion); + Task WriteAsync(TKey key, T value, long oldVersion, long newVersion); - Task<(T Value, long Version)> ReadAsync(string key); + Task<(T Value, long Version)> ReadAsync(TKey key); } } diff --git a/src/Squidex.Infrastructure/States/IStateFactory.cs b/src/Squidex.Infrastructure/States/IStateFactory.cs index b76180150..68a99e1a2 100644 --- a/src/Squidex.Infrastructure/States/IStateFactory.cs +++ b/src/Squidex.Infrastructure/States/IStateFactory.cs @@ -6,14 +6,23 @@ // All rights reserved. // ========================================================================== +using System; using System.Threading.Tasks; namespace Squidex.Infrastructure.States { public interface IStateFactory { - Task GetSingleAsync(string key) where T : IStatefulObject; + Task GetSingleAsync(string key) where T : IStatefulObject; - Task CreateAsync(string key) where T : IStatefulObject; + Task GetSingleAsync(Guid key) where T : IStatefulObject; + + Task GetSingleAsync(TKey key) where T : IStatefulObject; + + Task CreateAsync(string key) where T : IStatefulObject; + + Task CreateAsync(Guid key) where T : IStatefulObject; + + Task CreateAsync(TKey key) where T : IStatefulObject; } } diff --git a/src/Squidex.Infrastructure/States/IStatefulObject.cs b/src/Squidex.Infrastructure/States/IStatefulObject.cs index 0c1cbd9cd..b033bb435 100644 --- a/src/Squidex.Infrastructure/States/IStatefulObject.cs +++ b/src/Squidex.Infrastructure/States/IStatefulObject.cs @@ -10,8 +10,8 @@ using System.Threading.Tasks; namespace Squidex.Infrastructure.States { - public interface IStatefulObject + public interface IStatefulObject { - Task ActivateAsync(string key, IStore store); + Task ActivateAsync(TKey key, IStore store); } } diff --git a/src/Squidex.Infrastructure/States/IStore.cs b/src/Squidex.Infrastructure/States/IStore.cs index 72017044c..ab9feb98f 100644 --- a/src/Squidex.Infrastructure/States/IStore.cs +++ b/src/Squidex.Infrastructure/States/IStore.cs @@ -12,12 +12,12 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - public interface IStore + public interface IStore { - IPersistence WithEventSourcing(string key, Func, Task> applyEvent); + IPersistence WithEventSourcing(TKey key, Func, Task> applyEvent); - IPersistence WithSnapshots(string key, Func applySnapshot); + IPersistence WithSnapshots(TKey key, Func applySnapshot); - IPersistence WithSnapshotsAndEventSourcing(string key, Func applySnapshot, Func, Task> applyEvent); + IPersistence WithSnapshotsAndEventSourcing(TKey key, Func applySnapshot, Func, Task> applyEvent); } } diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index 4e633c967..7a6738d92 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -7,8 +7,6 @@ // ========================================================================== using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; @@ -16,217 +14,17 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - internal sealed class Persistence : IPersistence + internal sealed class Persistence : Persistence, IPersistence { - private readonly string ownerKey; - private readonly ISnapshotStore snapshotStore; - private readonly IStreamNameResolver streamNameResolver; - private readonly IEventStore eventStore; - private readonly IEventDataFormatter eventDataFormatter; - private readonly PersistenceMode persistenceMode; - private readonly Action invalidate; - private readonly Func applyState; - private readonly Func, Task> applyEvent; - private long versionSnapshot = EtagVersion.Empty; - private long versionEvents = EtagVersion.Empty; - private long version; - - public long Version - { - get { return version; } - } - - public Persistence(string ownerKey, + public Persistence(TKey ownerKey, Action invalidate, IEventStore eventStore, IEventDataFormatter eventDataFormatter, - ISnapshotStore snapshotStore, + ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver, - PersistenceMode persistenceMode, - Func applyState, Func, Task> applyEvent) + : base(ownerKey, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) { - this.ownerKey = ownerKey; - this.applyState = applyState; - this.applyEvent = applyEvent; - this.invalidate = invalidate; - this.eventStore = eventStore; - this.eventDataFormatter = eventDataFormatter; - this.persistenceMode = persistenceMode; - this.snapshotStore = snapshotStore; - this.streamNameResolver = streamNameResolver; - } - - public async Task ReadAsync(long expectedVersion = EtagVersion.Any) - { - versionSnapshot = EtagVersion.Empty; - versionEvents = EtagVersion.Empty; - - await ReadSnapshotAsync(); - await ReadEventsAsync(); - - UpdateVersion(); - - if (expectedVersion != EtagVersion.Any && expectedVersion != version) - { - if (version == EtagVersion.Empty) - { - throw new DomainObjectNotFoundException(ownerKey, typeof(TOwner)); - } - else - { - throw new DomainObjectVersionException(ownerKey, typeof(TOwner), version, expectedVersion); - } - } - } - - private async Task ReadSnapshotAsync() - { - if (UseSnapshots()) - { - var (state, position) = await snapshotStore.ReadAsync(ownerKey); - - if (position < EtagVersion.Empty) - { - position = EtagVersion.Empty; - } - - versionSnapshot = position; - versionEvents = position; - - if (applyState != null && position >= 0) - { - await applyState(state); - } - } - } - - private async Task ReadEventsAsync() - { - if (UseEventSourcing()) - { - var events = await eventStore.GetEventsAsync(GetStreamName(), versionEvents + 1); - - foreach (var @event in events) - { - versionEvents++; - - if (@event.EventStreamNumber != versionEvents) - { - throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); - } - - var parsedEvent = ParseKnownEvent(@event); - - if (parsedEvent != null && applyEvent != null) - { - await applyEvent(parsedEvent); - } - } - } - } - - public async Task WriteSnapshotAsync(TState state) - { - var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; - - if (newVersion != versionSnapshot) - { - try - { - await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); - } - catch (InconsistentStateException ex) - { - throw new DomainObjectVersionException(ownerKey, typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); - } - - versionSnapshot = newVersion; - } - - UpdateVersion(); - - invalidate?.Invoke(); - } - - public async Task WriteEventsAsync(IEnumerable> events) - { - Guard.NotNull(events, nameof(@events)); - - var eventArray = events.ToArray(); - - if (eventArray.Length > 0) - { - var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; - - var commitId = Guid.NewGuid(); - - var eventStream = GetStreamName(); - var eventData = GetEventData(eventArray, commitId); - - try - { - await eventStore.AppendEventsAsync(commitId, GetStreamName(), expectedVersion, eventData); - } - catch (WrongEventVersionException ex) - { - throw new DomainObjectVersionException(ownerKey, typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); - } - - versionEvents += eventArray.Length; - } - - UpdateVersion(); - - invalidate?.Invoke(); - } - - private EventData[] GetEventData(Envelope[] events, Guid commitId) - { - return @events.Select(x => eventDataFormatter.ToEventData(x, commitId, true)).ToArray(); - } - - private string GetStreamName() - { - return streamNameResolver.GetStreamName(typeof(TOwner), ownerKey); - } - - private bool UseSnapshots() - { - return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; - } - - private bool UseEventSourcing() - { - return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; - } - - private Envelope ParseKnownEvent(StoredEvent storedEvent) - { - try - { - return eventDataFormatter.Parse(storedEvent.Data); - } - catch (TypeNameNotFoundException) - { - return null; - } - } - - private void UpdateVersion() - { - if (persistenceMode == PersistenceMode.Snapshots) - { - version = versionSnapshot; - } - else if (persistenceMode == PersistenceMode.EventSourcing) - { - version = versionEvents; - } - else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing) - { - version = Math.Max(versionEvents, versionSnapshot); - } } } } diff --git a/src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs b/src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs new file mode 100644 index 000000000..ccc80d3aa --- /dev/null +++ b/src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs @@ -0,0 +1,232 @@ +// ========================================================================== +// Persistence.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 Squidex.Infrastructure.EventSourcing; + +#pragma warning disable RECS0012 // 'if' statement can be re-written as 'switch' statement + +namespace Squidex.Infrastructure.States +{ + internal class Persistence : IPersistence + { + private readonly TKey ownerKey; + private readonly ISnapshotStore snapshotStore; + private readonly IStreamNameResolver streamNameResolver; + private readonly IEventStore eventStore; + private readonly IEventDataFormatter eventDataFormatter; + private readonly PersistenceMode persistenceMode; + private readonly Action invalidate; + private readonly Func applyState; + private readonly Func, Task> applyEvent; + private long versionSnapshot = EtagVersion.Empty; + private long versionEvents = EtagVersion.Empty; + private long version; + + public long Version + { + get { return version; } + } + + public Persistence(TKey ownerKey, + Action invalidate, + IEventStore eventStore, + IEventDataFormatter eventDataFormatter, + ISnapshotStore snapshotStore, + IStreamNameResolver streamNameResolver, + PersistenceMode persistenceMode, + Func applyState, + Func, Task> applyEvent) + { + this.ownerKey = ownerKey; + this.applyState = applyState; + this.applyEvent = applyEvent; + this.invalidate = invalidate; + this.eventStore = eventStore; + this.eventDataFormatter = eventDataFormatter; + this.persistenceMode = persistenceMode; + this.snapshotStore = snapshotStore; + this.streamNameResolver = streamNameResolver; + } + + public async Task ReadAsync(long expectedVersion = EtagVersion.Any) + { + versionSnapshot = EtagVersion.Empty; + versionEvents = EtagVersion.Empty; + + await ReadSnapshotAsync(); + await ReadEventsAsync(); + + UpdateVersion(); + + if (expectedVersion != EtagVersion.Any && expectedVersion != version) + { + if (version == EtagVersion.Empty) + { + throw new DomainObjectNotFoundException(ownerKey.ToString(), typeof(TOwner)); + } + else + { + throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), version, expectedVersion); + } + } + } + + private async Task ReadSnapshotAsync() + { + if (UseSnapshots()) + { + var (state, position) = await snapshotStore.ReadAsync(ownerKey); + + if (position < EtagVersion.Empty) + { + position = EtagVersion.Empty; + } + + versionSnapshot = position; + versionEvents = position; + + if (applyState != null && position >= 0) + { + await applyState(state); + } + } + } + + private async Task ReadEventsAsync() + { + if (UseEventSourcing()) + { + var events = await eventStore.GetEventsAsync(GetStreamName(), versionEvents + 1); + + foreach (var @event in events) + { + versionEvents++; + + if (@event.EventStreamNumber != versionEvents) + { + throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps."); + } + + var parsedEvent = ParseKnownEvent(@event); + + if (parsedEvent != null && applyEvent != null) + { + await applyEvent(parsedEvent); + } + } + } + } + + public async Task WriteSnapshotAsync(TState state) + { + var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1; + + if (newVersion != versionSnapshot) + { + try + { + await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion); + } + catch (InconsistentStateException ex) + { + throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); + } + + versionSnapshot = newVersion; + } + + UpdateVersion(); + + invalidate?.Invoke(); + } + + public async Task WriteEventsAsync(IEnumerable> events) + { + Guard.NotNull(events, nameof(@events)); + + var eventArray = events.ToArray(); + + if (eventArray.Length > 0) + { + var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; + + var commitId = Guid.NewGuid(); + + var eventStream = GetStreamName(); + var eventData = GetEventData(eventArray, commitId); + + try + { + await eventStore.AppendEventsAsync(commitId, GetStreamName(), expectedVersion, eventData); + } + catch (WrongEventVersionException ex) + { + throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); + } + + versionEvents += eventArray.Length; + } + + UpdateVersion(); + + invalidate?.Invoke(); + } + + private EventData[] GetEventData(Envelope[] events, Guid commitId) + { + return @events.Select(x => eventDataFormatter.ToEventData(x, commitId, true)).ToArray(); + } + + private string GetStreamName() + { + return streamNameResolver.GetStreamName(typeof(TOwner), ownerKey.ToString()); + } + + private bool UseSnapshots() + { + return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + } + + private bool UseEventSourcing() + { + return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing; + } + + private Envelope ParseKnownEvent(StoredEvent storedEvent) + { + try + { + return eventDataFormatter.Parse(storedEvent.Data); + } + catch (TypeNameNotFoundException) + { + return null; + } + } + + private void UpdateVersion() + { + if (persistenceMode == PersistenceMode.Snapshots) + { + version = versionSnapshot; + } + else if (persistenceMode == PersistenceMode.EventSourcing) + { + version = versionEvents; + } + else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing) + { + version = Math.Max(versionEvents, versionSnapshot); + } + } + } +} diff --git a/src/Squidex.Infrastructure/States/StateFactory.cs b/src/Squidex.Infrastructure/States/StateFactory.cs index a2a9ce661..9ac202d72 100644 --- a/src/Squidex.Infrastructure/States/StateFactory.cs +++ b/src/Squidex.Infrastructure/States/StateFactory.cs @@ -27,12 +27,12 @@ namespace Squidex.Infrastructure.States private readonly object lockObject = new object(); private IDisposable pubSubSubscription; - public sealed class ObjectHolder where T : IStatefulObject + public sealed class ObjectHolder where T : IStatefulObject { private readonly Task activationTask; private readonly T obj; - public ObjectHolder(T obj, string key, IStore store) + public ObjectHolder(T obj, TKey key, IStore store) { this.obj = obj; @@ -81,11 +81,21 @@ namespace Squidex.Infrastructure.States }); } - public async Task CreateAsync(string key) where T : IStatefulObject + public Task CreateAsync(string key) where T : IStatefulObject + { + return CreateAsync(key); + } + + public Task CreateAsync(Guid key) where T : IStatefulObject + { + return CreateAsync(key); + } + + public async Task CreateAsync(TKey key) where T : IStatefulObject { Guard.NotNull(key, nameof(key)); - var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver); + var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver); var state = (T)services.GetService(typeof(T)); await state.ActivateAsync(key, stateStore); @@ -93,25 +103,35 @@ namespace Squidex.Infrastructure.States return state; } - public Task GetSingleAsync(string key) where T : IStatefulObject + public Task GetSingleAsync(string key) where T : IStatefulObject + { + return GetSingleAsync(key); + } + + public Task GetSingleAsync(Guid key) where T : IStatefulObject + { + return GetSingleAsync(key); + } + + public Task GetSingleAsync(TKey key) where T : IStatefulObject { Guard.NotNull(key, nameof(key)); lock (lockObject) { - if (statesCache.TryGetValue>(key, out var stateObj)) + if (statesCache.TryGetValue>(key, out var stateObj)) { return stateObj.ActivateAsync(); } var state = (T)services.GetService(typeof(T)); - var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver, () => + var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver, () => { - pubSub.Publish(new InvalidateMessage { Key = key }, false); + pubSub.Publish(new InvalidateMessage { Key = key.ToString() }, false); }); - stateObj = new ObjectHolder(state, key, stateStore); + stateObj = new ObjectHolder(state, key, stateStore); statesCache.CreateEntry(key) .SetValue(stateObj) diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs index 578762dc5..e200a9257 100644 --- a/src/Squidex.Infrastructure/States/Store.cs +++ b/src/Squidex.Infrastructure/States/Store.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - public sealed class Store : IStore + internal sealed class Store : IStore { private readonly Action invalidate; private readonly IServiceProvider services; @@ -34,28 +34,32 @@ namespace Squidex.Infrastructure.States this.streamNameResolver = streamNameResolver; } - public IPersistence WithEventSourcing(string key, Func, Task> applyEvent) + public IPersistence WithSnapshots(TKey key, Func applySnapshot) { - return CreatePersistence(key, PersistenceMode.EventSourcing, null, applyEvent); + return CreatePersistence(key, PersistenceMode.Snapshots, applySnapshot, null); } - public IPersistence WithSnapshots(string key, Func applySnapshot) + public IPersistence WithSnapshotsAndEventSourcing(TKey key, Func applySnapshot, Func, Task> applyEvent) { - return CreatePersistence(key, PersistenceMode.Snapshots, applySnapshot, null); + return CreatePersistence(key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); } - public IPersistence WithSnapshotsAndEventSourcing(string key, Func applySnapshot, Func, Task> applyEvent) + public IPersistence WithEventSourcing(TKey key, Func, Task> applyEvent) { - return CreatePersistence(key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); + Guard.NotDefault(key, nameof(key)); + + var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); + + return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); } - private IPersistence CreatePersistence(string key, PersistenceMode mode, Func applySnapshot, Func, Task> applyEvent) + private IPersistence CreatePersistence(TKey key, PersistenceMode mode, Func applySnapshot, Func, Task> applyEvent) { - Guard.NotNullOrEmpty(key, nameof(key)); + Guard.NotDefault(key, nameof(key)); - var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); + var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); - return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); + return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); } } } diff --git a/src/Squidex.Infrastructure/States/StoreExtensions.cs b/src/Squidex.Infrastructure/States/StoreExtensions.cs index 9fd7a8945..53c2fa929 100644 --- a/src/Squidex.Infrastructure/States/StoreExtensions.cs +++ b/src/Squidex.Infrastructure/States/StoreExtensions.cs @@ -14,39 +14,19 @@ namespace Squidex.Infrastructure.States { public static class StoreExtensions { - public static IPersistence WithEventSourcing(this IStore store, string key, Action> applyEvent) + public static IPersistence WithEventSourcing(this IStore store, TKey key, Action> applyEvent) { - return store.WithEventSourcing(key, x => - { - applyEvent(x); - - return TaskHelper.Done; - }); + return store.WithEventSourcing(key, applyEvent.ToAsync()); } - public static IPersistence WithSnapshots(this IStore store, string key, Action applySnapshot) + public static IPersistence WithSnapshots(this IStore store, TKey key, Action applySnapshot) { - return store.WithSnapshots(key, x => - { - applySnapshot(x); - - return TaskHelper.Done; - }); + return store.WithSnapshots(key, applySnapshot.ToAsync()); } - public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, string key, Action applySnapshot, Action> applyEvent) + public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, TKey key, Action applySnapshot, Action> applyEvent) { - return store.WithSnapshotsAndEventSourcing(key, x => - { - applySnapshot(x); - - return TaskHelper.Done; - }, x => - { - applyEvent(x); - - return TaskHelper.Done; - }); + return store.WithSnapshotsAndEventSourcing(key, applySnapshot.ToAsync(), applyEvent.ToAsync()); } } } diff --git a/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs b/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs index 944e6db4c..e8cb83234 100644 --- a/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs +++ b/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Threading.Tasks; namespace Squidex.Infrastructure.Tasks @@ -15,5 +16,15 @@ namespace Squidex.Infrastructure.Tasks public static void Forget(this Task task) { } + + public static Func ToAsync(this Action action) + { + return x => + { + action(x); + + return TaskHelper.Done; + }; + } } } diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index 06a5257c7..6f743c789 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using IdentityServer4.Stores; using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.Identity; @@ -64,8 +65,8 @@ namespace Squidex.Config.Domain .As() .As(); - services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) - .As>() + services.AddSingletonAs(c => new MongoSnapshotStore(mongoDatabase, c.GetRequiredService())) + .As>() .As(); services.AddSingletonAs(c => new MongoUserStore(mongoDatabase)) @@ -93,27 +94,27 @@ namespace Squidex.Config.Domain services.AddSingletonAs(c => new MongoAppRepository(mongoDatabase)) .As() - .As>() + .As>() .As(); services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase)) .As() - .As>() + .As>() .As(); services.AddSingletonAs(c => new MongoRuleRepository(mongoContentDatabase)) .As() - .As>() + .As>() .As(); services.AddSingletonAs(c => new MongoSchemaRepository(mongoDatabase)) .As() - .As>() + .As>() .As(); services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService())) .As() - .As>() + .As>() .As() .As(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs index 80e4db5e7..b3b48dc28 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs @@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers { handler.Init(domainObject); - await domainObject.ActivateAsync(Id.ToString(), A.Fake()); + await domainObject.ActivateAsync(Id, A.Fake>()); await action(domainObject); if (!handler.IsCreated && shouldCreate) @@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers { handler.Init(domainObject); - await domainObject.ActivateAsync(Id.ToString(), A.Fake()); + await domainObject.ActivateAsync(Id, A.Fake>()); await action(domainObject); if (!handler.IsUpdated && shouldUpdate) diff --git a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs index 943e02bd8..805d24005 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs @@ -23,7 +23,7 @@ namespace Squidex.Infrastructure.Commands { private readonly ISemanticLog log = A.Fake(); private readonly IServiceProvider serviceProvider = A.Fake(); - private readonly IStore store = A.Fake(); + private readonly IStore store = A.Fake>(); private readonly IStateFactory stateFactory = A.Fake(); private readonly IPersistence persistence = A.Fake>(); private readonly Envelope event1 = new Envelope(new MyEvent()); @@ -40,18 +40,18 @@ namespace Squidex.Infrastructure.Commands command = new MyCommand { AggregateId = domainObjectId, ExpectedVersion = EtagVersion.Any }; context = new CommandContext(command); - A.CallTo(() => store.WithSnapshots(domainObjectId.ToString(), A>.Ignored)) + A.CallTo(() => store.WithSnapshots(domainObjectId, A>.Ignored)) .Returns(persistence); - A.CallTo(() => stateFactory.CreateAsync(domainObjectId.ToString())) + A.CallTo(() => stateFactory.CreateAsync(domainObjectId)) .Returns(Task.FromResult(domainObject)); - A.CallTo(() => stateFactory.GetSingleAsync(domainObjectId.ToString())) + A.CallTo(() => stateFactory.GetSingleAsync(domainObjectId)) .Returns(Task.FromResult(domainObject)); sut = new AggregateHandler(stateFactory, serviceProvider, log); - domainObject.ActivateAsync(domainObjectId.ToString(), store).Wait(); + domainObject.ActivateAsync(domainObjectId, store).Wait(); } [Fact] diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs index 539cd839f..9d6478232 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs @@ -21,14 +21,14 @@ namespace Squidex.Infrastructure.Commands { public class DomainObjectBaseTests { - private readonly IStore store = A.Fake(); + private readonly IStore store = A.Fake>(); private readonly IPersistence persistence = A.Fake>(); private readonly Guid id = Guid.NewGuid(); private readonly MyDomainObject sut = new MyDomainObject(); public DomainObjectBaseTests() { - A.CallTo(() => store.WithSnapshots(id.ToString(), A>.Ignored)) + A.CallTo(() => store.WithSnapshots(id, A>.Ignored)) .Returns(persistence); } @@ -58,7 +58,7 @@ namespace Squidex.Infrastructure.Commands [Fact] public async Task Should_write_state_and_events_when_saved() { - await sut.ActivateAsync(id.ToString(), store); + await sut.ActivateAsync(id, store); var event1 = new MyEvent(); var event2 = new MyEvent(); @@ -84,7 +84,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) .Throws(new InvalidOperationException()); - await sut.ActivateAsync(id.ToString(), store); + await sut.ActivateAsync(id, store); var event1 = new MyEvent(); var event2 = new MyEvent(); diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs index 0fa7113b7..55a908e72 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -38,7 +38,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains private readonly IEventSubscription eventSubscription = A.Fake(); private readonly IPersistence persistence = A.Fake>(); private readonly ISemanticLog log = A.Fake(); - private readonly IStore store = A.Fake(); + private readonly IStore store = A.Fake>(); private readonly IEventDataFormatter formatter = A.Fake(); private readonly EventData eventData = new EventData(); private readonly Envelope envelope = new Envelope(new MyEvent()); @@ -54,7 +54,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains consumerName = eventConsumer.GetType().Name; - A.CallTo(() => store.WithSnapshots(consumerName, A>.Ignored)) + A.CallTo(() => store.WithSnapshots(consumerName, A>.Ignored)) .Invokes(new Action>((key, a) => apply = a)) .Returns(persistence); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs index 0ed94cac8..eae17945f 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs @@ -22,10 +22,10 @@ namespace Squidex.Infrastructure.States { public class StateEventSourcingTests { - private class MyStatefulObject : IStatefulObject + private class MyStatefulObject : IStatefulObject { private readonly List appliedEvents = new List(); - private IPersistence persistence; + private IPersistence persistence; public long ExpectedVersion { get; set; } @@ -34,9 +34,9 @@ namespace Squidex.Infrastructure.States get { return appliedEvents; } } - public Task ActivateAsync(string key, IStore store) + public Task ActivateAsync(string key, IStore store) { - persistence = store.WithEventSourcing(key, e => appliedEvents.Add(e.Payload)); + persistence = store.WithEventSourcing(key, e => appliedEvents.Add(e.Payload)); return persistence.ReadAsync(ExpectedVersion); } @@ -47,15 +47,15 @@ namespace Squidex.Infrastructure.States } } - private class MyStatefulObjectWithSnapshot : IStatefulObject + private class MyStatefulObjectWithSnapshot : IStatefulObject { private IPersistence persistence; public long ExpectedVersion { get; set; } - public Task ActivateAsync(string key, IStore store) + public Task ActivateAsync(string key, IStore store) { - persistence = store.WithSnapshotsAndEventSourcing(key, s => TaskHelper.Done, s => TaskHelper.Done); + persistence = store.WithSnapshotsAndEventSourcing(key, s => TaskHelper.Done, s => TaskHelper.Done); return persistence.ReadAsync(ExpectedVersion); } @@ -69,7 +69,7 @@ namespace Squidex.Infrastructure.States private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IPubSub pubSub = new InMemoryPubSub(true); private readonly IServiceProvider services = A.Fake(); - private readonly ISnapshotStore snapshotStore = A.Fake>(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly StateFactory sut; @@ -79,7 +79,7 @@ namespace Squidex.Infrastructure.States .Returns(statefulObject); A.CallTo(() => services.GetService(typeof(MyStatefulObjectWithSnapshot))) .Returns(statefulObjectWithSnapShot); - A.CallTo(() => services.GetService(typeof(ISnapshotStore))) + A.CallTo(() => services.GetService(typeof(ISnapshotStore))) .Returns(snapshotStore); A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObject), key)) diff --git a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs index 2465e5e53..f003c6ef6 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs @@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.States { public class StateSnapshotTests : IDisposable { - private class MyStatefulObject : IStatefulObject + private class MyStatefulObject : IStatefulObject { private IPersistence persistence; private int state; @@ -38,9 +38,9 @@ namespace Squidex.Infrastructure.States get { return state; } } - public Task ActivateAsync(string key, IStore store) + public Task ActivateAsync(string key, IStore store) { - persistence = store.WithSnapshots(key, s => state = s); + persistence = store.WithSnapshots(key, s => state = s); return persistence.ReadAsync(ExpectedVersion); } @@ -63,7 +63,7 @@ namespace Squidex.Infrastructure.States private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IPubSub pubSub = new InMemoryPubSub(true); private readonly IServiceProvider services = A.Fake(); - private readonly ISnapshotStore snapshotStore = A.Fake>(); + private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly StateFactory sut; @@ -71,7 +71,7 @@ namespace Squidex.Infrastructure.States { A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .Returns(statefulObject); - A.CallTo(() => services.GetService(typeof(ISnapshotStore))) + A.CallTo(() => services.GetService(typeof(ISnapshotStore))) .Returns(snapshotStore); sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver); @@ -91,7 +91,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 1)); - var actualObject = await sut.GetSingleAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); @@ -107,7 +107,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, EtagVersion.NotFound)); - var actualObject = await sut.GetSingleAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Equal(-1, statefulObject.Version); Assert.Equal( 0, statefulObject.State); @@ -121,7 +121,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, EtagVersion.Empty)); - await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -132,7 +132,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2)); - await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); + await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } [Fact] @@ -143,7 +143,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((0, EtagVersion.Empty)); - await sut.GetSingleAsync(key); + await sut.GetSingleAsync(key); } [Fact] @@ -151,7 +151,7 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject = await sut.GetSingleAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.NotNull(cache.Get(key)); @@ -162,12 +162,12 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject1 = await sut.GetSingleAsync(key); + var actualObject1 = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject1); Assert.NotNull(cache.Get(key)); - var actualObject2 = await sut.GetSingleAsync(key); + var actualObject2 = await sut.GetSingleAsync(key); A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .MustHaveHappened(Repeated.Exactly.Once); @@ -178,12 +178,12 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject1 = await sut.CreateAsync(key); + var actualObject1 = await sut.CreateAsync(key); Assert.Same(statefulObject, actualObject1); Assert.Null(cache.Get(key)); - var actualObject2 = await sut.CreateAsync(key); + var actualObject2 = await sut.CreateAsync(key); A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .MustHaveHappened(Repeated.Exactly.Twice); @@ -204,7 +204,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 13)); - var actualObject = await sut.GetSingleAsync(key); + var actualObject = await sut.GetSingleAsync(key); Assert.Same(statefulObject, actualObject); Assert.Equal(123, statefulObject.State); @@ -231,7 +231,7 @@ namespace Squidex.Infrastructure.States A.CallTo(() => snapshotStore.WriteAsync(key, 123, 13, 14)) .Throws(new InconsistentStateException(1, 1, new InvalidOperationException())); - var actualObject = await sut.GetSingleAsync(key); + var actualObject = await sut.GetSingleAsync(key); await Assert.ThrowsAsync(() => statefulObject.WriteStateAsync()); } @@ -241,7 +241,7 @@ namespace Squidex.Infrastructure.States { statefulObject.ExpectedVersion = EtagVersion.Any; - var actualObject = await sut.GetSingleAsync(key); + var actualObject = await sut.GetSingleAsync(key); await InvalidateCacheAsync(); @@ -260,7 +260,7 @@ namespace Squidex.Infrastructure.States for (var i = 0; i < 1000; i++) { - tasks.Add(Task.Run(() => sut.GetSingleAsync(key))); + tasks.Add(Task.Run(() => sut.GetSingleAsync(key))); } var retrievedStates = await Task.WhenAll(tasks); diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs index 23a0d0917..5ee760e7b 100644 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Infrastructure.TestHelpers { - internal sealed class MyDomainObject : DomainObjectBase + internal sealed class MyDomainObject : DomainObjectBase { } }