Browse Source

ETag support

pull/1/head
Sebastian 10 years ago
parent
commit
4c5f00ddbd
  1. 3
      src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs
  2. 60
      src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs
  3. 17
      src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs
  4. 8
      src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs
  5. 3
      src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs
  6. 21
      src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs
  7. 16
      src/Squidex.Infrastructure/CQRS/Commands/EntitySavedResult.cs
  8. 4
      src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs
  9. 1
      src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs
  10. 2
      src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs
  11. 3
      src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs
  12. 3
      src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs
  13. 29
      src/Squidex.Infrastructure/CQRS/Events/EnrichWithAggregateIdProcessor.cs
  14. 12
      src/Squidex.Infrastructure/DomainObjectVersionException.cs
  15. 11
      src/Squidex.Infrastructure/Tasks/TaskHelper.cs
  16. 4
      src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs
  17. 4
      src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs
  18. 4
      src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs
  19. 4
      src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs
  20. 24
      src/Squidex.Read.MongoDb/Utils/EntityMapper.cs
  21. 2
      src/Squidex.Read/Apps/IAppEntity.cs
  22. 2
      src/Squidex.Read/Contents/IContentEntity.cs
  23. 2
      src/Squidex.Read/IEntityWithCreatedBy.cs
  24. 2
      src/Squidex.Read/IEntityWithLastModifiedBy.cs
  25. 15
      src/Squidex.Read/IEntityWithVersion.cs
  26. 2
      src/Squidex.Read/Schemas/ISchemaEntity.cs
  27. 25
      src/Squidex.Write/Apps/AppCommandHandler.cs
  28. 20
      src/Squidex.Write/Contents/ContentCommandHandler.cs
  29. 34
      src/Squidex.Write/Schemas/SchemaCommandHandler.cs
  30. 2
      src/Squidex.Write/SquidexCommand.cs
  31. 13
      src/Squidex/Config/Domain/WriteModule.cs
  32. 3
      src/Squidex/Config/Identity/IdentityUsage.cs
  33. 3
      src/Squidex/Config/Swagger/XmlTagProcessor.cs
  34. 10
      src/Squidex/Controllers/Api/Apps/AppClientsController.cs
  35. 3
      src/Squidex/Controllers/Api/Apps/AppContributorsController.cs
  36. 6
      src/Squidex/Controllers/Api/Apps/AppController.cs
  37. 5
      src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs
  38. 5
      src/Squidex/Controllers/Api/Apps/Models/AppDto.cs
  39. 2
      src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs
  40. 7
      src/Squidex/Controllers/Api/EntityCreatedDto.cs
  41. 6
      src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs
  42. 3
      src/Squidex/Controllers/Api/Schemas/SchemasController.cs
  43. 19
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  44. 21
      src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs
  45. 28
      src/Squidex/Controllers/ContentApi/PingController.cs
  46. 5
      src/Squidex/Controllers/UI/Account/AccountController.cs
  47. 10
      src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs
  48. 3
      src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs
  49. 40
      src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs
  50. 38
      src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs
  51. 2
      src/Squidex/Views/Shared/Docs.cshtml
  52. 128
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs
  53. 7
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs
  54. 7
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs
  55. 8
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs
  56. 16
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs
  57. 14
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs
  58. 10
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs
  59. 56
      tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithAggregateIdProcessorTests.cs
  60. 3
      tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs
  61. 12
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs
  62. 8
      tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs
  63. 4
      tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs

3
src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs

@ -11,6 +11,7 @@ using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.MongoDb namespace Squidex.Infrastructure.MongoDb
{ {
@ -127,7 +128,7 @@ namespace Squidex.Infrastructure.MongoDb
protected virtual Task SetupCollectionAsync(IMongoCollection<TEntity> collection) protected virtual Task SetupCollectionAsync(IMongoCollection<TEntity> collection)
{ {
return Task.FromResult(true); return TaskHelper.Done;
} }
public virtual Task ClearAsync() public virtual Task ClearAsync()

60
src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs

@ -7,9 +7,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
{ {
@ -17,7 +15,6 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
private readonly IDomainObjectRepository domainObjectRepository; private readonly IDomainObjectRepository domainObjectRepository;
private readonly IDomainObjectFactory domainObjectFactory; private readonly IDomainObjectFactory domainObjectFactory;
private readonly IEnumerable<IEventProcessor> eventProcessors;
public IDomainObjectRepository Repository public IDomainObjectRepository Repository
{ {
@ -31,55 +28,72 @@ namespace Squidex.Infrastructure.CQRS.Commands
public AggregateHandler( public AggregateHandler(
IDomainObjectFactory domainObjectFactory, IDomainObjectFactory domainObjectFactory,
IDomainObjectRepository domainObjectRepository, IDomainObjectRepository domainObjectRepository)
IEnumerable<IEventProcessor> eventProcessors)
{ {
Guard.NotNull(eventProcessors, nameof(eventProcessors));
Guard.NotNull(domainObjectFactory, nameof(domainObjectFactory)); Guard.NotNull(domainObjectFactory, nameof(domainObjectFactory));
Guard.NotNull(domainObjectRepository, nameof(domainObjectRepository)); Guard.NotNull(domainObjectRepository, nameof(domainObjectRepository));
this.domainObjectFactory = domainObjectFactory; this.domainObjectFactory = domainObjectFactory;
this.domainObjectRepository = domainObjectRepository; this.domainObjectRepository = domainObjectRepository;
this.eventProcessors = eventProcessors;
} }
public async Task CreateAsync<T>(IAggregateCommand command, Func<T, Task> creator) where T : class, IAggregate public async Task CreateAsync<T>(CommandContext context, Func<T, Task> creator) where T : class, IAggregate
{ {
Guard.NotNull(creator, nameof(creator)); Guard.NotNull(creator, nameof(creator));
Guard.NotNull(command, nameof(command)); Guard.NotNull(context, nameof(context));
Guard.NotEmpty(command.AggregateId, nameof(command.AggregateId));
var aggregate = domainObjectFactory.CreateNew<T>(command.AggregateId); var aggregateCommand = GetCommand(context);
var aggregate = (T)domainObjectFactory.CreateNew(typeof(T), aggregateCommand.AggregateId);
await creator(aggregate); await creator(aggregate);
await Save(command, aggregate); await SaveAsync(aggregate);
if (!context.IsHandled)
{
context.Succeed(new EntityCreatedResult<Guid>(aggregate.Id, aggregate.Version));
}
} }
public async Task UpdateAsync<T>(IAggregateCommand command, Func<T, Task> updater) where T : class, IAggregate public async Task UpdateAsync<T>(CommandContext context, Func<T, Task> updater) where T : class, IAggregate
{ {
Guard.NotNull(updater, nameof(updater)); Guard.NotNull(updater, nameof(updater));
Guard.NotNull(command, nameof(command)); Guard.NotNull(context, nameof(context));
Guard.NotEmpty(command.AggregateId, nameof(command.AggregateId));
var aggregate = await domainObjectRepository.GetByIdAsync<T>(command.AggregateId); var aggregateCommand = GetCommand(context);
var aggregate = await domainObjectRepository.GetByIdAsync<T>(aggregateCommand.AggregateId, aggregateCommand.ExpectedVersion);
await updater(aggregate); 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(); var events = aggregate.GetUncomittedEvents();
foreach (var @event in events) foreach (var @event in events)
{ {
foreach (var eventProcessor in eventProcessors) @event.SetAggregateId(aggregate.Id);
{
await eventProcessor.ProcessEventAsync(@event, aggregate, command);
}
} }
await domainObjectRepository.SaveAsync(aggregate, events, Guid.NewGuid()); await domainObjectRepository.SaveAsync(aggregate, events, Guid.NewGuid());

17
src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs

@ -14,14 +14,9 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
public static class CommandingExtensions public static class CommandingExtensions
{ {
public static T CreateNew<T>(this IDomainObjectFactory factory, Guid id) where T : IAggregate public static Task CreateAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> creator) where T : class, IAggregate
{ {
return (T)factory.CreateNew(typeof(T), id); return handler.CreateAsync<T>(context, x =>
}
public static Task CreateAsync<T>(this IAggregateHandler handler, IAggregateCommand command, Action<T> creator) where T : class, IAggregate
{
return handler.CreateAsync<T>(command, x =>
{ {
creator(x); creator(x);
@ -29,12 +24,12 @@ namespace Squidex.Infrastructure.CQRS.Commands
}); });
} }
public static Task UpdateAsync<T>(this IAggregateHandler handler, IAggregateCommand command, Action<T> creator) where T : class, IAggregate public static Task UpdateAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> updater) where T : class, IAggregate
{ {
return handler.UpdateAsync<T>(command, x => return handler.UpdateAsync<T>(context, x =>
{ {
creator(x); updater(x);
return TaskHelper.Done; return TaskHelper.Done;
}); });
} }

8
src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs

@ -39,10 +39,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
this.nameResolver = nameResolver; this.nameResolver = nameResolver;
} }
public async Task<TDomainObject> GetByIdAsync<TDomainObject>(Guid id, int version = int.MaxValue) where TDomainObject : class, IAggregate public async Task<TDomainObject> GetByIdAsync<TDomainObject>(Guid id, long? expectedVersion = null) where TDomainObject : class, IAggregate
{ {
Guard.GreaterThan(version, 0, nameof(version));
var streamName = nameResolver.GetStreamName(typeof(TDomainObject), id); var streamName = nameResolver.GetStreamName(typeof(TDomainObject), id);
var events = await eventStore.GetEventsAsync(streamName).ToList(); var events = await eventStore.GetEventsAsync(streamName).ToList();
@ -61,9 +59,9 @@ namespace Squidex.Infrastructure.CQRS.Commands
domainObject.ApplyEvent(envelope); 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; return domainObject;

3
src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs

@ -8,6 +8,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using NodaTime; using NodaTime;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
{ {
@ -31,7 +32,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
timestampCommand.Timestamp = clock.GetCurrentInstant(); timestampCommand.Timestamp = clock.GetCurrentInstant();
} }
return Task.FromResult(false); return TaskHelper.False;
} }
} }
} }

21
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<T> : EntitySavedResult
{
public T IdOrValue { get; }
public EntityCreatedResult(T idOrValue, long version)
: base(version)
{
IdOrValue = idOrValue;
}
}
}

16
src/Squidex.Infrastructure/CQRS/Events/IEventProcessor.cs → src/Squidex.Infrastructure/CQRS/Commands/EntitySavedResult.cs

@ -1,18 +1,20 @@
// ========================================================================== // ==========================================================================
// IEventProcessor.cs // EntitySavedResult.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System.Threading.Tasks; namespace Squidex.Infrastructure.CQRS.Commands
using Squidex.Infrastructure.CQRS.Commands;
namespace Squidex.Infrastructure.CQRS.Events
{ {
public interface IEventProcessor public class EntitySavedResult
{ {
Task ProcessEventAsync(Envelope<IEvent> @event, IAggregate aggregate, ICommand command); public long Version { get; }
public EntitySavedResult(long version)
{
Version = version;
}
} }
} }

4
src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs

@ -13,8 +13,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
public interface IAggregateHandler public interface IAggregateHandler
{ {
Task CreateAsync<T>(IAggregateCommand command, Func<T, Task> creator) where T : class, IAggregate; Task CreateAsync<T>(CommandContext context, Func<T, Task> creator) where T : class, IAggregate;
Task UpdateAsync<T>(IAggregateCommand command, Func<T, Task> updater) where T : class, IAggregate; Task UpdateAsync<T>(CommandContext context, Func<T, Task> updater) where T : class, IAggregate;
} }
} }

