Skip to content

Commit b778146

Browse files
committed
feat: support cqrs-v2 responses
1 parent c378130 commit b778146

File tree

19 files changed

+312
-39
lines changed

19 files changed

+312
-39
lines changed

src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/CommandResponse.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ private CommandResponse(TView response)
148148
/// <remarks>
149149
/// This property can be null even if execution completed with no error.
150150
/// </remarks>
151-
public TView? Response { get; }
151+
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global
152+
public TView? Response { get; init; }
152153

153154
/// <summary>
154155
/// Create a <see cref="CommandResponse{TView,TError}" /> with given error.
@@ -184,4 +185,4 @@ public static CommandResponse<TView, TError> Success(TView? view)
184185
{
185186
return Response;
186187
}
187-
}
188+
}

src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ApiControllerBase.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ protected IActionResult HandleCommandResponse<TError>(CommandResponse<TError> re
4141
}
4242

4343
/// <summary>
44-
/// Handle command response and return 204 if success, 400 if error.
44+
/// Handle command response and return 200 if success, 400 if error.
4545
/// </summary>
4646
/// <param name="response">The command response.</param>
4747
/// <typeparam name="TResponse">The response type when success.</typeparam>
@@ -52,7 +52,7 @@ protected IActionResult HandleCommandResponse<TResponse, TError>(CommandResponse
5252
{
5353
if (response.IsSuccess())
5454
{
55-
return Ok(response.Response);
55+
return Request.Headers.CqrsVersion() > 1 ? Ok(response) : Ok(response.Response);
5656
}
5757

5858
return HandleCommandResponse((CommandResponse<TError>)response);
@@ -62,7 +62,7 @@ private IActionResult HandleErrorCommandResponse<TError>(CommandResponse<TError>
6262
where TError : Enumeration
6363
{
6464
var errorResponseType = CqrsHttpOptions.CommandErrorResponseType;
65-
if (Request.Headers.Accept.Contains("application/cqrs"))
65+
if (Request.Headers.Accept.Contains("application/cqrs") || Request.Headers.CqrsVersion() > 1)
6666
{
6767
errorResponseType = ErrorResponseType.Cqrs;
6868
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("Cnblogs.Architecture.IntegrationTests")]

src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CommandEndpointHandler.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ public CommandEndpointHandler(IMediator mediator, IOptions<CqrsHttpOptions> opti
5858
// check if response has result
5959
if (commandResponse is IObjectResponse objectResponse)
6060
{
61-
return Results.Ok(objectResponse.GetResult());
61+
return context.HttpContext.Request.Headers.CqrsVersion() > 1
62+
? Results.Extensions.Cqrs(response)
63+
: Results.Ok(objectResponse.GetResult());
6264
}
6365

6466
return Results.NoContent();
@@ -70,7 +72,8 @@ public CommandEndpointHandler(IMediator mediator, IOptions<CqrsHttpOptions> opti
7072
private IResult HandleErrorCommandResponse(CommandResponse response, HttpContext context)
7173
{
7274
var errorResponseType = _options.CommandErrorResponseType;
73-
if (context.Request.Headers.Accept.Contains("application/cqrs"))
75+
if (context.Request.Headers.Accept.Contains("application/cqrs")
76+
|| context.Request.Headers.Accept.Contains("application/cqrs-v2"))
7477
{
7578
errorResponseType = ErrorResponseType.Cqrs;
7679
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
2+
3+
internal static class CqrsHeaderNames
4+
{
5+
public const string CqrsVersion = "X-Cqrs-Version";
6+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
3+
namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
4+
5+
/// <summary>
6+
/// Send command response as json and report current cqrs version.
7+
/// </summary>
8+
/// <param name="value"></param>
9+
public class CqrsObjectResult(object? value) : ObjectResult(value)
10+
{
11+
/// <inheritdoc />
12+
public override Task ExecuteResultAsync(ActionContext context)
13+
{
14+
context.HttpContext.Response.Headers.AppendCurrentCqrsVersion();
15+
return base.ExecuteResultAsync(context);
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Microsoft.AspNetCore.Http;
2+
3+
namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
4+
5+
/// <summary>
6+
/// Send object as json and append X-Cqrs-Version header
7+
/// </summary>
8+
/// <param name="commandResponse"></param>
9+
public class CqrsResult(object commandResponse) : IResult
10+
{
11+
/// <inheritdoc />
12+
public Task ExecuteAsync(HttpContext httpContext)
13+
{
14+
httpContext.Response.Headers.Append("X-Cqrs-Version", "2");
15+
return httpContext.Response.WriteAsJsonAsync(commandResponse);
16+
}
17+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Microsoft.AspNetCore.Http;
2+
3+
namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
4+
5+
/// <summary>
6+
/// Extension methods for creating cqrs result.
7+
/// </summary>
8+
public static class CqrsResultExtensions
9+
{
10+
/// <summary>
11+
/// Write result as json and append cqrs response header.
12+
/// </summary>
13+
/// <param name="extensions"><see cref="IResultExtensions"/></param>
14+
/// <param name="result">The command response.</param>
15+
/// <returns></returns>
16+
public static IResult Cqrs(this IResultExtensions extensions, object result)
17+
{
18+
ArgumentNullException.ThrowIfNull(extensions);
19+
return new CqrsResult(result);
20+
}
21+
}

src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsRouteMapper.cs

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ public static class CqrsRouteMapper
2121

2222
private static readonly string[] GetAndHeadMethods = { "GET", "HEAD" };
2323

24+
private static readonly List<string> PostCommandPrefixes = new() { "Create", "Add", "New" };
25+
26+
private static readonly List<string> PutCommandPrefixes = new() { "Update", "Modify", "Replace", "Alter" };
27+
28+
private static readonly List<string> DeleteCommandPrefixes = new() { "Delete", "Remove", "Clean", "Clear", "Purge" };
29+
2430
/// <summary>
2531
/// Map a query API, using GET method. <typeparamref name="T"/> would been constructed from route and query string.
2632
/// </summary>
@@ -164,7 +170,23 @@ public static IEndpointConventionBuilder MapCommand<T>(
164170
this IEndpointRouteBuilder app,
165171
[StringSyntax("Route")] string route)
166172
{
167-
return app.MapCommand(route, ([AsParameters] T command) => command);
173+
var commandTypeName = typeof(T).Name;
174+
if (PostCommandPrefixes.Any(x => commandTypeName.StartsWith(x)))
175+
{
176+
return app.MapPostCommand<T>(route);
177+
}
178+
179+
if (PutCommandPrefixes.Any(x => commandTypeName.StartsWith(x)))
180+
{
181+
return app.MapPutCommand<T>(route);
182+
}
183+
184+
if (DeleteCommandPrefixes.Any(x => commandTypeName.StartsWith(x)))
185+
{
186+
return app.MapDeleteCommand<T>(route);
187+
}
188+
189+
return app.MapPutCommand<T>(route);
168190
}
169191

170192
/// <summary>
@@ -189,17 +211,17 @@ public static IEndpointConventionBuilder MapCommand(
189211
{
190212
EnsureDelegateReturnTypeIsCommand(handler);
191213
var commandTypeName = handler.Method.ReturnType.Name;
192-
if (commandTypeName.StartsWith("Create") || commandTypeName.StartsWith("Add"))
214+
if (PostCommandPrefixes.Any(x => commandTypeName.StartsWith(x)))
193215
{
194216
return app.MapPostCommand(route, handler);
195217
}
196218

197-
if (commandTypeName.StartsWith("Update") || commandTypeName.StartsWith("Replace"))
219+
if (PutCommandPrefixes.Any(x => commandTypeName.StartsWith(x)))
198220
{
199221
return app.MapPutCommand(route, handler);
200222
}
201223

202-
if (commandTypeName.StartsWith("Delete") || commandTypeName.StartsWith("Remove"))
224+
if (DeleteCommandPrefixes.Any(x => commandTypeName.StartsWith(x)))
203225
{
204226
return app.MapDeleteCommand(route, handler);
205227
}
@@ -297,6 +319,72 @@ public static IEndpointConventionBuilder MapDeleteCommand(
297319
return app.MapDelete(route, handler).AddEndpointFilter<CommandEndpointHandler>();
298320
}
299321

322+
/// <summary>
323+
/// Map prefix to POST method for further MapCommand() calls.
324+
/// </summary>
325+
/// <param name="app"><see cref="IEndpointRouteBuilder"/></param>
326+
/// <param name="prefix">The new prefix.</param>
327+
public static IEndpointRouteBuilder MapPrefixToPost(this IEndpointRouteBuilder app, string prefix)
328+
{
329+
PostCommandPrefixes.Add(prefix);
330+
return app;
331+
}
332+
333+
/// <summary>
334+
/// Stop mapping prefix to POST method for further MapCommand() calls.
335+
/// </summary>
336+
/// <param name="app"><see cref="IEndpointRouteBuilder"/></param>
337+
/// <param name="prefix">The new prefix.</param>
338+
public static IEndpointRouteBuilder StopMappingPrefixToPost(this IEndpointRouteBuilder app, string prefix)
339+
{
340+
PostCommandPrefixes.Remove(prefix);
341+
return app;
342+
}
343+
344+
/// <summary>
345+
/// Map prefix to PUT method for further MapCommand() calls.
346+
/// </summary>
347+
/// <param name="app"><see cref="IEndpointRouteBuilder"/></param>
348+
/// <param name="prefix">The new prefix.</param>
349+
public static IEndpointRouteBuilder MapPrefixToPut(this IEndpointRouteBuilder app, string prefix)
350+
{
351+
PutCommandPrefixes.Add(prefix);
352+
return app;
353+
}
354+
355+
/// <summary>
356+
/// Stop mapping prefix to PUT method for further MapCommand() calls.
357+
/// </summary>
358+
/// <param name="app"><see cref="IEndpointRouteBuilder"/></param>
359+
/// <param name="prefix">The new prefix.</param>
360+
public static IEndpointRouteBuilder StopMappingPrefixToPut(this IEndpointRouteBuilder app, string prefix)
361+
{
362+
PutCommandPrefixes.Remove(prefix);
363+
return app;
364+
}
365+
366+
/// <summary>
367+
/// Map prefix to DELETE method for further MapCommand() calls.
368+
/// </summary>
369+
/// <param name="app"><see cref="IEndpointRouteBuilder"/></param>
370+
/// <param name="prefix">The new prefix.</param>
371+
public static IEndpointRouteBuilder MapPrefixToDelete(this IEndpointRouteBuilder app, string prefix)
372+
{
373+
DeleteCommandPrefixes.Add(prefix);
374+
return app;
375+
}
376+
377+
/// <summary>
378+
/// Stop mapping prefix to DELETE method for further MapCommand() calls.
379+
/// </summary>
380+
/// <param name="app"><see cref="IEndpointRouteBuilder"/></param>
381+
/// <param name="prefix">The new prefix.</param>
382+
public static IEndpointRouteBuilder StopMappingPrefixToDelete(this IEndpointRouteBuilder app, string prefix)
383+
{
384+
DeleteCommandPrefixes.Remove(prefix);
385+
return app;
386+
}
387+
300388
private static void EnsureDelegateReturnTypeIsCommand(Delegate handler)
301389
{
302390
var isCommand = handler.Method.ReturnType.GetInterfaces().Where(x => x.IsGenericType)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.Net.Http.Headers;
2+
using Microsoft.AspNetCore.Http;
3+
4+
namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
5+
6+
internal static class CqrsVersionExtensions
7+
{
8+
private const int CurrentCqrsVersion = 2;
9+
10+
public static int CqrsVersion(this IHeaderDictionary headers)
11+
{
12+
return int.TryParse(headers[CqrsHeaderNames.CqrsVersion].ToString(), out var version) ? version : 1;
13+
}
14+
15+
public static int CqrsVersion(this HttpHeaders headers)
16+
{
17+
if (headers.Contains(CqrsHeaderNames.CqrsVersion) == false)
18+
{
19+
return 1;
20+
}
21+
22+
return headers.GetValues(CqrsHeaderNames.CqrsVersion).Select(x => int.TryParse(x, out var y) ? y : 1).Max();
23+
}
24+
25+
public static void CqrsVersion(this IHeaderDictionary headers, int version)
26+
{
27+
headers.Append(CqrsHeaderNames.CqrsVersion, version.ToString());
28+
}
29+
30+
public static void AppendCurrentCqrsVersion(this IHeaderDictionary headers)
31+
{
32+
headers.CqrsVersion(CurrentCqrsVersion);
33+
}
34+
35+
public static void AppendCurrentCqrsVersion(this HttpHeaders headers)
36+
{
37+
headers.Add(CqrsHeaderNames.CqrsVersion, CurrentCqrsVersion.ToString());
38+
}
39+
}

src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,12 @@
1111
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
1212
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.0" />
1313
</ItemGroup>
14+
<ItemGroup>
15+
<Compile Include="..\Cnblogs.Architecture.Ddd.Cqrs.AspNetCore\CqrsHeaderNames.cs">
16+
<Link>CqrsHeaderNames.cs</Link>
17+
</Compile>
18+
<Compile Include="..\Cnblogs.Architecture.Ddd.Cqrs.AspNetCore\CqrsVersionExtensions.cs">
19+
<Link>CqrsVersionExtensions.cs</Link>
20+
</Compile>
21+
</ItemGroup>
1422
</Project>

src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/CqrsServiceAgent.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Net.Http.Json;
33
using System.Text.Json;
44
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
5+
using Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
56
using Cnblogs.Architecture.Ddd.Domain.Abstractions;
67
using Cnblogs.Architecture.Ddd.Infrastructure.Abstractions;
78

@@ -266,7 +267,7 @@ private static async Task<CommandResponse<TResponse, TError>> HandleCommandRespo
266267

267268
try
268269
{
269-
if (httpResponseMessage.StatusCode == HttpStatusCode.OK)
270+
if (httpResponseMessage.StatusCode == HttpStatusCode.OK && httpResponseMessage.Headers.CqrsVersion() == 1)
270271
{
271272
var result = await httpResponseMessage.Content.ReadFromJsonAsync<TResponse>();
272273
return CommandResponse<TResponse, TError>.Success(result);

src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/InjectExtensions.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Net;
22
using System.Net.Http.Headers;
3+
using Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
34
using Microsoft.Extensions.DependencyInjection;
45
using Polly;
56
using Polly.Extensions.Http;
@@ -30,7 +31,7 @@ public static IHttpClientBuilder AddServiceAgent<TClient>(
3031
h =>
3132
{
3233
h.BaseAddress = new Uri(baseUri);
33-
h.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs"));
34+
h.AddCqrsAcceptHeaders();
3435
}).AddPolicyHandler(policy);
3536
}
3637

@@ -55,10 +56,16 @@ public static IHttpClientBuilder AddServiceAgent<TClient, TImplementation>(
5556
h =>
5657
{
5758
h.BaseAddress = new Uri(baseUri);
58-
h.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs"));
59+
h.AddCqrsAcceptHeaders();
5960
}).AddPolicyHandler(policy);
6061
}
6162

63+
private static void AddCqrsAcceptHeaders(this HttpClient h)
64+
{
65+
h.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs"));
66+
h.DefaultRequestHeaders.AppendCurrentCqrsVersion();
67+
}
68+
6269
private static IAsyncPolicy<HttpResponseMessage> GetDefaultPolicy()
6370
{
6471
return HttpPolicyExtensions.HandleTransientHttpError()

0 commit comments

Comments
 (0)