diff --git a/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs index 481e55b2f..99b5ced58 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs @@ -11,6 +11,7 @@ using System.Globalization; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; +using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.MongoDb { @@ -127,7 +128,7 @@ namespace Squidex.Infrastructure.MongoDb protected virtual Task SetupCollectionAsync(IMongoCollection collection) { - return Task.FromResult(true); + return TaskHelper.Done; } public virtual Task ClearAsync() diff --git a/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs index 795695484..4bdd4169d 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs @@ -7,9 +7,7 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Infrastructure.CQRS.Commands { @@ -17,7 +15,6 @@ namespace Squidex.Infrastructure.CQRS.Commands { private readonly IDomainObjectRepository domainObjectRepository; private readonly IDomainObjectFactory domainObjectFactory; - private readonly IEnumerable eventProcessors; public IDomainObjectRepository Repository { @@ -31,55 +28,72 @@ namespace Squidex.Infrastructure.CQRS.Commands public AggregateHandler( IDomainObjectFactory domainObjectFactory, - IDomainObjectRepository domainObjectRepository, - IEnumerable eventProcessors) + IDomainObjectRepository domainObjectRepository) { - Guard.NotNull(eventProcessors, nameof(eventProcessors)); Guard.NotNull(domainObjectFactory, nameof(domainObjectFactory)); Guard.NotNull(domainObjectRepository, nameof(domainObjectRepository)); this.domainObjectFactory = domainObjectFactory; this.domainObjectRepository = domainObjectRepository; - - this.eventProcessors = eventProcessors; } - public async Task CreateAsync(IAggregateCommand command, Func creator) where T : class, IAggregate + public async Task CreateAsync(CommandContext context, Func creator) where T : class, IAggregate { Guard.NotNull(creator, nameof(creator)); - Guard.NotNull(command, nameof(command)); - Guard.NotEmpty(command.AggregateId, nameof(command.AggregateId)); + Guard.NotNull(context, nameof(context)); - var aggregate = domainObjectFactory.CreateNew(command.AggregateId); + var aggregateCommand = GetCommand(context); + var aggregate = (T)domainObjectFactory.CreateNew(typeof(T), aggregateCommand.AggregateId); await creator(aggregate); - await Save(command, aggregate); + await SaveAsync(aggregate); + + if (!context.IsHandled) + { + context.Succeed(new EntityCreatedResult(aggregate.Id, aggregate.Version)); + } } - public async Task UpdateAsync(IAggregateCommand command, Func updater) where T : class, IAggregate + public async Task UpdateAsync(CommandContext context, Func updater) where T : class, IAggregate { Guard.NotNull(updater, nameof(updater)); - Guard.NotNull(command, nameof(command)); - Guard.NotEmpty(command.AggregateId, nameof(command.AggregateId)); + Guard.NotNull(context, nameof(context)); - var aggregate = await domainObjectRepository.GetByIdAsync(command.AggregateId); + var aggregateCommand = GetCommand(context); + var aggregate = await domainObjectRepository.GetByIdAsync(aggregateCommand.AggregateId, aggregateCommand.ExpectedVersion); await updater(aggregate); - await Save(command, aggregate); + await SaveAsync(aggregate); + + if (!context.IsHandled) + { + context.Succeed(new EntitySavedResult(aggregate.Version)); + } + } + + private IAggregateCommand GetCommand(CommandContext context) + { + var command = context.Command as IAggregateCommand; + + if (command == null) + { + throw new ArgumentException("Context must have an aggregate command.", nameof(context)); + } + + Guard.NotEmpty(command.AggregateId, "context.Command.AggregateId"); + + return command; } - private async Task Save(ICommand command, IAggregate aggregate) + private async Task SaveAsync(IAggregate aggregate) { var events = aggregate.GetUncomittedEvents(); foreach (var @event in events) { - foreach (var eventProcessor in eventProcessors) - { - await eventProcessor.ProcessEventAsync(@event, aggregate, command); - } + @event.SetAggregateId(aggregate.Id); } await domainObjectRepository.SaveAsync(aggregate, events, Guid.NewGuid()); diff --git a/src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs b/src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs index 07b57e503..c56f7599a 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs @@ -14,14 +14,9 @@ namespace Squidex.Infrastructure.CQRS.Commands { public static class CommandingExtensions { - public static T CreateNew(this IDomainObjectFactory factory, Guid id) where T : IAggregate + public static Task CreateAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IAggregate { - return (T)factory.CreateNew(typeof(T), id); - } - - public static Task CreateAsync(this IAggregateHandler handler, IAggregateCommand command, Action creator) where T : class, IAggregate - { - return handler.CreateAsync(command, x => + return handler.CreateAsync(context, x => { creator(x); @@ -29,12 +24,12 @@ namespace Squidex.Infrastructure.CQRS.Commands }); } - public static Task UpdateAsync(this IAggregateHandler handler, IAggregateCommand command, Action creator) where T : class, IAggregate + public static Task UpdateAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IAggregate { - return handler.UpdateAsync(command, x => + return handler.UpdateAsync(context, x => { - creator(x); - + updater(x); + return TaskHelper.Done; }); } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs index 7c657d2e6..517fa6583 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs @@ -39,10 +39,8 @@ namespace Squidex.Infrastructure.CQRS.Commands this.nameResolver = nameResolver; } - public async Task GetByIdAsync(Guid id, int version = int.MaxValue) where TDomainObject : class, IAggregate + public async Task GetByIdAsync(Guid id, long? expectedVersion = null) where TDomainObject : class, IAggregate { - Guard.GreaterThan(version, 0, nameof(version)); - var streamName = nameResolver.GetStreamName(typeof(TDomainObject), id); var events = await eventStore.GetEventsAsync(streamName).ToList(); @@ -61,9 +59,9 @@ namespace Squidex.Infrastructure.CQRS.Commands domainObject.ApplyEvent(envelope); } - if (domainObject.Version != version && version < int.MaxValue) + if (expectedVersion != null && domainObject.Version != expectedVersion.Value) { - throw new DomainObjectVersionException(id.ToString(), typeof(TDomainObject), domainObject.Version, version); + throw new DomainObjectVersionException(id.ToString(), typeof(TDomainObject), domainObject.Version, expectedVersion.Value); } return domainObject; diff --git a/src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs index 1f8bc892b..cccfb402a 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using NodaTime; +using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Commands { @@ -31,7 +32,7 @@ namespace Squidex.Infrastructure.CQRS.Commands timestampCommand.Timestamp = clock.GetCurrentInstant(); } - return Task.FromResult(false); + return TaskHelper.False; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs b/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs new file mode 100644 index 000000000..548d83429 --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// EntityCreatedResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.CQRS.Commands +{ + public sealed class EntityCreatedResult : EntitySavedResult + { + public T IdOrValue { get; } + + public EntityCreatedResult(T idOrValue, long version) + : base(version) + { + IdOrValue = idOrValue; + } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventProcessor.cs b/src/Squidex.Infrastructure/CQRS/Commands/EntitySavedResult.cs similarity index 54% rename from src/Squidex.Infrastructure/CQRS/Events/IEventProcessor.cs rename to src/Squidex.Infrastructure/CQRS/Commands/EntitySavedResult.cs index 9a94a51fe..deef50b7c 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/IEventProcessor.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/EntitySavedResult.cs @@ -1,18 +1,20 @@ // ========================================================================== -// IEventProcessor.cs +// EntitySavedResult.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS.Commands; - -namespace Squidex.Infrastructure.CQRS.Events +namespace Squidex.Infrastructure.CQRS.Commands { - public interface IEventProcessor + public class EntitySavedResult { - Task ProcessEventAsync(Envelope @event, IAggregate aggregate, ICommand command); + public long Version { get; } + + public EntitySavedResult(long version) + { + Version = version; + } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs index 0ebafa736..5d7ca75bb 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs @@ -13,8 +13,8 @@ namespace Squidex.Infrastructure.CQRS.Commands { public interface IAggregateHandler { - Task CreateAsync(IAggregateCommand command, Func creator) where T : class, IAggregate; + Task CreateAsync(CommandContext context, Func creator) where T : class, IAggregate; - Task UpdateAsync(IAggregateCommand command, Func updater) where T : class, IAggregate; + Task UpdateAsync(CommandContext context, Func updater) where T : class, IAggregate; } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs b/src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs index c1834662e..fb3516b3b 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs @@ -10,5 +10,6 @@ namespace Squidex.Infrastructure.CQRS.Commands { public interface ICommand { + long? ExpectedVersion { get; set; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs b/src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs index 925076952..2aa0ee679 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs @@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { public interface IDomainObjectRepository { - Task GetByIdAsync(Guid id, int version = int.MaxValue) where TDomainObject : class, IAggregate; + Task GetByIdAsync(Guid id, long? expectedVersion = null) where TDomainObject : class, IAggregate; Task SaveAsync(IAggregate domainObject, ICollection> events, Guid commitId); } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs index e75660e1d..0e31d7548 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf @@ -36,7 +37,7 @@ namespace Squidex.Infrastructure.CQRS.Commands logger.LogCritical(InfrastructureErrors.CommandUnknown, exception, "Unknown command {0}", context.Command); } - return Task.FromResult(false); + return TaskHelper.False; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs index 37a4e6d00..d95a02cd6 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Commands { @@ -24,7 +25,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { logger.LogInformation("Handling {0} command", context.Command); - return Task.FromResult(false); + return TaskHelper.False; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/EnrichWithAggregateIdProcessor.cs b/src/Squidex.Infrastructure/CQRS/Events/EnrichWithAggregateIdProcessor.cs deleted file mode 100644 index e8b07466d..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/EnrichWithAggregateIdProcessor.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// EnrichWithAggregateIdProcessor.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.CQRS.Events -{ - public sealed class EnrichWithAggregateIdProcessor : IEventProcessor - { - public Task ProcessEventAsync(Envelope @event, IAggregate aggregate, ICommand command) - { - var aggregateCommand = command as IAggregateCommand; - - if (aggregateCommand != null) - { - @event.SetAggregateId(aggregateCommand.AggregateId); - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Infrastructure/DomainObjectVersionException.cs b/src/Squidex.Infrastructure/DomainObjectVersionException.cs index 9a269cbbc..19599594d 100644 --- a/src/Squidex.Infrastructure/DomainObjectVersionException.cs +++ b/src/Squidex.Infrastructure/DomainObjectVersionException.cs @@ -12,20 +12,20 @@ namespace Squidex.Infrastructure { public class DomainObjectVersionException : DomainObjectException { - private readonly int currentVersion; - private readonly int expectedVersion; + private readonly long currentVersion; + private readonly long expectedVersion; - public int CurrentVersion + public long CurrentVersion { get { return currentVersion; } } - public int ExpectedVersion + public long ExpectedVersion { get { return expectedVersion; } } - public DomainObjectVersionException(string id, Type type, int currentVersion, int expectedVersion) + public DomainObjectVersionException(string id, Type type, long currentVersion, long expectedVersion) : base(FormatMessage(id, type, currentVersion, expectedVersion), id, type) { this.currentVersion = currentVersion; @@ -33,7 +33,7 @@ namespace Squidex.Infrastructure this.expectedVersion = expectedVersion; } - private static string FormatMessage(string id, Type type, int currentVersion, int expectedVersion) + private static string FormatMessage(string id, Type type, long currentVersion, long expectedVersion) { return $"Requested version {expectedVersion} for object '{id}' (type {type}), but found {currentVersion}."; } diff --git a/src/Squidex.Infrastructure/Tasks/TaskHelper.cs b/src/Squidex.Infrastructure/Tasks/TaskHelper.cs index c0ad8286c..eb2e1c1e0 100644 --- a/src/Squidex.Infrastructure/Tasks/TaskHelper.cs +++ b/src/Squidex.Infrastructure/Tasks/TaskHelper.cs @@ -13,6 +13,8 @@ namespace Squidex.Infrastructure.Tasks public static class TaskHelper { public static readonly Task Done = CreateDoneTask(); + public static readonly Task False = CreateResultTask(false); + public static readonly Task True = CreateResultTask(true); private static Task CreateDoneTask() { @@ -22,5 +24,14 @@ namespace Squidex.Infrastructure.Tasks return result.Task; } + + private static Task CreateResultTask(bool value) + { + var result = new TaskCompletionSource(); + + result.SetResult(value); + + return result.Task; + } } } \ No newline at end of file diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs index 6d280253c..8ee5515bf 100644 --- a/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs @@ -25,6 +25,10 @@ namespace Squidex.Read.MongoDb.Apps [BsonElement] public string MasterLanguage { get; set; } + [BsonRequired] + [BsonElement] + public long Version { get; set; } + [BsonRequired] [BsonElement] public HashSet Languages { get; set; } = new HashSet(); diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs index 6d9d4dfcf..729356ef9 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs @@ -36,6 +36,10 @@ namespace Squidex.Read.MongoDb.Contents [BsonElement] public string Text { get; set; } + [BsonRequired] + [BsonElement] + public long Version { get; set; } + [BsonRequired] [BsonElement] public Guid AppId { get; set; } diff --git a/src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs b/src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs index b3e7dec12..7a2ff5a2f 100644 --- a/src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs +++ b/src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Read.MongoDb.History { - public sealed class MongoHistoryEventEntity : MongoEntity, IAppRefEntity, ITrackCreatedByEntity + public sealed class MongoHistoryEventEntity : MongoEntity, IAppRefEntity, IEntityWithCreatedBy { [BsonRequired] [BsonElement] @@ -40,7 +40,7 @@ namespace Squidex.Read.MongoDb.History [BsonElement] public Dictionary Parameters { get; set; } - RefToken ITrackCreatedByEntity.CreatedBy + RefToken IEntityWithCreatedBy.CreatedBy { get { diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs index ddbced902..a76e2ae6e 100644 --- a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs @@ -33,6 +33,10 @@ namespace Squidex.Read.MongoDb.Schemas [BsonElement] public string Schema { get; set; } + [BsonRequired] + [BsonElement] + public long Version { get; set; } + [BsonRequired] [BsonElement] public Guid AppId { get; set; } diff --git a/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs b/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs index 0e628b70e..bff43a0be 100644 --- a/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs +++ b/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs @@ -23,6 +23,7 @@ namespace Squidex.Read.MongoDb.Utils SetId(headers, entity); + SetVersion(headers, entity); SetCreated(headers, entity); SetCreatedBy(@event, entity); @@ -33,6 +34,7 @@ namespace Squidex.Read.MongoDb.Utils public static T Update(SquidexEvent @event, EnvelopeHeaders headers, T entity) where T : MongoEntity, new() { + SetVersion(headers, entity); SetLastModified(headers, entity); SetLastModifiedBy(@event, entity); @@ -54,23 +56,33 @@ namespace Squidex.Read.MongoDb.Utils entity.LastModified = headers.Timestamp(); } + private static void SetVersion(EnvelopeHeaders headers, MongoEntity entity) + { + var withVersion = entity as IEntityWithVersion; + + if (withVersion != null) + { + withVersion.Version = headers.EventNumber(); + } + } + private static void SetCreatedBy(SquidexEvent @event, MongoEntity entity) { - var createdBy = entity as ITrackCreatedByEntity; + var withCreatedBy = entity as IEntityWithCreatedBy; - if (createdBy != null) + if (withCreatedBy != null) { - createdBy.CreatedBy = @event.Actor; + withCreatedBy.CreatedBy = @event.Actor; } } private static void SetLastModifiedBy(SquidexEvent @event, MongoEntity entity) { - var modifiedBy = entity as ITrackLastModifiedByEntity; + var withModifiedBy = entity as IEntityWithLastModifiedBy; - if (modifiedBy != null) + if (withModifiedBy != null) { - modifiedBy.LastModifiedBy = @event.Actor; + withModifiedBy.LastModifiedBy = @event.Actor; } } diff --git a/src/Squidex.Read/Apps/IAppEntity.cs b/src/Squidex.Read/Apps/IAppEntity.cs index 79b7aa401..455aa00dd 100644 --- a/src/Squidex.Read/Apps/IAppEntity.cs +++ b/src/Squidex.Read/Apps/IAppEntity.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure; namespace Squidex.Read.Apps { - public interface IAppEntity : IEntity + public interface IAppEntity : IEntity, IEntityWithVersion { string Name { get; } diff --git a/src/Squidex.Read/Contents/IContentEntity.cs b/src/Squidex.Read/Contents/IContentEntity.cs index 78460b80e..f672b129c 100644 --- a/src/Squidex.Read/Contents/IContentEntity.cs +++ b/src/Squidex.Read/Contents/IContentEntity.cs @@ -10,7 +10,7 @@ using Squidex.Core.Contents; namespace Squidex.Read.Contents { - public interface IContentEntity : IAppRefEntity, ITrackCreatedByEntity, ITrackLastModifiedByEntity + public interface IContentEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { bool IsPublished { get; } diff --git a/src/Squidex.Read/ITrackCreatedByEntity.cs b/src/Squidex.Read/IEntityWithCreatedBy.cs similarity index 91% rename from src/Squidex.Read/ITrackCreatedByEntity.cs rename to src/Squidex.Read/IEntityWithCreatedBy.cs index 6d79a3f7f..5aad9c2f9 100644 --- a/src/Squidex.Read/ITrackCreatedByEntity.cs +++ b/src/Squidex.Read/IEntityWithCreatedBy.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure; namespace Squidex.Read { - public interface ITrackCreatedByEntity + public interface IEntityWithCreatedBy { RefToken CreatedBy { get; set; } } diff --git a/src/Squidex.Read/ITrackLastModifiedByEntity.cs b/src/Squidex.Read/IEntityWithLastModifiedBy.cs similarity index 90% rename from src/Squidex.Read/ITrackLastModifiedByEntity.cs rename to src/Squidex.Read/IEntityWithLastModifiedBy.cs index d19b36266..11976ab9a 100644 --- a/src/Squidex.Read/ITrackLastModifiedByEntity.cs +++ b/src/Squidex.Read/IEntityWithLastModifiedBy.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure; namespace Squidex.Read { - public interface ITrackLastModifiedByEntity + public interface IEntityWithLastModifiedBy { RefToken LastModifiedBy { get; set; } } diff --git a/src/Squidex.Read/IEntityWithVersion.cs b/src/Squidex.Read/IEntityWithVersion.cs new file mode 100644 index 000000000..195e53bf2 --- /dev/null +++ b/src/Squidex.Read/IEntityWithVersion.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// IEntityWithVersion.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Read +{ + public interface IEntityWithVersion + { + long Version { get; set; } + } +} diff --git a/src/Squidex.Read/Schemas/ISchemaEntity.cs b/src/Squidex.Read/Schemas/ISchemaEntity.cs index 33c5479ce..5d01a6f54 100644 --- a/src/Squidex.Read/Schemas/ISchemaEntity.cs +++ b/src/Squidex.Read/Schemas/ISchemaEntity.cs @@ -8,7 +8,7 @@ namespace Squidex.Read.Schemas { - public interface ISchemaEntity : IAppRefEntity, ITrackCreatedByEntity, ITrackLastModifiedByEntity + public interface ISchemaEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { string Name { get; } diff --git a/src/Squidex.Write/Apps/AppCommandHandler.cs b/src/Squidex.Write/Apps/AppCommandHandler.cs index d77de95f9..ddaad7535 100644 --- a/src/Squidex.Write/Apps/AppCommandHandler.cs +++ b/src/Squidex.Write/Apps/AppCommandHandler.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Tasks; using Squidex.Read.Apps.Repositories; using Squidex.Read.Users.Repositories; using Squidex.Write.Apps.Commands; @@ -51,10 +52,8 @@ namespace Squidex.Write.Apps throw new ValidationException("Cannot create a new app", error); } - await handler.CreateAsync(command, x => + await handler.CreateAsync(context, x => { - x.Create(command); - context.Succeed(command.AggregateId); }); } @@ -70,15 +69,15 @@ namespace Squidex.Write.Apps throw new ValidationException("Cannot assign contributor to app", error); } - await handler.UpdateAsync(command, x => + await handler.UpdateAsync(context, x => { - x.AssignContributor(command); + context.Succeed(new EntitySavedResult(x.Version)); }); } protected Task On(AttachClient command, CommandContext context) { - return handler.UpdateAsync(command, x => + return handler.UpdateAsync(context, x => { x.AttachClient(command, keyGenerator.GenerateKey()); @@ -88,37 +87,37 @@ namespace Squidex.Write.Apps protected Task On(RemoveContributor command, CommandContext context) { - return handler.UpdateAsync(command, x => x.RemoveContributor(command)); + return handler.UpdateAsync(context, x => x.RemoveContributor(command)); } protected Task On(RenameClient command, CommandContext context) { - return handler.UpdateAsync(command, x => x.RenameClient(command)); + return handler.UpdateAsync(context, x => x.RenameClient(command)); } protected Task On(RevokeClient command, CommandContext context) { - return handler.UpdateAsync(command, x => x.RevokeClient(command)); + return handler.UpdateAsync(context, x => x.RevokeClient(command)); } protected Task On(AddLanguage command, CommandContext context) { - return handler.UpdateAsync(command, x => x.AddLanguage(command)); + return handler.UpdateAsync(context, x => x.AddLanguage(command)); } protected Task On(RemoveLanguage command, CommandContext context) { - return handler.UpdateAsync(command, x => x.RemoveLanguage(command)); + return handler.UpdateAsync(context, x => x.RemoveLanguage(command)); } protected Task On(SetMasterLanguage command, CommandContext context) { - return handler.UpdateAsync(command, x => x.SetMasterLanguage(command)); + return handler.UpdateAsync(context, x => x.SetMasterLanguage(command)); } public Task HandleAsync(CommandContext context) { - return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context); + return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); } } } diff --git a/src/Squidex.Write/Contents/ContentCommandHandler.cs b/src/Squidex.Write/Contents/ContentCommandHandler.cs index e63785ca8..fe4376422 100644 --- a/src/Squidex.Write/Contents/ContentCommandHandler.cs +++ b/src/Squidex.Write/Contents/ContentCommandHandler.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Tasks; using Squidex.Read.Apps.Services; using Squidex.Read.Schemas.Services; using Squidex.Write.Contents.Commands; @@ -42,46 +43,41 @@ namespace Squidex.Write.Contents { await ValidateAsync(command, () => "Failed to create content"); - await handler.CreateAsync(command, s => - { - s.Create(command); - - context.Succeed(command.ContentId); - }); + await handler.CreateAsync(context, c => c.Create(command)); } protected async Task On(UpdateContent command, CommandContext context) { await ValidateAsync(command, () => "Failed to update content"); - await handler.UpdateAsync(command, s => s.Update(command)); + await handler.UpdateAsync(context, c => c.Update(command)); } protected async Task On(PatchContent command, CommandContext context) { await ValidateAsync(command, () => "Failed to patch content"); - await handler.UpdateAsync(command, s => s.Patch(command)); + await handler.UpdateAsync(context, c => c.Patch(command)); } protected Task On(PublishContent command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Publish(command)); + return handler.UpdateAsync(context, c => c.Publish(command)); } protected Task On(UnpublishContent command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Unpublish(command)); + return handler.UpdateAsync(context, c => c.Unpublish(command)); } protected Task On(DeleteContent command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Delete(command)); + return handler.UpdateAsync(context, c => c.Delete(command)); } public Task HandleAsync(CommandContext context) { - return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context); + return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); } private async Task ValidateAsync(ContentDataCommand command, Func message) diff --git a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs index 2e8af2d13..7bcd2cc34 100644 --- a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs +++ b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Tasks; using Squidex.Read.Schemas.Services; using Squidex.Write.Schemas.Commands; @@ -41,77 +42,72 @@ namespace Squidex.Write.Schemas throw new ValidationException("Cannot create a new schema", error); } - await handler.CreateAsync(command, s => - { - s.Create(command); - - context.Succeed(command.Name); - }); + await handler.CreateAsync(context, s => s.Create(command)); } protected Task On(AddField command, CommandContext context) { - return handler.UpdateAsync(command, s => + return handler.UpdateAsync(context, s => { s.AddField(command); - context.Succeed(s.Schema.Fields.Values.First(x => x.Name == command.Name).Id); + context.Succeed(new EntityCreatedResult(s.Schema.Fields.Values.First(x => x.Name == command.Name).Id, s.Version)); }); } protected Task On(DeleteSchema command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Delete(command)); + return handler.UpdateAsync(context, s => s.Delete(command)); } protected Task On(DeleteField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.DeleteField(command)); + return handler.UpdateAsync(context, s => s.DeleteField(command)); } protected Task On(DisableField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.DisableField(command)); + return handler.UpdateAsync(context, s => s.DisableField(command)); } protected Task On(EnableField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.EnableField(command)); + return handler.UpdateAsync(context, s => s.EnableField(command)); } protected Task On(HideField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.HideField(command)); + return handler.UpdateAsync(context, s => s.HideField(command)); } protected Task On(ShowField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.ShowField(command)); + return handler.UpdateAsync(context, s => s.ShowField(command)); } protected Task On(UpdateSchema command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Update(command)); + return handler.UpdateAsync(context, s => s.Update(command)); } protected Task On(UpdateField command, CommandContext context) { - return handler.UpdateAsync(command, s => { s.UpdateField(command); }); + return handler.UpdateAsync(context, s => s.UpdateField(command)); } protected Task On(PublishSchema command, CommandContext context) { - return handler.UpdateAsync(command, s => { s.Publish(command); }); + return handler.UpdateAsync(context, s => s.Publish(command)); } protected Task On(UnpublishSchema command, CommandContext context) { - return handler.UpdateAsync(command, s => { s.Unpublish(command); }); + return handler.UpdateAsync(context, s => s.Unpublish(command)); } public Task HandleAsync(CommandContext context) { - return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context); + return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); } } } diff --git a/src/Squidex.Write/SquidexCommand.cs b/src/Squidex.Write/SquidexCommand.cs index ae3637166..aed4baa52 100644 --- a/src/Squidex.Write/SquidexCommand.cs +++ b/src/Squidex.Write/SquidexCommand.cs @@ -14,5 +14,7 @@ namespace Squidex.Write public abstract class SquidexCommand : ICommand { public RefToken Actor { get; set; } + + public long? ExpectedVersion { get; set; } } } diff --git a/src/Squidex/Config/Domain/WriteModule.cs b/src/Squidex/Config/Domain/WriteModule.cs index 767a48e86..37206e9cc 100644 --- a/src/Squidex/Config/Domain/WriteModule.cs +++ b/src/Squidex/Config/Domain/WriteModule.cs @@ -10,7 +10,6 @@ using Autofac; using Microsoft.Extensions.Configuration; using Squidex.Core.Schemas; using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.CQRS.Events; using Squidex.Pipeline.CommandHandlers; using Squidex.Write.Apps; using Squidex.Write.Contents; @@ -29,6 +28,10 @@ namespace Squidex.Config.Domain protected override void Load(ContainerBuilder builder) { + builder.RegisterType() + .As() + .SingleInstance(); + builder.RegisterType() .As() .SingleInstance(); @@ -45,10 +48,6 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.RegisterType() - .As() - .SingleInstance(); - builder.RegisterType() .AsSelf() .SingleInstance(); @@ -69,6 +68,10 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); + builder.RegisterType() + .As() + .SingleInstance(); + builder.Register>(c => (id => new AppDomainObject(id, 0))) .AsSelf() .SingleInstance(); diff --git a/src/Squidex/Config/Identity/IdentityUsage.cs b/src/Squidex/Config/Identity/IdentityUsage.cs index c83eb7a51..1fef70487 100644 --- a/src/Squidex/Config/Identity/IdentityUsage.cs +++ b/src/Squidex/Config/Identity/IdentityUsage.cs @@ -19,6 +19,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; using Squidex.Core.Identity; +using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf @@ -140,7 +141,7 @@ namespace Squidex.Config.Identity { context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); - return Task.FromResult(true); + return TaskHelper.Done; } public override async Task CreatingTicket(OAuthCreatingTicketContext context) diff --git a/src/Squidex/Config/Swagger/XmlTagProcessor.cs b/src/Squidex/Config/Swagger/XmlTagProcessor.cs index b104971fb..19847d1bb 100644 --- a/src/Squidex/Config/Swagger/XmlTagProcessor.cs +++ b/src/Squidex/Config/Swagger/XmlTagProcessor.cs @@ -12,6 +12,7 @@ using NJsonSchema.Infrastructure; using NSwag.Annotations; using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; +using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf @@ -57,7 +58,7 @@ namespace Squidex.Config.Swagger context.OperationDescription.Operation.Tags.Add(tagAttribute.Name); } - return Task.FromResult(true); + return TaskHelper.True; } } } diff --git a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs b/src/Squidex/Controllers/Api/Apps/AppClientsController.cs index fd5237a82..90807b329 100644 --- a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppClientsController.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Controllers.Api.Apps.Models; using Squidex.Core.Identity; @@ -64,6 +65,8 @@ namespace Squidex.Controllers.Api.Apps var response = entity.Clients.Select(x => SimpleMapper.Map(x, new ClientDto())).ToList(); + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(response); } @@ -82,15 +85,15 @@ namespace Squidex.Controllers.Api.Apps /// [HttpPost] [Route("apps/{app}/clients/")] - [ProducesResponseType(typeof(ClientDto[]), 201)] + [ProducesResponseType(typeof(ClientDto), 201)] public async Task PostClient(string app, [FromBody] CreateAppClientDto request) { var context = await CommandBus.PublishAsync(SimpleMapper.Map(request, new AttachClient())); - var result = context.Result(); + var result = context.Result>().IdOrValue; var response = SimpleMapper.Map(result, new ClientDto()); - return StatusCode(201, response); + return CreatedAtAction(nameof(GetClients), new { app }, response); } /// @@ -105,7 +108,6 @@ namespace Squidex.Controllers.Api.Apps /// [HttpPut] [Route("apps/{app}/clients/{clientId}/")] - [ProducesResponseType(typeof(ClientDto[]), 201)] public async Task PutClient(string app, string clientId, [FromBody] UpdateAppClientDto request) { await CommandBus.PublishAsync(SimpleMapper.Map(request, new RenameClient { Id = clientId })); diff --git a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs b/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs index d94e47244..bd9133b3e 100644 --- a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; @@ -60,6 +61,8 @@ namespace Squidex.Controllers.Api.Apps var response = entity.Contributors.Select(x => SimpleMapper.Map(x, new ContributorDto())).ToList(); + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(response); } diff --git a/src/Squidex/Controllers/Api/Apps/AppController.cs b/src/Squidex/Controllers/Api/Apps/AppController.cs index e34195ab2..4e6a90b51 100644 --- a/src/Squidex/Controllers/Api/Apps/AppController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppController.cs @@ -92,9 +92,11 @@ namespace Squidex.Controllers.Api.Apps var command = SimpleMapper.Map(request, new CreateApp()); var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - return CreatedAtAction(nameof(GetApps), new EntityCreatedDto { Id = result.ToString() }); + var result = context.Result>(); + var response = new EntityCreatedDto { Id = result.ToString(), Version = result.Version }; + + return CreatedAtAction(nameof(GetApps), response); } } } diff --git a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs b/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs index 3dcc070b9..b1f37a5da 100644 --- a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; @@ -66,6 +67,8 @@ namespace Squidex.Controllers.Api.Apps return SimpleMapper.Map(x, new AppLanguageDto { IsMasterLanguage = isMasterLanguage }); }).ToList(); + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(model); } @@ -89,7 +92,7 @@ namespace Squidex.Controllers.Api.Apps var response = SimpleMapper.Map(request.Language, new AppLanguageDto()); - return StatusCode(201, response); + return CreatedAtAction(nameof(GetLanguages), new { app }, response); } /// diff --git a/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs b/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs index 2321855ea..9e6a4b929 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs +++ b/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs @@ -24,6 +24,11 @@ namespace Squidex.Controllers.Api.Apps.Models [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] public string Name { get; set; } + /// + /// The version of the app. + /// + public long Version { get; set; } + /// /// The name of the app. /// diff --git a/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs b/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs index 63ac305e0..86ca96ae2 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs +++ b/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs @@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations; namespace Squidex.Controllers.Api.Apps.Models { - public sealed class ClientDto + public class ClientDto { /// /// The client id. diff --git a/src/Squidex/Controllers/Api/EntityCreatedDto.cs b/src/Squidex/Controllers/Api/EntityCreatedDto.cs index fad9e6c60..b60d8dbb4 100644 --- a/src/Squidex/Controllers/Api/EntityCreatedDto.cs +++ b/src/Squidex/Controllers/Api/EntityCreatedDto.cs @@ -10,12 +10,17 @@ using System.ComponentModel.DataAnnotations; namespace Squidex.Controllers.Api { - public class EntityCreatedDto + public sealed class EntityCreatedDto { /// /// Id of the created entity. /// [Required] public string Id { get; set; } + + /// + /// The new version of the entity. + /// + public long Version { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs b/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs index 47b9d56ba..88d0ee4a6 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs +++ b/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs @@ -54,9 +54,11 @@ namespace Squidex.Controllers.Api.Schemas var command = new AddField { Name = request.Name, Properties = request.Properties.ToProperties() }; var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - return StatusCode(201, new EntityCreatedDto { Id = result.ToString() }); + var result = context.Result>().IdOrValue; + var response = new EntityCreatedDto { Id = result.ToString() }; + + return StatusCode(201, response); } /// diff --git a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs index 25bfbfd9f..e6b9cd0f9 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs +++ b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; @@ -80,6 +81,8 @@ namespace Squidex.Controllers.Api.Schemas var model = entity.ToModel(); + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(model); } diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 4599cc4ae..a69b4fcbe 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using Squidex.Controllers.Api; using Squidex.Controllers.ContentApi.Models; using Squidex.Core.Contents; @@ -91,20 +92,22 @@ namespace Squidex.Controllers.ContentApi return NotFound(); } - var content = await contentRepository.FindContentAsync(schemaEntity.Id, id); + var entity = await contentRepository.FindContentAsync(schemaEntity.Id, id); - if (content == null) + if (entity == null) { return NotFound(); } - var model = SimpleMapper.Map(content, new ContentDto()); + var model = SimpleMapper.Map(entity, new ContentDto()); - if (content.Data != null) + if (entity.Data != null) { - model.Data = content.Data.ToApiModel(schemaEntity.Schema, App.Languages, App.MasterLanguage, hidden); + model.Data = entity.Data.ToApiModel(schemaEntity.Schema, App.Languages, App.MasterLanguage, hidden); } + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(model); } @@ -115,9 +118,11 @@ namespace Squidex.Controllers.ContentApi var command = new CreateContent { Data = request, ContentId = Guid.NewGuid() }; var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - return CreatedAtAction(nameof(GetContent), new { id = result }, new EntityCreatedDto { Id = result.ToString() }); + var result = context.Result>().IdOrValue; + var response = new EntityCreatedDto { Id = result.ToString() }; + + return CreatedAtAction(nameof(GetContent), new { id = result }, response); } [HttpPut] diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs index 140bb0b8d..10bd02d85 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs @@ -80,6 +80,7 @@ namespace Squidex.Controllers.ContentApi.Generator GenerateSecurityDefinitions(); GenerateSecurityRequirements(); GenerateDefaultErrors(); + GeneratePing(); return document; } @@ -108,7 +109,7 @@ namespace Squidex.Controllers.ContentApi.Generator { ["x-logo"] = new { url = urlOptions.BuildUrl("images/logo-white.png", false), backgroundColor = "#3f83df" } }, - Title = $"Suidex API for {app.Name} App", + Title = $"Suidex API for {app.Name} App" }; } @@ -206,6 +207,24 @@ namespace Squidex.Controllers.ContentApi.Generator } } + private void GeneratePing() + { + var swaggerOperation = AddOperation(SwaggerOperationMethod.Get, null, $"ping/{app.Name}", operation => + { + operation.OperationId = "MakePingTest"; + + operation.Description = "Make a simple request, e.g. to test credentials."; + + operation.Summary = "Make Test"; + + }); + + foreach (var operation in swaggerOperation.Values) + { + operation.Tags = new List { "PingTest" }; + } + } + private SwaggerOperations GenerateSchemaQueryOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { return AddOperation(SwaggerOperationMethod.Get, null, $"{appBasePath}/{schema.Name}", operation => diff --git a/src/Squidex/Controllers/ContentApi/PingController.cs b/src/Squidex/Controllers/ContentApi/PingController.cs new file mode 100644 index 000000000..cc07b3fb8 --- /dev/null +++ b/src/Squidex/Controllers/ContentApi/PingController.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// PingController.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Squidex.Core.Identity; +using Squidex.Pipeline; + +namespace Squidex.Controllers.ContentApi +{ + [Authorize(Roles = SquidexRoles.AppEditor)] + [ApiExceptionFilter] + [ServiceFilter(typeof(AppFilterAttribute))] + public class PingController : ControllerBase + { + [HttpGet] + [Route("ping/{app}/")] + public IActionResult GetPing() + { + return Ok(); + } + } +} diff --git a/src/Squidex/Controllers/UI/Account/AccountController.cs b/src/Squidex/Controllers/UI/Account/AccountController.cs index 3e160cb73..b701907d1 100644 --- a/src/Squidex/Controllers/UI/Account/AccountController.cs +++ b/src/Squidex/Controllers/UI/Account/AccountController.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Options; using Squidex.Config; using Squidex.Config.Identity; using Squidex.Core.Identity; +using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf // ReSharper disable RedundantIfElseBlock @@ -216,7 +217,7 @@ namespace Squidex.Controllers.UI.Account { if (isFirst || !identityOptions.Value.LockAutomatically) { - return Task.FromResult(true); + return TaskHelper.True; } return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100))); @@ -226,7 +227,7 @@ namespace Squidex.Controllers.UI.Account { if (!isFirst) { - return Task.FromResult(true); + return TaskHelper.True; } return MakeIdentityOperation(() => userManager.AddToRoleAsync(user, SquidexRoles.Administrator)); diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs index 9ceb6d18e..ed0fc424c 100644 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Http; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Tasks; using Squidex.Write; // ReSharper disable InvertIf @@ -37,15 +38,10 @@ namespace Squidex.Pipeline.CommandHandlers FindActorFromSubject() ?? FindActorFromClient(); - if (actorToken == null) - { - throw new SecurityException("No actor with subject or client id available"); - } - - squidexCommand.Actor = actorToken; + squidexCommand.Actor = actorToken ?? throw new SecurityException("No actor with subject or client id available"); } - return Task.FromResult(false); + return TaskHelper.False; } private RefToken FindActorFromSubject() diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs index 30482bd9f..993ce1e7e 100644 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Tasks; using Squidex.Write; // ReSharper disable InvertIf @@ -42,7 +43,7 @@ namespace Squidex.Pipeline.CommandHandlers appCommand.AppId = new NamedId(appFeature.App.Id, appFeature.App.Name); } - return Task.FromResult(false); + return TaskHelper.False; } } } diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs new file mode 100644 index 000000000..c6637360e --- /dev/null +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// EnrichWithExpectedVersionHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Pipeline.CommandHandlers +{ + public sealed class EnrichWithExpectedVersionHandler : ICommandHandler + { + private readonly IHttpContextAccessor httpContextAccessor; + + public EnrichWithExpectedVersionHandler(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + public Task HandleAsync(CommandContext context) + { + var headers = httpContextAccessor.HttpContext.Request.GetTypedHeaders(); + var headerMatch = headers.IfMatch?.FirstOrDefault(); + + if (!string.IsNullOrWhiteSpace(headerMatch?.Tag) && long.TryParse(headerMatch.Tag, NumberStyles.Any, CultureInfo.InvariantCulture, out long expectedVersion)) + { + context.Command.ExpectedVersion = expectedVersion; + } + + return TaskHelper.False; + } + } +} diff --git a/src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs b/src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs new file mode 100644 index 000000000..37e18f5fb --- /dev/null +++ b/src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// SetVersionAsETagHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Pipeline.CommandHandlers +{ + public class SetVersionAsETagHandler : ICommandHandler + { + private readonly IHttpContextAccessor httpContextAccessor; + + public SetVersionAsETagHandler(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + public Task HandleAsync(CommandContext context) + { + var result = context.Result() as EntitySavedResult; + + if (result != null) + { + httpContextAccessor.HttpContext.Response.Headers["ETag"] = new StringValues(result.Version.ToString()); + } + + return TaskHelper.False; + } + } +} diff --git a/src/Squidex/Views/Shared/Docs.cshtml b/src/Squidex/Views/Shared/Docs.cshtml index 48ce58060..e0c66898d 100644 --- a/src/Squidex/Views/Shared/Docs.cshtml +++ b/src/Squidex/Views/Shared/Docs.cshtml @@ -26,7 +26,7 @@ - + diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs index c51c8490c..9c4b627af 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs @@ -25,11 +25,13 @@ namespace Squidex.Infrastructure.CQRS.Commands private sealed class MyCommand : IAggregateCommand { public Guid AggregateId { get; set; } + + public long? ExpectedVersion { get; set; } } private sealed class MyDomainObject : DomainObject { - public MyDomainObject(Guid id, int version) + public MyDomainObject(Guid id, int version) : base(id, version) { } @@ -48,19 +50,16 @@ namespace Squidex.Infrastructure.CQRS.Commands private readonly Mock factory = new Mock(); private readonly Mock repository = new Mock(); - private readonly Mock processor1 = new Mock(); - private readonly Mock processor2 = new Mock(); private readonly Envelope event1 = new Envelope(new MyEvent()); private readonly Envelope event2 = new Envelope(new MyEvent()); + private readonly CommandContext context; private readonly MyCommand command; private readonly AggregateHandler sut; private readonly MyDomainObject domainObject; public AggregateHandlerTests() { - var processors = new[] { processor1.Object, processor2.Object }; - - sut = new AggregateHandler(factory.Object, repository.Object, processors); + sut = new AggregateHandler(factory.Object, repository.Object); domainObject = new MyDomainObject(Guid.NewGuid(), 1) @@ -68,6 +67,7 @@ namespace Squidex.Infrastructure.CQRS.Commands .RaiseNewEvent(event2); command = new MyCommand { AggregateId = domainObject.Id }; + context = new CommandContext(command); } [Fact] @@ -89,21 +89,23 @@ namespace Squidex.Infrastructure.CQRS.Commands .Returns(domainObject) .Verifiable(); - await TestFlowAsync(async () => - { - MyDomainObject passedDomainObject = null; + repository.Setup(x => x.SaveAsync(domainObject, It.IsAny>>(), It.IsAny())) + .Returns(TaskHelper.Done) + .Verifiable(); - await sut.CreateAsync(command, x => - { - passedDomainObject = x; + MyDomainObject passedDomainObject = null; - return TaskHelper.Done; - }); + await sut.CreateAsync(context, async x => + { + await Task.Delay(1); - Assert.Equal(domainObject, passedDomainObject); + passedDomainObject = x; }); - factory.VerifyAll(); + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result>()); + + repository.VerifyAll(); } [Fact] @@ -113,95 +115,69 @@ namespace Squidex.Infrastructure.CQRS.Commands .Returns(domainObject) .Verifiable(); - await TestFlowAsync(async () => - { - MyDomainObject passedDomainObject = null; + repository.Setup(x => x.SaveAsync(domainObject, It.IsAny>>(), It.IsAny())) + .Returns(TaskHelper.Done) + .Verifiable(); - await sut.CreateAsync(command, x => - { - passedDomainObject = x; - }); + MyDomainObject passedDomainObject = null; - Assert.Equal(domainObject, passedDomainObject); + await sut.CreateAsync(context, x => + { + passedDomainObject = x; }); - factory.VerifyAll(); + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result>()); + + repository.VerifyAll(); } [Fact] public async Task Update_async_should_create_domain_object_and_save() { - repository.Setup(x => x.GetByIdAsync(command.AggregateId, int.MaxValue)) + repository.Setup(x => x.GetByIdAsync(command.AggregateId, null)) .Returns(Task.FromResult(domainObject)) .Verifiable(); - await TestFlowAsync(async () => - { - MyDomainObject passedDomainObject = null; + repository.Setup(x => x.SaveAsync(domainObject, It.IsAny>>(), It.IsAny())) + .Returns(TaskHelper.Done) + .Verifiable(); - await sut.UpdateAsync(command, x => - { - passedDomainObject = x; + MyDomainObject passedDomainObject = null; - return TaskHelper.Done; - }); + await sut.UpdateAsync(context, async x => + { + await Task.Delay(1); - Assert.Equal(domainObject, passedDomainObject); + passedDomainObject = x; }); + + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result()); + + repository.VerifyAll(); } [Fact] public async Task Update_sync_should_create_domain_object_and_save() { - repository.Setup(x => x.GetByIdAsync(command.AggregateId, int.MaxValue)) + repository.Setup(x => x.GetByIdAsync(command.AggregateId, null)) .Returns(Task.FromResult(domainObject)) .Verifiable(); - await TestFlowAsync(async () => - { - MyDomainObject passedDomainObject = null; - - await sut.UpdateAsync(command, x => - { - passedDomainObject = x; - }); - - Assert.Equal(domainObject, passedDomainObject); - }); - } - - private async Task TestFlowAsync(Func action) - { - repository.Setup(x => x.SaveAsync(domainObject, - It.IsAny>>(), - It.IsAny())) - .Returns(TaskHelper.Done) - .Verifiable(); - - processor1.Setup(x => x.ProcessEventAsync( - It.Is>(y => y.Payload == event1.Payload), domainObject, command)) + repository.Setup(x => x.SaveAsync(domainObject, It.IsAny>>(), It.IsAny())) .Returns(TaskHelper.Done) .Verifiable(); - processor2.Setup(x => x.ProcessEventAsync( - It.Is>(y => y.Payload == event1.Payload), domainObject, command)) - .Returns(TaskHelper.Done) - .Verifiable(); - - processor1.Setup(x => x.ProcessEventAsync( - It.Is>(y => y.Payload == event2.Payload), domainObject, command)) - .Returns(TaskHelper.Done) - .Verifiable(); + MyDomainObject passedDomainObject = null; - processor2.Setup(x => x.ProcessEventAsync( - It.Is>(y => y.Payload == event2.Payload), domainObject, command)) - .Returns(TaskHelper.Done) - .Verifiable(); - - await action(); + await sut.UpdateAsync(context, x => + { + passedDomainObject = x; + }); - processor1.VerifyAll(); - processor2.VerifyAll(); + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result()); repository.VerifyAll(); } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs index 189894e93..f98ca3d70 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using Moq; using System; using Xunit; @@ -13,11 +14,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { public class CommandContextTests { - private readonly MyCommand command = new MyCommand(); - - private sealed class MyCommand : ICommand - { - } + private readonly ICommand command = new Mock().Object; [Fact] public void Should_instantiate_and_provide_command() diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs index 997a3050c..850366649 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs @@ -14,6 +14,7 @@ using Squidex.Infrastructure.CQRS.Events; using Xunit; using System.Collections.Generic; using System.Linq; +using Squidex.Infrastructure.Tasks; // ReSharper disable ImplicitlyCapturedClosure // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable @@ -141,7 +142,8 @@ namespace Squidex.Infrastructure.CQRS.Commands eventDataFormatter.Setup(x => x.ToEventData(It.Is>(e => e.Payload == event2), commitId)).Returns(eventData2); eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 122, It.Is>(e => e.Count() == 2))) - .Returns(Task.FromResult(true)).Verifiable(); + .Returns(TaskHelper.Done) + .Verifiable(); domainObject.AddEvent(event1); domainObject.AddEvent(event2); @@ -166,7 +168,8 @@ namespace Squidex.Infrastructure.CQRS.Commands eventDataFormatter.Setup(x => x.ToEventData(It.Is>(e => e.Payload == event2), commitId)).Returns(eventData2); eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 122, new List { eventData1, eventData2 })) - .Throws(new WrongEventVersionException(1, 2)).Verifiable(); + .Throws(new WrongEventVersionException(1, 2)) + .Verifiable(); domainObject.AddEvent(event1); domainObject.AddEvent(event2); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs index 4140b90f8..c24c679d4 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs @@ -15,13 +15,11 @@ namespace Squidex.Infrastructure.CQRS.Commands { public sealed class EnrichWithTimestampHandlerTests { - private sealed class MyNormalCommand : ICommand - { - } - private sealed class MyTimestampCommand : ITimestampCommand { public Instant Timestamp { get; set; } + + public long? ExpectedVersion { get; set; } } private readonly Mock clock = new Mock(); @@ -47,7 +45,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { var sut = new EnrichWithTimestampHandler(clock.Object); - var result = await sut.HandleAsync(new CommandContext(new MyNormalCommand())); + var result = await sut.HandleAsync(new CommandContext(new Mock().Object)); Assert.False(result); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs index 408a4b423..e56468030 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs @@ -8,18 +8,16 @@ using System; using System.Threading.Tasks; +using Moq; +using Squidex.Infrastructure.Tasks; using Xunit; namespace Squidex.Infrastructure.CQRS.Commands { public class InMemoryCommandBusTests { - private readonly MyCommand command = new MyCommand(); - - private sealed class MyCommand : ICommand - { - } - + private readonly ICommand command = new Mock().Object; + private sealed class HandledHandler : ICommandHandler { public ICommand LastCommand; @@ -28,7 +26,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { LastCommand = context.Command; - return Task.FromResult(true); + return TaskHelper.True; } } @@ -40,7 +38,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { LastCommand = context.Command; - return Task.FromResult(false); + return TaskHelper.False; } } @@ -64,7 +62,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { LastException = context.Exception; - return Task.FromResult(false); + return TaskHelper.False; } } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs index 60f3da8c0..e2290acb2 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using Xunit; using System.Collections.Generic; using System.Linq; +using Moq; namespace Squidex.Infrastructure.CQRS.Commands { @@ -19,6 +20,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { private readonly MyLogger logger = new MyLogger(); private readonly LogExceptionHandler sut; + private readonly ICommand command = new Mock().Object; private sealed class MyLogger : ILogger { @@ -40,10 +42,6 @@ namespace Squidex.Infrastructure.CQRS.Commands } } - private sealed class MyCommand : ICommand - { - } - public LogExceptionHandlerTests() { sut = new LogExceptionHandler(logger); @@ -52,7 +50,7 @@ namespace Squidex.Infrastructure.CQRS.Commands [Fact] public async Task Should_do_nothing_if_command_is_succeeded() { - var context = new CommandContext(new MyCommand()); + var context = new CommandContext(command); context.Succeed(); @@ -65,7 +63,7 @@ namespace Squidex.Infrastructure.CQRS.Commands [Fact] public async Task Should_log_if_command_failed() { - var context = new CommandContext(new MyCommand()); + var context = new CommandContext(command); context.Fail(new InvalidOperationException()); @@ -78,8 +76,8 @@ namespace Squidex.Infrastructure.CQRS.Commands [Fact] public async Task Should_log_if_command_is_not_handled() { - var context = new CommandContext(new MyCommand()); - + var context = new CommandContext(command); + var isHandled = await sut.HandleAsync(context); Assert.False(isHandled); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs index c09e6a353..cbf04c40b 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs @@ -9,6 +9,7 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Moq; using Xunit; namespace Squidex.Infrastructure.CQRS.Commands @@ -17,6 +18,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { private readonly MyLogger logger = new MyLogger(); private readonly LogExecutingHandler sut; + private readonly ICommand command = new Mock().Object; private sealed class MyLogger : ILogger { @@ -37,11 +39,7 @@ namespace Squidex.Infrastructure.CQRS.Commands return null; } } - - private sealed class MyCommand : ICommand - { - } - + public LogExecutingHandlerTests() { sut = new LogExecutingHandler(logger); @@ -50,7 +48,7 @@ namespace Squidex.Infrastructure.CQRS.Commands [Fact] public async Task Should_log_once() { - var context = new CommandContext(new MyCommand()); + var context = new CommandContext(command); var isHandled = await sut.HandleAsync(context); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithAggregateIdProcessorTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithAggregateIdProcessorTests.cs deleted file mode 100644 index f75c712a3..000000000 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithAggregateIdProcessorTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// EnrichWithAggregateIdProcessorTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS.Commands; -using Xunit; - -namespace Squidex.Infrastructure.CQRS.Events -{ - public class EnrichWithAggregateIdProcessorTests - { - public sealed class MyAggregateIdCommand : IAggregateCommand - { - public Guid AggregateId { get; set; } - } - - public sealed class MyNormalCommand : ICommand - { - } - - public sealed class MyEvent : IEvent - { - } - - private readonly EnrichWithAggregateIdProcessor sut = new EnrichWithAggregateIdProcessor(); - - [Fact] - public async Task Should_not_do_anything_if_not_aggregate_command() - { - var envelope = new Envelope(new MyEvent()); - - await sut.ProcessEventAsync(envelope, null, new MyNormalCommand()); - - Assert.False(envelope.Headers.Contains("AggregateId")); - } - - [Fact] - public async Task Should_attach_aggregate_to_event_envelope() - { - var aggregateId = Guid.NewGuid(); - var aggregateCommand = new MyAggregateIdCommand { AggregateId = aggregateId }; - - var envelope = new Envelope(new MyEvent()); - - await sut.ProcessEventAsync(envelope, null, aggregateCommand); - - Assert.Equal(aggregateId, envelope.Headers.AggregateId()); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs index 4848058b0..e05616428 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs @@ -12,6 +12,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Moq; +using Squidex.Infrastructure.Tasks; using Xunit; // ReSharper disable UnusedAutoPropertyAccessor.Local @@ -134,7 +135,7 @@ namespace Squidex.Infrastructure.CQRS.Events { consumerInfo.LastHandledEventNumber = 2L; - eventConsumer.Setup(x => x.On(envelope1)).Returns(Task.FromResult(true)); + eventConsumer.Setup(x => x.On(envelope1)).Returns(TaskHelper.True); eventConsumer.Setup(x => x.On(envelope2)).Throws(new InvalidOperationException()); sut.Subscribe(eventConsumer.Object); diff --git a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs index aed05cd5b..2d36e6f38 100644 --- a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs @@ -48,7 +48,9 @@ namespace Squidex.Write.Apps { var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId }); - appRepository.Setup(x => x.FindAppAsync(AppName)).Returns(Task.FromResult(new Mock().Object)).Verifiable(); + appRepository.Setup(x => x.FindAppAsync(AppName)) + .Returns(Task.FromResult(new Mock().Object)) + .Verifiable(); await TestCreate(app, async _ => { @@ -63,7 +65,9 @@ namespace Squidex.Write.Apps { var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId }); - appRepository.Setup(x => x.FindAppAsync(AppName)).Returns(Task.FromResult(null)).Verifiable(); + appRepository.Setup(x => x.FindAppAsync(AppName)) + .Returns(Task.FromResult(null)) + .Verifiable(); await TestCreate(app, async _ => { @@ -135,7 +139,9 @@ namespace Squidex.Write.Apps [Fact] public async Task AttachClient_should_update_domain_object() { - keyGenerator.Setup(x => x.GenerateKey()).Returns(clientSecret).Verifiable(); + keyGenerator.Setup(x => x.GenerateKey()) + .Returns(clientSecret) + .Verifiable(); CreateApp(); diff --git a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs index b1d977fa2..68ebd1251 100644 --- a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs @@ -40,7 +40,9 @@ namespace Squidex.Write.Schemas { var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); - schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)).Returns(Task.FromResult(new Mock().Object)).Verifiable(); + schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)) + .Returns(Task.FromResult(new Mock().Object)) + .Verifiable(); await TestCreate(schema, async _ => { @@ -55,7 +57,9 @@ namespace Squidex.Write.Schemas { var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); - schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)).Returns(Task.FromResult(null)).Verifiable(); + schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)) + .Returns(Task.FromResult(null)) + .Verifiable(); await TestCreate(schema, async _ => { diff --git a/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs index c594f85e2..ecb1fdfbd 100644 --- a/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs +++ b/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs @@ -32,14 +32,14 @@ namespace Squidex.Write.TestHelpers IsUpdated = false; } - public Task CreateAsync(IAggregateCommand command, Func creator) where V : class, IAggregate + public Task CreateAsync(CommandContext context, Func creator) where V : class, IAggregate { IsCreated = true; return creator(domainObject as V); } - public Task UpdateAsync(IAggregateCommand command, Func updater) where V : class, IAggregate + public Task UpdateAsync(CommandContext context, Func updater) where V : class, IAggregate { IsUpdated = true;