diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/CommandResponse.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/CommandResponse.cs
index feb4507..89fab2e 100644
--- a/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/CommandResponse.cs
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/CommandResponse.cs
@@ -5,7 +5,7 @@ namespace Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
///
/// Response returned by .
///
-public abstract record CommandResponse : IValidationResponse, ILockableResponse
+public record CommandResponse : IValidationResponse, ILockableResponse
{
///
/// Check if validation fails.
@@ -69,6 +69,7 @@ public CommandResponse()
public CommandResponse(TError errorCode)
{
ErrorCode = errorCode;
+ ErrorMessage = errorCode.Name;
}
///
@@ -173,9 +174,9 @@ private CommandResponse(TView response)
///
/// The model to return.
/// A with given result.
- public static CommandResponse Success(TView view)
+ public static CommandResponse Success(TView? view)
{
- return new CommandResponse(view);
+ return view is null ? Success() : new CommandResponse(view);
}
///
diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ApiControllerBase.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ApiControllerBase.cs
index d25e7c3..a60a200 100644
--- a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ApiControllerBase.cs
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ApiControllerBase.cs
@@ -61,10 +61,17 @@ protected IActionResult HandleCommandResponse(CommandResponse
private IActionResult HandleErrorCommandResponse(CommandResponse response)
where TError : Enumeration
{
- return CqrsHttpOptions.CommandErrorResponseType switch
+ var errorResponseType = CqrsHttpOptions.CommandErrorResponseType;
+ if (Request.Headers.Accept.Contains("application/cqrs"))
+ {
+ errorResponseType = ErrorResponseType.Cqrs;
+ }
+
+ return errorResponseType switch
{
ErrorResponseType.PlainText => MapErrorCommandResponseToPlainText(response),
ErrorResponseType.ProblemDetails => MapErrorCommandResponseToProblemDetails(response),
+ ErrorResponseType.Cqrs => MapErrorCommandResponseToCqrsResponse(response),
ErrorResponseType.Custom => CustomErrorCommandResponseMap(response),
_ => throw new ArgumentOutOfRangeException(
$"Unsupported CommandErrorResponseType: {CqrsHttpOptions.CommandErrorResponseType}")
@@ -90,6 +97,12 @@ protected virtual IActionResult CustomErrorCommandResponseMap(CommandRes
return MapErrorCommandResponseToPlainText(response);
}
+ private IActionResult MapErrorCommandResponseToCqrsResponse(CommandResponse response)
+ where TError : Enumeration
+ {
+ return BadRequest(response);
+ }
+
private IActionResult MapErrorCommandResponseToProblemDetails(CommandResponse response)
where TError : Enumeration
{
diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CommandEndpointHandler.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CommandEndpointHandler.cs
index 0616ee6..359ba80 100644
--- a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CommandEndpointHandler.cs
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CommandEndpointHandler.cs
@@ -69,10 +69,17 @@ public CommandEndpointHandler(IMediator mediator, IOptions opti
private IResult HandleErrorCommandResponse(CommandResponse response, HttpContext context)
{
- return _options.CommandErrorResponseType switch
+ var errorResponseType = _options.CommandErrorResponseType;
+ if (context.Request.Headers.Accept.Contains("application/cqrs"))
+ {
+ errorResponseType = ErrorResponseType.Cqrs;
+ }
+
+ return errorResponseType switch
{
ErrorResponseType.PlainText => HandleErrorCommandResponseWithPlainText(response),
ErrorResponseType.ProblemDetails => HandleErrorCommandResponseWithProblemDetails(response),
+ ErrorResponseType.Cqrs => HandleErrorCommandResponseWithCqrs(response),
ErrorResponseType.Custom => _options.CustomCommandErrorResponseMapper?.Invoke(response, context)
?? HandleErrorCommandResponseWithPlainText(response),
_ => throw new ArgumentOutOfRangeException(
@@ -80,6 +87,11 @@ private IResult HandleErrorCommandResponse(CommandResponse response, HttpContext
};
}
+ private static IResult HandleErrorCommandResponseWithCqrs(CommandResponse response)
+ {
+ return Results.BadRequest(response);
+ }
+
private static IResult HandleErrorCommandResponseWithPlainText(CommandResponse response)
{
if (response.IsValidationError)
diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ErrorResponseType.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ErrorResponseType.cs
index bb7cf82..3b569f1 100644
--- a/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ErrorResponseType.cs
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ErrorResponseType.cs
@@ -1,3 +1,5 @@
+using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
+
namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
///
@@ -15,6 +17,11 @@ public enum ErrorResponseType
///
ProblemDetails,
+ ///
+ /// Returns
+ ///
+ Cqrs,
+
///
/// Handles command error by custom logic.
///
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 7802fbf..8115e6f 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
@@ -5,9 +5,9 @@
-
+
-
+
diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/CqrsServiceAgent.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/CqrsServiceAgent.cs
new file mode 100644
index 0000000..c82684b
--- /dev/null
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/CqrsServiceAgent.cs
@@ -0,0 +1,261 @@
+using System.Net;
+using System.Net.Http.Json;
+using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
+using Cnblogs.Architecture.Ddd.Infrastructure.Abstractions;
+
+namespace Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent;
+
+///
+/// Service Agent for CQRS
+///
+public abstract class CqrsServiceAgent
+{
+ ///
+ /// The underlying .
+ ///
+ protected HttpClient HttpClient { get; }
+
+ ///
+ /// Create a service agent for cqrs api.
+ ///
+ /// The underlying HttpClient.
+ protected CqrsServiceAgent(HttpClient httpClient)
+ {
+ HttpClient = httpClient;
+ }
+
+ ///
+ /// Execute a command with DELETE method.
+ ///
+ /// The url.
+ /// Response type.
+ /// The response.
+ public async Task> DeleteCommandAsync(string url)
+ {
+ var response = await HttpClient.DeleteAsync(url);
+ return await HandleCommandResponseAsync(response);
+ }
+
+ ///
+ /// Execute a command with DELETE method.
+ ///
+ /// The route of the API.
+ public async Task> DeleteCommandAsync(string url)
+ {
+ var response = await HttpClient.DeleteAsync(url);
+ return await HandleCommandResponseAsync(response);
+ }
+
+ ///
+ /// Execute a command with POST method.
+ ///
+ /// The route of the API.
+ public async Task> PostCommandAsync(string url)
+ {
+ var response = await HttpClient.PostAsync(url, new StringContent(string.Empty));
+ return await HandleCommandResponseAsync(response);
+ }
+
+ ///
+ /// Execute a command with POST method and payload.
+ ///
+ /// The route of the API.
+ /// The request body.
+ /// The type of request body.
+ public async Task> PostCommandAsync(string url, TPayload payload)
+ {
+ var response = await HttpClient.PostAsJsonAsync(url, payload);
+ return await HandleCommandResponseAsync(response);
+ }
+
+ ///
+ /// Execute a command with POST method and payload.
+ ///
+ /// The route of the API.
+ /// The request body.
+ /// The type of response body.
+ /// The type of request body.
+ /// The response body.
+ public async Task> PostCommandAsync(
+ string url,
+ TPayload payload)
+ {
+ var response = await HttpClient.PostAsJsonAsync(url, payload);
+ return await HandleCommandResponseAsync(response);
+ }
+
+ ///
+ /// Execute a command with PUT method and payload.
+ ///
+ /// The route of API.
+ public async Task> PutCommandAsync(string url)
+ {
+ var response = await HttpClient.PutAsync(url, new StringContent(string.Empty));
+ return await HandleCommandResponseAsync(response);
+ }
+
+ ///
+ /// Execute a command with PUT method and payload.
+ ///
+ /// The route of API.
+ /// The request body.
+ /// The type of request body.
+ /// The command response.
+ public async Task> PutCommandAsync(string url, TPayload payload)
+ {
+ var response = await HttpClient.PutAsJsonAsync(url, payload);
+ return await HandleCommandResponseAsync(response);
+ }
+
+ ///
+ /// Execute a command with PUT method and payload.
+ ///
+ /// The route of API.
+ /// The request body.
+ /// The type of response body.
+ /// The type of request body.
+ /// The response body.
+ public async Task> PutCommandAsync(
+ string url,
+ TPayload payload)
+ {
+ var response = await HttpClient.PutAsJsonAsync(url, payload);
+ return await HandleCommandResponseAsync(response);
+ }
+
+ ///
+ /// Query item with GET method.
+ ///
+ /// The route of the API.
+ /// The type of item to get.
+ /// The query result, can be null if item does not exists or status code is 404.
+ public async Task GetItemAsync(string url)
+ {
+ try
+ {
+ return await HttpClient.GetFromJsonAsync(url);
+ }
+ catch (HttpRequestException e)
+ {
+ if (e.StatusCode == HttpStatusCode.NotFound)
+ {
+ return default;
+ }
+
+ throw;
+ }
+ }
+
+ ///
+ /// Batch get items with GET method.
+ ///
+ /// The route of the API.
+ /// The name of id field.
+ /// The id list.
+ /// The type of the query result item.
+ /// The type of the id.
+ /// A list of items that contains id that in , the order or count of the items are not guaranteed.
+ public async Task> BatchGetItemsAsync(
+ string url,
+ string paramName,
+ IEnumerable ids)
+ where TId : notnull
+ {
+ var query = string.Join(
+ '&',
+ ids.Select(i => $"{WebUtility.UrlEncode(paramName)}={WebUtility.UrlEncode(i.ToString())}"));
+ url = $"{url}{(url.Contains('?') ? '&' : '?')}{query}";
+ return await HttpClient.GetFromJsonAsync>(url) ?? new List();
+ }
+
+ ///
+ /// Get paged list of items based on url.
+ ///
+ /// The route of the API.
+ /// The paging parameters, including page size and page index.
+ /// Specifies the order of items to return.
+ /// The type of items to query.
+ /// The paged list of items. An empty list is returned when there is no result.
+ public async Task> ListPagedItemsAsync(
+ string url,
+ PagingParams? pagingParams = null,
+ string? orderByString = null)
+ {
+ return await ListPagedItemsAsync(url, pagingParams?.PageIndex, pagingParams?.PageSize, orderByString);
+ }
+
+ ///
+ /// Get paged list of items based on url.
+ ///
+ /// The route of the API.
+ /// The page index.
+ /// The page size.
+ /// Specifies the order of items to return.
+ /// The type of items to query.
+ /// The paged list of items. An empty list is returned when there is no result.
+ public async Task> ListPagedItemsAsync(
+ string url,
+ int? pageIndex,
+ int? pageSize,
+ string? orderByString = null)
+ {
+ if (pageIndex.HasValue && pageSize.HasValue)
+ {
+ var query = $"pageIndex={pageIndex}&pageSize={pageSize}&orderByString={orderByString}";
+ url = url.Contains('?') ? url + "&" + query : url + "?" + query;
+ }
+
+ return await HttpClient.GetFromJsonAsync>(url) ?? new PagedList();
+ }
+
+ private static async Task> HandleCommandResponseAsync(
+ HttpResponseMessage httpResponseMessage)
+ {
+ if (httpResponseMessage.IsSuccessStatusCode)
+ {
+ var result = await httpResponseMessage.Content.ReadFromJsonAsync();
+ return CommandResponse.Success(result);
+ }
+
+ var response = await httpResponseMessage.Content.ReadFromJsonAsync();
+ if (response is null)
+ {
+ return CommandResponse.Fail(ServiceAgentError.UnknownError);
+ }
+
+ return new CommandResponse
+ {
+ IsConcurrentError = response.IsConcurrentError,
+ IsValidationError = response.IsValidationError,
+ ErrorMessage = response.ErrorMessage,
+ LockAcquired = response.LockAcquired,
+ ValidationErrors = response.ValidationErrors,
+ ErrorCode = new ServiceAgentError(1, response.ErrorMessage)
+ };
+ }
+
+ private static async Task> HandleCommandResponseAsync(
+ HttpResponseMessage message)
+ {
+ if (message.IsSuccessStatusCode)
+ {
+ return CommandResponse.Success();
+ }
+
+ var response = await message.Content.ReadFromJsonAsync();
+ if (response is null)
+ {
+ return CommandResponse.Fail(ServiceAgentError.UnknownError);
+ }
+
+ return new CommandResponse
+ {
+ IsConcurrentError = response.IsConcurrentError,
+ IsValidationError = response.IsValidationError,
+ ErrorMessage = response.ErrorMessage,
+ LockAcquired = response.LockAcquired,
+ ValidationErrors = response.ValidationErrors,
+ ErrorCode = new ServiceAgentError(1, response.ErrorMessage)
+ };
+ }
+}
diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/IApiException.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/IApiException.cs
index ce4db8f..47f3328 100644
--- a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/IApiException.cs
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/IApiException.cs
@@ -6,6 +6,7 @@ namespace Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent;
/// Defines exceptions threw when doing an API call.
///
/// The type of this API exception.
+[Obsolete("Try migrate to CqrsServiceAgent")]
public interface IApiException
where TException : Exception, IApiException
{
diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/InjectExtensions.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/InjectExtensions.cs
new file mode 100644
index 0000000..a950eb4
--- /dev/null
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/InjectExtensions.cs
@@ -0,0 +1,50 @@
+using System.Net.Http.Headers;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent;
+
+///
+/// Inject helper for service agent
+///
+public static class InjectExtensions
+{
+ ///
+ /// Inject a service agent to services.
+ ///
+ /// The .
+ /// The base uri for api.
+ /// The type of service agent
+ ///
+ public static IHttpClientBuilder AddServiceAgent(this IServiceCollection services, string baseUri)
+ where T : CqrsServiceAgent
+ {
+ return services.AddHttpClient(
+ h =>
+ {
+ h.BaseAddress = new Uri(baseUri);
+ h.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs"));
+ });
+ }
+
+ ///
+ /// Inject a service agent to services.
+ ///
+ /// The .
+ /// The base uri for api.
+ /// The type of api client.
+ /// The type of service agent
+ ///
+ public static IHttpClientBuilder AddServiceAgent(
+ this IServiceCollection services,
+ string baseUri)
+ where TClient : class
+ where TImplementation : CqrsServiceAgent, TClient
+ {
+ return services.AddHttpClient(
+ h =>
+ {
+ h.BaseAddress = new Uri(baseUri);
+ h.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs"));
+ });
+ }
+}
diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/ServiceAgentBase.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/ServiceAgentBase.cs
index fd568a1..2d60860 100644
--- a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/ServiceAgentBase.cs
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/ServiceAgentBase.cs
@@ -9,6 +9,7 @@ namespace Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent;
/// Base class for service agent.
///
/// The type of exception that this service agent throws.
+[Obsolete("Try migrate to CqrsServiceAgent")]
public abstract class ServiceAgentBase
where TException : Exception, IApiException
{
diff --git a/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/ServiceAgentError.cs b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/ServiceAgentError.cs
new file mode 100644
index 0000000..d014f7c
--- /dev/null
+++ b/src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/ServiceAgentError.cs
@@ -0,0 +1,24 @@
+using Cnblogs.Architecture.Ddd.Domain.Abstractions;
+
+namespace Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent;
+
+///
+/// ServiceAgent errors.
+///
+public class ServiceAgentError : Enumeration
+{
+ ///
+ /// The default error code.
+ ///
+ public static readonly ServiceAgentError UnknownError = new(-1, "Unknown error");
+
+ ///
+ /// Create a service agent error.
+ ///
+ /// The error code.
+ /// The error name.
+ public ServiceAgentError(int id, string name)
+ : base(id, name)
+ {
+ }
+}
diff --git a/test/Cnblogs.Architecture.IntegrationTests/CommandResponseHandlerTests.cs b/test/Cnblogs.Architecture.IntegrationTests/CommandResponseHandlerTests.cs
index 121c7c9..e6cfe28 100644
--- a/test/Cnblogs.Architecture.IntegrationTests/CommandResponseHandlerTests.cs
+++ b/test/Cnblogs.Architecture.IntegrationTests/CommandResponseHandlerTests.cs
@@ -1,4 +1,6 @@
+using System.Net.Http.Headers;
using System.Net.Http.Json;
+using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
using Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
using Cnblogs.Architecture.IntegrationTestProject;
using Cnblogs.Architecture.IntegrationTestProject.Application.Commands;
@@ -31,7 +33,7 @@ public async Task MinimalApi_HavingError_BadRequestAsync(bool needValidationErro
// Act
var response = await builder.CreateClient().PutAsJsonAsync(
"/api/v1/strings/1",
- new UpdatePayload(needValidationError, needExecutionError));
+ new UpdatePayload(needExecutionError, needValidationError));
var content = await response.Content.ReadAsStringAsync();
// Assert
@@ -63,7 +65,7 @@ public async Task MinimalApi_HavingError_ProblemDetailsAsync(bool needValidation
// Act
var response = await builder.CreateClient().PutAsJsonAsync(
"/api/v1/strings/1",
- new UpdatePayload(needValidationError, needExecutionError));
+ new UpdatePayload(needExecutionError, needValidationError));
var content = await response.Content.ReadFromJsonAsync();
// Assert
@@ -71,6 +73,27 @@ public async Task MinimalApi_HavingError_ProblemDetailsAsync(bool needValidation
content.Should().NotBeNull();
}
+ [Theory]
+ [MemberData(nameof(ErrorPayloads))]
+ public async Task MinimalApi_HavingError_CommandResponseAsync(bool needValidationError, bool needExecutionError)
+ {
+ // Arrange
+ var builder = new WebApplicationFactory();
+
+ // Act
+ var client = builder.CreateClient();
+ client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs"));
+ var response = await client.PutAsJsonAsync(
+ "/api/v1/strings/1",
+ new UpdatePayload(needExecutionError, needValidationError));
+ var commandResponse = await response.Content.ReadFromJsonAsync();
+
+ // Assert
+ response.Should().HaveClientError();
+ commandResponse.Should().NotBeNull();
+ commandResponse!.IsSuccess().Should().BeFalse();
+ }
+
[Theory]
[MemberData(nameof(ErrorPayloads))]
public async Task MinimalApi_HavingError_CustomContentAsync(bool needValidationError, bool needExecutionError)
@@ -143,6 +166,27 @@ public async Task Mvc_HavingError_ProblemDetailAsync(bool needValidationError, b
content.Should().NotBeNull();
}
+ [Theory]
+ [MemberData(nameof(ErrorPayloads))]
+ public async Task Mvc_HavingError_CommandResponseAsync(bool needValidationError, bool needExecutionError)
+ {
+ // Arrange
+ var builder = new WebApplicationFactory();
+
+ // Act
+ var client = builder.CreateClient();
+ client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/cqrs"));
+ var response = await client.PutAsJsonAsync(
+ "/api/v1/mvc/strings/1",
+ new UpdatePayload(needValidationError, needExecutionError));
+ var content = await response.Content.ReadFromJsonAsync();
+
+ // Assert
+ response.Should().HaveClientError();
+ content.Should().NotBeNull();
+ content!.IsSuccess().Should().BeFalse();
+ }
+
[Theory]
[MemberData(nameof(ErrorPayloads))]
public async Task Mvc_HavingError_CustomContentAsync(bool needValidationError, bool needExecutionError)