Browse Source

ETag support

pull/1/head
Sebastian 9 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 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<TEntity> collection)
{
return Task.FromResult(true);
return TaskHelper.Done;
}
public virtual Task ClearAsync()

60
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<IEventProcessor> eventProcessors;
public IDomainObjectRepository Repository
{
@ -31,55 +28,72 @@ namespace Squidex.Infrastructure.CQRS.Commands
public AggregateHandler(
IDomainObjectFactory domainObjectFactory,
IDomainObjectRepository domainObjectRepository,
IEnumerable<IEventProcessor> 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<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(command, nameof(command));
Guard.NotEmpty(command.AggregateId, nameof(command.AggregateId));
Guard.NotNull(context, nameof(context));
var aggregate = domainObjectFactory.CreateNew<T>(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<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(command, nameof(command));
Guard.NotEmpty(command.AggregateId, nameof(command.AggregateId));
Guard.NotNull(context, nameof(context));
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 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());

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

@ -14,14 +14,9 @@ namespace Squidex.Infrastructure.CQRS.Commands
{
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);
}
public static Task CreateAsync<T>(this IAggregateHandler handler, IAggregateCommand command, Action<T> creator) where T : class, IAggregate
{
return handler.CreateAsync<T>(command, x =>
return handler.CreateAsync<T>(context, 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;
});
}

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

@ -39,10 +39,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
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 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;

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

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
// ==========================================================================
// 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<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
{
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
{
long? ExpectedVersion { get; set; }
}
}

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

@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
{
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);
}

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

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

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
{
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}.";
}

11
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<bool> False = CreateResultTask(false);
public static readonly Task<bool> True = CreateResultTask(true);
private static Task CreateDoneTask()
{
@ -22,5 +24,14 @@ namespace Squidex.Infrastructure.Tasks
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]
public string MasterLanguage { get; set; }
[BsonRequired]
[BsonElement]
public long Version { get; set; }
[BsonRequired]
[BsonElement]
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]
public string Text { get; set; }
[BsonRequired]
[BsonElement]
public long Version { get; set; }
[BsonRequired]
[BsonElement]
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
{
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<string, string> Parameters { get; set; }
RefToken ITrackCreatedByEntity.CreatedBy
RefToken IEntityWithCreatedBy.CreatedBy
{
get
{

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

24
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<T>(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;
}
}

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

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

2
src/Squidex.Read/ITrackCreatedByEntity.cs → 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; }
}

2
src/Squidex.Read/ITrackLastModifiedByEntity.cs → 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; }
}

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
{
public interface ISchemaEntity : IAppRefEntity, ITrackCreatedByEntity, ITrackLastModifiedByEntity
public interface ISchemaEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion
{
string Name { get; }

25
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<AppDomainObject>(command, x =>
await handler.CreateAsync<AppDomainObject>(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<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)
{
return handler.UpdateAsync<AppDomainObject>(command, x =>
return handler.UpdateAsync<AppDomainObject>(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<AppDomainObject>(command, x => x.RemoveContributor(command));
return handler.UpdateAsync<AppDomainObject>(context, x => x.RemoveContributor(command));
}
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)
{
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)
{
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)
{
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)
{
return handler.UpdateAsync<AppDomainObject>(command, x => x.SetMasterLanguage(command));
return handler.UpdateAsync<AppDomainObject>(context, x => x.SetMasterLanguage(command));
}
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.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<ContentDomainObject>(command, s =>
{
s.Create(command);
context.Succeed(command.ContentId);
});
await handler.CreateAsync<ContentDomainObject>(context, c => c.Create(command));
}
protected async Task On(UpdateContent command, CommandContext context)
{
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)
{
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)
{
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)
{
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)
{
return handler.UpdateAsync<ContentDomainObject>(command, s => s.Delete(command));
return handler.UpdateAsync<ContentDomainObject>(context, c => c.Delete(command));
}
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)

34
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<SchemaDomainObject>(command, s =>
{
s.Create(command);
context.Succeed(command.Name);
});
await handler.CreateAsync<SchemaDomainObject>(context, s => s.Create(command));
}
protected Task On(AddField command, CommandContext context)
{
return handler.UpdateAsync<SchemaDomainObject>(command, s =>
return handler.UpdateAsync<SchemaDomainObject>(context, s =>
{
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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
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)
{
return handler.UpdateAsync<SchemaDomainObject>(command, s => { s.Unpublish(command); });
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.Unpublish(command));
}
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 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 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<EnrichWithExpectedVersionHandler>()
.As<ICommandHandler>()
.SingleInstance();
builder.RegisterType<EnrichWithTimestampHandler>()
.As<ICommandHandler>()
.SingleInstance();
@ -45,10 +48,6 @@ namespace Squidex.Config.Domain
.As<ICommandHandler>()
.SingleInstance();
builder.RegisterType<EnrichWithAggregateIdProcessor>()
.As<IEventProcessor>()
.SingleInstance();
builder.RegisterType<ClientKeyGenerator>()
.AsSelf()
.SingleInstance();
@ -69,6 +68,10 @@ namespace Squidex.Config.Domain
.As<ICommandHandler>()
.SingleInstance();
builder.RegisterType<SetVersionAsETagHandler>()
.As<ICommandHandler>()
.SingleInstance();
builder.Register<DomainObjectFactoryFunction<AppDomainObject>>(c => (id => new AppDomainObject(id, 0)))
.AsSelf()
.SingleInstance();

3
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)

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

10
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
/// </remarks>
[HttpPost]
[Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto[]), 201)]
[ProducesResponseType(typeof(ClientDto), 201)]
public async Task<IActionResult> PostClient(string app, [FromBody] CreateAppClientDto request)
{
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());
return StatusCode(201, response);
return CreatedAtAction(nameof(GetClients), new { app }, response);
}
/// <summary>
@ -105,7 +108,6 @@ namespace Squidex.Controllers.Api.Apps
/// </returns>
[HttpPut]
[Route("apps/{app}/clients/{clientId}/")]
[ProducesResponseType(typeof(ClientDto[]), 201)]
public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] UpdateAppClientDto request)
{
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 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);
}

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 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 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);
}
/// <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]+)*$")]
public string Name { get; set; }
/// <summary>
/// The version of the app.
/// </summary>
public long Version { get; set; }
/// <summary>
/// The name of the app.
/// </summary>

