Skip to content

Commit ff7e799

Browse files
Introduce VersionCheckMiddleware and update dependencies
Replaces inline version check in Startup.cs with a dedicated VersionCheckMiddleware class for centralized request version validation. Adds unit tests for the middleware. Updates multiple .csproj files to use newer package versions for improved compatibility and maintenance.
1 parent ef5c282 commit ff7e799

9 files changed

Lines changed: 233 additions & 112 deletions

File tree

TransactionProcessorACL.BusinessLogic.Tests/TransactionProcessorACL.BusinessLogic.Tests.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,21 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="ClientProxyBase" Version="2025.12.2" />
10+
<PackageReference Include="ClientProxyBase" Version="2026.2.1" />
1111
<PackageReference Include="Lamar" Version="15.0.1" />
1212
<PackageReference Include="MediatR" Version="14.0.0" />
13-
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
13+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
1414
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
1515
<PackageReference Include="Moq" Version="4.20.72" />
1616
<PackageReference Include="Shouldly" Version="4.3.0" />
17-
<PackageReference Include="TransactionProcessor.Client" Version="2025.12.1" />
18-
<PackageReference Include="TransactionProcessor.IntegrationTesting.Helpers" Version="2025.12.1" />
17+
<PackageReference Include="TransactionProcessor.Client" Version="2026.1.1" />
18+
<PackageReference Include="TransactionProcessor.IntegrationTesting.Helpers" Version="2026.1.1" />
1919
<PackageReference Include="xunit" Version="2.9.3" />
2020
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
2121
<PrivateAssets>all</PrivateAssets>
2222
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2323
</PackageReference>
24-
<PackageReference Include="coverlet.msbuild" Version="6.0.4">
24+
<PackageReference Include="coverlet.msbuild" Version="8.0.0">
2525
<PrivateAssets>all</PrivateAssets>
2626
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2727
</PackageReference>

TransactionProcessorACL.BusinessLogic/TransactionProcessorACL.BusinessLogic.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="ClientProxyBase" Version="2025.12.2" />
8+
<PackageReference Include="ClientProxyBase" Version="2026.2.1" />
99
<PackageReference Include="MediatR" Version="14.0.0" />
1010
<PackageReference Include="SecurityService.Client" Version="2025.12.2" />
11-
<PackageReference Include="Shared" Version="2025.12.2" />
12-
<PackageReference Include="TransactionProcessor.Client" Version="2025.12.1" />
11+
<PackageReference Include="Shared" Version="2026.2.1" />
12+
<PackageReference Include="TransactionProcessor.Client" Version="2026.1.1" />
1313
</ItemGroup>
1414

1515
<ItemGroup>

TransactionProcessorACL.IntegrationTests/TransactionProcessorACL.IntegrationTests.csproj

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,26 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="ClientProxyBase" Version="2025.12.2" />
10+
<PackageReference Include="ClientProxyBase" Version="2026.2.1" />
1111
<PackageReference Include="Ductus.FluentDocker" Version="2.85.0" />
1212
<PackageReference Include="EventStoreProjections" Version="2023.12.3" />
1313
<PackageReference Include="MessagingService.IntegrationTesting.Helpers" Version="2025.12.1" />
14-
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" />
15-
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
14+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
15+
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" />
1616
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
17-
<PackageReference Include="Reqnroll.Tools.MsBuild.Generation" Version="3.3.0" />
18-
<PackageReference Include="NUnit" Version="4.4.0" />
19-
<PackageReference Include="NUnit3TestAdapter" Version="6.0.1" />
20-
<PackageReference Include="Reqnroll" Version="3.3.0" />
21-
<PackageReference Include="Reqnroll.NUnit" Version="3.3.0" />
17+
<PackageReference Include="Reqnroll.Tools.MsBuild.Generation" Version="3.3.3" />
18+
<PackageReference Include="NUnit" Version="4.5.0" />
19+
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
20+
<PackageReference Include="Reqnroll" Version="3.3.3" />
21+
<PackageReference Include="Reqnroll.NUnit" Version="3.3.3" />
2222
<PackageReference Include="SecurityService.Client" Version="2025.12.2" />
2323
<PackageReference Include="SecurityService.IntegrationTesting.Helpers" Version="2025.12.2" />
24-
<PackageReference Include="Shared.IntegrationTesting" Version="2025.12.2" />
24+
<PackageReference Include="Shared.IntegrationTesting" Version="2026.2.1" />
2525
<PackageReference Include="Shouldly" Version="4.3.0" />
26-
<PackageReference Include="TransactionProcessor.Client" Version="2025.12.1" />
27-
<PackageReference Include="TransactionProcessor.IntegrationTesting.Helpers" Version="2025.12.1" />
26+
<PackageReference Include="TransactionProcessor.Client" Version="2026.1.1" />
27+
<PackageReference Include="TransactionProcessor.IntegrationTesting.Helpers" Version="2026.1.1" />
2828