1
src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs

@ -10,5 +10,6 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
public interface ICommand public interface ICommand
{ {
long? ExpectedVersion { get; set; }
} }
} }

2
src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs

@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
public interface IDomainObjectRepository public interface IDomainObjectRepository
{ {
Task<TDomainObject> GetByIdAsync<TDomainObject>(Guid id, int version = int.MaxValue) where TDomainObject : class, IAggregate; Task<TDomainObject> GetByIdAsync<TDomainObject>(Guid id, long? expectedVersion = null) where TDomainObject : class, IAggregate;
Task SaveAsync(IAggregate domainObject, ICollection<Envelope<IEvent>> events, Guid commitId); Task SaveAsync(IAggregate domainObject, ICollection<Envelope<IEvent>> events, Guid commitId);
} }

3
src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs

@ -8,6 +8,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Squidex.Infrastructure.Tasks;
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -36,7 +37,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
logger.LogCritical(InfrastructureErrors.CommandUnknown, exception, "Unknown command {0}", context.Command); logger.LogCritical(InfrastructureErrors.CommandUnknown, exception, "Unknown command {0}", context.Command);
} }
return Task.FromResult(false); return TaskHelper.False;
} }
} }
} }

3
src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs

@ -8,6 +8,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
{ {
@ -24,7 +25,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
logger.LogInformation("Handling {0} command", context.Command); logger.LogInformation("Handling {0} command", context.Command);
return Task.FromResult(false); return TaskHelper.False;
} }
} }
} }

29
src/Squidex.Infrastructure/CQRS/Events/EnrichWithAggregateIdProcessor.cs

@ -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<IEvent> @event, IAggregate aggregate, ICommand command)
{
var aggregateCommand = command as IAggregateCommand;
if (aggregateCommand != null)
{
@event.SetAggregateId(aggregateCommand.AggregateId);
}
return TaskHelper.Done;
}
}
}

12
src/Squidex.Infrastructure/DomainObjectVersionException.cs

