diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/CommandResponse.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/CommandResponse.cs index 89fab2e..614059b 100644 --- a/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/CommandResponse.cs +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/CommandResponse.cs @@ -148,7 +148,8 @@ private CommandResponse(TView response) /// /// This property can be null even if execution completed with no error. /// - public TView? Response { get; } + // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Global + public TView? Response { get; init; } /// /// Create a with given error. @@ -184,4 +185,4 @@ public static CommandResponse Success(TView? view) { return Response; } -} \ No newline at end of file +} diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ApiControllerBase.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ApiControllerBase.cs index aa78382..78fbe03 100644 --- a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ApiControllerBase.cs +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ApiControllerBase.cs @@ -41,7 +41,7 @@ protected IActionResult HandleCommandResponse(CommandResponse re } /// - /// Handle command response and return 204 if success, 400 if error. + /// Handle command response and return 200 if success, 400 if error. /// /// The command response. /// The response type when success. @@ -52,7 +52,7 @@ protected IActionResult HandleCommandResponse(CommandResponse { if (response.IsSuccess()) { - return Ok(response.Response); + return Request.Headers.CqrsVersion() > 1 ? Ok(response) : Ok(response.Response); } return HandleCommandResponse((CommandResponse)response); @@ -62,7 +62,7 @@ private IActionResult HandleErrorCommandResponse(CommandResponse 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; } diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/AssemblyInfo.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/AssemblyInfo.cs new file mode 100644 index 0000000..f8afacf --- /dev/null +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Cnblogs.Architecture.IntegrationTests")] diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CommandEndpointHandler.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CommandEndpointHandler.cs index c5300f3..0b3b675 100644 --- a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CommandEndpointHandler.cs +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CommandEndpointHandler.cs @@ -58,7 +58,9 @@ public CommandEndpointHandler(IMediator mediator, IOptions 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(); @@ -70,7 +72,8 @@ public CommandEndpointHandler(IMediator mediator, IOptions 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; } diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsHeaderNames.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsHeaderNames.cs new file mode 100644 index 0000000..e33a1bb --- /dev/null +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsHeaderNames.cs @@ -0,0 +1,6 @@ +namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore; + +internal static class CqrsHeaderNames +{ + public const string CqrsVersion = "X-Cqrs-Version"; +} diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsObjectResult.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsObjectResult.cs new file mode 100644 index 0000000..3395296 --- /dev/null +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsObjectResult.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore; + +/// +/// Send command response as json and report current cqrs version. +/// +/// +public class CqrsObjectResult(object? value) : ObjectResult(value) +{ + /// + public override Task ExecuteResultAsync(ActionContext context) + { + context.HttpContext.Response.Headers.AppendCurrentCqrsVersion(); + return base.ExecuteResultAsync(context); + } +} diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsResult.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsResult.cs new file mode 100644 index 0000000..e391fd0 --- /dev/null +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsResult.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Http; + +namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore; + +/// +/// Send object as json and append X-Cqrs-Version header +/// +/// +public class CqrsResult(object commandResponse) : IResult +{ + /// + public Task ExecuteAsync(HttpContext httpContext) + { + httpContext.Response.Headers.Append("X-Cqrs-Version", "2"); + return httpContext.Response.WriteAsJsonAsync(commandResponse); + } +} diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsResultExtensions.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsResultExtensions.cs new file mode 100644 index 0000000..cb6a2ef --- /dev/null +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsResultExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; + +namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore; + +/// +/// Extension methods for creating cqrs result. +/// +public static class CqrsResultExtensions +{ + /// + /// Write result as json and append cqrs response header. + /// + /// + /// The command response. + /// + public static IResult Cqrs(this IResultExtensions extensions, object result) + { + ArgumentNullException.ThrowIfNull(extensions); + return new CqrsResult(result); + } +} diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsRouteMapper.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsRouteMapper.cs index f968a22..5b4ad3a 100644 --- a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsRouteMapper.cs +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsRouteMapper.cs @@ -21,6 +21,12 @@ public static class CqrsRouteMapper private static readonly string[] GetAndHeadMethods = { "GET", "HEAD" }; + private static readonly List PostCommandPrefixes = new() { "Create", "Add", "New" }; + + private static readonly List PutCommandPrefixes = new() { "Update", "Modify", "Replace", "Alter" }; + + private static readonly List DeleteCommandPrefixes = new() { "Delete", "Remove", "Clean", "Clear", "Purge" }; + /// /// Map a query API, using GET method. would been constructed from route and query string. /// @@ -164,7 +170,23 @@ public static IEndpointConventionBuilder MapCommand( 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(route); + } + + if (PutCommandPrefixes.Any(x => commandTypeName.StartsWith(x))) + { + return app.MapPutCommand(route); + } + + if (DeleteCommandPrefixes.Any(x => commandTypeName.StartsWith(x))) + { + return app.MapDeleteCommand(route); + } + + return app.MapPutCommand(route); } /// @@ -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); } @@ -297,6 +319,72 @@ public static IEndpointConventionBuilder MapDeleteCommand( return app.MapDelete(route, handler).AddEndpointFilter(); } + /// + /// Map prefix to POST method for further MapCommand() calls. + /// + /// + /// The new prefix. + public static IEndpointRouteBuilder MapPrefixToPost(this IEndpointRouteBuilder app, string prefix) + { + PostCommandPrefixes.Add(prefix); + return app; + } + + /// + /// Stop mapping prefix to POST method for further MapCommand() calls. + /// + /// + /// The new prefix. + public static IEndpointRouteBuilder StopMappingPrefixToPost(this IEndpointRouteBuilder app, string prefix) + { + PostCommandPrefixes.Remove(prefix); + return app; + } + + /// + /// Map prefix to PUT method for further MapCommand() calls. + /// + /// + /// The new prefix. + public static IEndpointRouteBuilder MapPrefixToPut(this IEndpointRouteBuilder app, string prefix) + { + PutCommandPrefixes.Add(prefix); + return app; + } + + /// + /// Stop mapping prefix to PUT method for further MapCommand() calls. + /// + /// + /// The new prefix. + public static IEndpointRouteBuilder StopMappingPrefixToPut(this IEndpointRouteBuilder app, string prefix) + { + PutCommandPrefixes.Remove(prefix); + return app; + } + + /// + /// Map prefix to DELETE method for further MapCommand() calls. + /// + /// + /// The new prefix. + public static IEndpointRouteBuilder MapPrefixToDelete(this IEndpointRouteBuilder app, string prefix) + { + DeleteCommandPrefixes.Add(prefix); + return app; + } + + /// + /// Stop mapping prefix to DELETE method for further MapCommand() calls. + /// + /// + /// The new prefix. + 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) diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsVersionExtensions.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsVersionExtensions.cs new file mode 100644 index 0000000..f5d5fec --- /dev/null +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CqrsVersionExtensions.cs @@ -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()); + } +} diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj index e340534..223d37c 100644 --- a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent.csproj @@ -11,4 +11,12 @@ + + + CqrsHeaderNames.cs + + + CqrsVersionExtensions.cs + + diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/CqrsServiceAgent.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/CqrsServiceAgent.cs index a8b7bff..19f4da5 100644 --- a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/CqrsServiceAgent.cs +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/CqrsServiceAgent.cs @@ -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; @@ -266,7 +267,7 @@ private static async Task> HandleCommandRespo try { - if (httpResponseMessage.StatusCode == HttpStatusCode.OK) + if (httpResponseMessage.StatusCode == HttpStatusCode.OK && httpResponseMessage.Headers.CqrsVersion() == 1) { var result = await httpResponseMessage.Content.ReadFromJsonAsync(); return CommandResponse.Success(result); diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/InjectExtensions.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/InjectExtensions.cs index 53bb635..a8ed4e5 100644 --- a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/InjectExtensions.cs +++ b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/InjectExtensions.cs @@ -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; @@ -30,7 +31,7 @@ public static IHttpClientBuilder AddServiceAgent( h => { h.BaseAddress = new Uri(baseUri); - h.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs")); + h.AddCqrsAcceptHeaders(); }).AddPolicyHandler(policy); } @@ -55,10 +56,16 @@ public static IHttpClientBuilder AddServiceAgent( 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 GetDefaultPolicy() { return HttpPolicyExtensions.HandleTransientHttpError() diff --git a/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/CommandHandlers.cs b/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/CommandHandlers.cs index 1496920..fec1c5c 100644 --- a/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/CommandHandlers.cs +++ b/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/CommandHandlers.cs @@ -5,41 +5,34 @@ namespace Cnblogs.Architecture.IntegrationTestProject.Application.Commands; -public class CommandHandlers - : ICommandHandler, ICommandHandler, - ICommandHandler +public class CommandHandlers(IMediator mediator) + : ICommandHandler, ICommandHandler, + ICommandHandler { - private readonly IMediator _mediator; - - public CommandHandlers(IMediator mediator) - { - _mediator = mediator; - } - /// - public async Task> Handle(CreateCommand request, CancellationToken cancellationToken) + public async Task> Handle(CreateCommand request, CancellationToken cancellationToken) { - await _mediator.Publish(new StringCreatedDomainEvent(request.Data ?? string.Empty), cancellationToken); + await mediator.Publish(new StringCreatedDomainEvent(request.Data ?? string.Empty), cancellationToken); return request.NeedError - ? CommandResponse.Fail(TestError.Default) - : CommandResponse.Success(); + ? CommandResponse.Fail(TestError.Default) + : CommandResponse.Success("create success"); } /// - public Task> Handle(UpdateCommand request, CancellationToken cancellationToken) + public Task> Handle(UpdateCommand request, CancellationToken cancellationToken) { return Task.FromResult( request.NeedExecutionError - ? CommandResponse.Fail(TestError.Default) - : CommandResponse.Success()); + ? CommandResponse.Fail(TestError.Default) + : CommandResponse.Success("update success")); } /// - public Task> Handle(DeleteCommand request, CancellationToken cancellationToken) + public Task> Handle(DeleteCommand request, CancellationToken cancellationToken) { return Task.FromResult( request.NeedError - ? CommandResponse.Fail(TestError.Default) - : CommandResponse.Success()); + ? CommandResponse.Fail(TestError.Default) + : CommandResponse.Success("delete success")); } } diff --git a/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/CreateCommand.cs b/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/CreateCommand.cs index eeb78ef..964f08d 100644 --- a/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/CreateCommand.cs +++ b/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/CreateCommand.cs @@ -3,4 +3,4 @@ namespace Cnblogs.Architecture.IntegrationTestProject.Application.Commands; -public record CreateCommand(bool NeedError, string? Data = null, bool ValidateOnly = false) : ICommand; +public record CreateCommand(bool NeedError, string? Data = null, bool ValidateOnly = false) : ICommand; diff --git a/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/DeleteCommand.cs b/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/DeleteCommand.cs index d137b40..94816b9 100644 --- a/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/DeleteCommand.cs +++ b/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/DeleteCommand.cs @@ -3,4 +3,4 @@ namespace Cnblogs.Architecture.IntegrationTestProject.Application.Commands; -public record DeleteCommand(int Id, bool NeedError, bool ValidateOnly = false) : ICommand; \ No newline at end of file +public record DeleteCommand(int Id, bool NeedError, bool ValidateOnly = false) : ICommand; diff --git a/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/UpdateCommand.cs b/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/UpdateCommand.cs index 73a3b5e..9121554 100644 --- a/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/UpdateCommand.cs +++ b/test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/UpdateCommand.cs @@ -8,7 +8,7 @@ public record UpdateCommand( bool NeedValidationError, bool NeedExecutionError, bool ValidateOnly = false) - : ICommand, IValidatable + : ICommand, IValidatable { /// public void Validate(ValidationErrors errors) diff --git a/test/Cnblogs.Architecture.IntegrationTests/CommandResponseHandlerTests.cs b/test/Cnblogs.Architecture.IntegrationTests/CommandResponseHandlerTests.cs index dfe5c10..145eb5b 100644 --- a/test/Cnblogs.Architecture.IntegrationTests/CommandResponseHandlerTests.cs +++ b/test/Cnblogs.Architecture.IntegrationTests/CommandResponseHandlerTests.cs @@ -23,6 +23,75 @@ public class CommandResponseHandlerTests new object[] { true, false }, new object[] { false, true } }; + [Fact] + public async Task MinimalApi_NoCqrsVersionHeader_RawResultAsync() + { + // Arrange + var builder = new WebApplicationFactory(); + var client = builder.CreateClient(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs")); + + // Act + var response = await client.PutAsJsonAsync("/api/v1/strings/1", new UpdatePayload()); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + content.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task MinimalApi_CqrsV2_CommandResponseAsync() + { + // Arrange + var builder = new WebApplicationFactory(); + var client = builder.CreateClient(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs")); + client.DefaultRequestHeaders.AppendCurrentCqrsVersion(); + + // Act + var response = await client.PutAsJsonAsync("/api/v1/strings/1", new UpdatePayload()); + var content = await response.Content.ReadFromJsonAsync>(); + + // Assert + content.Should().NotBeNull(); + content!.Response.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Mvc_NoCqrsVersionHeader_RawResultAsync() + { + // Arrange + var builder = new WebApplicationFactory(); + var client = builder.CreateClient(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs")); + + // Act + var response = await client.PutAsJsonAsync("/api/v1/mvc/strings/1", new UpdatePayload()); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + response.Should().BeSuccessful(); + content.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Mvc_CurrentCqrsVersion_CommandResponseAsync() + { + // Arrange + var builder = new WebApplicationFactory(); + var client = builder.CreateClient(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs")); + client.DefaultRequestHeaders.AppendCurrentCqrsVersion(); + + // Act + var response = await client.PutAsJsonAsync("/api/v1/mvc/strings/1", new UpdatePayload()); + var content = await response.Content.ReadFromJsonAsync>(); + + // Assert + response.Should().BeSuccessful(); + content!.Response.Should().NotBeNull(); + } + [Theory] [MemberData(nameof(ErrorPayloads))] public async Task MinimalApi_HavingError_BadRequestAsync(bool needValidationError, bool needExecutionError) diff --git a/test/Cnblogs.Architecture.IntegrationTests/IntegrationEventPublishTests.cs b/test/Cnblogs.Architecture.IntegrationTests/IntegrationEventPublishTests.cs index 664d29a..8fbb04c 100644 --- a/test/Cnblogs.Architecture.IntegrationTests/IntegrationEventPublishTests.cs +++ b/test/Cnblogs.Architecture.IntegrationTests/IntegrationEventPublishTests.cs @@ -39,7 +39,7 @@ public async Task EventBus_PublishEvent_SuccessAsync() // Assert response.Should().BeSuccessful(); - content.Should().BeNullOrEmpty(); + content.Should().NotBeNullOrEmpty(); await eventBusMock.Received(1).PublishAsync( Arg.Any(), Arg.Is(t => t.Message == data)); @@ -77,7 +77,7 @@ public async Task EventBus_Downgrading_DowngradeAsync() // Assert response.Should().BeSuccessful(); - content.Should().BeNullOrEmpty(); + content.Should().NotBeNullOrEmpty(); await eventBusMock.Received(2).PublishAsync( Arg.Any(), Arg.Is(t => t.Message == data));