2
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
{
/// <summary>
/// The client id.

7
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
{
/// <summary>
/// Id of the created entity.
/// </summary>
[Required]
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 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>

3
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);
}

19
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<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]

21
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<string> { "PingTest" };
}
}
private SwaggerOperations GenerateSchemaQueryOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema)
{
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.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));

10
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()

3
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<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>
</head>
<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>
</body>

128
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<IDomainObjectFactory> factory = new Mock<IDomainObjectFactory>();
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> event2 = new Envelope<IEvent>(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<ICollection<Envelope<IEvent>>>(), It.IsAny<Guid>()))
.Returns(TaskHelper.Done)
.Verifiable();
await sut.CreateAsync<MyDomainObject>(command, x =>
{
passedDomainObject = x;
MyDomainObject passedDomainObject = null;
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]
@ -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<ICollection<Envelope<IEvent>>>(), It.IsAny<Guid>()))
.Returns(TaskHelper.Done)
.Verifiable();
await sut.CreateAsync<MyDomainObject>(command, x =>
{
passedDomainObject = x;
});
MyDomainObject passedDomainObject = null;
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]
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))
.Verifiable();
await TestFlowAsync(async () =>
{
MyDomainObject passedDomainObject = null;
repository.Setup(x => x.SaveAsync(domainObject, It.IsAny<ICollection<Envelope<IEvent>>>(), It.IsAny<Guid>()))
.Returns(TaskHelper.Done)
.Verifiable();
await sut.UpdateAsync<MyDomainObject>(command, x =>
{
passedDomainObject = x;
MyDomainObject passedDomainObject = null;
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]
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))
.Verifiable();
await TestFlowAsync(async () =>
{
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))
repository.Setup(x => x.SaveAsync(domainObject, It.IsAny<ICollection<Envelope<IEvent>>>(), It.IsAny<Guid>()))
.Returns(TaskHelper.Done)
.Verifiable();
processor2.Setup(x => x.ProcessEventAsync(
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();
MyDomainObject passedDomainObject = null;
processor2.Setup(x => x.ProcessEventAsync(
It.Is<Envelope<IEvent>>(y => y.Payload == event2.Payload), domainObject, command))
.Returns(TaskHelper.Done)
.Verifiable();
await action();
await sut.UpdateAsync<MyDomainObject>(context, x =>
{
passedDomainObject = x;
});
processor1.VerifyAll();
processor2.VerifyAll();
Assert.Equal(domainObject, passedDomainObject);
Assert.NotNull(context.Result<EntitySavedResult>());
repository.VerifyAll();
}

7
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<ICommand>().Object;
[Fact]
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 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<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)))
.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<Envelope<IEvent>>(e => e.Payload == event2), commitId)).Returns(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(event2);

8
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<IClock> clock = new Mock<IClock>();
@ -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<ICommand>().Object));
Assert.False(result);

16
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<ICommand>().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;
}
}

14
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<ICommand>().Object;
private sealed class MyLogger : ILogger<LogExceptionHandler>
{
@ -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);

10
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<ICommand>().Object;
private sealed class MyLogger : ILogger<LogExecutingHandler>
{
@ -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);

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 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);

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 });
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 _ =>
{
@ -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<IAppEntity>(null)).Verifiable();
appRepository.Setup(x => x.FindAppAsync(AppName))
.Returns(Task.FromResult<IAppEntity>(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();

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 });
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 _ =>
{
@ -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<ISchemaEntityWithSchema>(null)).Verifiable();
schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName))
.Returns(Task.FromResult<ISchemaEntityWithSchema>(null))
.Verifiable();
await TestCreate(schema, async _ =>
{

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

@ -32,14 +32,14 @@ namespace Squidex.Write.TestHelpers
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;
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;

Loading…
Cancel
Save