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