Skip to content

Commit 7892c3a

Browse files
Merge pull request #99 from wemogy/main
Release
2 parents e67e395 + f7984a3 commit 7892c3a

File tree

9 files changed

+314
-13
lines changed

9 files changed

+314
-13
lines changed

src/core/Wemogy.CQRS.UnitTests/DependencyInjectionTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Threading.Tasks;
23
using FluentAssertions;
34
using Microsoft.Extensions.DependencyInjection;
@@ -25,15 +26,15 @@ public async Task CallingAddCQRSMultipleTimesInDifferentAssembliesShouldWork()
2526
var commands = serviceProvider.GetRequiredService<ICommands>();
2627
var helloAssemblyACommand = new PrintHelloAssemblyACommand();
2728
var helloAssemblyBCommand = new PrintHelloAssemblyBCommand();
28-
var trackUserLoginCommand = new TrackUserLoginCommand("test-user-id");
29+
var trackUserLoginCommand = new TrackUserLoginCommand(Guid.NewGuid().ToString());
2930

3031
// Act
3132
var trackUserLoginCommandException = await Record.ExceptionAsync(() => commands.RunAsync(trackUserLoginCommand));
3233
var helloAssemblyACommandException = await Record.ExceptionAsync(() => commands.RunAsync(helloAssemblyACommand));
3334
var helloAssemblyBCommandException = await Record.ExceptionAsync(() => commands.RunAsync(helloAssemblyBCommand));
3435

3536
// Assert
36-
TrackUserLoginCommandHandler.CallCount.Should().Be(1);
37+
TrackUserLoginCommandHandler.ExecutedCount[trackUserLoginCommand.UserId].Should().Be(1);
3738
trackUserLoginCommandException.Should().BeNull();
3839
PrintHelloAssemblyACommandHandler.CallCount.Should().Be(1);
3940
helloAssemblyACommandException.Should().BeNull();
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1+
using System.Collections.Generic;
12
using System.Threading.Tasks;
23
using Wemogy.CQRS.Commands.Abstractions;
34

45
namespace Wemogy.CQRS.UnitTests.TestApplication.Commands.TrackUserLogin;
56

