Browse Source

Command handdlers redesigned.

pull/95/head
Sebastian Stehle 8 years ago
parent
commit
73c21f6c3a
  1. 13
      src/Squidex.Domain.Apps.Write/Apps/AppCommandHandler.cs
  2. 11
      src/Squidex.Domain.Apps.Write/Assets/AssetCommandHandler.cs
  3. 10
      src/Squidex.Domain.Apps.Write/Contents/ContentCommandHandler.cs
  4. 13
      src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandHandler.cs
  5. 8
      src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs
  6. 39
      src/Squidex.Infrastructure/CQRS/Commands/CommandContext.cs
  7. 5
      src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs
  8. 8
      src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampCommandHandler.cs
  9. 3
      src/Squidex.Infrastructure/CQRS/Commands/ICommandHandler.cs
  10. 32
      src/Squidex.Infrastructure/CQRS/Commands/InMemoryCommandBus.cs
  11. 72
      src/Squidex.Infrastructure/CQRS/Commands/LogCommandHandler.cs
  12. 53
      src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs
  13. 37
      src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs
  14. 10
      src/Squidex/Config/Domain/WriteModule.cs
  15. 18
      src/Squidex/Pipeline/CommandHandlers/ETagCommandHandler.cs
  16. 12
      src/Squidex/Pipeline/CommandHandlers/EnrichWithActorCommandHandler.cs
  17. 9
      src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdCommandHandler.cs
  18. 8
      src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdCommandHandler.cs
  19. 36
      src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs
  20. 79
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs
  21. 11
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampCommandHandlerTests.cs
  22. 42
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs
  23. 47
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogCommandHandlerTests.cs
  24. 54
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs

13
src/Squidex.Domain.Apps.Write/Apps/AppCommandHandler.cs

@ -6,6 +6,7 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Read.Apps.Repositories; using Squidex.Domain.Apps.Read.Apps.Repositories;
using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Read.Apps.Services;
@ -13,7 +14,6 @@ using Squidex.Domain.Apps.Write.Apps.Commands;
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.Shared.Users; using Squidex.Shared.Users;
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Write.Apps
{ {
a.Create(command); a.Create(command);
context.Succeed(EntityCreatedResult.Create(a.Id, a.Version)); context.Complete(EntityCreatedResult.Create(a.Id, a.Version));
}); });
} }
@ -120,7 +120,7 @@ namespace Squidex.Domain.Apps.Write.Apps
a.ChangePlan(command); a.ChangePlan(command);
} }
context.Succeed(result); context.Complete(result);
} }
}); });
} }
@ -160,9 +160,12 @@ namespace Squidex.Domain.Apps.Write.Apps
return handler.UpdateAsync<AppDomainObject>(context, a => a.UpdateLanguage(command)); return handler.UpdateAsync<AppDomainObject>(context, a => a.UpdateLanguage(command));
} }
public Task<bool> HandleAsync(CommandContext context) public async Task HandleAsync(CommandContext context, Func<Task> next)
{ {
return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
} }
} }
} }

11
src/Squidex.Domain.Apps.Write/Assets/AssetCommandHandler.cs

@ -6,13 +6,13 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Write.Assets.Commands; using Squidex.Domain.Apps.Write.Assets.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Write.Assets namespace Squidex.Domain.Apps.Write.Assets
{ {
@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Write.Assets
await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead()); await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead());
context.Succeed(EntityCreatedResult.Create(a.Id, a.Version)); context.Complete(EntityCreatedResult.Create(a.Id, a.Version));
}); });
await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), asset.Id.ToString(), asset.FileVersion, null); await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), asset.Id.ToString(), asset.FileVersion, null);
@ -89,9 +89,12 @@ namespace Squidex.Domain.Apps.Write.Assets
return handler.UpdateAsync<AssetDomainObject>(context, a => a.Delete(command)); return handler.UpdateAsync<AssetDomainObject>(context, a => a.Delete(command));
} }
public Task<bool> HandleAsync(CommandContext context) public async Task HandleAsync(CommandContext context, Func<Task> next)
{ {
return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
} }
} }
} }

10
src/Squidex.Domain.Apps.Write/Contents/ContentCommandHandler.cs