@ -12,20 +12,20 @@ namespace Squidex.Infrastructure
{ {
public class DomainObjectVersionException : DomainObjectException public class DomainObjectVersionException : DomainObjectException
{ {
private readonly int currentVersion; private readonly long currentVersion;
private readonly int expectedVersion; private readonly long expectedVersion;
public int CurrentVersion public long CurrentVersion
{ {
get { return currentVersion; } get { return currentVersion; }
} }
public int ExpectedVersion public long ExpectedVersion
{ {
get { return 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) : base(FormatMessage(id, type, currentVersion, expectedVersion), id, type)
{ {
this.currentVersion = currentVersion; this.currentVersion = currentVersion;
@ -33,7 +33,7 @@ namespace Squidex.Infrastructure
this.expectedVersion = expectedVersion; 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}."; return $"Requested version {expectedVersion} for object '{id}' (type {type}), but found {currentVersion}.";
} }

11
src/Squidex.Infrastructure/Tasks/TaskHelper.cs

@ -13,6 +13,8 @@ namespace Squidex.Infrastructure.Tasks
public static class TaskHelper public static class TaskHelper
{ {
public static readonly Task Done = CreateDoneTask(); public static readonly Task Done = CreateDoneTask();
public static readonly Task<bool> False = CreateResultTask(false);
public static readonly Task<bool> True = CreateResultTask(true);
private static Task CreateDoneTask() private static Task CreateDoneTask()
{ {
@ -22,5 +24,14 @@ namespace Squidex.Infrastructure.Tasks
return result.Task; return result.Task;
} }
private static Task<bool> CreateResultTask(bool value)
{
var result = new TaskCompletionSource<bool>();
result.SetResult(value);
return result.Task;
}
} }
} }

4
src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs

@ -25,6 +25,10 @@ namespace Squidex.Read.MongoDb.Apps
[BsonElement] [BsonElement]
public string MasterLanguage { get; set; } public string MasterLanguage { get; set; }
[BsonRequired]
[BsonElement]
public long Version { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
public HashSet<string> Languages { get; set; } = new HashSet<string>(); public HashSet<string> Languages { get; set; } = new HashSet<string>();

4
src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs

@ -36,6 +36,10 @@ namespace Squidex.Read.MongoDb.Contents
[BsonElement] [BsonElement]
public string Text { get; set; } public string Text { get; set; }
[BsonRequired]
[BsonElement]
public long Version { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
public Guid AppId { get; set; } public Guid AppId { get; set; }

4
src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs

@ -14,7 +14,7 @@ using Squidex.Infrastructure.MongoDb;
namespace Squidex.Read.MongoDb.History namespace Squidex.Read.MongoDb.History
{ {
public sealed class MongoHistoryEventEntity : MongoEntity, IAppRefEntity, ITrackCreatedByEntity public sealed class MongoHistoryEventEntity : MongoEntity, IAppRefEntity, IEntityWithCreatedBy
{ {
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
@ -40,7 +40,7 @@ namespace Squidex.Read.MongoDb.History
[BsonElement] [BsonElement]
public Dictionary<string, string> Parameters { get; set; } public Dictionary<string, string> Parameters { get; set; }
RefToken ITrackCreatedByEntity.CreatedBy RefToken IEntityWithCreatedBy.CreatedBy
{ {
get get
{ {

4
src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs

@ -33,6 +33,10 @@ namespace Squidex.Read.MongoDb.Schemas
[BsonElement] [BsonElement]
public string Schema { get; set; } public string Schema { get; set; }
[BsonRequired]
[BsonElement]
public long Version { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement] [BsonElement]
public Guid AppId { get; set; } public Guid AppId { get; set; }

24
src/Squidex.Read.MongoDb/Utils/EntityMapper.cs

@ -23,6 +23,7 @@ namespace Squidex.Read.MongoDb.Utils
SetId(headers, entity); SetId(headers, entity);
SetVersion(headers, entity);
SetCreated(headers, entity); SetCreated(headers, entity);
SetCreatedBy(@event, entity); SetCreatedBy(@event, entity);
@ -33,6 +34,7 @@ namespace Squidex.Read.MongoDb.Utils
public static T Update<T>(SquidexEvent @event, EnvelopeHeaders headers, T entity) where T : MongoEntity, new() public static T Update<T>(SquidexEvent @event, EnvelopeHeaders headers, T entity) where T : MongoEntity, new()
{ {
SetVersion(headers, entity);
SetLastModified(headers, entity); SetLastModified(headers, entity);
SetLastModifiedBy(@event, entity); SetLastModifiedBy(@event, entity);
@ -54,23 +56,33 @@ namespace Squidex.Read.MongoDb.Utils
entity.LastModified = headers.Timestamp(); 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) 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) 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;
} }
} }

2
src/Squidex.Read/Apps/IAppEntity.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure;
namespace Squidex.Read.Apps namespace Squidex.Read.Apps
{ {
public interface IAppEntity : IEntity public interface IAppEntity : IEntity, IEntityWithVersion
{ {
string Name { get; } string Name { get; }

2
src/Squidex.Read/Contents/IContentEntity.cs

@ -10,7 +10,7 @@ using Squidex.Core.Contents;
namespace Squidex.Read.Contents namespace Squidex.Read.Contents
{ {
public interface IContentEntity : IAppRefEntity, ITrackCreatedByEntity, ITrackLastModifiedByEntity public interface IContentEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion
{ {
bool IsPublished { get; } bool IsPublished { get; }

2
src/Squidex.Read/ITrackCreatedByEntity.cs → src/Squidex.Read/IEntityWithCreatedBy.cs

@ -10,7 +10,7 @@ using Squidex.Infrastructure;
namespace Squidex.Read namespace Squidex.Read
{ {
public interface ITrackCreatedByEntity public interface IEntityWithCreatedBy
{ {
RefToken CreatedBy { get; set; } RefToken CreatedBy { get; set; }
} }

2
src/Squidex.Read/ITrackLastModifiedByEntity.cs → src/Squidex.Read/IEntityWithLastModifiedBy.cs

@ -10,7 +10,7 @@ using Squidex.Infrastructure;
namespace Squidex.Read namespace Squidex.Read
{ {
public interface ITrackLastModifiedByEntity public interface IEntityWithLastModifiedBy
{ {
RefToken LastModifiedBy { get; set; } RefToken LastModifiedBy { get; set; }
} }

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

2
src/Squidex.Read/Schemas/ISchemaEntity.cs

@ -8,7 +8,7 @@
namespace Squidex.Read.Schemas namespace Squidex.Read.Schemas
{ {
public interface ISchemaEntity : IAppRefEntity, ITrackCreatedByEntity, ITrackLastModifiedByEntity public interface ISchemaEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion
{ {
string Name { get; } string Name { get; }

25
src/Squidex.Write/Apps/AppCommandHandler.cs

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Tasks;
using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Repositories;
using Squidex.Read.Users.Repositories; using Squidex.Read.Users.Repositories;
using Squidex.Write.Apps.Commands; using Squidex.Write.Apps.Commands;
@ -51,10 +52,8 @@ namespace Squidex.Write.Apps
throw new ValidationException("Cannot create a new app", error); throw new ValidationException("Cannot create a new app", error);
} }
await handler.CreateAsync<AppDomainObject>(command, x => await handler.CreateAsync<AppDomainObject>(context, x =>
{ {
x.Create(command);
context.Succeed(command.AggregateId); context.Succeed(command.AggregateId);
}); });
} }
@ -70,15 +69,15 @@ namespace Squidex.Write.Apps
throw new ValidationException("Cannot assign contributor to app", error); throw new ValidationException("Cannot assign contributor to app", error);
} }
await handler.UpdateAsync<AppDomainObject>(command, x => await handler.UpdateAsync<AppDomainObject>(context, x =>
{ {
x.AssignContributor(command); context.Succeed(new EntitySavedResult(x.Version));
}); });
} }
protected Task On(AttachClient command, CommandContext context) protected Task On(AttachClient command, CommandContext context)
{ {
return handler.UpdateAsync<AppDomainObject>(command, x => return handler.UpdateAsync<AppDomainObject>(context, x =>
{ {
x.AttachClient(command, keyGenerator.GenerateKey()); x.AttachClient(command, keyGenerator.GenerateKey());
@ -88,37 +87,37 @@ namespace Squidex.Write.Apps
protected Task On(RemoveContributor command, CommandContext context) protected Task On(RemoveContributor command, CommandContext context)
{ {
return handler.UpdateAsync<AppDomainObject>(command, x => x.RemoveContributor(command)); return handler.UpdateAsync<AppDomainObject>(context, x => x.RemoveContributor(command));
} }
protected Task On(RenameClient command, CommandContext context) protected Task On(RenameClient command, CommandContext context)
{ {
return handler.UpdateAsync<AppDomainObject>(command, x => x.RenameClient(command)); return handler.UpdateAsync<AppDomainObject>(context, x => x.RenameClient(command));
} }
protected Task On(RevokeClient command, CommandContext context) protected Task On(RevokeClient command, CommandContext context)
{ {
return handler.UpdateAsync<AppDomainObject>(command, x => x.RevokeClient(command)); return handler.UpdateAsync<AppDomainObject>(context, x => x.RevokeClient(command));
} }
protected Task On(AddLanguage command, CommandContext context) protected Task On(AddLanguage command, CommandContext context)
{ {
return handler.UpdateAsync<AppDomainObject>(command, x => x.AddLanguage(command)); return handler.UpdateAsync<AppDomainObject>(context, x => x.AddLanguage(command));
} }
protected Task On(RemoveLanguage command, CommandContext context) protected Task On(RemoveLanguage command, CommandContext context)
{ {
return handler.UpdateAsync<AppDomainObject>(command, x => x.RemoveLanguage(command)); return handler.UpdateAsync<AppDomainObject>(context, x => x.RemoveLanguage(command));
} }
protected Task On(SetMasterLanguage command, CommandContext context) protected Task On(SetMasterLanguage command, CommandContext context)
{ {
return handler.UpdateAsync<AppDomainObject>(command, x => x.SetMasterLanguage(command)); return handler.UpdateAsync<AppDomainObject>(context, x => x.SetMasterLanguage(command));
} }
public Task<bool> HandleAsync(CommandContext context) public Task<bool> HandleAsync(CommandContext context)
{ {
return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context); return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context);
} }
} }
} }

20
src/Squidex.Write/Contents/ContentCommandHandler.cs

@ -12,6 +12,7 @@ using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Tasks;
using Squidex.Read.Apps.Services; using Squidex.Read.Apps.Services;
using Squidex.Read.Schemas.Services; using Squidex.Read.Schemas.Services;
using Squidex.Write.Contents.Commands; using Squidex.Write.Contents.Commands;
@ -42,46 +43,41 @@ namespace Squidex.Write.Contents
{ {
await ValidateAsync(command, () => "Failed to create content"); await ValidateAsync(command, () => "Failed to create content");
await handler.CreateAsync<ContentDomainObject>(command, s => await handler.CreateAsync<ContentDomainObject>(context, c => c.Create(command));
{
s.Create(command);
context.Succeed(command.ContentId);
});
} }
protected async Task On(UpdateContent command, CommandContext context) protected async Task On(UpdateContent command, CommandContext context)
{ {
await ValidateAsync(command, () => "Failed to update content"); await ValidateAsync(command, () => "Failed to update content");
await handler.UpdateAsync<ContentDomainObject>(command, s => s.Update(command)); await handler.UpdateAsync<ContentDomainObject>(context, c => c.Update(command));
} }
protected async Task On(PatchContent command, CommandContext context) protected async Task On(PatchContent command, CommandContext context)
{ {
await ValidateAsync(command, () => "Failed to patch content"); await ValidateAsync(command, () => "Failed to patch content");
await handler.UpdateAsync<ContentDomainObject>(command, s => s.Patch(command)); await handler.UpdateAsync<ContentDomainObject>(context, c => c.Patch(command));
} }
protected Task On(PublishContent command, CommandContext context) protected Task On(PublishContent command, CommandContext context)
{ {
return handler.UpdateAsync<ContentDomainObject>(command, s => s.Publish(command)); return handler.UpdateAsync<ContentDomainObject>(context, c => c.Publish(command));
} }
protected Task On(UnpublishContent command, CommandContext context) protected Task On(UnpublishContent command, CommandContext context)
{ {
return handler.UpdateAsync<ContentDomainObject>(command, s => s.Unpublish(command)); return handler.UpdateAsync<ContentDomainObject>(context, c => c.Unpublish(command));
} }
protected Task On(DeleteContent command, CommandContext context) protected Task On(DeleteContent command, CommandContext context)
{ {
return handler.UpdateAsync<ContentDomainObject>(command, s => s.Delete(command)); return handler.UpdateAsync<ContentDomainObject>(context, c => c.Delete(command));
} }
public Task<bool> HandleAsync(CommandContext context) public Task<bool> 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<string> message) private async Task ValidateAsync(ContentDataCommand command, Func<string> message)

34
src/Squidex.Write/Schemas/SchemaCommandHandler.cs

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Tasks;
using Squidex.Read.Schemas.Services; using Squidex.Read.Schemas.Services;
using Squidex.Write.Schemas.Commands; using Squidex.Write.Schemas.Commands;
@ -41,77 +42,72 @@ namespace Squidex.Write.Schemas
throw new ValidationException("Cannot create a new schema", error); throw new ValidationException("Cannot create a new schema", error);
} }
await handler.CreateAsync<SchemaDomainObject>(command, s => await handler.CreateAsync<SchemaDomainObject>(context, s => s.Create(command));
{
s.Create(command);
context.Succeed(command.Name);
});
} }
protected Task On(AddField command, CommandContext context) protected Task On(AddField command, CommandContext context)
{ {
return handler.UpdateAsync<SchemaDomainObject>(command, s => return handler.UpdateAsync<SchemaDomainObject>(context, s =>
{ {
s.AddField(command); s.AddField(command);
context.Succeed(s.Schema.Fields.Values.First(x => x.Name == command.Name).Id); context.Succeed(new EntityCreatedResult<long>(s.Schema.Fields.Values.First(x => x.Name == command.Name).Id, s.Version));
}); });
} }
protected Task On(DeleteSchema command, CommandContext context) protected Task On(DeleteSchema command, CommandContext context)
{ {
return handler.UpdateAsync<SchemaDomainObject>(command, s => s.Delete(command)); return handler.UpdateAsync<SchemaDomainObject>(context, s => s.Delete(command));
} }
protected Task On(DeleteField command, CommandContext context) protected Task On(DeleteField command, CommandContext context)
{ {
return handler.UpdateAsync<SchemaDomainObject>(command, s => s.DeleteField(command)); return handler.UpdateAsync<SchemaDomainObject>(context, s => s.DeleteField(command));
} }
protected Task On(DisableField command, CommandContext context) protected Task On(DisableField command, CommandContext context)
{ {
return handler.UpdateAsync<SchemaDomainObject>(command, s => s.DisableField(command)); return handler.UpdateAsync<SchemaDomainObject>(context, s => s.DisableField(command));
} }
protected Task On(EnableField command, CommandContext context) protected Task On(EnableField command, CommandContext context)
{ {
return handler.UpdateAsync<SchemaDomainObject>(command, s => s.EnableField(command)); return handler.UpdateAsync<SchemaDomainObject>(context, s => s.EnableField(command));
} }
protected Task On(HideField command, CommandContext context) protected Task On(HideField command, CommandContext context)
{ {
return handler.UpdateAsync<SchemaDomainObject>(command, s => s.HideField(command)); return handler.UpdateAsync<SchemaDomainObject>(context, s => s.HideField(command));
} }
protected Task On(ShowField command, CommandContext context) protected Task On(ShowField command, CommandContext context)
{ {
return handler.UpdateAsync<SchemaDomainObject>(command, s => s.ShowField(command)); return handler.UpdateAsync<SchemaDomainObject>(context, s => s.ShowField(command));
} }
protected Task On(UpdateSchema command, CommandContext context) protected Task On(UpdateSchema command, CommandContext context)
{ {
return handler.UpdateAsync<SchemaDomainObject>(command, s => s.Update(command)); return handler.UpdateAsync<SchemaDomainObject>(context, s => s.Update(command));
} }
protected Task On(UpdateField command, CommandContext context) protected Task On(UpdateField command, CommandContext context)
{ {
return handler.UpdateAsync<SchemaDomainObject>(command, s => { s.UpdateField(command); }); return handler.UpdateAsync<SchemaDomainObject>(context, s => s.UpdateField(command));
} }
protected Task On(PublishSchema command, CommandContext context) protected Task On(PublishSchema command, CommandContext context)
{ {
return handler.UpdateAsync<SchemaDomainObject>(command, s => { s.Publish(command); }); return handler.UpdateAsync<SchemaDomainObject>(context, s => s.Publish(command));
} }
protected Task On(UnpublishSchema command, CommandContext context) protected Task On(UnpublishSchema command, CommandContext context)
{ {
return handler.UpdateAsync<SchemaDomainObject>(command, s => { s.Unpublish(command); }); return handler.UpdateAsync<SchemaDomainObject>(context, s => s.Unpublish(command));
} }
public Task<bool> HandleAsync(CommandContext context) public Task<bool> HandleAsync(CommandContext context)
{ {
return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context); return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context);
} }
} }
} }

2
src/Squidex.Write/SquidexCommand.cs

@ -14,5 +14,7 @@ namespace Squidex.Write
public abstract class SquidexCommand : ICommand public abstract class SquidexCommand : ICommand
{ {
public RefToken Actor { get; set; } public RefToken Actor { get; set; }
public long? ExpectedVersion { get; set; }
} }
} }

13
src/Squidex/Config/Domain/WriteModule.cs

@ -10,7 +10,6 @@ using Autofac;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Squidex.Core.Schemas; using Squidex.Core.Schemas;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Pipeline.CommandHandlers; using Squidex.Pipeline.CommandHandlers;
using Squidex.Write.Apps; using Squidex.Write.Apps;
using Squidex.Write.Contents; using Squidex.Write.Contents;
@ -29,6 +28,10 @@ namespace Squidex.Config.Domain
protected override void Load(ContainerBuilder builder) protected override void Load(ContainerBuilder builder)
{ {
builder.RegisterType<EnrichWithExpectedVersionHandler>()
.As<ICommandHandler>()
.SingleInstance();
builder.RegisterType<EnrichWithTimestampHandler>() builder.RegisterType<EnrichWithTimestampHandler>()
.As<ICommandHandler>() .As<ICommandHandler>()
.SingleInstance(); .SingleInstance();
@ -45,10 +48,6 @@ namespace Squidex.Config.Domain
.As<ICommandHandler>() .As<ICommandHandler>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<EnrichWithAggregateIdProcessor>()
.As<IEventProcessor>()
.SingleInstance();
builder.RegisterType<ClientKeyGenerator>() builder.RegisterType<ClientKeyGenerator>()
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();
@ -69,6 +68,10 @@ namespace Squidex.Config.Domain
.As<ICommandHandler>() .As<ICommandHandler>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<SetVersionAsETagHandler>()
.As<ICommandHandler>()
.SingleInstance();
builder.Register<DomainObjectFactoryFunction<AppDomainObject>>(c => (id => new AppDomainObject(id, 0))) builder.Register<DomainObjectFactoryFunction<AppDomainObject>>(c => (id => new AppDomainObject(id, 0)))
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();

3
src/Squidex/Config/Identity/IdentityUsage.cs

@ -19,6 +19,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Core.Identity; using Squidex.Core.Identity;
using Squidex.Infrastructure.Tasks;
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -140,7 +141,7 @@ namespace Squidex.Config.Identity
{ {
context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); context.Response.Redirect(context.RedirectUri + "&prompt=select_account");
return Task.FromResult(true); return TaskHelper.Done;
} }
public override async Task CreatingTicket(OAuthCreatingTicketContext context) public override async Task CreatingTicket(OAuthCreatingTicketContext context)

3
src/Squidex/Config/Swagger/XmlTagProcessor.cs

@ -12,6 +12,7 @@ using NJsonSchema.Infrastructure;
using NSwag.Annotations; using NSwag.Annotations;
using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors;
using NSwag.SwaggerGeneration.Processors.Contexts; using NSwag.SwaggerGeneration.Processors.Contexts;
using Squidex.Infrastructure.Tasks;
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -57,7 +58,7 @@ namespace Squidex.Config.Swagger
context.OperationDescription.Operation.Tags.Add(tagAttribute.Name); context.OperationDescription.Operation.Tags.Add(tagAttribute.Name);
} }
return Task.FromResult(true); return TaskHelper.True;
} }
} }
} }