67
public class TrackUserLoginCommandHandler : ICommandHandler<TrackUserLoginCommand>
78
{
8-
public static int CallCount { get; private set; }
9+
public static Dictionary<string, int> ExecutedCount { get; } = new ();
910

1011
public Task HandleAsync(TrackUserLoginCommand command)
1112
{
12-
CallCount++;
13+
if (ExecutedCount.TryGetValue(command.UserId, out var count))
14+
{
15+
ExecutedCount[command.UserId] = count + 1;
16+
}
17+
else
18+
{
19+
ExecutedCount[command.UserId] = 1;
20+
}
21+
1322
return Task.CompletedTask;
1423
}
1524
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using System.Net;
2+
using System.Text.Json;
3+
using FluentAssertions;
4+
using Moq;
5+
using RestSharp;
6+
using Wemogy.CQRS.Common.ValueObjects;
7+
using Wemogy.CQRS.Extensions.FastEndpoints.Common;
8+
using Wemogy.CQRS.Extensions.FastEndpoints.RemoteQueryRunners;
9+
using Wemogy.CQRS.Extensions.FastEndpoints.UnitTests.TestApplication.Queries.RequestTestContext;
10+
using Wemogy.CQRS.Extensions.FastEndpoints.UnitTests.TestApplication.ValueObjects;
11+
12+
namespace Wemogy.CQRS.Extensions.FastEndpoints.UnitTests.HttpRemoteQueryRunners;
13+
14+
public class HttpRemoteQueryRunnerTests
15+
{
16+
[Fact]
17+
public async Task QueryAsync_ShouldRetryRequestAndReturnResult()
18+
{
19+
// Arrange
20+
var query = new RequestTestContextQuery();
21+
var testContext = new TestContext()
22+
{
23+
UserId = Guid.NewGuid().ToString()
24+
};
25+
var resultContent = JsonSerializer.Serialize(testContext, JsonOptions.JsonSerializerOptions);
26+
var responses = new Queue<RestResponse>();
27+
responses.Enqueue(new RestResponse()
28+
{
29+
StatusCode = HttpStatusCode.ServiceUnavailable
30+
});
31+
responses.Enqueue(new RestResponse()
32+
{
33+
StatusCode = HttpStatusCode.OK,
34+
Content = resultContent,
35+
36+
// Set IsSuccessful to true
37+
IsSuccessStatusCode = true,
38+
ResponseStatus = ResponseStatus.Completed
39+
});
40+
41+
var restClientMock = new Mock<IRestClient>();
42+
restClientMock
43+
.Setup(
44+
m => m.ExecuteAsync(
45+
It.IsAny<RestRequest>(),
46+
It.IsAny<CancellationToken>()))
47+
.ReturnsAsync(responses.Dequeue);
48+
49+
var httpRemoteQueryRunner = new HttpRemoteQueryRunner<RequestTestContextQuery, TestContext>(restClientMock.Object, string.Empty);
50+
var request = new QueryRequest<RequestTestContextQuery>(query, new List<CommandQueryDependency>());
51+
52+
// Act
53+
var result = await httpRemoteQueryRunner.QueryAsync(request, CancellationToken.None);
54+
55+
// Assert
56+
result.Should().BeEquivalentTo(testContext);
57+
responses.Should().BeEmpty();
58+
}
59+
60+
[Fact]
61+
public async Task QueryAsync_ShouldThrowWithoutResultAfterMaxRetryReachedRequest()
62+
{
63+
// Arrange
64+
var query = new RequestTestContextQuery();
65+
var testContext = new TestContext()
66+
{
67+
UserId = Guid.NewGuid().ToString()
68+
};
69+
var restClientMock = new Mock<IRestClient>();
70+
restClientMock
71+
.Setup(
72+
m => m.ExecuteAsync(
73+
It.IsAny<RestRequest>(),
74+
It.IsAny<CancellationToken>()))
75+
.ReturnsAsync(() => new RestResponse()
76+
{
77+
StatusCode = HttpStatusCode.ServiceUnavailable
78+
});
79+
80+
var httpRemoteQueryRunner = new HttpRemoteQueryRunner<RequestTestContextQuery, TestContext>(restClientMock.Object, string.Empty);
81+
var request = new QueryRequest<RequestTestContextQuery>(query, new List<CommandQueryDependency>());
82+
83+
// Act
84+
var exception = await Record.ExceptionAsync(() => httpRemoteQueryRunner.QueryAsync(request, CancellationToken.None));
85+
86+
// Assert
87+
exception.Should().NotBeNull();
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using System.Net;
2+
using System.Text.Json;
3+
using FluentAssertions;
4+
using Moq;
5+
using RestSharp;
6+
using Wemogy.CQRS.Common.ValueObjects;
7+
using Wemogy.CQRS.Extensions.FastEndpoints.Common;
8+
using Wemogy.CQRS.Extensions.FastEndpoints.RemoteCommandRunners;
9+
using Wemogy.CQRS.Extensions.FastEndpoints.UnitTests.TestApplication.Commands.Greeting;
10+
using Wemogy.CQRS.Extensions.FastEndpoints.UnitTests.TestApplication.Commands.LogTestContext;
11+
12+
namespace Wemogy.CQRS.Extensions.FastEndpoints.UnitTests.RemoteCommandRunners;
13+
14+
public class HttpRemoteCommandRunnerTests
15+
{
16+
[Fact]
17+
public async Task RunAsync_ShouldRetryRequest()
18+
{
19+
// Arrange
20+
var command = new LogTestContextCommand();
21+
var responses = new Queue<RestResponse>();
22+
responses.Enqueue(new RestResponse()
23+
{
24+
StatusCode = HttpStatusCode.ServiceUnavailable
25+
});
26+
responses.Enqueue(new RestResponse()
27+
{
28+
StatusCode = HttpStatusCode.OK,
29+
30+
// Set IsSuccessful to true
31+
IsSuccessStatusCode = true,
32+
ResponseStatus = ResponseStatus.Completed
33+
});
34+
35+
var restClientMock = new Mock<IRestClient>();
36+
restClientMock
37+
.Setup(
38+
m => m.ExecuteAsync(
39+
It.IsAny<RestRequest>(),
40+
It.IsAny<CancellationToken>()))
41+
.ReturnsAsync(responses.Dequeue);
42+
43+
var httpRemoteCommandRunner = new HttpRemoteCommandRunner<LogTestContextCommand>(restClientMock.Object, string.Empty);
44+
var request = new CommandRequest<LogTestContextCommand>(command, new List<CommandQueryDependency>());
45+
46+
// Act
47+
var exception = await Record.ExceptionAsync(() => httpRemoteCommandRunner.RunAsync(request));
48+
49+
// Assert
50+
exception.Should().BeNull();
51+
responses.Should().BeEmpty();
52+
}
53+
54+
[Fact]
55+
public async Task RunAsync_ShouldThrowAfterMaxRetryReachedRequest()
56+
{
57+
// Arrange
58+
var command = new LogTestContextCommand();
59+
var restClientMock = new Mock<IRestClient>();
60+
restClientMock
61+
.Setup(
62+
m => m.ExecuteAsync(
63+
It.IsAny<RestRequest>(),
64+
It.IsAny<CancellationToken>()))
65+
.ReturnsAsync(() => new RestResponse()
66+
{
67+
StatusCode = HttpStatusCode.ServiceUnavailable
68+
});
69+
70+
var httpRemoteCommandRunner = new HttpRemoteCommandRunner<LogTestContextCommand>(restClientMock.Object, string.Empty);
71+
var request = new CommandRequest<LogTestContextCommand>(command, new List<CommandQueryDependency>());
72+
73+
// Act
74+
var exception = await Record.ExceptionAsync(() => httpRemoteCommandRunner.RunAsync(request));
75+
76+
// Assert
77+
exception.Should().NotBeNull();
78+
}
79+
80+
[Fact]
81+
public async Task RunAsync_ShouldRetryRequestAndReturnResult()
82+
{
83+
// Arrange
84+
var command = new GreetingCommand("Max");
85+
var resultContent = JsonSerializer.Serialize("Hello, Max!", JsonOptions.JsonSerializerOptions);
86+
var responses = new Queue<RestResponse>();
87+
responses.Enqueue(new RestResponse()
88+
{
89+
StatusCode = HttpStatusCode.ServiceUnavailable
90+
});
91+
responses.Enqueue(new RestResponse()
92+
{
93+
StatusCode = HttpStatusCode.OK,
94+
Content = resultContent,
95+
96+
// Set IsSuccessful to true
97+
IsSuccessStatusCode = true,
98+
ResponseStatus = ResponseStatus.Completed
99+
});
100+
101+
var restClientMock = new Mock<IRestClient>();
102+
restClientMock
103+
.Setup(
104+
m => m.ExecuteAsync(
105+
It.IsAny<RestRequest>(),
106+
It.IsAny<CancellationToken>()))
107+
.ReturnsAsync(responses.Dequeue);
108+
109+
var httpRemoteCommandRunner = new HttpRemoteCommandRunner<GreetingCommand, string>(restClientMock.Object, string.Empty);
110+
var request = new CommandRequest<GreetingCommand>(command, new List<CommandQueryDependency>());
111+
112+
// Act
113+
var result = await httpRemoteCommandRunner.RunAsync(request);
114+
115+
// Assert
116+
result.Should().Be("Hello, Max!");
117+
responses.Should().BeEmpty();
118+
}
119+
120+
[Fact]
121+
public async Task RunAsync_ShouldThrowWithoutResultAfterMaxRetryReachedRequest()
122+
{
123+
// Arrange
124+
var command = new GreetingCommand("Max");
125+
var restClientMock = new Mock<IRestClient>();
126+
restClientMock
127+
.Setup(
128+
m => m.ExecuteAsync(
129+
It.IsAny<RestRequest>(),
130+
It.IsAny<CancellationToken>()))
131+
.ReturnsAsync(() => new RestResponse()
132+
{
133+
StatusCode = HttpStatusCode.ServiceUnavailable
134+
});
135+
136+
var httpRemoteCommandRunner = new HttpRemoteCommandRunner<GreetingCommand, string>(restClientMock.Object, string.Empty);
137+
var request = new CommandRequest<GreetingCommand>(command, new List<CommandQueryDependency>());
138+
139+
// Act
140+
var exception = await Record.ExceptionAsync(() => httpRemoteCommandRunner.RunAsync(request));
141+
142+
// Assert
143+
exception.Should().NotBeNull();
144+
}
145+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Wemogy.CQRS.Commands.Abstractions;
2+
3+
namespace Wemogy.CQRS.Extensions.FastEndpoints.UnitTests.TestApplication.Commands.Greeting;
4+
5+
public class GreetingCommand : ICommand<string>
6+
{
7+
public string Name { get; }
8+
9+
public GreetingCommand(string name)
10+
{
11+
Name = name;
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Wemogy.CQRS.Commands.Abstractions;
2+
3+
namespace Wemogy.CQRS.Extensions.FastEndpoints.UnitTests.TestApplication.Commands.Greeting;
4+
5+
public class GreetingCommandHandler : ICommandHandler<GreetingCommand, string>
6+
{
7+
public Task<string> HandleAsync(GreetingCommand command)
8+
{
9+
return Task.FromResult($"Hello, {command.Name}!");
10+
}
11+
}

src/extensions/fastEndpoints/Wemogy.CQRS.Extensions.FastEndpoints/RemoteCommandRunners/HttpRemoteCommandRunner`1.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using Polly;
2+
using Polly.Contrib.WaitAndRetry;
3+
using Polly.Retry;
14
using RestSharp;
25
using Wemogy.Core.Errors;
36
using Wemogy.Core.Extensions;
@@ -12,17 +15,25 @@ namespace Wemogy.CQRS.Extensions.FastEndpoints.RemoteCommandRunners;
1215
public class HttpRemoteCommandRunner<TCommand> : IRemoteCommandRunner<TCommand>
1316
where TCommand : ICommandBase
1417
{
15-
private readonly RestClient _restClient;
18+
private readonly IRestClient _restClient;
1619

1720
/// <summary>
1821
/// This is the sub-path of the client base path
1922
/// </summary>
2023
private readonly string _urlPath;
24+
private readonly IAsyncPolicy<RestResponse> _retryPolicy;
2125

22-
public HttpRemoteCommandRunner(RestClient restClient, string urlPath)
26+
public HttpRemoteCommandRunner(IRestClient restClient, string urlPath)
2327
{
2428
_restClient = restClient;
2529
_urlPath = urlPath;
30+
var retryCount = 3;
31+
var delay = Backoff.ExponentialBackoff(
32+
TimeSpan.FromMilliseconds(100),
33+
retryCount);
34+
_retryPolicy = Policy
35+
.HandleResult<RestResponse>(x => !x.IsSuccessful)
36+
.WaitAndRetryAsync(delay);
2637
}
2738

2839
public async Task RunAsync(CommandRequest<TCommand> command)
@@ -33,7 +44,7 @@ public async Task RunAsync(CommandRequest<TCommand> command)
3344

3445
try
3546
{
36-
var response = await _restClient.ExecutePostAsync(request);
47+
var response = await _retryPolicy.ExecuteAsync(() => _restClient.ExecutePostAsync(request));
3748

3849
if (!response.IsSuccessful)
3950
{

src/extensions/fastEndpoints/Wemogy.CQRS.Extensions.FastEndpoints/RemoteCommandRunners/HttpRemoteCommandRunner`2.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Text.Json;
2+
using Polly;
3+
using Polly.Contrib.WaitAndRetry;
24
using RestSharp;
35
using Wemogy.Core.Errors;
46
using Wemogy.Core.Extensions;
@@ -14,17 +16,25 @@ namespace Wemogy.CQRS.Extensions.FastEndpoints.RemoteCommandRunners;
1416
public class HttpRemoteCommandRunner<TCommand, TResult> : IRemoteCommandRunner<TCommand, TResult>
1517
where TCommand : ICommand<TResult>
1618
{
17-
private readonly RestClient _restClient;
19+
private readonly IRestClient _restClient;
1820

1921
/// <summary>
2022
/// This is the sub-path of the client base path
2123
/// </summary>
2224
private readonly string _urlPath;
25+
private readonly IAsyncPolicy<RestResponse> _retryPolicy;
2326

24-
public HttpRemoteCommandRunner(RestClient restClient, string urlPath)
27+
public HttpRemoteCommandRunner(IRestClient restClient, string urlPath)
2528
{
2629
_restClient = restClient;
2730
_urlPath = urlPath;
31+
var retryCount = 3;
32+
var delay = Backoff.ExponentialBackoff(
33+
TimeSpan.FromMilliseconds(100),
34+
retryCount);
35+
_retryPolicy = Policy
36+
.HandleResult<RestResponse>(x => !x.IsSuccessful)
37+
.WaitAndRetryAsync(delay);
2838
}
2939

3040
public async Task<TResult> RunAsync(CommandRequest<TCommand> command)
@@ -33,7 +43,7 @@ public async Task<TResult> RunAsync(CommandRequest<TCommand> command)
3343
var request = new RestRequest(_urlPath)
3444
.AddJsonBody(command);
3545

36-
var response = await _restClient.PostAsync(request);
46+
var response = await _retryPolicy.ExecuteAsync(() => _restClient.PostAsync(request));
3747

3848
if (!response.IsSuccessful)
3949
{

0 commit comments

Comments
 (0)