@ -20,7 +20,6 @@ using Squidex.Domain.Apps.Write.Contents.Commands;
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;
// ReSharper disable ConvertToLambdaExpression // ReSharper disable ConvertToLambdaExpression
@ -63,7 +62,7 @@ namespace Squidex.Domain.Apps.Write.Contents
{ {
c.Create(command); c.Create(command);
context.Succeed(EntityCreatedResult.Create(command.Data, c.Version)); context.Complete(EntityCreatedResult.Create(command.Data, c.Version));
}); });
} }
@ -96,9 +95,12 @@ namespace Squidex.Domain.Apps.Write.Contents
return handler.UpdateAsync<ContentDomainObject>(context, c => c.Delete(command)); return handler.UpdateAsync<ContentDomainObject>(context, c => c.Delete(command));
} }
public Task<bool> HandleAsync(CommandContext context) public async Task HandleAsync(CommandContext context, Func<Task> next)
{ {
return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
} }
private async Task ValidateAsync(ContentDataCommand command, Func<string> message, bool enrich = false) private async Task ValidateAsync(ContentDataCommand command, Func<string> message, bool enrich = false)

13
src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandHandler.cs

@ -6,6 +6,7 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Read.Schemas.Services;
@ -13,7 +14,6 @@ using Squidex.Domain.Apps.Write.Schemas.Commands;
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;
namespace Squidex.Domain.Apps.Write.Schemas namespace Squidex.Domain.Apps.Write.Schemas
{ {
@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Write.Schemas
{ {
s.Create(command); s.Create(command);
context.Succeed(EntityCreatedResult.Create(s.Id, s.Version)); context.Complete(EntityCreatedResult.Create(s.Id, s.Version));
}); });
} }
@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Write.Schemas
{ {
s.AddField(command); s.AddField(command);
context.Succeed(EntityCreatedResult.Create(s.Schema.FieldsById.Values.First(x => x.Name == command.Name).Id, s.Version)); context.Complete(EntityCreatedResult.Create(s.Schema.FieldsById.Values.First(x => x.Name == command.Name).Id, s.Version));
}); });
} }
@ -125,9 +125,12 @@ namespace Squidex.Domain.Apps.Write.Schemas
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.Unpublish(command)); return handler.UpdateAsync<SchemaDomainObject>(context, s => s.Unpublish(command));
} }
public Task<bool> HandleAsync(CommandContext context) public async Task HandleAsync(CommandContext context, Func<Task> next)
{ {
return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
} }
} }
} }

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

@ -50,9 +50,9 @@ namespace Squidex.Infrastructure.CQRS.Commands
await SaveAsync(aggregate); await SaveAsync(aggregate);
if (!context.IsHandled) if (!context.IsCompleted)
{ {
context.Succeed(new EntityCreatedResult<Guid>(aggregate.Id, aggregate.Version)); context.Complete(new EntityCreatedResult<Guid>(aggregate.Id, aggregate.Version));
} }
return aggregate; return aggregate;
@ -70,9 +70,9 @@ namespace Squidex.Infrastructure.CQRS.Commands
await SaveAsync(aggregate); await SaveAsync(aggregate);
if (!context.IsHandled) if (!context.IsCompleted)
{ {
context.Succeed(new EntitySavedResult(aggregate.Version)); context.Complete(new EntitySavedResult(aggregate.Version));
} }
return aggregate; return aggregate;

39
src/Squidex.Infrastructure/CQRS/Commands/CommandContext.cs

@ -14,7 +14,6 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
private readonly ICommand command; private readonly ICommand command;
private readonly Guid contextId = Guid.NewGuid(); private readonly Guid contextId = Guid.NewGuid();
private Exception exception;
private Tuple<object> result; private Tuple<object> result;
public ICommand Command public ICommand Command
@ -22,31 +21,16 @@ namespace Squidex.Infrastructure.CQRS.Commands
get { return command; } get { return command; }
} }
public bool IsHandled public Guid ContextId
{
get { return IsSucceeded || IsFailed; }
}
public bool IsFailed
{ {
get { return exception != null; } get { return contextId; }
} }
public bool IsSucceeded public bool IsCompleted
{ {
get { return result != null; } get { return result != null; }
} }
public Exception Exception
{
get { return exception; }
}
public Guid ContextId
{
get { return contextId; }
}
public CommandContext(ICommand command) public CommandContext(ICommand command)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
@ -54,26 +38,11 @@ namespace Squidex.Infrastructure.CQRS.Commands
this.command = command; this.command = command;
} }
public void Succeed(object resultValue = null) public void Complete(object resultValue = null)
{ {
if (IsHandled)
{
return;
}
result = Tuple.Create(resultValue); result = Tuple.Create(resultValue);
} }
public void Fail(Exception handlerException)
{
if (IsFailed)
{
return;
}
exception = handlerException;
}
public T Result<T>() public T Result<T>()
{ {
return (T)result?.Item1; return (T)result?.Item1;

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

@ -33,5 +33,10 @@ namespace Squidex.Infrastructure.CQRS.Commands
return TaskHelper.Done; return TaskHelper.Done;
}); });
} }
public static Task HandleAsync(this ICommandHandler commandHandler, CommandContext context)
{
return commandHandler.HandleAsync(context, () => TaskHelper.Done);
}
} }
} }

