Skip to content

Commit

Permalink
Merge pull request #182 from cnblogs/fix-string-result-deserialization
Browse files Browse the repository at this point in the history
feat: support cqrs-v2 responses
  • Loading branch information
ikesnowy authored Nov 29, 2023
2 parents c378130 + b778146 commit c5241e3
Show file tree
Hide file tree
Showing 19 changed files with 312 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ private CommandResponse(TView response)
/// <remarks>
/// This property can be null even if execution completed with no error.
/// </remarks>
public TView? Response { get; }
// ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global
public TView? Response { get; init; }

/// <summary>
/// Create a <see cref="CommandResponse{TView,TError}" /> with given error.
Expand Down Expand Up @@ -184,4 +185,4 @@ public static CommandResponse<TView, TError> Success(TView? view)
{
return Response;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ protected IActionResult HandleCommandResponse<TError>(CommandResponse<TError> re
}

/// <summary>
/// Handle command response and return 204 if success, 400 if error.
/// Handle command response and return 200 if success, 400 if error.
/// </summary>
/// <param name="response">The command response.</param>
/// <typeparam name="TResponse">The response type when success.</typeparam>
Expand All @@ -52,7 +52,7 @@ protected IActionResult HandleCommandResponse<TResponse, TError>(CommandResponse
{
if (response.IsSuccess())
{
return Ok(response.Response);
return Request.Headers.CqrsVersion() > 1 ? Ok(response) : Ok(response.Response);
}

return HandleCommandResponse((CommandResponse<TError>)response);
Expand All @@ -62,7 +62,7 @@ private IActionResult HandleErrorCommandResponse<TError>(CommandResponse<TError>
where TError : Enumeration
{
var errorResponseType = CqrsHttpOptions.CommandErrorResponseType;
if (Request.Headers.Accept.Contains("application/cqrs"))
if (Request.Headers.Accept.Contains("application/cqrs") || Request.Headers.CqrsVersion() > 1)
{
errorResponseType = ErrorResponseType.Cqrs;
}
Expand Down
3 changes: 3 additions & 0 deletions src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Cnblogs.Architecture.IntegrationTests")]
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ public CommandEndpointHandler(IMediator mediator, IOptions<CqrsHttpOptions> opti
// check if response has result
if (commandResponse is IObjectResponse objectResponse)
{
return Results.Ok(objectResponse.GetResult());
return context.HttpContext.Request.Headers.CqrsVersion() > 1
? Results.Extensions.Cqrs(response)
: Results.Ok(objectResponse.GetResult());
}

return Results.NoContent();
Expand All @@ -70,7 +72,8 @@ public CommandEndpointHandler(IMediator mediator, IOptions<CqrsHttpOptions> opti
private IResult HandleErrorCommandResponse(CommandResponse response, HttpContext context)
{
var errorResponseType = _options.CommandErrorResponseType;
if (context.Request.Headers.Accept.Contains("application/cqrs"))
if (context.Request.Headers.Accept.Contains("application/cqrs")
|| context.Request.Headers.Accept.Contains("application/cqrs-v2"))
{
errorResponseType = ErrorResponseType.Cqrs;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;

internal static class CqrsHeaderNames
{
public const string CqrsVersion = "X-Cqrs-Version";
}
17 changes: 17 additions & 0 deletions src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsObjectResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Mvc;

namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;

/// <summary>
/// Send command response as json and report current cqrs version.
/// </summary>
/// <param name="value"></param>
public class CqrsObjectResult(object? value) : ObjectResult(value)
{
/// <inheritdoc />
public override Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers.AppendCurrentCqrsVersion();
return base.ExecuteResultAsync(context);
}
}
17 changes: 17 additions & 0 deletions src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Http;

namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;

/// <summary>
/// Send object as json and append X-Cqrs-Version header
/// </summary>
/// <param name="commandResponse"></param>
public class CqrsResult(object commandResponse) : IResult
{
/// <inheritdoc />
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.Headers.Append("X-Cqrs-Version", "2");
return httpContext.Response.WriteAsJsonAsync(commandResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Http;

namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;

/// <summary>
/// Extension methods for creating cqrs result.
/// </summary>
public static class CqrsResultExtensions
{
/// <summary>
/// Write result as json and append cqrs response header.
/// </summary>
/// <param name="extensions"><see cref="IResultExtensions"/></param>
/// <param name="result">The command response.</param>
/// <returns></returns>
public static IResult Cqrs(this IResultExtensions extensions, object result)
{
ArgumentNullException.ThrowIfNull(extensions);
return new CqrsResult(result);
}
}
96 changes: 92 additions & 4 deletions src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsRouteMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ public static class CqrsRouteMapper

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

private static readonly List<string> PostCommandPrefixes = new() { "Create", "Add", "New" };

private static readonly List<string> PutCommandPrefixes = new() { "Update", "Modify", "Replace", "Alter" };

private static readonly List<string> DeleteCommandPrefixes = new() { "Delete", "Remove", "Clean", "Clear", "Purge" };

/// <summary>
/// Map a query API, using GET method. <typeparamref name="T"/> would been constructed from route and query string.
/// </summary>
Expand Down Expand Up @@ -164,7 +170,23 @@ public static IEndpointConventionBuilder MapCommand<T>(
this IEndpointRouteBuilder app,
[StringSyntax("Route")] string route)
{
return app.MapCommand(route, ([AsParameters] T command) => command);
var commandTypeName = typeof(T).Name;
if (PostCommandPrefixes.Any(x => commandTypeName.StartsWith(x)))
{
return app.MapPostCommand<T>(route);
}

if (PutCommandPrefixes.Any(x => commandTypeName.StartsWith(x)))
{
return app.MapPutCommand<T>(route);
}

if (DeleteCommandPrefixes.Any(x => commandTypeName.StartsWith(x)))
{
return app.MapDeleteCommand<T>(route);
}

return app.MapPutCommand<T>(route);
}

/// <summary>
Expand All @@ -189,17 +211,17 @@ public static IEndpointConventionBuilder MapCommand(
{
EnsureDelegateReturnTypeIsCommand(handler);
var commandTypeName = handler.Method.ReturnType.Name;
if (commandTypeName.StartsWith("Create") || commandTypeName.StartsWith("Add"))
if (PostCommandPrefixes.Any(x => commandTypeName.StartsWith(x)))
{
return app.MapPostCommand(route, handler);
}

if (commandTypeName.StartsWith("Update") || commandTypeName.StartsWith("Replace"))
if (PutCommandPrefixes.Any(x => commandTypeName.StartsWith(x)))
{
return app.MapPutCommand(route, handler);
}

if (commandTypeName.StartsWith("Delete") || commandTypeName.StartsWith("Remove"))
if (DeleteCommandPrefixes.Any(x => commandTypeName.StartsWith(x)))
{
return app.MapDeleteCommand(route, handler);
}
Expand Down Expand Up @@ -297,6 +319,72 @@ public static IEndpointConventionBuilder MapDeleteCommand(
return app.MapDelete(route, handler).AddEndpointFilter<CommandEndpointHandler>();
}

/// <summary>
/// Map prefix to POST method for further MapCommand() calls.
/// </summary>
/// <param name="app"><see cref="IEndpointRouteBuilder"/></param>
/// <param name="prefix">The new prefix.</param>
public static IEndpointRouteBuilder MapPrefixToPost(this IEndpointRouteBuilder app, string prefix)
{
PostCommandPrefixes.Add(prefix);
return app;
}

/// <summary>
/// Stop mapping prefix to POST method for further MapCommand() calls.
/// </summary>
/// <param name="app"><see cref="IEndpointRouteBuilder"/></param>
/// <param name="prefix">The new prefix.</param>
public static IEndpointRouteBuilder StopMappingPrefixToPost(this IEndpointRouteBuilder app, string prefix)
{
PostCommandPrefixes.Remove(prefix);
return app;
}

/// <summary>
/// Map prefix to PUT method for further MapCommand() calls.
/// </summary>
/// <param name="app"><see cref="IEndpointRouteBuilder"/></param>
/// <param name="prefix">The new prefix.</param>
public static IEndpointRouteBuilder MapPrefixToPut(this IEndpointRouteBuilder app, string prefix)
{
PutCommandPrefixes.Add(prefix);
return app;
}

/// <summary>
/// Stop mapping prefix to PUT method for further MapCommand() calls.
/// </summary>
/// <param name="app"><see cref="IEndpointRouteBuilder"/></param>
/// <param name="prefix">The new prefix.</param>
public static IEndpointRouteBuilder StopMappingPrefixToPut(this IEndpointRouteBuilder app, string prefix)
{
PutCommandPrefixes.Remove(prefix);
return app;
}

/// <summary>
/// Map prefix to DELETE method for further MapCommand() calls.
/// </summary>
/// <param name="app"><see cref="IEndpointRouteBuilder"/></param>
/// <param name="prefix">The new prefix.</param>
public static IEndpointRouteBuilder MapPrefixToDelete(this IEndpointRouteBuilder app, string prefix)
{
DeleteCommandPrefixes.Add(prefix);
return app;
}

/// <summary>
/// Stop mapping prefix to DELETE method for further MapCommand() calls.
/// </summary>
/// <param name="app"><see cref="IEndpointRouteBuilder"/></param>
/// <param name="prefix">The new prefix.</param>
public static IEndpointRouteBuilder StopMappingPrefixToDelete(this IEndpointRouteBuilder app, string prefix)
{
DeleteCommandPrefixes.Remove(prefix);
return app;
}

private static void EnsureDelegateReturnTypeIsCommand(Delegate handler)
{
var isCommand = handler.Method.ReturnType.GetInterfaces().Where(x => x.IsGenericType)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Http;

namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;

internal static class CqrsVersionExtensions
{
private const int CurrentCqrsVersion = 2;

public static int CqrsVersion(this IHeaderDictionary headers)
{
return int.TryParse(headers[CqrsHeaderNames.CqrsVersion].ToString(), out var version) ? version : 1;
}

public static int CqrsVersion(this HttpHeaders headers)
{
if (headers.Contains(CqrsHeaderNames.CqrsVersion) == false)
{
return 1;
}

return headers.GetValues(CqrsHeaderNames.CqrsVersion).Select(x => int.TryParse(x, out var y) ? y : 1).Max();
}

public static void CqrsVersion(this IHeaderDictionary headers, int version)
{
headers.Append(CqrsHeaderNames.CqrsVersion, version.ToString());
}

public static void AppendCurrentCqrsVersion(this IHeaderDictionary headers)
{
headers.CqrsVersion(CurrentCqrsVersion);
}

public static void AppendCurrentCqrsVersion(this HttpHeaders headers)
{
headers.Add(CqrsHeaderNames.CqrsVersion, CurrentCqrsVersion.ToString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,12 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Cnblogs.Architecture.Ddd.Cqrs.AspNetCore\CqrsHeaderNames.cs">
<Link>CqrsHeaderNames.cs</Link>
</Compile>
<Compile Include="..\Cnblogs.Architecture.Ddd.Cqrs.AspNetCore\CqrsVersionExtensions.cs">
<Link>CqrsVersionExtensions.cs</Link>
</Compile>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Net.Http.Json;
using System.Text.Json;
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
using Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
using Cnblogs.Architecture.Ddd.Domain.Abstractions;
using Cnblogs.Architecture.Ddd.Infrastructure.Abstractions;

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

try
{
if (httpResponseMessage.StatusCode == HttpStatusCode.OK)
if (httpResponseMessage.StatusCode == HttpStatusCode.OK && httpResponseMessage.Headers.CqrsVersion() == 1)
{
var result = await httpResponseMessage.Content.ReadFromJsonAsync<TResponse>();
return CommandResponse<TResponse, TError>.Success(result);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Net;
using System.Net.Http.Headers;
using Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
using Microsoft.Extensions.DependencyInjection;
using Polly;
using Polly.Extensions.Http;
Expand Down Expand Up @@ -30,7 +31,7 @@ public static IHttpClientBuilder AddServiceAgent<TClient>(
h =>
{
h.BaseAddress = new Uri(baseUri);
h.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs"));
h.AddCqrsAcceptHeaders();
}).AddPolicyHandler(policy);
}

Expand All @@ -55,10 +56,16 @@ public static IHttpClientBuilder AddServiceAgent<TClient, TImplementation>(
h =>
{
h.BaseAddress = new Uri(baseUri);
h.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs"));
h.AddCqrsAcceptHeaders();
}).AddPolicyHandler(policy);
}

private static void AddCqrsAcceptHeaders(this HttpClient h)
{
h.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs"));
h.DefaultRequestHeaders.AppendCurrentCqrsVersion();
}

private static IAsyncPolicy<HttpResponseMessage> GetDefaultPolicy()
{
return HttpPolicyExtensions.HandleTransientHttpError()
Expand Down
Loading

0 comments on commit c5241e3

Please sign in to comment.