10
src/Squidex/Controllers/Api/Apps/AppClientsController.cs

@ -10,6 +10,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Controllers.Api.Apps.Models; using Squidex.Controllers.Api.Apps.Models;
using Squidex.Core.Identity; 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(); var response = entity.Clients.Select(x => SimpleMapper.Map(x, new ClientDto())).ToList();
Response.Headers["ETag"] = new StringValues(entity.Version.ToString());
return Ok(response); return Ok(response);
} }
@ -82,15 +85,15 @@ namespace Squidex.Controllers.Api.Apps
/// </remarks> /// </remarks>
[HttpPost] [HttpPost]
[Route("apps/{app}/clients/")] [Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto[]), 201)] [ProducesResponseType(typeof(ClientDto), 201)]
public async Task<IActionResult> PostClient(string app, [FromBody] CreateAppClientDto request) public async Task<IActionResult> PostClient(string app, [FromBody] CreateAppClientDto request)
{ {
var context = await CommandBus.PublishAsync(SimpleMapper.Map(request, new AttachClient())); var context = await CommandBus.PublishAsync(SimpleMapper.Map(request, new AttachClient()));
var result = context.Result<AppClient>();
var result = context.Result<EntityCreatedResult<AppClient>>().IdOrValue;
var response = SimpleMapper.Map(result, new ClientDto()); var response = SimpleMapper.Map(result, new ClientDto());
return StatusCode(201, response); return CreatedAtAction(nameof(GetClients), new { app }, response);
} }
/// <summary> /// <summary>
@ -105,7 +108,6 @@ namespace Squidex.Controllers.Api.Apps
/// </returns> /// </returns>
[HttpPut] [HttpPut]
[Route("apps/{app}/clients/{clientId}/")] [Route("apps/{app}/clients/{clientId}/")]
[ProducesResponseType(typeof(ClientDto[]), 201)]
public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] UpdateAppClientDto request) public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] UpdateAppClientDto request)
{ {
await CommandBus.PublishAsync(SimpleMapper.Map(request, new RenameClient { Id = clientId })); await CommandBus.PublishAsync(SimpleMapper.Map(request, new RenameClient { Id = clientId }));

3
src/Squidex/Controllers/Api/Apps/AppContributorsController.cs

@ -10,6 +10,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection; 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(); var response = entity.Contributors.Select(x => SimpleMapper.Map(x, new ContributorDto())).ToList();
Response.Headers["ETag"] = new StringValues(entity.Version.ToString());
return Ok(response); return Ok(response);
} }