8
src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs → src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampCommandHandler.cs

@ -1,14 +1,14 @@
// ========================================================================== // ==========================================================================
// EnrichWithTimestampHandler.cs // EnrichWithTimestampCommandHandler.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
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
{ {
@ -23,14 +23,14 @@ namespace Squidex.Infrastructure.CQRS.Commands
this.clock = clock; this.clock = clock;
} }
public Task<bool> HandleAsync(CommandContext context) public Task HandleAsync(CommandContext context, Func<Task> next)
{ {
if (context.Command is ITimestampCommand timestampCommand) if (context.Command is ITimestampCommand timestampCommand)
{ {
timestampCommand.Timestamp = clock.GetCurrentInstant(); timestampCommand.Timestamp = clock.GetCurrentInstant();
} }
return TaskHelper.False; return next();
} }
} }
} }

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

@ -6,12 +6,13 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
{ {
public interface ICommandHandler public interface ICommandHandler
{ {
Task<bool> HandleAsync(CommandContext context); Task HandleAsync(CommandContext context, Func<Task> next);
} }
} }

32
src/Squidex.Infrastructure/CQRS/Commands/InMemoryCommandBus.cs

@ -8,19 +8,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
{ {
public sealed class InMemoryCommandBus : ICommandBus public sealed class InMemoryCommandBus : ICommandBus
{ {
private readonly IEnumerable<ICommandHandler> handlers; private readonly List<ICommandHandler> handlers;
public InMemoryCommandBus(IEnumerable<ICommandHandler> handlers) public InMemoryCommandBus(IEnumerable<ICommandHandler> handlers)
{ {
Guard.NotNull(handlers, nameof(handlers)); Guard.NotNull(handlers, nameof(handlers));
this.handlers = handlers; this.handlers = handlers.Reverse().ToList();
} }
public async Task<CommandContext> PublishAsync(ICommand command) public async Task<CommandContext> PublishAsync(ICommand command)
@ -29,29 +31,21 @@ namespace Squidex.Infrastructure.CQRS.Commands
var context = new CommandContext(command); var context = new CommandContext(command);
var next = new Func<Task>(() => TaskHelper.Done);
foreach (var handler in handlers) foreach (var handler in handlers)
{ {
try next = Join(handler, context, next);
{
var isHandled = await handler.HandleAsync(context);
if (isHandled)
{
context.Succeed();
}
}
catch (Exception ex)
{
context.Fail(ex);
}
} }
if (context.Exception != null) await next();
{
throw context.Exception;
}
return context; return context;
} }
private static Func<Task> Join(ICommandHandler handler, CommandContext context, Func<Task> next)
{
return () => handler.HandleAsync(context, next);
}
} }
} }

72
src/Squidex.Infrastructure/CQRS/Commands/LogCommandHandler.cs

@ -0,0 +1,72 @@
// ==========================================================================
// LogExecutingHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure.Log;
namespace Squidex.Infrastructure.CQRS.Commands
{
public sealed class LogCommandHandler : ICommandHandler
{
private readonly ISemanticLog log;
public LogCommandHandler(ISemanticLog log)
{
Guard.NotNull(log, nameof(log));
this.log = log;
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
try
{
log.LogInformation(w => w
.WriteProperty("action", "HandleCommand.")
.WriteProperty("actionId", context.ContextId.ToString())
.WriteProperty("state", "Started")
.WriteProperty("commandType", context.Command.GetType().Name));
using (log.MeasureInformation(w => w
.WriteProperty("action", "HandleCommand.")
.WriteProperty("actionId", context.ContextId.ToString())
.WriteProperty("state", "Completed")
.WriteProperty("commandType", context.Command.GetType().Name)))
{
await next();
}
log.LogInformation(w => w
.WriteProperty("action", "HandleCommand.")
.WriteProperty("actionId", context.ContextId.ToString())
.WriteProperty("state", "Succeeded")
.WriteProperty("commandType", context.Command.GetType().Name));
}
catch (Exception ex)
{
log.LogError(ex, w => w
.WriteProperty("action", "HandleCommand.")
.WriteProperty("actionId", context.ContextId.ToString())
.WriteProperty("state", "Failed")
.WriteProperty("commandType", context.Command.GetType().Name));
throw;
}
if (!context.IsCompleted)
{
log.LogFatal(w => w
.WriteProperty("action", "HandleCommand.")
.WriteProperty("actionId", context.ContextId.ToString())
.WriteProperty("state", "Unhandled")
.WriteProperty("commandType", context.Command.GetType().Name));
}
}
}
}

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