29-
<PackageReference Include="coverlet.collector" Version="6.0.4">
29+
<PackageReference Include="coverlet.collector" Version="8.0.0">
3030
<PrivateAssets>all</PrivateAssets>
3131
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
3232
</PackageReference>

TransactionProcessorACL.Testing/TransactionProcessorACL.Testing.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<PackageReference Include="ClientProxyBase" Version="2025.12.2" />
10-
<PackageReference Include="TransactionProcessor.Client" Version="2025.12.1" />
11-
<PackageReference Include="TransactionProcessor.IntegrationTesting.Helpers" Version="2025.12.1" />
9+
<PackageReference Include="ClientProxyBase" Version="2026.2.1" />
10+
<PackageReference Include="TransactionProcessor.Client" Version="2026.1.1" />
11+
<PackageReference Include="TransactionProcessor.IntegrationTesting.Helpers" Version="2026.1.1" />
1212
</ItemGroup>
1313

1414
<ItemGroup>
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System.IO;
2+
using System.Text;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using MediatR;
6+
using Microsoft.AspNetCore.Http;
7+
using Moq;
8+
using Shouldly;
9+
using SimpleResults;
10+
using TransactionProcessorACL.BusinessLogic.Requests;
11+
using TransactionProcessorACL.Middleware;
12+
using Xunit;
13+
14+
namespace TransactionProcessorACL.Tests.General
15+
{
16+
public class VersionCheckMiddlewareTests
17+
{
18+
[Fact]
19+
public async Task InvokeAsync_WhenMediatorReturnsSuccess_CallsNextAndLeavesStatusUnchanged()
20+
{
21+
// Arrange
22+
var json = "{\"application_version\":\"1.2.3\"}";
23+
var context = new DefaultHttpContext();
24+
context.Request.Path = "/api/transactions";
25+
context.Request.ContentType = "application/json";
26+
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
27+
context.Request.ContentLength = context.Request.Body.Length;
28+
29+
bool nextCalled = false;
30+
RequestDelegate next = ctx =>
31+
{
32+
nextCalled = true;
33+
ctx.Response.StatusCode = 200;
34+
return Task.CompletedTask;
35+
};
36+
37+
var mediatorMock = new Mock<IMediator>(MockBehavior.Strict);
38+
mediatorMock
39+
.Setup(m => m.Send(It.IsAny<VersionCheckCommands.VersionCheckCommand>(), It.IsAny<CancellationToken>()))
40+
.ReturnsAsync(Result.Success);
41+
42+
var middleware = new VersionCheckMiddleware(next);
43+
44+
// Act
45+
await middleware.InvokeAsync(context, mediatorMock.Object);
46+
47+
// Assert
48+
nextCalled.ShouldBeTrue();
49+
context.Response.StatusCode.ShouldBe(200);
50+
mediatorMock.Verify(m => m.Send(It.IsAny<VersionCheckCommands.VersionCheckCommand>(), It.IsAny<CancellationToken>()), Times.Once);
51+
}
52+
53+
[Fact]
54+
public async Task InvokeAsync_WhenMediatorReturnsFailure_SetsStatus505AndDoesNotCallNext()
55+
{
56+
// Arrange
57+
var json = "{\"application_version\":\"1.2.3\"}";
58+
var context = new DefaultHttpContext();
59+
context.Request.Path = "/api/transactions";
60+
context.Request.ContentType = "application/json";
61+
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
62+
context.Request.ContentLength = context.Request.Body.Length;
63+
64+
bool nextCalled = false;
65+
RequestDelegate next = ctx =>
66+
{
67+
nextCalled = true;
68+
ctx.Response.StatusCode = 200;
69+
return Task.CompletedTask;
70+
};
71+
72+
var mediatorMock = new Mock<IMediator>(MockBehavior.Strict);
73+
mediatorMock
74+
.Setup(m => m.Send(It.IsAny<VersionCheckCommands.VersionCheckCommand>(), It.IsAny<CancellationToken>()))
75+
.ReturnsAsync(Result.Failure); // simulate old/invalid version
76+
77+
var middleware = new VersionCheckMiddleware(next);
78+
79+
// Act
80+
await middleware.InvokeAsync(context, mediatorMock.Object);
81+
82+
// Assert
83+
nextCalled.ShouldBeFalse();
84+
context.Response.StatusCode.ShouldBe(505);
85+
mediatorMock.Verify(m => m.Send(It.IsAny<VersionCheckCommands.VersionCheckCommand>(), It.IsAny<CancellationToken>()), Times.Once);
86+
}
87+
88+
[Fact]
89+
public async Task InvokeAsync_WhenPathIsHealth_SkipsVersionCheckAndCallsNext()
90+
{
91+
// Arrange
92+
var context = new DefaultHttpContext();
93+
context.Request.Path = "/health/ready";
94+
95+
bool nextCalled = false;
96+
RequestDelegate next = ctx =>
97+
{
98+
nextCalled = true;
99+
ctx.Response.StatusCode = 200;
100+
return Task.CompletedTask;
101+
};
102+
103+
var mediatorMock = new Mock<IMediator>(MockBehavior.Strict);
104+
// mediator should not be called for health paths
105+
106+
var middleware = new VersionCheckMiddleware(next);
107+
108+
// Act
109+
await middleware.InvokeAsync(context, mediatorMock.Object);
110+
111+
// Assert
112+
nextCalled.ShouldBeTrue();
113+
context.Response.StatusCode.ShouldBe(200);
114+
mediatorMock.Verify(m => m.Send(It.IsAny<VersionCheckCommands.VersionCheckCommand>(), It.IsAny<CancellationToken>()), Times.Never);
115+
}
116+
}
117+
}