6
src/Squidex/Controllers/Api/Apps/AppController.cs

@ -92,9 +92,11 @@ namespace Squidex.Controllers.Api.Apps
var command = SimpleMapper.Map(request, new CreateApp()); var command = SimpleMapper.Map(request, new CreateApp());
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<Guid>();
return CreatedAtAction(nameof(GetApps), new EntityCreatedDto { Id = result.ToString() }); var result = context.Result<EntityCreatedResult<Guid>>();
var response = new EntityCreatedDto { Id = result.ToString(), Version = result.Version };
return CreatedAtAction(nameof(GetApps), response);
} }
} }
} }

5
src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs

@ -10,6 +10,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -66,6 +67,8 @@ namespace Squidex.Controllers.Api.Apps
return SimpleMapper.Map(x, new AppLanguageDto { IsMasterLanguage = isMasterLanguage }); return SimpleMapper.Map(x, new AppLanguageDto { IsMasterLanguage = isMasterLanguage });
}).ToList(); }).ToList();
Response.Headers["ETag"] = new StringValues(entity.Version.ToString());
return Ok(model); return Ok(model);
} }
@ -89,7 +92,7 @@ namespace Squidex.Controllers.Api.Apps
var response = SimpleMapper.Map(request.Language, new AppLanguageDto()); var response = SimpleMapper.Map(request.Language, new AppLanguageDto());
return StatusCode(201, response); return CreatedAtAction(nameof(GetLanguages), new { app }, response);
} }
/// <summary> /// <summary>

5
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]+)*$")] [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")]
public string Name { get; set; } public string Name { get; set; }
/// <summary>
/// The version of the app.
/// </summary>
public long Version { get; set; }
/// <summary> /// <summary>
/// The name of the app. /// The name of the app.
/// </summary> /// </summary>

2
src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs

@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Apps.Models namespace Squidex.Controllers.Api.Apps.Models
{ {
public sealed class ClientDto public class ClientDto
{ {
/// <summary> /// <summary>
/// The client id. /// The client id.

7
src/Squidex/Controllers/Api/EntityCreatedDto.cs

@ -10,12 +10,17 @@ using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api namespace Squidex.Controllers.Api
{ {
public class EntityCreatedDto public sealed class EntityCreatedDto
{ {
/// <summary> /// <summary>
/// Id of the created entity. /// Id of the created entity.
/// </summary> /// </summary>
[Required] [Required]
public string Id { get; set; } public string Id { get; set; }
/// <summary>
/// The new version of the entity.
/// </summary>
public long Version { get; set; }
} }
} }

6
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 command = new AddField { Name = request.Name, Properties = request.Properties.ToProperties() };
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<long>();
return StatusCode(201, new EntityCreatedDto { Id = result.ToString() }); var result = context.Result<EntityCreatedResult<long>>().IdOrValue;
var response = new EntityCreatedDto { Id = result.ToString() };
return StatusCode(201, response);
} }
/// <summary> /// <summary>

3
src/Squidex/Controllers/Api/Schemas/SchemasController.cs

@ -10,6 +10,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using NSwag.Annotations; using NSwag.Annotations;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -80,6 +81,8 @@ namespace Squidex.Controllers.Api.Schemas
var model = entity.ToModel(); var model = entity.ToModel();
Response.Headers["ETag"] = new StringValues(entity.Version.ToString());
return Ok(model); return Ok(model);
} }