@ -1,53 +0,0 @@
// ==========================================================================
// LogExceptionHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
// ReSharper disable InvertIf
namespace Squidex.Infrastructure.CQRS.Commands
{
public sealed class LogExceptionHandler : ICommandHandler
{
private readonly ISemanticLog log;
public LogExceptionHandler(ISemanticLog log)
{
Guard.NotNull(log, nameof(log));
this.log = log;
}
public Task<bool> HandleAsync(CommandContext context)
{
var exception = context.Exception;
if (exception != null)
{
log.LogError(exception, w => w
.WriteProperty("action", "HandleCommand.")
.WriteProperty("actionId", context.ContextId.ToString())
.WriteProperty("state", "Failed")
.WriteProperty("commandType", context.Command.GetType().Name));
}
if (!context.IsHandled)
{
log.LogFatal(exception, w => w
.WriteProperty("action", "HandleCommand.")
.WriteProperty("actionId", context.ContextId.ToString())
.WriteProperty("state", "Unhandled")
.WriteProperty("commandType", context.Command.GetType().Name));
}
return TaskHelper.False;
}
}
}

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

@ -1,37 +0,0 @@
// ==========================================================================
// LogExecutingHandler.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Commands
{
public sealed class LogExecutingHandler : ICommandHandler
{
private readonly ISemanticLog log;
public LogExecutingHandler(ISemanticLog log)
{
Guard.NotNull(log, nameof(log));
this.log = log;
}
public Task<bool> HandleAsync(CommandContext context)
{
log.LogInformation(w => w
.WriteProperty("action", "HandleCommand.")
.WriteProperty("actionId", context.ContextId.ToString())
.WriteProperty("state", "Started")
.WriteProperty("commandType", context.Command.GetType().Name));
return TaskHelper.False;
}
}
}

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

@ -31,7 +31,7 @@ namespace Squidex.Config.Domain
protected override void Load(ContainerBuilder builder) protected override void Load(ContainerBuilder builder)
{ {
builder.RegisterType<EnrichWithExpectedVersionHandler>() builder.RegisterType<ETagCommandHandler>()
.As<ICommandHandler>() .As<ICommandHandler>()
.SingleInstance(); .SingleInstance();
@ -39,15 +39,15 @@ namespace Squidex.Config.Domain
.As<ICommandHandler>() .As<ICommandHandler>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<EnrichWithActorHandler>() builder.RegisterType<EnrichWithActorCommandHandler>()
.As<ICommandHandler>() .As<ICommandHandler>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<EnrichWithAppIdHandler>() builder.RegisterType<EnrichWithAppIdCommandHandler>()
.As<ICommandHandler>() .As<ICommandHandler>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<EnrichWithSchemaIdHandler>() builder.RegisterType<EnrichWithSchemaIdCommandHandler>()
.As<ICommandHandler>() .As<ICommandHandler>()
.SingleInstance(); .SingleInstance();
@ -71,7 +71,7 @@ namespace Squidex.Config.Domain
.As<ICommandHandler>() .As<ICommandHandler>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<SetVersionAsETagHandler>() builder.RegisterType<ETagCommandHandler>()
.As<ICommandHandler>() .As<ICommandHandler>()
.SingleInstance(); .SingleInstance();

18
src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs → src/Squidex/Pipeline/CommandHandlers/ETagCommandHandler.cs

@ -1,29 +1,30 @@
// ========================================================================== // ==========================================================================
// EnrichWithExpectedVersionHandler.cs // ETagCommandHandler.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.Globalization; using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Pipeline.CommandHandlers namespace Squidex.Pipeline.CommandHandlers
{ {
public sealed class EnrichWithExpectedVersionHandler : ICommandHandler public class ETagCommandHandler : ICommandHandler
{ {
private readonly IHttpContextAccessor httpContextAccessor; private readonly IHttpContextAccessor httpContextAccessor;
public EnrichWithExpectedVersionHandler(IHttpContextAccessor httpContextAccessor) public ETagCommandHandler(IHttpContextAccessor httpContextAccessor)
{ {
this.httpContextAccessor = httpContextAccessor; this.httpContextAccessor = httpContextAccessor;
} }
public Task<bool> HandleAsync(CommandContext context) public async Task HandleAsync(CommandContext context, Func<Task> next)
{ {
var headers = httpContextAccessor.HttpContext.Request.Headers; var headers = httpContextAccessor.HttpContext.Request.Headers;
var headerMatch = headers["If-Match"].ToString(); var headerMatch = headers["If-Match"].ToString();
@ -33,7 +34,12 @@ namespace Squidex.Pipeline.CommandHandlers
context.Command.ExpectedVersion = expectedVersion; context.Command.ExpectedVersion = expectedVersion;
} }
return TaskHelper.False; await next();
if (context.Result<object>() is EntitySavedResult result)
{
httpContextAccessor.HttpContext.Response.Headers["ETag"] = new StringValues(result.Version.ToString());
}
} }
} }
} }