TransactionProcessorACL.Tests/TransactionProcessorACL.Tests.csproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="ClientProxyBase" Version="2025.12.2" />
11-
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" />
12-
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
10+
<PackageReference Include="ClientProxyBase" Version="2026.2.1" />
11+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
12+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
1313
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
1414
<PackageReference Include="Moq" Version="4.20.72" />
1515
<PackageReference Include="Shouldly" Version="4.3.0" />
16-
<PackageReference Include="TransactionProcessor.Client" Version="2025.12.1" />
17-
<PackageReference Include="TransactionProcessor.IntegrationTesting.Helpers" Version="2025.12.1" />
16+
<PackageReference Include="TransactionProcessor.Client" Version="2026.1.1" />
17+
<PackageReference Include="TransactionProcessor.IntegrationTesting.Helpers" Version="2026.1.1" />
1818
<PackageReference Include="xunit" Version="2.9.3" />
1919
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
2020
<PrivateAssets>all</PrivateAssets>
2121
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2222
</PackageReference>
23-
<PackageReference Include="coverlet.msbuild" Version="6.0.4">
23+
<PackageReference Include="coverlet.msbuild" Version="8.0.0">
2424
<PrivateAssets>all</PrivateAssets>
2525
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2626
</PackageReference>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
 using System.IO;
