From f4f5c357bea6d92c0f2aab746c12c4bd1f81ef62 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 10 Dec 2017 18:35:34 +0100 Subject: [PATCH] A lot of small fixes. --- .../Apps/MongoAppRepository.cs | 5 +- .../Assets/MongoAssetRepository.cs | 5 +- .../Contents/MongoContentRepository.cs | 4 +- .../Rules/MongoRuleRepository.cs | 5 +- .../Schemas/MongoSchemaRepository.cs | 7 ++- .../Apps/AppCommandMiddleware.cs | 18 +++---- .../Assets/AssetCommandMiddleware.cs | 6 +-- .../Contents/ContentQueryService.cs | 2 +- .../Contents/IContentQueryService.cs | 3 +- .../DomainObjectState.cs | 2 + .../EntityMapper.cs | 11 +++- .../IUpdateableEntityWithVersion.cs} | 10 ++-- .../Rules/RuleCommandMiddleware.cs | 8 +-- .../Schemas/SchemaCommandMiddleware.cs | 28 +++++----- .../SquidexCommand.cs | 2 +- .../EventSourcing/Events/GetEventStore.cs | 2 +- .../EventSourcing/MongoEventStore.cs | 10 ++-- .../States/MongoSnapshotStore.cs | 2 +- .../Commands/AggregateHandler.cs | 18 +++++-- .../Commands/DomainObjectBase.cs | 41 ++++++++++----- .../Commands/ICommand.cs | 2 +- .../Commands/IDomainState.cs | 15 ++++++ src/Squidex.Infrastructure/EtagVersion.cs | 19 +++++++ .../EventSourcing/CommonHeaders.cs | 2 + .../EventSourcing/EnvelopeExtensions.cs | 14 ++++- .../States/IPersistence.cs | 2 +- .../States/Persistence.cs | 25 +++++---- .../Controllers/Apps/AppClientsController.cs | 3 +- .../ETagCommandMiddleware.cs | 5 ++ .../Contents/GraphQLTests.cs | 6 +-- .../Commands/AggregateHandlerTests.cs | 51 +++++++++++++++---- .../Commands/CommandContextTests.cs | 2 +- .../Commands/DomainObjectBaseTests.cs | 10 ++-- ...richWithTimestampCommandMiddlewareTests.cs | 2 +- .../CompoundEventConsumerTests.cs | 5 +- .../EventSourcing/EnvelopeExtensionsTests.cs | 13 ++++- .../EventSourcing/EventDataFormatterTests.cs | 6 +-- .../Grains/EventConsumerGrainTests.cs | 7 +-- .../PropertiesBagTests.cs | 14 ++--- .../States/StateEventSourcingTests.cs | 25 ++++----- .../States/StateSnapshotTests.cs | 20 ++++---- .../{Commands => }/TestHelpers/MyCommand.cs | 5 +- .../TestHelpers/MyDomainObject.cs | 6 +-- .../TestHelpers/MyDomainState.cs | 17 +++++++ .../{Commands => }/TestHelpers/MyEvent.cs | 3 +- 45 files changed, 302 insertions(+), 166 deletions(-) rename src/{Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs => Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs} (64%) create mode 100644 src/Squidex.Infrastructure/Commands/IDomainState.cs create mode 100644 src/Squidex.Infrastructure/EtagVersion.cs rename tests/Squidex.Infrastructure.Tests/{Commands => }/TestHelpers/MyCommand.cs (80%) rename tests/Squidex.Infrastructure.Tests/{Commands => }/TestHelpers/MyDomainObject.cs (78%) create mode 100644 tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs rename tests/Squidex.Infrastructure.Tests/{Commands => }/TestHelpers/MyEvent.cs (82%) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index f77ab41fa..1c65bb489 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; @@ -74,15 +75,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return (existing.State, existing.Version); } - return (null, -1); + return (null, EtagVersion.NotFound); } public async Task WriteAsync(string key, AppState value, long oldVersion, long newVersion) { try { - value.Version = newVersion; - await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.UserIds, value.Contributors.Keys.ToArray()) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 36dc642cb..a470fe74b 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -15,6 +15,7 @@ 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; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; @@ -53,7 +54,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets return (existing.State, existing.Version); } - return (null, -1); + return (null, EtagVersion.NotFound); } public async Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0) @@ -116,8 +117,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { try { - value.Version = newVersion; - await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.State, value) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index bac856632..720cab81b 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -89,8 +89,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents try { - value.Version = newVersion; - await Collection.InsertOneAsync(document); } catch (MongoWriteException ex) @@ -126,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return (SimpleMapper.Map(existing, new ContentState()), existing.Version); } - return (null, -1); + return (null, EtagVersion.NotFound); } public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs index 13f5f31b6..fe6a69c05 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; @@ -47,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules return (existing.State, existing.Version); } - return (null, -1); + return (null, EtagVersion.NotFound); } public async Task> QueryRuleIdsAsync(Guid appId) @@ -63,8 +64,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { try { - value.Version = newVersion; - await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.State, value) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs index e7174a83f..23ccea311 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; @@ -47,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas return (existing.State, existing.Version); } - return (null, -1); + return (null, EtagVersion.NotFound); } public async Task FindSchemaIdAsync(Guid appId, string name) @@ -62,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas public async Task> QuerySchemaIdsAsync(Guid appId) { var schemaEntities = - await Collection.Find(x => x.State.AppId == appId).Only(x => x.Id) + await Collection.Find(x => x.AppId == appId).Only(x => x.Id) .ToListAsync(); return schemaEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); @@ -72,8 +73,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas { try { - value.Version = newVersion; - await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, Update .Set(x => x.State, value) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs index 532d13b0b..22ea628b2 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(AssignContributor command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, async a => + return handler.UpdateSyncedAsync(context, async a => { await GuardAppContributors.CanAssign(a.State.Contributors, command, userResolver, appPlansProvider.GetPlan(a.State.Plan?.PlanId)); @@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(RemoveContributor command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppContributors.CanRemove(a.State.Contributors, command); @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(AttachClient command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppClients.CanAttach(a.State.Clients, command); @@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(UpdateClient command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppClients.CanUpdate(a.State.Clients, command); @@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(RevokeClient command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppClients.CanRevoke(a.State.Clients, command); @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(AddLanguage command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppLanguages.CanAdd(a.State.LanguagesConfig, command); @@ -120,7 +120,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(RemoveLanguage command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppLanguages.CanRemove(a.State.LanguagesConfig, command); @@ -130,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(UpdateLanguage command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAppLanguages.CanUpdate(a.State.LanguagesConfig, command); @@ -140,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Apps protected Task On(ChangePlan command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, async a => + return handler.UpdateSyncedAsync(context, async a => { GuardApp.CanChangePlan(command, a.State.Plan, appPlansProvider); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index 9a9a4ff52..887c7acdb 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Assets try { - var asset = await handler.handler.UpdateSyncedAsync(context, async a => + var asset = await handler.UpdateSyncedAsync(context, async a => { GuardAsset.CanUpdate(command); @@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Assets protected Task On(RenameAsset command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAsset.CanRename(command, a.State.FileName); @@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Assets protected Task On(DeleteAsset command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, a => + return handler.UpdateSyncedAsync(context, a => { GuardAsset.CanDelete(command); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index d9144cafb..95f95ee31 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var schema = await FindSchemaAsync(app, schemaIdOrName); var content = - version > 0 ? + version > EtagVersion.Empty ? await contentRepository.FindContentAsync(app, schema, id, version) : await contentRepository.FindContentAsync(app, schema, id); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index a9a01d2df..8baaccd33 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -12,6 +12,7 @@ using System.Security.Claims; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents { @@ -21,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Task<(ISchemaEntity Schema, long Total, IReadOnlyList Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query); - Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = -1); + Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id, long version = EtagVersion.Any); Task FindSchemaAsync(IAppEntity app, string schemaIdOrName); } diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index 2770d4e1a..dc311eb6c 100644 --- a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -10,10 +10,12 @@ using System; using Newtonsoft.Json; using NodaTime; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities { public abstract class DomainObjectState : Cloneable, + IDomainState, IEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, diff --git a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs index 77c711a05..81bb15aa1 100644 --- a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs +++ b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs @@ -23,6 +23,7 @@ namespace Squidex.Domain.Apps.Entities SetCreatedBy(entity, @event); SetLastModified(entity, headers); SetLastModifiedBy(entity, @event); + SetVersion(entity, headers); updater?.Invoke(entity); @@ -31,12 +32,20 @@ namespace Squidex.Domain.Apps.Entities private static void SetId(IEntity entity, EnvelopeHeaders headers) { - if (entity is IUpdateableEntity updateable) + if (entity is IUpdateableEntity updateable && updateable.Id == Guid.Empty) { updateable.Id = headers.AggregateId(); } } + private static void SetVersion(IEntity entity, EnvelopeHeaders headers) + { + if (entity is IUpdateableEntityWithVersion updateable) + { + updateable.Version = headers.EventStreamNumber(); + } + } + private static void SetCreated(IEntity entity, EnvelopeHeaders headers) { if (entity is IUpdateableEntity updateable && updateable.Created == default(Instant)) diff --git a/src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs b/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs similarity index 64% rename from src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs rename to src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs index 3908d3cd2..229f7ea2d 100644 --- a/src/Squidex.Infrastructure/EventSourcing/ExpectedVersion.cs +++ b/src/Squidex.Domain.Apps.Entities/IUpdateableEntityWithVersion.cs @@ -1,17 +1,15 @@ // ========================================================================== -// ExpectedVersion.cs +// IUpdateableEntityWithVersion.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing +namespace Squidex.Domain.Apps.Entities { - public static class ExpectedVersion + public interface IUpdateableEntityWithVersion { - public const int Any = -2; - - public const int Empty = -1; + long Version { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs index 140c5f27a..1217d9105 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(UpdateRule command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, async c => + return handler.UpdateSyncedAsync(context, async c => { await GuardRule.CanUpdate(command, appProvider); @@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(EnableRule command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, r => + return handler.UpdateSyncedAsync(context, r => { GuardRule.CanEnable(command, r.State.RuleDef); @@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(DisableRule command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, r => + return handler.UpdateSyncedAsync(context, r => { GuardRule.CanDisable(command, r.State.RuleDef); @@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Rules protected Task On(DeleteRule command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, c => + return handler.UpdateSyncedAsync(context, c => { GuardRule.CanDelete(command); diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs index 5c2904f56..87f0c4d4b 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(AddField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanAdd(s.State.SchemaDef, command); @@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(DeleteField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanDelete(s.State.SchemaDef, command); @@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(LockField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanLock(s.State.SchemaDef, command); @@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(HideField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanHide(s.State.SchemaDef, command); @@ -89,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(ShowField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanShow(s.State.SchemaDef, command); @@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(DisableField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanDisable(s.State.SchemaDef, command); @@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(EnableField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanEnable(s.State.SchemaDef, command); @@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(UpdateField command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchemaField.CanUpdate(s.State.SchemaDef, command); @@ -129,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(ReorderFields command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchema.CanReorder(s.State.SchemaDef, command); @@ -139,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(UpdateSchema command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchema.CanUpdate(s.State.SchemaDef, command); @@ -149,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(PublishSchema command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchema.CanPublish(s.State.SchemaDef, command); @@ -159,7 +159,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(UnpublishSchema command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchema.CanUnpublish(s.State.SchemaDef, command); @@ -169,7 +169,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(ConfigureScripts command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchema.CanConfigureScripts(s.State.SchemaDef, command); @@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Entities.State.SchemaDefs protected Task On(DeleteSchema command, CommandContext context) { - return handler.handler.UpdateSyncedAsync(context, s => + return handler.UpdateSyncedAsync(context, s => { GuardSchema.CanDelete(s.State.SchemaDef, command); diff --git a/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs b/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs index ae4c6cb8d..c22d7861d 100644 --- a/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs +++ b/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs @@ -15,6 +15,6 @@ namespace Squidex.Domain.Apps.Entities { public RefToken Actor { get; set; } - public long? ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } } } diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs index cd65c5e43..cd6659044 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs @@ -89,7 +89,7 @@ namespace Squidex.Infrastructure.EventSourcing public Task AppendEventsAsync(Guid commitId, string streamName, ICollection events) { - return AppendEventsInternalAsync(streamName, ExpectedVersion.Any, events); + return AppendEventsInternalAsync(streamName, EtagVersion.Any, events); } public Task AppendEventsAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index 0c2dba475..7eb6942d8 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -129,12 +129,12 @@ namespace Squidex.Infrastructure.EventSourcing public Task AppendEventsAsync(Guid commitId, string streamName, ICollection events) { - return AppendEventsInternalAsync(commitId, streamName, ExpectedVersion.Any, events); + return AppendEventsInternalAsync(commitId, streamName, EtagVersion.Any, events); } public Task AppendEventsAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) { - Guard.GreaterEquals(expectedVersion, -1, nameof(expectedVersion)); + Guard.GreaterEquals(expectedVersion, EtagVersion.Any, nameof(expectedVersion)); return AppendEventsInternalAsync(commitId, streamName, expectedVersion, events); } @@ -151,7 +151,7 @@ namespace Squidex.Infrastructure.EventSourcing var currentVersion = await GetEventStreamOffset(streamName); - if (expectedVersion != ExpectedVersion.Any && expectedVersion != currentVersion) + if (expectedVersion != EtagVersion.Any && expectedVersion != currentVersion) { throw new WrongEventVersionException(currentVersion, expectedVersion); } @@ -174,7 +174,7 @@ namespace Squidex.Infrastructure.EventSourcing { currentVersion = await GetEventStreamOffset(streamName); - if (expectedVersion != ExpectedVersion.Any) + if (expectedVersion != EtagVersion.Any) { throw new WrongEventVersionException(currentVersion, expectedVersion); } @@ -210,7 +210,7 @@ namespace Squidex.Infrastructure.EventSourcing return document[nameof(MongoEventCommit.EventStreamOffset)].ToInt64() + document[nameof(MongoEventCommit.EventsCount)].ToInt64(); } - return -1; + return EtagVersion.Empty; } private static FilterDefinition CreateFilter(string streamFilter, StreamPosition streamPosition) diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 68f354dde..c6f6cee6b 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -41,7 +41,7 @@ namespace Squidex.Infrastructure.States return (existing.Doc, existing.Version); } - return (default(T), -1); + return (default(T), EtagVersion.NotFound); } public async Task WriteAsync(string key, T value, long oldVersion, long newVersion) diff --git a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs index b30298026..c51c651bf 100644 --- a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs @@ -65,10 +65,15 @@ namespace Squidex.Infrastructure.Commands { Guard.NotNull(context, nameof(context)); - var domainObjectCommand = GetCommand(context); - var domainObjectId = domainObjectCommand.AggregateId; + var domainCommand = GetCommand(context); + var domainObjectId = domainCommand.AggregateId; var domainObject = await stateFactory.CreateAsync(domainObjectId.ToString()); + if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version) + { + throw new DomainObjectVersionException(domainObjectId.ToString(), typeof(T), domainObject.Version, domainCommand.ExpectedVersion); + } + await handler(domainObject); await domainObject.WriteAsync(log); @@ -92,13 +97,18 @@ namespace Squidex.Infrastructure.Commands { Guard.NotNull(context, nameof(context)); - var domainObjectCommand = GetCommand(context); - var domainObjectId = domainObjectCommand.AggregateId; + var domainCommand = GetCommand(context); + var domainObjectId = domainCommand.AggregateId; using (await lockPool.LockAsync(Tuple.Create(typeof(T), domainObjectId))) { var domainObject = await stateFactory.GetSingleAsync(domainObjectId.ToString()); + if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version) + { + throw new DomainObjectVersionException(domainObjectId.ToString(), typeof(T), domainObject.Version, domainCommand.ExpectedVersion); + } + await handler(domainObject); await domainObject.WriteAsync(log); diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index 32135d0fb..06c81f26f 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -15,16 +15,16 @@ using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObjectBase : IDomainObject where TState : new() + public abstract class DomainObjectBase : IDomainObject where TState : IDomainState, new() { private readonly List> uncomittedEvents = new List>(); private Guid id; - private TState state = new TState(); + private TState state; private IPersistence persistence; public long Version { - get { return persistence?.Version ?? -1; } + get { return state.Version; } } public TState State @@ -32,6 +32,12 @@ namespace Squidex.Infrastructure.Commands get { return state; } } + protected DomainObjectBase() + { + state = new TState(); + state.Version = EtagVersion.Empty; + } + public IReadOnlyList> GetUncomittedEvents() { return uncomittedEvents; @@ -78,19 +84,26 @@ namespace Squidex.Infrastructure.Commands public async Task WriteAsync(ISemanticLog log) { - await persistence.WriteSnapshotAsync(state); + var events = uncomittedEvents; - try - { - await persistence.WriteEventsAsync(uncomittedEvents.ToArray()); - } - catch (Exception ex) - { - log.LogFatal(ex, w => w.WriteProperty("action", "writeEvents")); - } - finally + if (events.Count > 0) { - uncomittedEvents.Clear(); + state.Version += events.Count; + + await persistence.WriteSnapshotAsync(state); + + try + { + await persistence.WriteEventsAsync(uncomittedEvents.ToArray()); + } + catch (Exception ex) + { + log.LogFatal(ex, w => w.WriteProperty("action", "writeEvents")); + } + finally + { + uncomittedEvents.Clear(); + } } } } diff --git a/src/Squidex.Infrastructure/Commands/ICommand.cs b/src/Squidex.Infrastructure/Commands/ICommand.cs index f28392eb4..b64b682d4 100644 --- a/src/Squidex.Infrastructure/Commands/ICommand.cs +++ b/src/Squidex.Infrastructure/Commands/ICommand.cs @@ -10,6 +10,6 @@ namespace Squidex.Infrastructure.Commands { public interface ICommand { - long? ExpectedVersion { get; set; } + long ExpectedVersion { get; set; } } } diff --git a/src/Squidex.Infrastructure/Commands/IDomainState.cs b/src/Squidex.Infrastructure/Commands/IDomainState.cs new file mode 100644 index 000000000..7cd9bceff --- /dev/null +++ b/src/Squidex.Infrastructure/Commands/IDomainState.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// IDomainState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.Commands +{ + public interface IDomainState + { + long Version { get; set; } + } +} diff --git a/src/Squidex.Infrastructure/EtagVersion.cs b/src/Squidex.Infrastructure/EtagVersion.cs new file mode 100644 index 000000000..d99ec1fd1 --- /dev/null +++ b/src/Squidex.Infrastructure/EtagVersion.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// EtagVersion.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure +{ + public static class EtagVersion + { + public const long Any = -2; + + public const long Empty = -1; + + public const long NotFound = long.MinValue; + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs b/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs index 276289788..713d3a3da 100644 --- a/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs +++ b/src/Squidex.Infrastructure/EventSourcing/CommonHeaders.cs @@ -20,6 +20,8 @@ namespace Squidex.Infrastructure.EventSourcing public static readonly string EventStreamNumber = "EventStreamNumber"; + public static readonly string SnapshotVersion = "SnapshotVersion"; + public static readonly string Timestamp = "Timestamp"; public static readonly string Actor = "Actor"; diff --git a/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs b/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs index e1cc4f7b5..fe76d145f 100644 --- a/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs +++ b/src/Squidex.Infrastructure/EventSourcing/EnvelopeExtensions.cs @@ -26,9 +26,21 @@ namespace Squidex.Infrastructure.EventSourcing return envelope; } + public static long SnapshotVersion(this EnvelopeHeaders headers) + { + return headers[CommonHeaders.SnapshotVersion].ToInt64(CultureInfo.InvariantCulture); + } + + public static Envelope SetSnapshotVersion(this Envelope envelope, long value) where T : class + { + envelope.Headers.Set(CommonHeaders.SnapshotVersion, value); + + return envelope; + } + public static long EventStreamNumber(this EnvelopeHeaders headers) { - return headers[CommonHeaders.EventStreamNumber].ToInt32(CultureInfo.InvariantCulture); + return headers[CommonHeaders.EventStreamNumber].ToInt64(CultureInfo.InvariantCulture); } public static Envelope SetEventStreamNumber(this Envelope envelope, long value) where T : class diff --git a/src/Squidex.Infrastructure/States/IPersistence.cs b/src/Squidex.Infrastructure/States/IPersistence.cs index 3c9528178..462cf5cc3 100644 --- a/src/Squidex.Infrastructure/States/IPersistence.cs +++ b/src/Squidex.Infrastructure/States/IPersistence.cs @@ -20,6 +20,6 @@ namespace Squidex.Infrastructure.States Task WriteSnapshotAsync(TState state); - Task ReadAsync(long expectedVersion = ExpectedVersion.Any); + Task ReadAsync(long expectedVersion = EtagVersion.Any); } } diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index 542320f75..4e633c967 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -12,6 +12,8 @@ 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 sealed class Persistence : IPersistence @@ -25,8 +27,8 @@ namespace Squidex.Infrastructure.States private readonly Action invalidate; private readonly Func applyState; private readonly Func, Task> applyEvent; - private long versionSnapshot = -1; - private long versionEvents = -1; + private long versionSnapshot = EtagVersion.Empty; + private long versionEvents = EtagVersion.Empty; private long version; public long Version @@ -55,19 +57,19 @@ namespace Squidex.Infrastructure.States this.streamNameResolver = streamNameResolver; } - public async Task ReadAsync(long expectedVersion = ExpectedVersion.Any) + public async Task ReadAsync(long expectedVersion = EtagVersion.Any) { - versionSnapshot = -1; - versionEvents = -1; + versionSnapshot = EtagVersion.Empty; + versionEvents = EtagVersion.Empty; await ReadSnapshotAsync(); await ReadEventsAsync(); UpdateVersion(); - if (expectedVersion != ExpectedVersion.Any && expectedVersion != version) + if (expectedVersion != EtagVersion.Any && expectedVersion != version) { - if (version == ExpectedVersion.Empty) + if (version == EtagVersion.Empty) { throw new DomainObjectNotFoundException(ownerKey, typeof(TOwner)); } @@ -84,6 +86,11 @@ namespace Squidex.Infrastructure.States { var (state, position) = await snapshotStore.ReadAsync(ownerKey); + if (position < EtagVersion.Empty) + { + position = EtagVersion.Empty; + } + versionSnapshot = position; versionEvents = position; @@ -150,7 +157,7 @@ namespace Squidex.Infrastructure.States if (eventArray.Length > 0) { - var expectedVersion = UseEventSourcing() ? version : ExpectedVersion.Any; + var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any; var commitId = Guid.NewGuid(); @@ -159,7 +166,7 @@ namespace Squidex.Infrastructure.States try { - await eventStore.AppendEventsAsync(commitId, GetStreamName(), Version, eventData); + await eventStore.AppendEventsAsync(commitId, GetStreamName(), expectedVersion, eventData); } catch (WrongEventVersionException ex) { diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index 6f0130bc5..59f74abfd 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Apps.Models; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; @@ -81,7 +82,7 @@ namespace Squidex.Areas.Api.Controllers.Apps await CommandBus.PublishAsync(command); - var response = SimpleMapper.Map(command, new ClientDto { Name = command.Id }); + var response = SimpleMapper.Map(command, new ClientDto { Name = command.Id, Permission = AppClientPermission.Editor }); return CreatedAtAction(nameof(GetClients), new { app }, response); } diff --git a/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs b/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs index 82029aea4..f2a9ad3f8 100644 --- a/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs +++ b/src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs @@ -11,6 +11,7 @@ using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; +using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; namespace Squidex.Pipeline.CommandMiddlewares @@ -33,6 +34,10 @@ namespace Squidex.Pipeline.CommandMiddlewares { context.Command.ExpectedVersion = expectedVersion; } + else + { + context.Command.ExpectedVersion = EtagVersion.Any; + } await next(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs index b43cfe6ab..3ace057fe 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs @@ -483,7 +483,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var refContents = new List { contentRef }; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, EtagVersion.Any)) .Returns((schema, content)); A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, A>.That.Matches(x => x.Contains(contentRefId)))) @@ -543,7 +543,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var refAssets = new List { assetRef }; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, EtagVersion.Any)) .Returns((schema, content)); A.CallTo(() => assetRepository.QueryAsync(app.Id, null, A>.That.Matches(x => x.Contains(assetRefId)), null, int.MaxValue, 0)) @@ -602,7 +602,7 @@ namespace Squidex.Domain.Apps.Entities.Contents }} }}"; - A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, -1)) + A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId, EtagVersion.Any)) .Returns((schema, content)); var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); diff --git a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs index 1a020997d..5ca90705e 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs @@ -10,11 +10,11 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; -using Squidex.Infrastructure.Commands.TestHelpers; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.Commands @@ -29,17 +29,16 @@ namespace Squidex.Infrastructure.Commands private readonly Envelope event1 = new Envelope(new MyEvent()); private readonly Envelope event2 = new Envelope(new MyEvent()); private readonly CommandContext context; + private readonly CommandContext invalidContext = new CommandContext(A.Dummy()); private readonly Guid domainObjectId = Guid.NewGuid(); + private readonly MyCommand command; private readonly MyDomainObject domainObject = new MyDomainObject(); private readonly AggregateHandler sut; - public sealed class MyEvent : IEvent - { - } - public AggregateHandlerTests() { - context = new CommandContext(new MyCommand { AggregateId = domainObjectId }); + command = new MyCommand { AggregateId = domainObjectId }; + context = new CommandContext(command); A.CallTo(() => store.WithSnapshots(domainObjectId.ToString(), A>.Ignored)) .Returns(persistence); @@ -58,13 +57,29 @@ namespace Squidex.Infrastructure.Commands [Fact] public Task Create_with_task_should_throw_exception_if_not_aggregate_command() { - return Assert.ThrowsAnyAsync(() => sut.CreateAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); + return Assert.ThrowsAnyAsync(() => sut.CreateAsync(invalidContext, x => TaskHelper.False)); } [Fact] public Task Create_synced_with_task_should_throw_exception_if_not_aggregate_command() { - return Assert.ThrowsAnyAsync(() => sut.CreateSyncedAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); + return Assert.ThrowsAnyAsync(() => sut.CreateSyncedAsync(invalidContext, x => TaskHelper.False)); + } + + [Fact] + public Task Create_with_task_should_should_throw_exception_if_version_is_wrong() + { + command.ExpectedVersion = 2; + + return Assert.ThrowsAnyAsync(() => sut.CreateAsync(context, x => TaskHelper.False)); + } + + [Fact] + public Task Create_synced_with_task_should_should_throw_exception_if_version_is_wrong() + { + command.ExpectedVersion = 2; + + return Assert.ThrowsAnyAsync(() => sut.CreateSyncedAsync(context, x => TaskHelper.False)); } [Fact] @@ -150,13 +165,29 @@ namespace Squidex.Infrastructure.Commands [Fact] public Task Update_with_task_should_throw_exception_if_not_aggregate_command() { - return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); + return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(invalidContext, x => TaskHelper.False)); } [Fact] public Task Update_synced_with_task_should_throw_exception_if_not_aggregate_command() { - return Assert.ThrowsAnyAsync(() => sut.UpdateSyncedAsync(new CommandContext(A.Dummy()), x => TaskHelper.False)); + return Assert.ThrowsAnyAsync(() => sut.UpdateSyncedAsync(invalidContext, x => TaskHelper.False)); + } + + [Fact] + public Task Update_with_task_should_should_throw_exception_if_version_is_wrong() + { + command.ExpectedVersion = 2; + + return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(context, x => TaskHelper.False)); + } + + [Fact] + public Task Update_synced_with_task_should_should_throw_exception_if_version_is_wrong() + { + command.ExpectedVersion = 2; + + return Assert.ThrowsAnyAsync(() => sut.UpdateSyncedAsync(context, x => TaskHelper.False)); } [Fact] diff --git a/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs index 8e7256bdb..e8d043fcc 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/CommandContextTests.cs @@ -7,7 +7,7 @@ // ========================================================================== using System; -using Squidex.Infrastructure.Commands.TestHelpers; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.Commands diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs index aea30eb48..bbd13e61d 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs @@ -11,10 +11,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FakeItEasy; -using Squidex.Infrastructure.Commands.TestHelpers; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.Commands @@ -35,7 +35,7 @@ namespace Squidex.Infrastructure.Commands [Fact] public void Should_instantiate() { - Assert.Equal(-1, sut.Version); + Assert.Equal(EtagVersion.NotFound, sut.Version); } [Fact] @@ -47,7 +47,7 @@ namespace Squidex.Infrastructure.Commands sut.RaiseEvent(event1); sut.RaiseEvent(event2); - Assert.Equal(-1, sut.Version); + Assert.Equal(EtagVersion.NotFound, sut.Version); Assert.Equal(new IEvent[] { event1, event2 }, sut.GetUncomittedEvents().Select(x => x.Payload).ToArray()); sut.ClearUncommittedEvents(); @@ -71,7 +71,7 @@ namespace Squidex.Infrastructure.Commands sut.RaiseEvent(event1); sut.RaiseEvent(event2); - var newState = "STATE"; + var newState = new MyDomainState(); sut.UpdateState(newState); @@ -104,7 +104,7 @@ namespace Squidex.Infrastructure.Commands sut.RaiseEvent(event1); sut.RaiseEvent(event2); - var newState = "STATE"; + var newState = new MyDomainState(); sut.UpdateState(newState); diff --git a/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs index 01f0dda59..febba61c0 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using FakeItEasy; using NodaTime; -using Squidex.Infrastructure.Commands.TestHelpers; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.Commands diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs index 817867902..39927fbca 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/CompoundEventConsumerTests.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using FakeItEasy; using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.EventSourcing @@ -18,10 +19,6 @@ namespace Squidex.Infrastructure.EventSourcing private readonly IEventConsumer consumer1 = A.Fake(); private readonly IEventConsumer consumer2 = A.Fake(); - private sealed class MyEvent : IEvent - { - } - [Fact] public void Should_return_given_name() { diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs index 9c5bb6101..6670ee657 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/EnvelopeExtensionsTests.cs @@ -81,7 +81,18 @@ namespace Squidex.Infrastructure.EventSourcing sut.SetEventStreamNumber(eventStreamNumber); Assert.Equal(eventStreamNumber, sut.Headers.EventStreamNumber()); - Assert.Equal(eventStreamNumber, sut.Headers["EventStreamNumber"].ToInt32(culture)); + Assert.Equal(eventStreamNumber, sut.Headers["EventStreamNumber"].ToInt64(culture)); + } + + [Fact] + public void Should_set_and_get_snapshot_version() + { + const int snapshotVersion = 123; + + sut.SetSnapshotVersion(snapshotVersion); + + Assert.Equal(snapshotVersion, sut.Headers.SnapshotVersion()); + Assert.Equal(snapshotVersion, sut.Headers["SnapshotVersion"].ToInt64(culture)); } } } diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs index 6c0261cf5..82f6b0946 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/EventDataFormatterTests.cs @@ -11,17 +11,13 @@ using System.Linq; using Newtonsoft.Json; using NodaTime; using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.EventSourcing { public class EventDataFormatterTests { - public sealed class MyEvent : IEvent - { - public string MyProperty { get; set; } - } - public sealed class MyOldEvent : IEvent, IMigratedEvent { public string MyProperty { get; set; } diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs index 85f873321..0fa7113b7 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -12,16 +12,13 @@ using FakeItEasy; using FluentAssertions; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.EventSourcing.Grains { public class EventConsumerGrainTests { - public sealed class MyEvent : IEvent - { - } - public sealed class MyEventConsumerGrain : EventConsumerGrain { public MyEventConsumerGrain(IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISemanticLog log) @@ -67,7 +64,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.Name) .Returns(consumerName); - A.CallTo(() => persistence.ReadAsync(ExpectedVersion.Any)) + A.CallTo(() => persistence.ReadAsync(EtagVersion.Any)) .Invokes(new Action(s => apply(state))); A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) diff --git a/tests/Squidex.Infrastructure.Tests/PropertiesBagTests.cs b/tests/Squidex.Infrastructure.Tests/PropertiesBagTests.cs index 4ebdd6038..7f2b1c5be 100644 --- a/tests/Squidex.Infrastructure.Tests/PropertiesBagTests.cs +++ b/tests/Squidex.Infrastructure.Tests/PropertiesBagTests.cs @@ -86,7 +86,7 @@ namespace Squidex.Infrastructure Assert.True(bag.Contains("NewKey")); Assert.Equal(1, bag.Count); - Assert.Equal(123, bag["NewKey"].ToInt32(c)); + Assert.Equal(123, bag["NewKey"].ToInt64(c)); Assert.False(bag.Contains("OldKey")); } @@ -174,7 +174,7 @@ namespace Squidex.Infrastructure { bag.Set("Key", "abc"); - Assert.Throws(() => bag["Key"].ToInt32(CultureInfo.InvariantCulture)); + Assert.Throws(() => bag["Key"].ToInt64(CultureInfo.InvariantCulture)); } [Fact] @@ -214,7 +214,7 @@ namespace Squidex.Infrastructure { bag.Set("Key", long.MaxValue); - Assert.Throws(() => bag["Key"].ToInt32(c)); + Assert.Throws(() => bag["Key"].ToInt64(c)); } [Fact] @@ -347,7 +347,7 @@ namespace Squidex.Infrastructure private void AssertNumber() { - AssertInt32(123); + AssertInt64(123); AssertInt64(123); AssertSingle(123); AssertDouble(123); @@ -420,10 +420,10 @@ namespace Squidex.Infrastructure Assert.Equal(expected, (long?)dynamicBag.Key); } - private void AssertInt32(int expected) + private void AssertInt64(int expected) { - Assert.Equal(expected, bag["Key"].ToInt32(c)); - Assert.Equal(expected, bag["Key"].ToNullableInt32(c)); + Assert.Equal(expected, bag["Key"].ToInt64(c)); + Assert.Equal(expected, bag["Key"].ToNullableInt64(c)); Assert.Equal(expected, (int)dynamicBag.Key); Assert.Equal(expected, (int?)dynamicBag.Key); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs index c7ae719ce..0ed94cac8 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs @@ -15,16 +15,13 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.States { public class StateEventSourcingTests { - public sealed class MyEvent : IEvent - { - } - private class MyStatefulObject : IStatefulObject { private readonly List appliedEvents = new List(); @@ -115,7 +112,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_read_events_from_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = ExpectedVersion.Any; + statefulObjectWithSnapShot.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -131,7 +128,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_events_are_older_than_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = ExpectedVersion.Any; + statefulObjectWithSnapShot.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -144,7 +141,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_events_have_gaps_to_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = ExpectedVersion.Any; + statefulObjectWithSnapShot.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); @@ -177,7 +174,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_throw_exception_if_noting_expected() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; SetupEventStore(0); @@ -187,7 +184,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_provide_state_from_services_and_add_to_cache() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; SetupEventStore(0); @@ -200,7 +197,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_serve_next_request_from_cache() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; SetupEventStore(0); @@ -218,7 +215,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_write_to_store_with_previous_position() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; InvalidateMessage message = null; @@ -248,7 +245,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_position() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; SetupEventStore(3); @@ -263,7 +260,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_remove_from_cache_when_invalidation_message_received() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; var actualObject = await sut.GetSingleAsync(key); @@ -275,7 +272,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_return_same_instance_for_parallel_requests() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => ((object)1, 1L))); diff --git a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs index b7d91bb0c..9ccb867df 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs @@ -100,7 +100,7 @@ namespace Squidex.Infrastructure.States statefulObject.ExpectedVersion = 0; A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((0, -1)); + .Returns((0, EtagVersion.Empty)); await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); } @@ -119,10 +119,10 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_throw_exception_if_noting_expected() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((0, -1)); + .Returns((0, EtagVersion.Empty)); await sut.GetSingleAsync(key); } @@ -130,7 +130,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_provide_state_from_services_and_add_to_cache() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; var actualObject = await sut.GetSingleAsync(key); @@ -141,7 +141,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_serve_next_request_from_cache() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; var actualObject1 = await sut.GetSingleAsync(key); @@ -157,7 +157,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_serve_next_request_from_cache_when_detached() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; var actualObject1 = await sut.CreateAsync(key); @@ -173,7 +173,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_write_to_store_with_previous_version() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; InvalidateMessage message = null; @@ -204,7 +204,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_wrap_exception_when_writing_to_store_with_previous_version() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((123, 13)); @@ -220,7 +220,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_remove_from_cache_when_invalidation_message_received() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; var actualObject = await sut.GetSingleAsync(key); @@ -232,7 +232,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_return_same_instance_for_parallel_requests() { - statefulObject.ExpectedVersion = ExpectedVersion.Any; + statefulObject.ExpectedVersion = EtagVersion.Any; A.CallTo(() => snapshotStore.ReadAsync(key)) .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L))); diff --git a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyCommand.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs similarity index 80% rename from tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyCommand.cs rename to tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs index c610d9d08..6a5ee2a63 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyCommand.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs @@ -8,14 +8,15 @@ using System; using NodaTime; +using Squidex.Infrastructure.Commands; -namespace Squidex.Infrastructure.Commands.TestHelpers +namespace Squidex.Infrastructure.TestHelpers { internal sealed class MyCommand : IAggregateCommand, ITimestampCommand { public Guid AggregateId { get; set; } - public long? ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } public Instant Timestamp { get; set; } } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs similarity index 78% rename from tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs rename to tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs index a725a7a13..23a0d0917 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyDomainObject.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs @@ -6,11 +6,11 @@ // All rights reserved. // ========================================================================== -using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Commands; -namespace Squidex.Infrastructure.Commands.TestHelpers +namespace Squidex.Infrastructure.TestHelpers { - internal sealed class MyDomainObject : DomainObjectBase + internal sealed class MyDomainObject : DomainObjectBase { } } diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs new file mode 100644 index 000000000..ce0665b42 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainState.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// MyDomainState.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure.Commands; + +namespace Squidex.Infrastructure.TestHelpers +{ + public class MyDomainState : IDomainState + { + public long Version { get; set; } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyEvent.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs similarity index 82% rename from tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyEvent.cs rename to tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs index e8d7e0f4b..cf2d048ce 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/TestHelpers/MyEvent.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyEvent.cs @@ -8,9 +8,10 @@ using Squidex.Infrastructure.EventSourcing; -namespace Squidex.Infrastructure.Commands.TestHelpers +namespace Squidex.Infrastructure.TestHelpers { internal sealed class MyEvent : IEvent { + public string MyProperty { get; set; } } } \ No newline at end of file