12
src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs → src/Squidex/Pipeline/CommandHandlers/EnrichWithActorCommandHandler.cs

@ -1,11 +1,12 @@
// ========================================================================== // ==========================================================================
// EnrichWithActorHandler.cs // EnrichWithActorCommandHandler.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.Security; using System.Security;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -13,22 +14,21 @@ using Squidex.Domain.Apps.Write;
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;
// ReSharper disable InvertIf // ReSharper disable InvertIf
namespace Squidex.Pipeline.CommandHandlers namespace Squidex.Pipeline.CommandHandlers
{ {
public class EnrichWithActorHandler : ICommandHandler public class EnrichWithActorCommandHandler : ICommandHandler
{ {
private readonly IHttpContextAccessor httpContextAccessor; private readonly IHttpContextAccessor httpContextAccessor;
public EnrichWithActorHandler(IHttpContextAccessor httpContextAccessor) public EnrichWithActorCommandHandler(IHttpContextAccessor httpContextAccessor)
{ {
this.httpContextAccessor = httpContextAccessor; this.httpContextAccessor = httpContextAccessor;
} }
public Task<bool> HandleAsync(CommandContext context) public Task HandleAsync(CommandContext context, Func<Task> next)
{ {
if (context.Command is SquidexCommand squidexCommand && squidexCommand.Actor == null) if (context.Command is SquidexCommand squidexCommand && squidexCommand.Actor == null)
{ {
@ -46,7 +46,7 @@ namespace Squidex.Pipeline.CommandHandlers
squidexCommand.Actor = actorToken; squidexCommand.Actor = actorToken;
} }
return TaskHelper.False; return next();
} }
private RefToken FindActorFromSubject() private RefToken FindActorFromSubject()

9
src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs → src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdCommandHandler.cs

@ -12,22 +12,21 @@ using Microsoft.AspNetCore.Http;
using Squidex.Domain.Apps.Write; using Squidex.Domain.Apps.Write;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Tasks;
// ReSharper disable InvertIf // ReSharper disable InvertIf
namespace Squidex.Pipeline.CommandHandlers namespace Squidex.Pipeline.CommandHandlers
{ {
public sealed class EnrichWithAppIdHandler : ICommandHandler public sealed class EnrichWithAppIdCommandHandler : ICommandHandler
{ {
private readonly IHttpContextAccessor httpContextAccessor; private readonly IHttpContextAccessor httpContextAccessor;
public EnrichWithAppIdHandler(IHttpContextAccessor httpContextAccessor) public EnrichWithAppIdCommandHandler(IHttpContextAccessor httpContextAccessor)
{ {
this.httpContextAccessor = httpContextAccessor; this.httpContextAccessor = httpContextAccessor;
} }
public Task<bool> HandleAsync(CommandContext context) public Task HandleAsync(CommandContext context, Func<Task> next)
{ {
if (context.Command is AppCommand appCommand && appCommand.AppId == null) if (context.Command is AppCommand appCommand && appCommand.AppId == null)
{ {
@ -41,7 +40,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 TaskHelper.False; return next();
} }
} }
} }

8
src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs → src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdCommandHandler.cs

@ -20,19 +20,19 @@ using Squidex.Infrastructure.CQRS.Commands;
namespace Squidex.Pipeline.CommandHandlers namespace Squidex.Pipeline.CommandHandlers
{ {
public sealed class EnrichWithSchemaIdHandler : ICommandHandler public sealed class EnrichWithSchemaIdCommandHandler : ICommandHandler
{ {
private readonly ISchemaProvider schemas; private readonly ISchemaProvider schemas;
private readonly IActionContextAccessor actionContextAccessor; private readonly IActionContextAccessor actionContextAccessor;
public EnrichWithSchemaIdHandler(ISchemaProvider schemas, IActionContextAccessor actionContextAccessor) public EnrichWithSchemaIdCommandHandler(ISchemaProvider schemas, IActionContextAccessor actionContextAccessor)
{ {
this.schemas = schemas; this.schemas = schemas;
this.actionContextAccessor = actionContextAccessor; this.actionContextAccessor = actionContextAccessor;
} }
public async Task<bool> HandleAsync(CommandContext context) public async Task HandleAsync(CommandContext context, Func<Task> next)
{ {
if (context.Command is SchemaCommand schemaCommand && schemaCommand.SchemaId == null) if (context.Command is SchemaCommand schemaCommand && schemaCommand.SchemaId == null)
{ {
@ -62,7 +62,7 @@ namespace Squidex.Pipeline.CommandHandlers
} }
} }
return false; await next();
} }
} }
} }

36
src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs

@ -1,36 +0,0 @@
// ==========================================================================
// 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)
{
if (context.Result<object>() is EntitySavedResult result)
{
httpContextAccessor.HttpContext.Response.Headers["ETag"] = new StringValues(result.Version.ToString());
}
return TaskHelper.False;
}
}
}

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

@ -22,40 +22,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
var sut = new CommandContext(command); var sut = new CommandContext(command);
Assert.Equal(command, sut.Command); Assert.Equal(command, sut.Command);
Assert.Null(sut.Exception); Assert.False(sut.IsCompleted);
Assert.False(sut.IsHandled); Assert.NotEqual(Guid.Empty, sut.ContextId);
Assert.False(sut.IsSucceeded);
Assert.False(sut.IsFailed);
}
[Fact]
public void Should_provide_exception_when_failed()
{
var exc = new InvalidOperationException();
var sut = new CommandContext(command);
sut.Fail(exc);
Assert.Equal(exc, sut.Exception);
Assert.True(sut.IsHandled);
Assert.True(sut.IsFailed);
Assert.False(sut.IsSucceeded);
}
[Fact]
public void Should_not_update_exception_when_failed()
{
var exc1 = new InvalidOperationException();
var exc2 = new InvalidOperationException();
var sut = new CommandContext(command);
sut.Fail(exc1);
sut.Fail(exc2);
Assert.Equal(exc1, sut.Exception);
Assert.True(sut.IsHandled);
Assert.True(sut.IsFailed);
Assert.False(sut.IsSucceeded);
} }
[Fact] [Fact]
@ -63,54 +31,17 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
var sut = new CommandContext(command); var sut = new CommandContext(command);
sut.Succeed(); sut.Complete();
Assert.Null(sut.Exception); Assert.True(sut.IsCompleted);
Assert.True(sut.IsSucceeded);
Assert.True(sut.IsHandled);
Assert.False(sut.IsFailed);
}
[Fact]
public void Should_replace_status_when_already_succeeded()
{
var sut = new CommandContext(command);
sut.Succeed(Guid.NewGuid());
sut.Fail(new Exception());
Assert.NotNull(sut.Exception);
Assert.True(sut.IsHandled);
Assert.True(sut.IsFailed);
Assert.True(sut.IsSucceeded);
} }
[Fact] [Fact]
public void Should_provide_result_valid_when_succeeded_with_value() public void Should_provide_result_valid_when_succeeded_with_value()
{
var guid = Guid.NewGuid();
var sut = new CommandContext(command);
sut.Succeed(guid);
Assert.Equal(guid, sut.Result<Guid>());
Assert.True(sut.IsHandled);
Assert.True(sut.IsSucceeded);
Assert.False(sut.IsFailed);
}
[Fact]
public void Should_not_change_status_when_already_failed()
{ {
var sut = new CommandContext(command); var sut = new CommandContext(command);
sut.Fail(new Exception()); sut.Complete("RESULT");
sut.Succeed(Guid.NewGuid());
Assert.NotNull(sut.Exception);
Assert.True(sut.IsHandled);
Assert.True(sut.IsFailed);
Assert.False(sut.IsSucceeded);
} }
} }
} }