19
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -12,6 +12,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Squidex.Controllers.Api; using Squidex.Controllers.Api;
using Squidex.Controllers.ContentApi.Models; using Squidex.Controllers.ContentApi.Models;
using Squidex.Core.Contents; using Squidex.Core.Contents;
@ -91,20 +92,22 @@ namespace Squidex.Controllers.ContentApi
return NotFound(); 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(); 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); return Ok(model);
} }
@ -115,9 +118,11 @@ namespace Squidex.Controllers.ContentApi
var command = new CreateContent { Data = request, ContentId = Guid.NewGuid() }; var command = new CreateContent { Data = request, ContentId = Guid.NewGuid() };
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<Guid>();
return CreatedAtAction(nameof(GetContent), new { id = result }, new EntityCreatedDto { Id = result.ToString() }); var result = context.Result<EntityCreatedResult<Guid>>().IdOrValue;
var response = new EntityCreatedDto { Id = result.ToString() };
return CreatedAtAction(nameof(GetContent), new { id = result }, response);
} }
[HttpPut] [HttpPut]

21
src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs

@ -80,6 +80,7 @@ namespace Squidex.Controllers.ContentApi.Generator
GenerateSecurityDefinitions(); GenerateSecurityDefinitions();
GenerateSecurityRequirements(); GenerateSecurityRequirements();
GenerateDefaultErrors(); GenerateDefaultErrors();
GeneratePing();
return document; return document;
} }
@ -108,7 +109,7 @@ namespace Squidex.Controllers.ContentApi.Generator
{ {
["x-logo"] = new { url = urlOptions.BuildUrl("images/logo-white.png", false), backgroundColor = "#3f83df" } ["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<string> { "PingTest" };
}
}
private SwaggerOperations GenerateSchemaQueryOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) private SwaggerOperations GenerateSchemaQueryOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema)
{ {
return AddOperation(SwaggerOperationMethod.Get, null, $"{appBasePath}/{schema.Name}", operation => return AddOperation(SwaggerOperationMethod.Get, null, $"{appBasePath}/{schema.Name}", operation =>

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

5
src/Squidex/Controllers/UI/Account/AccountController.cs

@ -22,6 +22,7 @@ using Microsoft.Extensions.Options;
using Squidex.Config; using Squidex.Config;
using Squidex.Config.Identity; using Squidex.Config.Identity;
using Squidex.Core.Identity; using Squidex.Core.Identity;
using Squidex.Infrastructure.Tasks;
// ReSharper disable InvertIf // ReSharper disable InvertIf
// ReSharper disable RedundantIfElseBlock // ReSharper disable RedundantIfElseBlock
@ -216,7 +217,7 @@ namespace Squidex.Controllers.UI.Account
{ {
if (isFirst || !identityOptions.Value.LockAutomatically) if (isFirst || !identityOptions.Value.LockAutomatically)
{ {
return Task.FromResult(true); return TaskHelper.True;
} }
return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100))); return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)));
@ -226,7 +227,7 @@ namespace Squidex.Controllers.UI.Account
{ {
if (!isFirst) if (!isFirst)
{ {
return Task.FromResult(true); return TaskHelper.True;
} }
return MakeIdentityOperation(() => userManager.AddToRoleAsync(user, SquidexRoles.Administrator)); return MakeIdentityOperation(() => userManager.AddToRoleAsync(user, SquidexRoles.Administrator));

10
src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Http;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Tasks;
using Squidex.Write; using Squidex.Write;
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -37,15 +38,10 @@ namespace Squidex.Pipeline.CommandHandlers
FindActorFromSubject() ?? FindActorFromSubject() ??
FindActorFromClient(); FindActorFromClient();
if (actorToken == null) squidexCommand.Actor = actorToken ?? throw new SecurityException("No actor with subject or client id available");
{
throw new SecurityException("No actor with subject or client id available");
}
squidexCommand.Actor = actorToken;
} }
return Task.FromResult(false); return TaskHelper.False;
} }
private RefToken FindActorFromSubject() private RefToken FindActorFromSubject()

3
src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Tasks;
using Squidex.Write; using Squidex.Write;
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -42,7 +43,7 @@ namespace Squidex.Pipeline.CommandHandlers
appCommand.AppId = new NamedId<Guid>(appFeature.App.Id, appFeature.App.Name); appCommand.AppId = new NamedId<Guid>(appFeature.App.Id, appFeature.App.Name);
} }
return Task.FromResult(false); return TaskHelper.False;
} }
} }
} }

40
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<bool> 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;
}
}
}

38
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<bool> HandleAsync(CommandContext context)
{
var result = context.Result<object>() as EntitySavedResult;
if (result != null)
{
httpContextAccessor.HttpContext.Response.Headers["ETag"] = new StringValues(result.Version.ToString());
}
return TaskHelper.False;
}
}
}

2
src/Squidex/Views/Shared/Docs.cshtml

@ -26,7 +26,7 @@
</style> </style>
</head> </head>
<body> <body>
<redoc spec-url="@Url.Content(ViewBag.Specification)"></redoc> <redoc lazy-rendering="true" spec-url="@Url.Content(ViewBag.Specification)"></redoc>
<script src="https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"></script> <script src="https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"></script>
</body> </body>

128
tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs

@ -25,11 +25,13 @@ namespace Squidex.Infrastructure.CQRS.Commands
private sealed class MyCommand : IAggregateCommand private sealed class MyCommand : IAggregateCommand
{ {
public Guid AggregateId { get; set; } public Guid AggregateId { get; set; }
public long? ExpectedVersion { get; set; }
} }
private sealed class MyDomainObject : DomainObject private sealed class MyDomainObject : DomainObject
{ {
public MyDomainObject(Guid id, int version) public MyDomainObject(Guid id, int version)
: base(id, version) : base(id, version)
{ {
} }
@ -48,19 +50,16 @@ namespace Squidex.Infrastructure.CQRS.Commands
private readonly Mock<IDomainObjectFactory> factory = new Mock<IDomainObjectFactory>(); private readonly Mock<IDomainObjectFactory> factory = new Mock<IDomainObjectFactory>();
private readonly Mock<IDomainObjectRepository> repository = new Mock<IDomainObjectRepository>(); private readonly Mock<IDomainObjectRepository> repository = new Mock<IDomainObjectRepository>();
private readonly Mock<IEventProcessor> processor1 = new Mock<IEventProcessor>();
private readonly Mock<IEventProcessor> processor2 = new Mock<IEventProcessor>();
private readonly Envelope<IEvent> event1 = new Envelope<IEvent>(new MyEvent()); private readonly Envelope<IEvent> event1 = new Envelope<IEvent>(new MyEvent());
private readonly Envelope<IEvent> event2 = new Envelope<IEvent>(new MyEvent()); private readonly Envelope<IEvent> event2 = new Envelope<IEvent>(new MyEvent());
private readonly CommandContext context;
private readonly MyCommand command; private readonly MyCommand command;
private readonly AggregateHandler sut; private readonly AggregateHandler sut;
private readonly MyDomainObject domainObject; private readonly MyDomainObject domainObject;
public AggregateHandlerTests() public AggregateHandlerTests()
{ {
var processors = new[] { processor1.Object, processor2.Object }; sut = new AggregateHandler(factory.Object, repository.Object);
sut = new AggregateHandler(factory.Object, repository.Object, processors);
domainObject = domainObject =
new MyDomainObject(Guid.NewGuid(), 1) new MyDomainObject(Guid.NewGuid(), 1)
@ -68,6 +67,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
.RaiseNewEvent(event2); .RaiseNewEvent(event2);
command = new MyCommand { AggregateId = domainObject.Id }; command = new MyCommand { AggregateId = domainObject.Id };
context = new CommandContext(command);
} }
[Fact] [Fact]
@ -89,21 +89,23 @@ namespace Squidex.Infrastructure.CQRS.Commands
.Returns(domainObject) .Returns(domainObject)
.Verifiable(); .Verifiable();
await TestFlowAsync(async () => repository.Setup(x => x.SaveAsync(domainObject, It.IsAny<ICollection<Envelope<IEvent>>>(), It.IsAny<Guid>()))
{ .Returns(TaskHelper.Done)
MyDomainObject passedDomainObject = null; .Verifiable();
await sut.CreateAsync<MyDomainObject>(command, x => MyDomainObject passedDomainObject = null;
{
passedDomainObject = x;
return TaskHelper.Done; await sut.CreateAsync<MyDomainObject>(context, async x =>
}); {
await Task.Delay(1);
Assert.Equal(domainObject, passedDomainObject); passedDomainObject = x;
}); });
factory.VerifyAll(); Assert.Equal(domainObject, passedDomainObject);
Assert.NotNull(context.Result<EntityCreatedResult<Guid>>());
repository.VerifyAll();
} }
[Fact] [Fact]
@ -113,95 +115,69 @@ namespace Squidex.Infrastructure.CQRS.Commands
.Returns(domainObject) .Returns(domainObject)
.Verifiable(); .Verifiable();
await TestFlowAsync(async () => repository.Setup(x => x.SaveAsync(domainObject, It.IsAny<ICollection<Envelope<IEvent>>>(), It.IsAny<Guid>()))
{ .Returns(TaskHelper.Done)
MyDomainObject passedDomainObject = null; .Verifiable();
await sut.CreateAsync<MyDomainObject>(command, x => MyDomainObject passedDomainObject = null;
{
passedDomainObject = x;
});
Assert.Equal(domainObject, passedDomainObject); await sut.CreateAsync<MyDomainObject>(context, x =>
{
passedDomainObject = x;
}); });
factory.VerifyAll(); Assert.Equal(domainObject, passedDomainObject);
Assert.NotNull(context.Result<EntityCreatedResult<Guid>>());
repository.VerifyAll();
} }
[Fact] [Fact]
public async Task Update_async_should_create_domain_object_and_save() public async Task Update_async_should_create_domain_object_and_save()
{ {
repository.Setup(x => x.GetByIdAsync<MyDomainObject>(command.AggregateId, int.MaxValue)) repository.Setup(x => x.GetByIdAsync<MyDomainObject>(command.AggregateId, null))
.Returns(Task.FromResult(domainObject)) .Returns(Task.FromResult(domainObject))
.Verifiable(); .Verifiable();
await TestFlowAsync(async () => repository.Setup(x => x.SaveAsync(domainObject, It.IsAny<ICollection<Envelope<IEvent>>>(), It.IsAny<Guid>()))
{ .Returns(TaskHelper.Done)
MyDomainObject passedDomainObject = null; .Verifiable();
await sut.UpdateAsync<MyDomainObject>(command, x => MyDomainObject passedDomainObject = null;
{
passedDomainObject = x;
return TaskHelper.Done; await sut.UpdateAsync<MyDomainObject>(context, async x =>
}); {
await Task.Delay(1);
Assert.Equal(domainObject, passedDomainObject); passedDomainObject = x;
}); });
Assert.Equal(domainObject, passedDomainObject);
Assert.NotNull(context.Result<EntitySavedResult>());
repository.VerifyAll();
} }
[Fact] [Fact]
public async Task Update_sync_should_create_domain_object_and_save() public async Task Update_sync_should_create_domain_object_and_save()
{ {
repository.Setup(x => x.GetByIdAsync<MyDomainObject>(command.AggregateId, int.MaxValue)) repository.Setup(x => x.GetByIdAsync<MyDomainObject>(command.AggregateId, null))
.Returns(Task.FromResult(domainObject)) .Returns(Task.FromResult(domainObject))
.Verifiable(); .Verifiable();
await TestFlowAsync(async () => repository.Setup(x => x.SaveAsync(domainObject, It.IsAny<ICollection<Envelope<IEvent>>>(), It.IsAny<Guid>()))
{
MyDomainObject passedDomainObject = null;
await sut.UpdateAsync<MyDomainObject>(command, x =>
{
passedDomainObject = x;
});
Assert.Equal(domainObject, passedDomainObject);
});
}
private async Task TestFlowAsync(Func<Task> action)
{
repository.Setup(x => x.SaveAsync(domainObject,
It.IsAny<ICollection<Envelope<IEvent>>>(),
It.IsAny<Guid>()))
.Returns(TaskHelper.Done)
.Verifiable();
processor1.Setup(x => x.ProcessEventAsync(
It.Is<Envelope<IEvent>>(y => y.Payload == event1.Payload), domainObject, command))
.Returns(TaskHelper.Done) .Returns(TaskHelper.Done)
.Verifiable(); .Verifiable();
processor2.Setup(x => x.ProcessEventAsync( MyDomainObject passedDomainObject = null;
It.Is<Envelope<IEvent>>(y => y.Payload == event1.Payload), domainObject, command))
.Returns(TaskHelper.Done)
.Verifiable();
processor1.Setup(x => x.ProcessEventAsync(
It.Is<Envelope<IEvent>>(y => y.Payload == event2.Payload), domainObject, command))
.Returns(TaskHelper.Done)
.Verifiable();
processor2.Setup(x => x.ProcessEventAsync( await sut.UpdateAsync<MyDomainObject>(context, x =>
It.Is<Envelope<IEvent>>(y => y.Payload == event2.Payload), domainObject, command)) {
.Returns(TaskHelper.Done) passedDomainObject = x;
.Verifiable(); });
await action();
processor1.VerifyAll(); Assert.Equal(domainObject, passedDomainObject);
processor2.VerifyAll(); Assert.NotNull(context.Result<EntitySavedResult>());
repository.VerifyAll(); repository.VerifyAll();
} }

7
tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs

@ -6,6 +6,7 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using Moq;
using System; using System;
using Xunit; using Xunit;
@ -13,11 +14,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
public class CommandContextTests public class CommandContextTests
{ {
private readonly MyCommand command = new MyCommand(); private readonly ICommand command = new Mock<ICommand>().Object;
private sealed class MyCommand : ICommand
{
}
[Fact] [Fact]
public void Should_instantiate_and_provide_command() public void Should_instantiate_and_provide_command()

7
tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs

@ -14,6 +14,7 @@ using Squidex.Infrastructure.CQRS.Events;
using Xunit; using Xunit;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Squidex.Infrastructure.Tasks;
// ReSharper disable ImplicitlyCapturedClosure // ReSharper disable ImplicitlyCapturedClosure
// ReSharper disable PrivateFieldCanBeConvertedToLocalVariable // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable
@ -141,7 +142,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
eventDataFormatter.Setup(x => x.ToEventData(It.Is<Envelope<IEvent>>(e => e.Payload == event2), commitId)).Returns(eventData2); eventDataFormatter.Setup(x => x.ToEventData(It.Is<Envelope<IEvent>>(e => e.Payload == event2), commitId)).Returns(eventData2);
eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 122, It.Is<IEnumerable<EventData>>(e => e.Count() == 2))) eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 122, It.Is<IEnumerable<EventData>>(e => e.Count() == 2)))
.Returns(Task.FromResult(true)).Verifiable(); .Returns(TaskHelper.Done)
.Verifiable();
domainObject.AddEvent(event1); domainObject.AddEvent(event1);
domainObject.AddEvent(event2); domainObject.AddEvent(event2);
@ -166,7 +168,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
eventDataFormatter.Setup(x => x.ToEventData(It.Is<Envelope<IEvent>>(e => e.Payload == event2), commitId)).Returns(eventData2); eventDataFormatter.Setup(x => x.ToEventData(It.Is<Envelope<IEvent>>(e => e.Payload == event2), commitId)).Returns(eventData2);
eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 122, new List<EventData> { eventData1, eventData2 })) eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 122, new List<EventData> { eventData1, eventData2 }))
.Throws(new WrongEventVersionException(1, 2)).Verifiable(); .Throws(new WrongEventVersionException(1, 2))
.Verifiable();
domainObject.AddEvent(event1); domainObject.AddEvent(event1);
domainObject.AddEvent(event2); domainObject.AddEvent(event2);

8
tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs

@ -15,13 +15,11 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
public sealed class EnrichWithTimestampHandlerTests public sealed class EnrichWithTimestampHandlerTests
{ {
private sealed class MyNormalCommand : ICommand
{
}
private sealed class MyTimestampCommand : ITimestampCommand private sealed class MyTimestampCommand : ITimestampCommand
{ {
public Instant Timestamp { get; set; } public Instant Timestamp { get; set; }
public long? ExpectedVersion { get; set; }
} }
private readonly Mock<IClock> clock = new Mock<IClock>(); private readonly Mock<IClock> clock = new Mock<IClock>();
@ -47,7 +45,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
var sut = new EnrichWithTimestampHandler(clock.Object); var sut = new EnrichWithTimestampHandler(clock.Object);
var result = await sut.HandleAsync(new CommandContext(new MyNormalCommand())); var result = await sut.HandleAsync(new CommandContext(new Mock<ICommand>().Object));
Assert.False(result); Assert.False(result);

16
tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs

@ -8,18 +8,16 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Moq;
using Squidex.Infrastructure.Tasks;
using Xunit; using Xunit;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
{ {
public class InMemoryCommandBusTests public class InMemoryCommandBusTests
{ {
private readonly MyCommand command = new MyCommand(); private readonly ICommand command = new Mock<ICommand>().Object;
private sealed class MyCommand : ICommand
{
}
private sealed class HandledHandler : ICommandHandler private sealed class HandledHandler : ICommandHandler
{ {
public ICommand LastCommand; public ICommand LastCommand;
@ -28,7 +26,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
LastCommand = context.Command; LastCommand = context.Command;
return Task.FromResult(true); return TaskHelper.True;
} }
} }
@ -40,7 +38,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
LastCommand = context.Command; LastCommand = context.Command;
return Task.FromResult(false); return TaskHelper.False;
} }
} }
@ -64,7 +62,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
LastException = context.Exception; LastException = context.Exception;
return Task.FromResult(false); return TaskHelper.False;
} }
} }

14
tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs

@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging;
using Xunit; using Xunit;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Moq;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
{ {
@ -19,6 +20,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
private readonly MyLogger logger = new MyLogger(); private readonly MyLogger logger = new MyLogger();
private readonly LogExceptionHandler sut; private readonly LogExceptionHandler sut;
private readonly ICommand command = new Mock<ICommand>().Object;
private sealed class MyLogger : ILogger<LogExceptionHandler> private sealed class MyLogger : ILogger<LogExceptionHandler>
{ {
@ -40,10 +42,6 @@ namespace Squidex.Infrastructure.CQRS.Commands
} }
} }
private sealed class MyCommand : ICommand
{
}
public LogExceptionHandlerTests() public LogExceptionHandlerTests()
{ {
sut = new LogExceptionHandler(logger); sut = new LogExceptionHandler(logger);
@ -52,7 +50,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
[Fact] [Fact]
public async Task Should_do_nothing_if_command_is_succeeded() public async Task Should_do_nothing_if_command_is_succeeded()
{ {
var context = new CommandContext(new MyCommand()); var context = new CommandContext(command);
context.Succeed(); context.Succeed();
@ -65,7 +63,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
[Fact] [Fact]
public async Task Should_log_if_command_failed() public async Task Should_log_if_command_failed()
{ {
var context = new CommandContext(new MyCommand()); var context = new CommandContext(command);
context.Fail(new InvalidOperationException()); context.Fail(new InvalidOperationException());
@ -78,8 +76,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
[Fact] [Fact]
public async Task Should_log_if_command_is_not_handled() 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); var isHandled = await sut.HandleAsync(context);
Assert.False(isHandled); Assert.False(isHandled);

10
tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs

@ -9,6 +9,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq;
using Xunit; using Xunit;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
@ -17,6 +18,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
private readonly MyLogger logger = new MyLogger(); private readonly MyLogger logger = new MyLogger();
private readonly LogExecutingHandler sut; private readonly LogExecutingHandler sut;
private readonly ICommand command = new Mock<ICommand>().Object;
private sealed class MyLogger : ILogger<LogExecutingHandler> private sealed class MyLogger : ILogger<LogExecutingHandler>
{ {
@ -37,11 +39,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
return null; return null;
} }
} }
private sealed class MyCommand : ICommand
{
}
public LogExecutingHandlerTests() public LogExecutingHandlerTests()
{ {
sut = new LogExecutingHandler(logger); sut = new LogExecutingHandler(logger);
@ -50,7 +48,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
[Fact] [Fact]
public async Task Should_log_once() public async Task Should_log_once()
{ {
var context = new CommandContext(new MyCommand()); var context = new CommandContext(command);
var isHandled = await sut.HandleAsync(context); var isHandled = await sut.HandleAsync(context);

56
tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithAggregateIdProcessorTests.cs

@ -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<IEvent>(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<IEvent>(new MyEvent());
await sut.ProcessEventAsync(envelope, null, aggregateCommand);
Assert.Equal(aggregateId, envelope.Headers.AggregateId());
}
}
}

3
tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs

@ -12,6 +12,7 @@ using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq; using Moq;
using Squidex.Infrastructure.Tasks;
using Xunit; using Xunit;
// ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable UnusedAutoPropertyAccessor.Local
@ -134,7 +135,7 @@ namespace Squidex.Infrastructure.CQRS.Events
{ {
consumerInfo.LastHandledEventNumber = 2L; 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()); eventConsumer.Setup(x => x.On(envelope2)).Throws(new InvalidOperationException());
sut.Subscribe(eventConsumer.Object); sut.Subscribe(eventConsumer.Object);

12
tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

@ -48,7 +48,9 @@ namespace Squidex.Write.Apps
{ {
var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId }); var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId });
appRepository.Setup(x => x.FindAppAsync(AppName)).Returns(Task.FromResult(new Mock<IAppEntity>().Object)).Verifiable(); appRepository.Setup(x => x.FindAppAsync(AppName))
.Returns(Task.FromResult(new Mock<IAppEntity>().Object))
.Verifiable();
await TestCreate(app, async _ => await TestCreate(app, async _ =>
{ {
@ -63,7 +65,9 @@ namespace Squidex.Write.Apps
{ {
var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId }); var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId });
appRepository.Setup(x => x.FindAppAsync(AppName)).Returns(Task.FromResult<IAppEntity>(null)).Verifiable(); appRepository.Setup(x => x.FindAppAsync(AppName))
.Returns(Task.FromResult<IAppEntity>(null))
.Verifiable();
await TestCreate(app, async _ => await TestCreate(app, async _ =>
{ {
@ -135,7 +139,9 @@ namespace Squidex.Write.Apps
[Fact] [Fact]
public async Task AttachClient_should_update_domain_object() 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(); CreateApp();

8
tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs

@ -40,7 +40,9 @@ namespace Squidex.Write.Schemas
{ {
var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId });
schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)).Returns(Task.FromResult(new Mock<ISchemaEntityWithSchema>().Object)).Verifiable(); schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName))
.Returns(Task.FromResult(new Mock<ISchemaEntityWithSchema>().Object))
.Verifiable();
await TestCreate(schema, async _ => await TestCreate(schema, async _ =>
{ {
@ -55,7 +57,9 @@ namespace Squidex.Write.Schemas
{ {
var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId });
schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)).Returns(Task.FromResult<ISchemaEntityWithSchema>(null)).Verifiable(); schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName))
.Returns(Task.FromResult<ISchemaEntityWithSchema>(null))
.Verifiable();
await TestCreate(schema, async _ => await TestCreate(schema, async _ =>
{ {

4
tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs

@ -32,14 +32,14 @@ namespace Squidex.Write.TestHelpers
IsUpdated = false; IsUpdated = false;
} }
public Task CreateAsync<V>(IAggregateCommand command, Func<V, Task> creator) where V : class, IAggregate public Task CreateAsync<V>(CommandContext context, Func<V, Task> creator) where V : class, IAggregate
{ {
IsCreated = true; IsCreated = true;
return creator(domainObject as V); return creator(domainObject as V);
} }
public Task UpdateAsync<V>(IAggregateCommand command, Func<V, Task> updater) where V : class, IAggregate public Task UpdateAsync<V>(CommandContext context, Func<V, Task> updater) where V : class, IAggregate
{ {
IsUpdated = true; IsUpdated = true;

Loading…
Cancel
Save