@ -0,0 +1,37 @@ | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using MediatR; | |||
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; | |||
using Microsoft.Extensions.Logging; | |||
namespace Ordering.API.Infrastructure.Behaviors | |||
{ | |||
public class DuplicateCommandBehavior<TCommand, TResponse> : IPipelineBehavior<TCommand, TResponse> | |||
where TCommand : ICommand | |||
{ | |||
private readonly IMediator _mediator; | |||
private readonly IRequestManager _requestManager; | |||
public DuplicateCommandBehavior(IMediator mediator, IRequestManager requestManager) | |||
{ | |||
_mediator = mediator; | |||
_requestManager = requestManager; | |||
} | |||
async Task<TResponse> IPipelineBehavior<TCommand, TResponse>.Handle(TCommand command, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) | |||
{ | |||
var alreadyExists = await _requestManager.ExistAsync(command.CommandId); | |||
if (alreadyExists) | |||
{ | |||
var duplicateCommand = new DuplicateCommandResponse<TCommand, TResponse>.DuplicateCommand<TCommand>(command); | |||
return await _mediator.Send(duplicateCommand, cancellationToken); | |||
} | |||
await _requestManager.CreateRequestForCommandAsync<TCommand>(command.CommandId); | |||
var response = await next(); | |||
return response; | |||
} | |||
} | |||
} |
@ -0,0 +1,30 @@ | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using MediatR; | |||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands | |||
{ | |||
public abstract class DuplicateCommandResponse<TCommand, TResponse> : IRequestHandler<DuplicateCommandResponse<TCommand, TResponse>.DuplicateCommand<TCommand>, TResponse> | |||
{ | |||
public class DuplicateCommand<TCommand> : IRequest<TResponse> | |||
{ | |||
public DuplicateCommand(TCommand command) | |||
{ | |||
Command = command; | |||
} | |||
public TCommand Command { get; } | |||
} | |||
/// <summary> | |||
/// Creates the result value to return if a previous request was found | |||
/// </summary> | |||
/// <returns></returns> | |||
protected abstract Task<TResponse> CreateResponseForDuplicateCommand(TCommand command); | |||
Task<TResponse> IRequestHandler<DuplicateCommand<TCommand>, TResponse>.Handle(DuplicateCommand<TCommand> request, CancellationToken cancellationToken) | |||
{ | |||
return CreateResponseForDuplicateCommand(request.Command); | |||
} | |||
} | |||
} |
@ -0,0 +1,9 @@ | |||
using System; | |||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands | |||
{ | |||
public interface ICommand | |||
{ | |||
Guid CommandId { get; } | |||
} | |||
} |
@ -1,17 +0,0 @@ | |||
using MediatR; | |||
using System; | |||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands | |||
{ | |||
public class IdentifiedCommand<T, R> : IRequest<R> | |||
where T : IRequest<R> | |||
{ | |||
public T Command { get; } | |||
public Guid Id { get; } | |||
public IdentifiedCommand(T command, Guid id) | |||
{ | |||
Command = command; | |||
Id = id; | |||
} | |||
} | |||
} |
@ -1,64 +0,0 @@ | |||
using MediatR; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands | |||
{ | |||
/// <summary> | |||
/// Provides a base implementation for handling duplicate request and ensuring idempotent updates, in the cases where | |||
/// a requestid sent by client is used to detect duplicate requests. | |||
/// </summary> | |||
/// <typeparam name="T">Type of the command handler that performs the operation if request is not duplicated</typeparam> | |||
/// <typeparam name="R">Return value of the inner command handler</typeparam> | |||
public class IdentifiedCommandHandler<T, R> : IRequestHandler<IdentifiedCommand<T, R>, R> | |||
where T : IRequest<R> | |||
{ | |||
private readonly IMediator _mediator; | |||
private readonly IRequestManager _requestManager; | |||
public IdentifiedCommandHandler(IMediator mediator, IRequestManager requestManager) | |||
{ | |||
_mediator = mediator; | |||
_requestManager = requestManager; | |||
} | |||
/// <summary> | |||
/// Creates the result value to return if a previous request was found | |||
/// </summary> | |||
/// <returns></returns> | |||
protected virtual R CreateResultForDuplicateRequest() | |||
{ | |||
return default(R); | |||
} | |||
/// <summary> | |||
/// This method handles the command. It just ensures that no other request exists with the same ID, and if this is the case | |||
/// just enqueues the original inner command. | |||
/// </summary> | |||
/// <param name="message">IdentifiedCommand which contains both original command & request ID</param> | |||
/// <returns>Return value of inner command or default value if request same ID was found</returns> | |||
public async Task<R> Handle(IdentifiedCommand<T, R> message, CancellationToken cancellationToken) | |||
{ | |||
var alreadyExists = await _requestManager.ExistAsync(message.Id); | |||
if (alreadyExists) | |||
{ | |||
return CreateResultForDuplicateRequest(); | |||
} | |||
else | |||
{ | |||
await _requestManager.CreateRequestForCommandAsync<T>(message.Id); | |||
try | |||
{ | |||
// Send the embeded business command to mediator so it runs its related CommandHandler | |||
var result = await _mediator.Send(message.Command); | |||
return result; | |||
} | |||
catch | |||
{ | |||
return default(R); | |||
} | |||
} | |||
} | |||
} | |||
} |
@ -1,13 +0,0 @@ | |||
using FluentValidation; | |||
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; | |||
namespace Ordering.API.Application.Validations | |||
{ | |||
public class IdentifiedCommandValidator : AbstractValidator<IdentifiedCommand<CreateOrderCommand,bool>> | |||
{ | |||
public IdentifiedCommandValidator() | |||
{ | |||
RuleFor(command => command.Id).NotEmpty(); | |||
} | |||
} | |||
} |
@ -1,91 +0,0 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace UnitTest.Ordering.Application | |||
{ | |||
using global::Ordering.API.Application.Models; | |||
using MediatR; | |||
using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; | |||
using Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Idempotency; | |||
using Moq; | |||
using System.Collections; | |||
using System.Collections.Generic; | |||
using System.Threading.Tasks; | |||
using Xunit; | |||
public class IdentifiedCommandHandlerTest | |||
{ | |||
private readonly Mock<IRequestManager> _requestManager; | |||
private readonly Mock<IMediator> _mediator; | |||
public IdentifiedCommandHandlerTest() | |||
{ | |||
_requestManager = new Mock<IRequestManager>(); | |||
_mediator = new Mock<IMediator>(); | |||
} | |||
[Fact] | |||
public async Task Handler_sends_command_when_order_no_exists() | |||
{ | |||
// Arrange | |||
var fakeGuid = Guid.NewGuid(); | |||
var fakeOrderCmd = new IdentifiedCommand<CreateOrderCommand, bool>(FakeOrderRequest(), fakeGuid); | |||
_requestManager.Setup(x => x.ExistAsync(It.IsAny<Guid>())) | |||
.Returns(Task.FromResult(false)); | |||
_mediator.Setup(x => x.Send(It.IsAny<IRequest<bool>>(),default(System.Threading.CancellationToken))) | |||
.Returns(Task.FromResult(true)); | |||
//Act | |||
var handler = new IdentifiedCommandHandler<CreateOrderCommand, bool>(_mediator.Object, _requestManager.Object); | |||
var cltToken = new System.Threading.CancellationToken(); | |||
var result = await handler.Handle(fakeOrderCmd, cltToken); | |||
//Assert | |||
Assert.True(result); | |||
_mediator.Verify(x => x.Send(It.IsAny<IRequest<bool>>(), default(System.Threading.CancellationToken)), Times.Once()); | |||
} | |||
[Fact] | |||
public async Task Handler_sends_no_command_when_order_already_exists() | |||
{ | |||
// Arrange | |||
var fakeGuid = Guid.NewGuid(); | |||
var fakeOrderCmd = new IdentifiedCommand<CreateOrderCommand, bool>(FakeOrderRequest(), fakeGuid); | |||
_requestManager.Setup(x => x.ExistAsync(It.IsAny<Guid>())) | |||
.Returns(Task.FromResult(true)); | |||
_mediator.Setup(x => x.Send(It.IsAny<IRequest<bool>>(), default(System.Threading.CancellationToken))) | |||
.Returns(Task.FromResult(true)); | |||
//Act | |||
var handler = new IdentifiedCommandHandler<CreateOrderCommand, bool>(_mediator.Object, _requestManager.Object); | |||
var cltToken = new System.Threading.CancellationToken(); | |||
var result = await handler.Handle(fakeOrderCmd, cltToken); | |||
//Assert | |||
Assert.False(result); | |||
_mediator.Verify(x => x.Send(It.IsAny<IRequest<bool>>(), default(System.Threading.CancellationToken)), Times.Never()); | |||
} | |||
private CreateOrderCommand FakeOrderRequest(Dictionary<string, object> args = null) | |||
{ | |||
return new CreateOrderCommand( | |||
new List<BasketItem>(), | |||
userId: args != null && args.ContainsKey("userId") ? (string)args["userId"] : null, | |||
userName: args != null && args.ContainsKey("userName") ? (string)args["userName"] : null, | |||
city: args != null && args.ContainsKey("city") ? (string)args["city"] : null, | |||
street: args != null && args.ContainsKey("street") ? (string)args["street"] : null, | |||
state: args != null && args.ContainsKey("state") ? (string)args["state"] : null, | |||
country: args != null && args.ContainsKey("country") ? (string)args["country"] : null, | |||
zipcode: args != null && args.ContainsKey("zipcode") ? (string)args["zipcode"] : null, | |||
cardNumber: args != null && args.ContainsKey("cardNumber") ? (string)args["cardNumber"] : "1234", | |||
cardExpiration: args != null && args.ContainsKey("cardExpiration") ? (DateTime)args["cardExpiration"] : DateTime.MinValue, | |||
cardSecurityNumber: args != null && args.ContainsKey("cardSecurityNumber") ? (string)args["cardSecurityNumber"] : "123", | |||
cardHolderName: args != null && args.ContainsKey("cardHolderName") ? (string)args["cardHolderName"] : "XXX", | |||
cardTypeId: args != null && args.ContainsKey("cardTypeId") ? (int)args["cardTypeId"] : 0); | |||
} | |||
} | |||
} |