11
tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs → tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampCommandHandlerTests.cs

@ -1,5 +1,5 @@
// ========================================================================== // ==========================================================================
// EnrichWithTimestampHandlerTests.cs // EnrichWithTimestampCommandHandlerTests.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
@ -13,7 +13,7 @@ using Xunit;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
{ {
public sealed class EnrichWithTimestampHandlerTests public sealed class EnrichWithTimestampCommandHandlerTests
{ {
private sealed class MyTimestampCommand : ITimestampCommand private sealed class MyTimestampCommand : ITimestampCommand
{ {
@ -35,9 +35,8 @@ namespace Squidex.Infrastructure.CQRS.Commands
var command = new MyTimestampCommand(); var command = new MyTimestampCommand();
var result = await sut.HandleAsync(new CommandContext(command)); await sut.HandleAsync(new CommandContext(command));
Assert.False(result);
Assert.Equal(utc, command.Timestamp); Assert.Equal(utc, command.Timestamp);
} }
@ -46,9 +45,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
var sut = new EnrichWithTimestampHandler(clock); var sut = new EnrichWithTimestampHandler(clock);
var result = await sut.HandleAsync(new CommandContext(A.Dummy<ICommand>())); await sut.HandleAsync(new CommandContext(A.Dummy<ICommand>()));
Assert.False(result);
A.CallTo(() => clock.GetCurrentInstant()).MustNotHaveHappened(); A.CallTo(() => clock.GetCurrentInstant()).MustNotHaveHappened();
} }

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

@ -22,10 +22,12 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
public ICommand LastCommand; public ICommand LastCommand;
public Task<bool> HandleAsync(CommandContext context) public Task HandleAsync(CommandContext context, Func<Task> next)
{ {
LastCommand = context.Command; LastCommand = context.Command;
context.Complete(true);
return TaskHelper.True; return TaskHelper.True;
} }
} }
@ -34,11 +36,11 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
public ICommand LastCommand; public ICommand LastCommand;
public Task<bool> HandleAsync(CommandContext context) public Task HandleAsync(CommandContext context, Func<Task> next)
{ {
LastCommand = context.Command; LastCommand = context.Command;
return TaskHelper.False; return TaskHelper.Done;
} }
} }
@ -46,7 +48,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
public ICommand LastCommand; public ICommand LastCommand;
public Task<bool> HandleAsync(CommandContext context) public Task HandleAsync(CommandContext context, Func<Task> next)
{ {
LastCommand = context.Command; LastCommand = context.Command;
@ -54,25 +56,13 @@ namespace Squidex.Infrastructure.CQRS.Commands
} }
} }
private sealed class AfterThrowHandler : ICommandHandler
{
public Exception LastException;
public Task<bool> HandleAsync(CommandContext context)
{
LastException = context.Exception;
return TaskHelper.False;
}
}
[Fact] [Fact]
public async Task Should_not_set_handled_if_no_handler_registered() public async Task Should_not_set_handled_if_no_handler_registered()
{ {
var sut = new InMemoryCommandBus(new ICommandHandler[0]); var sut = new InMemoryCommandBus(new ICommandHandler[0]);
var ctx = await sut.PublishAsync(command); var ctx = await sut.PublishAsync(command);
Assert.False(ctx.IsHandled); Assert.False(ctx.IsCompleted);
} }
[Fact] [Fact]
@ -84,13 +74,11 @@ namespace Squidex.Infrastructure.CQRS.Commands
var ctx = await sut.PublishAsync(command); var ctx = await sut.PublishAsync(command);
Assert.Equal(command, handler.LastCommand); Assert.Equal(command, handler.LastCommand);
Assert.False(ctx.IsSucceeded); Assert.False(ctx.IsCompleted);
Assert.False(ctx.IsHandled);
Assert.Null(ctx.Exception);
} }
[Fact] [Fact]
public async Task Should_set_succeeded_if_handler_returns_true() public async Task Should_set_succeeded_if_handler_marks_completed()
{ {
var handler = new HandledHandler(); var handler = new HandledHandler();
@ -98,23 +86,19 @@ namespace Squidex.Infrastructure.CQRS.Commands
var ctx = await sut.PublishAsync(command); var ctx = await sut.PublishAsync(command);
Assert.Equal(command, handler.LastCommand); Assert.Equal(command, handler.LastCommand);
Assert.True(ctx.IsSucceeded); Assert.True(ctx.IsCompleted);
Assert.True(ctx.IsHandled);
Assert.Null(ctx.Exception);
} }
[Fact] [Fact]
public async Task Should_throw_and_call_all_handlers_if_first_handler_fails() public async Task Should_throw_and_call_all_handlers_if_first_handler_fails()
{ {
var handler1 = new ThrowHandledHandler(); var handler = new ThrowHandledHandler();
var handler2 = new AfterThrowHandler();
var sut = new InMemoryCommandBus(new ICommandHandler[] { handler1, handler2 }); var sut = new InMemoryCommandBus(new ICommandHandler[] { handler });
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sut.PublishAsync(command)); await Assert.ThrowsAsync<InvalidOperationException>(async () => await sut.PublishAsync(command));
Assert.Equal(command, handler1.LastCommand); Assert.Equal(command, handler.LastCommand);
Assert.IsType<InvalidOperationException>(handler2.LastException);
} }
} }
} }