2+
using System.Text;
3+
using System.Text.Json;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using MediatR;
7+
using Microsoft.AspNetCore.Http;
8+
using SimpleResults;
9+
using TransactionProcessorACL.BusinessLogic.Requests;
10+
11+
namespace TransactionProcessorACL.Middleware;
12+
13+
public sealed class VersionCheckMiddleware {
14+
private readonly RequestDelegate _next;
15+
16+
public VersionCheckMiddleware(RequestDelegate next) {
17+
_next = next;
18+
}
19+
20+
public async Task InvokeAsync(HttpContext context,
21+
IMediator mediator) {
22+
var path = context.Request.Path;
23+
24+
if (path.StartsWithSegments("/health") || path.StartsWithSegments("/healthui")) {
25+
await _next(context);
26+
return;
27+
}
28+
29+
context.Request.EnableBuffering();
30+
31+
string? applicationVersion = null;
32+
33+
if (context.Request.ContentType?.Contains("application/json") == true) {
34+
using var reader = new StreamReader(context.Request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true);
35+
36+
string body = await reader.ReadToEndAsync().ConfigureAwait(false);
37+
38+
// Reset the request body stream position so the endpoint can read it
39+
context.Request.Body.Position = 0;
40+
41+
try {
42+
using var json = JsonDocument.Parse(body);
43+
if (json.RootElement.TryGetProperty("application_version", out JsonElement versionProp)) {
44+
applicationVersion = versionProp.GetString();
45+
}
46+
}
47+
catch {
48+
// Ignore JSON parse errors — allow request to continue
49+
}
50+
}
51+
52+
// Fallback to querystring if needed
53+
if (string.IsNullOrEmpty(applicationVersion)) {
54+
applicationVersion = context.Request.Query["applicationVersion"];
55+
}
56+
57+
CancellationToken cancellationToken = context.RequestAborted;
58+
59+
VersionCheckCommands.VersionCheckCommand versionCheckCommand = new(applicationVersion);
60+
Result versionCheckResult = await mediator.Send(versionCheckCommand, cancellationToken).ConfigureAwait(false);
61+
if (versionCheckResult.IsFailed) {
62+
context.Response.StatusCode = 505;
63+
return; // stop the pipeline
64+
}
65+
66+
await _next(context).ConfigureAwait(false);
67+
}
68+
}

TransactionProcessorACL/Startup.cs

Lines changed: 2 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ namespace TransactionProcessorACL
2525
using System.Text;
2626
using System.Text.Json;
2727
using System.Threading;
28+
using TransactionProcessorACL.Middleware;
2829
using ILogger = Microsoft.Extensions.Logging.ILogger;
2930

3031
[ExcludeFromCodeCoverage]
@@ -80,72 +81,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF
8081
app.AddRequestLogging();
8182
app.AddResponseLogging();
8283
app.AddExceptionHandler();
83-
84-
app.Use(async (context, next) =>
85-
{
86-
//if (context.Request.Path.StartsWithSegments("/health"))
87-
var path = context.Request.Path;
88-
89-
if (path.StartsWithSegments("/health") ||
90-
path.StartsWithSegments("/healthui"))
91-
{
92-
await next();
93-
return;
94-
}
95-
96-
// Enable buffering so we can read the body multiple times
97-
context.Request.EnableBuffering();
98-
99-
string? applicationVersion = null;
100-
101-
// Only read the body if it's JSON (optional safety check)
102-
if (context.Request.ContentType?.Contains("application/json") == true)
103-
{
104-
using var reader = new StreamReader(
105-
context.Request.Body,
106-
encoding: Encoding.UTF8,
107-
detectEncodingFromByteOrderMarks: false,
108-
bufferSize: 1024,
109-
leaveOpen: true);
110-
111-
string body = await reader.ReadToEndAsync();
112-
113-
// Reset the request body stream position so the endpoint can read it
114-
context.Request.Body.Position = 0;
115-
116-
// Parse JSON (use System.Text.Json)
117-
try
118-
{
119-
var json = JsonDocument.Parse(body);
120-
if (json.RootElement.TryGetProperty("application_version", out JsonElement versionProp))
121-
{
122-
applicationVersion = versionProp.GetString();
123-
}
124-
}
125-
catch
126-
{
127-
// Ignore JSON parse errors — allow request to continue
128-
}
129-
}
130-
131-
// Fallback to querystring if needed
132-
applicationVersion ??= context.Request.Query["applicationVersion"];
133-
134-
// TODO: move to middleware class
135-
CancellationToken cancellationToken = context.RequestAborted;
136-
var mediator = context.RequestServices.GetRequiredService<IMediator>();
137-
138-
VersionCheckCommands.VersionCheckCommand versionCheckCommand = new(applicationVersion);
139-
Result versionCheckResult = await mediator.Send(versionCheckCommand, cancellationToken);
140-
if(versionCheckResult.IsFailed)
141-
{
142-
context.Response.StatusCode = 505;
143-
return; // stop the pipeline
144-
}
145-
146-
await next(); // Call the next middleware / endpoint
147-
148-
});
84+
app.UseMiddleware<VersionCheckMiddleware>();
14985

15086
app.UseRouting();
15187

0 commit comments

Comments
 (0)