47
tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs → tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogCommandHandlerTests.cs

@ -8,10 +8,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
using Xunit; using Xunit;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
@ -19,16 +19,19 @@ namespace Squidex.Infrastructure.CQRS.Commands
public class LogExceptionHandlerTests public class LogExceptionHandlerTests
{ {
private readonly MyLog log = new MyLog(); private readonly MyLog log = new MyLog();
private readonly LogExceptionHandler sut; private readonly LogCommandHandler sut;
private readonly ICommand command = A.Dummy<ICommand>(); private readonly ICommand command = A.Dummy<ICommand>();
private sealed class MyLog : ISemanticLog private sealed class MyLog : ISemanticLog
{ {
public HashSet<SemanticLogLevel> LogLevels { get; } = new HashSet<SemanticLogLevel>(); public int LogCount { get; private set; }
public Dictionary<SemanticLogLevel, int> LogLevels { get; } = new Dictionary<SemanticLogLevel, int>();
public void Log(SemanticLogLevel logLevel, Action<IObjectWriter> action) public void Log(SemanticLogLevel logLevel, Action<IObjectWriter> action)
{ {
LogLevels.Add(logLevel); LogCount++;
LogLevels[logLevel] = LogLevels.GetOrDefault(logLevel) + 1;
} }
public ISemanticLog CreateScope(Action<IObjectWriter> objectWriter) public ISemanticLog CreateScope(Action<IObjectWriter> objectWriter)
@ -39,33 +42,38 @@ namespace Squidex.Infrastructure.CQRS.Commands
public LogExceptionHandlerTests() public LogExceptionHandlerTests()
{ {
sut = new LogExceptionHandler(log); sut = new LogCommandHandler(log);
} }
[Fact] [Fact]
public async Task Should_do_nothing_if_command_is_succeeded() public async Task Should_log_before_and_after_request()
{ {
var context = new CommandContext(command); var context = new CommandContext(command);
context.Succeed(); await sut.HandleAsync(context, () =>
{
context.Complete(true);
var isHandled = await sut.HandleAsync(context); return TaskHelper.Done;
});
Assert.False(isHandled); Assert.Equal(3, log.LogCount);
Assert.Equal(0, log.LogLevels.Count); Assert.Equal(3, log.LogLevels[SemanticLogLevel.Information]);
} }
[Fact] [Fact]
public async Task Should_log_if_command_failed() public async Task Should_log_error_if_command_failed()
{ {
var context = new CommandContext(command); var context = new CommandContext(command);
context.Fail(new InvalidOperationException()); await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
var isHandled = await sut.HandleAsync(context); await sut.HandleAsync(context, () => throw new InvalidOperationException());
});
Assert.False(isHandled); Assert.Equal(3, log.LogCount);
Assert.Equal(new[] { SemanticLogLevel.Error }, log.LogLevels.ToArray()); Assert.Equal(2, log.LogLevels[SemanticLogLevel.Information]);
Assert.Equal(1, log.LogLevels[SemanticLogLevel.Error]);
} }
[Fact] [Fact]
@ -73,10 +81,11 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
var context = new CommandContext(command); var context = new CommandContext(command);
var isHandled = await sut.HandleAsync(context); await sut.HandleAsync(context, () => TaskHelper.Done);
Assert.False(isHandled); Assert.Equal(4, log.LogCount);
Assert.Equal(new[] { SemanticLogLevel.Fatal }, log.LogLevels.ToArray()); Assert.Equal(3, log.LogLevels[SemanticLogLevel.Information]);
Assert.Equal(1, log.LogLevels[SemanticLogLevel.Fatal]);
} }
} }
} }

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

@ -1,54 +0,0 @@
// ==========================================================================
// LogExecutingHandlerTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.Log;
using Xunit;
namespace Squidex.Infrastructure.CQRS.Commands
{
public class LogExecutingHandlerTests
{
private readonly MyLog log = new MyLog();
private readonly LogExecutingHandler sut;
private readonly ICommand command = A.Dummy<ICommand>();
private sealed class MyLog : ISemanticLog
{
public int LogCount { get; private set; }
public void Log(SemanticLogLevel logLevel, Action<IObjectWriter> action)
{
LogCount++;
}
public ISemanticLog CreateScope(Action<IObjectWriter> objectWriter)
{
throw new NotSupportedException();
}
}
public LogExecutingHandlerTests()
{
sut = new LogExecutingHandler(log);
}
[Fact]
public async Task Should_log_once()
{
var context = new CommandContext(command);
var isHandled = await sut.HandleAsync(context);
Assert.False(isHandled);
Assert.Equal(1, log.LogCount);
}
}
}
Loading…
Cancel
Save