Skip to content

Commit 9ca966b

Browse files
authored
Merge pull request #93 from cnblogs/support-cqrs-service-agent
feat: add cqrs service agent implementation
2 parents ac1baec + 8cf1a17 commit 9ca966b

File tree

11 files changed

+423
-9
lines changed

11 files changed

+423
-9
lines changed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
55
/// <summary>
66
/// Response returned by <see cref="ICommand{TError}"/>.
77
/// </summary>
8-
public abstract record CommandResponse : IValidationResponse, ILockableResponse
8+
public record CommandResponse : IValidationResponse, ILockableResponse
99
{
1010
/// <summary>
1111
/// Check if validation fails.
@@ -69,6 +69,7 @@ public CommandResponse()
6969
public CommandResponse(TError errorCode)
7070
{
7171
ErrorCode = errorCode;
72+
ErrorMessage = errorCode.Name;
7273
}
7374

7475
/// <summary>
@@ -173,9 +174,9 @@ private CommandResponse(TView response)
173174
/// </summary>
174175
/// <param name="view">The model to return.</param>
175176
/// <returns>A <see cref="CommandResponse{TView, TError}"/> with given result.</returns>
176-
public static CommandResponse<TView, TError> Success(TView view)
177+
public static CommandResponse<TView, TError> Success(TView? view)
177178
{
178-
return new CommandResponse<TView, TError>(view);
179+
return view is null ? Success() : new CommandResponse<TView, TError>(view);
179180
}
180181

181182
/// <inheritdoc />

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,17 @@ protected IActionResult HandleCommandResponse<TResponse, TError>(CommandResponse
6161
private IActionResult HandleErrorCommandResponse<TError>(CommandResponse<TError> response)
6262
where TError : Enumeration
6363
{
64-
return CqrsHttpOptions.CommandErrorResponseType switch
64+
var errorResponseType = CqrsHttpOptions.CommandErrorResponseType;
65+
if (Request.Headers.Accept.Contains("application/cqrs"))
66+
{
67+
errorResponseType = ErrorResponseType.Cqrs;
68+
}
69+
70+
return errorResponseType switch
6571
{
6672
ErrorResponseType.PlainText => MapErrorCommandResponseToPlainText(response),
6773
ErrorResponseType.ProblemDetails => MapErrorCommandResponseToProblemDetails(response),
74+
ErrorResponseType.Cqrs => MapErrorCommandResponseToCqrsResponse(response),
6875
ErrorResponseType.Custom => CustomErrorCommandResponseMap(response),
6976
_ => throw new ArgumentOutOfRangeException(
7077
$"Unsupported CommandErrorResponseType: {CqrsHttpOptions.CommandErrorResponseType}")
@@ -90,6 +97,12 @@ protected virtual IActionResult CustomErrorCommandResponseMap<TError>(CommandRes
9097
return MapErrorCommandResponseToPlainText(response);
9198
}
9299

100+
private IActionResult MapErrorCommandResponseToCqrsResponse<TError>(CommandResponse<TError> response)
101+
where TError : Enumeration
102+
{
103+
return BadRequest(response);
104+
}
105+
93106
private IActionResult MapErrorCommandResponseToProblemDetails<TError>(CommandResponse<TError> response)
94107
where TError : Enumeration
95108
{

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,29 @@ public CommandEndpointHandler(IMediator mediator, IOptions<CqrsHttpOptions> opti
6969

7070
private IResult HandleErrorCommandResponse(CommandResponse response, HttpContext context)
7171
{
72-
return _options.CommandErrorResponseType switch
72+
var errorResponseType = _options.CommandErrorResponseType;
73+
if (context.Request.Headers.Accept.Contains("application/cqrs"))
74+
{
75+
errorResponseType = ErrorResponseType.Cqrs;
76+
}
77+
78+
return errorResponseType switch
7379
{
7480
ErrorResponseType.PlainText => HandleErrorCommandResponseWithPlainText(response),
7581
ErrorResponseType.ProblemDetails => HandleErrorCommandResponseWithProblemDetails(response),
82+
ErrorResponseType.Cqrs => HandleErrorCommandResponseWithCqrs(response),
7683
ErrorResponseType.Custom => _options.CustomCommandErrorResponseMapper?.Invoke(response, context)
7784
?? HandleErrorCommandResponseWithPlainText(response),
7885
_ => throw new ArgumentOutOfRangeException(
7986
$"Unsupported CommandErrorResponseType: {_options.CommandErrorResponseType}")
8087
};
8188
}
8289

90+
private static IResult HandleErrorCommandResponseWithCqrs(CommandResponse response)
91+
{
92+
return Results.BadRequest(response);
93+
}
94+
8395
private static IResult HandleErrorCommandResponseWithPlainText(CommandResponse response)
8496
{
8597
if (response.IsValidationError)

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
2+
13
namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;
24

35
/// <summary>
@@ -15,6 +17,11 @@ public enum ErrorResponseType
1517
/// </summary>
1618
ProblemDetails,
1719

20+
/// <summary>
21+
/// Returns <see cref="CommandResponse"/>
22+
/// </summary>
23+
Cqrs,
24+
1825
/// <summary>
1926
/// Handles command error by custom logic.
2027
/// </summary>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
</Description>
66
</PropertyGroup>
77
<ItemGroup>
8-
<ProjectReference Include="..\Cnblogs.Architecture.Ddd.Cqrs.DependencyInjection\Cnblogs.Architecture.Ddd.Cqrs.DependencyInjection.csproj"/>
8+
<ProjectReference Include="..\Cnblogs.Architecture.Ddd.Cqrs.DependencyInjection\Cnblogs.Architecture.Ddd.Cqrs.DependencyInjection.csproj" />
99
</ItemGroup>
1010
<ItemGroup>
11-
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0"/>
11+
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
1212
</ItemGroup>
1313
</Project>
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
using System.Net;
2+
using System.Net.Http.Json;
3+
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
4+
using Cnblogs.Architecture.Ddd.Infrastructure.Abstractions;
5+
6+
namespace Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent;
7+
8+
/// <summary>
9+
/// Service Agent for CQRS
10+
/// </summary>
11+
public abstract class CqrsServiceAgent
12+
{
13+
/// <summary>
14+
/// The underlying <see cref="HttpClient"/>.
15+
/// </summary>
16+
protected HttpClient HttpClient { get; }
17+
18+
/// <summary>
19+
/// Create a service agent for cqrs api.
20+
/// </summary>
21+
/// <param name="httpClient">The underlying HttpClient.</param>
22+
protected CqrsServiceAgent(HttpClient httpClient)
23+
{
24+
HttpClient = httpClient;
25+
}
26+
27+
/// <summary>
28+
/// Execute a command with DELETE method.
29+
/// </summary>
30+
/// <param name="url">The url.</param>
31+
/// <typeparam name="TResponse">Response type.</typeparam>
32+
/// <returns>The response.</returns>
33+
public async Task<CommandResponse<TResponse, ServiceAgentError>> DeleteCommandAsync<TResponse>(string url)
34+
{
35+
var response = await HttpClient.DeleteAsync(url);
36+
return await HandleCommandResponseAsync<TResponse>(response);
37+
}
38+
39+
/// <summary>
40+
/// Execute a command with DELETE method.
41+
/// </summary>
42+
/// <param name="url">The route of the API.</param>
43+
public async Task<CommandResponse<ServiceAgentError>> DeleteCommandAsync(string url)
44+
{
45+
var response = await HttpClient.DeleteAsync(url);
46+
return await HandleCommandResponseAsync(response);
47+
}
48+
49+
/// <summary>
50+
/// Execute a command with POST method.
51+
/// </summary>
52+
/// <param name="url">The route of the API.</param>
53+
public async Task<CommandResponse<ServiceAgentError>> PostCommandAsync(string url)
54+
{
55+
var response = await HttpClient.PostAsync(url, new StringContent(string.Empty));
56+
return await HandleCommandResponseAsync(response);
57+
}
58+
59+
/// <summary>
60+
/// Execute a command with POST method and payload.
61+
/// </summary>
62+
/// <param name="url">The route of the API.</param>
63+
/// <param name="payload">The request body.</param>
64+
/// <typeparam name="TPayload">The type of request body.</typeparam>
65+
public async Task<CommandResponse<ServiceAgentError>> PostCommandAsync<TPayload>(string url, TPayload payload)
66+
{
67+
var response = await HttpClient.PostAsJsonAsync(url, payload);
68+
return await HandleCommandResponseAsync(response);
69+
}
70+
71+
/// <summary>
72+
/// Execute a command with POST method and payload.
73+
/// </summary>
74+
/// <param name="url">The route of the API.</param>
75+
/// <param name="payload">The request body.</param>
76+
/// <typeparam name="TResponse">The type of response body.</typeparam>
77+
/// <typeparam name="TPayload">The type of request body.</typeparam>
78+
/// <returns>The response body.</returns>
79+
public async Task<CommandResponse<TResponse, ServiceAgentError>> PostCommandAsync<TResponse, TPayload>(
80+
string url,
81+
TPayload payload)
82+
{
83+
var response = await HttpClient.PostAsJsonAsync(url, payload);
84+
return await HandleCommandResponseAsync<TResponse>(response);
85+
}
86+
87+
/// <summary>
88+
/// Execute a command with PUT method and payload.
89+
/// </summary>
90+
/// <param name="url">The route of API.</param>
91+
public async Task<CommandResponse<ServiceAgentError>> PutCommandAsync(string url)
92+
{
93+
var response = await HttpClient.PutAsync(url, new StringContent(string.Empty));
94+
return await HandleCommandResponseAsync(response);
95+
}
96+
97+
/// <summary>
98+
/// Execute a command with PUT method and payload.
99+
/// </summary>
100+
/// <param name="url">The route of API.</param>
101+
/// <param name="payload">The request body.</param>
102+
/// <typeparam name="TPayload">The type of request body.</typeparam>
103+
/// <returns>The command response.</returns>
104+
public async Task<CommandResponse<ServiceAgentError>> PutCommandAsync<TPayload>(string url, TPayload payload)
105+
{
106+
var response = await HttpClient.PutAsJsonAsync(url, payload);
107+
return await HandleCommandResponseAsync(response);
108+
}
109+
110+
/// <summary>
111+
/// Execute a command with PUT method and payload.
112+
/// </summary>
113+
/// <param name="url">The route of API.</param>
114+
/// <param name="payload">The request body.</param>
115+
/// <typeparam name="TResponse">The type of response body.</typeparam>
116+
/// <typeparam name="TPayload">The type of request body.</typeparam>
117+
/// <returns>The response body.</returns>
118+
public async Task<CommandResponse<TResponse, ServiceAgentError>> PutCommandAsync<TResponse, TPayload>(
119+
string url,
120+
TPayload payload)
121+
{
122+
var response = await HttpClient.PutAsJsonAsync(url, payload);
123+
return await HandleCommandResponseAsync<TResponse>(response);
124+
}
125+
126+
/// <summary>
127+
/// Query item with GET method.
128+
/// </summary>
129+
/// <param name="url">The route of the API.</param>
130+
/// <typeparam name="T">The type of item to get.</typeparam>
131+
/// <returns>The query result, can be null if item does not exists or status code is 404.</returns>
132+
public async Task<T?> GetItemAsync<T>(string url)
133+
{
134+
try
135+
{
136+
return await HttpClient.GetFromJsonAsync<T>(url);
137+
}
138+
catch (HttpRequestException e)
139+
{
140+
if (e.StatusCode == HttpStatusCode.NotFound)
141+
{
142+
return default;
143+
}
144+
145+
throw;
146+
}
147+
}
148+
149+
/// <summary>
150+
/// Batch get items with GET method.
151+
/// </summary>
152+
/// <param name="url">The route of the API.</param>
153+
/// <param name="paramName">The name of id field.</param>
154+
/// <param name="ids">The id list.</param>
155+
/// <typeparam name="TResponse">The type of the query result item.</typeparam>
156+
/// <typeparam name="TId">The type of the id.</typeparam>
157+
/// <returns>A list of items that contains id that in <paramref name="ids"/>, the order or count of the items are not guaranteed.</returns>
158+
public async Task<List<TResponse>> BatchGetItemsAsync<TResponse, TId>(
159+
string url,
160+
string paramName,
161+
IEnumerable<TId> ids)
162+
where TId : notnull
163+
{
164+
var query = string.Join(
165+
'&',
166+
ids.Select(i => $"{WebUtility.UrlEncode(paramName)}={WebUtility.UrlEncode(i.ToString())}"));
167+
url = $"{url}{(url.Contains('?') ? '&' : '?')}{query}";
168+
return await HttpClient.GetFromJsonAsync<List<TResponse>>(url) ?? new List<TResponse>();
169+
}
170+
171+
/// <summary>
172+
/// Get paged list of items based on url.
173+
/// </summary>
174+
/// <param name="url">The route of the API.</param>
175+
/// <param name="pagingParams">The paging parameters, including page size and page index.</param>
176+
/// <param name="orderByString">Specifies the order of items to return.</param>
177+
/// <typeparam name="TItem">The type of items to query.</typeparam>
178+
/// <returns>The paged list of items. An empty list is returned when there is no result.</returns>
179+
public async Task<PagedList<TItem>> ListPagedItemsAsync<TItem>(
180+
string url,
181+
PagingParams? pagingParams = null,
182+
string? orderByString = null)
183+
{
184+
return await ListPagedItemsAsync<TItem>(url, pagingParams?.PageIndex, pagingParams?.PageSize, orderByString);
185+
}
186+
187+
/// <summary>
188+
/// Get paged list of items based on url.
189+
/// </summary>
190+
/// <param name="url">The route of the API.</param>
191+
/// <param name="pageIndex">The page index.</param>
192+
/// <param name="pageSize">The page size.</param>
193+
/// <param name="orderByString">Specifies the order of items to return.</param>
194+
/// <typeparam name="TItem">The type of items to query.</typeparam>
195+
/// <returns>The paged list of items. An empty list is returned when there is no result.</returns>
196+
public async Task<PagedList<TItem>> ListPagedItemsAsync<TItem>(
197+
string url,
198+
int? pageIndex,
199+
int? pageSize,
200+
string? orderByString = null)
201+
{
202+
if (pageIndex.HasValue && pageSize.HasValue)
203+
{
204+
var query = $"pageIndex={pageIndex}&pageSize={pageSize}&orderByString={orderByString}";
205+
url = url.Contains('?') ? url + "&" + query : url + "?" + query;
206+
}
207+
208+
return await HttpClient.GetFromJsonAsync<PagedList<TItem>>(url) ?? new PagedList<TItem>();
209+
}
210+
211+
private static async Task<CommandResponse<TResponse, ServiceAgentError>> HandleCommandResponseAsync<TResponse>(
212+
HttpResponseMessage httpResponseMessage)
213+
{
214+
if (httpResponseMessage.IsSuccessStatusCode)
215+
{
216+
var result = await httpResponseMessage.Content.ReadFromJsonAsync<TResponse>();
217+
return CommandResponse<TResponse, ServiceAgentError>.Success(result);
218+
}
219+
220+
var response = await httpResponseMessage.Content.ReadFromJsonAsync<CommandResponse>();
221+
if (response is null)
222+
{
223+
return CommandResponse<TResponse, ServiceAgentError>.Fail(ServiceAgentError.UnknownError);
224+
}
225+
226+
return new CommandResponse<TResponse, ServiceAgentError>
227+
{
228+
IsConcurrentError = response.IsConcurrentError,
229+
IsValidationError = response.IsValidationError,
230+
ErrorMessage = response.ErrorMessage,
231+
LockAcquired = response.LockAcquired,
232+
ValidationErrors = response.ValidationErrors,
233+
ErrorCode = new ServiceAgentError(1, response.ErrorMessage)
234+
};
235+
}
236+
237+
private static async Task<CommandResponse<ServiceAgentError>> HandleCommandResponseAsync(
238+
HttpResponseMessage message)
239+
{
240+
if (message.IsSuccessStatusCode)
241+
{
242+
return CommandResponse<ServiceAgentError>.Success();
243+
}
244+
245+
var response = await message.Content.ReadFromJsonAsync<CommandResponse>();
246+
if (response is null)
247+
{
248+
return CommandResponse<ServiceAgentError>.Fail(ServiceAgentError.UnknownError);
249+
}
250+
251+
return new CommandResponse<ServiceAgentError>
252+
{
253+
IsConcurrentError = response.IsConcurrentError,
254+
IsValidationError = response.IsValidationError,
255+
ErrorMessage = response.ErrorMessage,
256+
LockAcquired = response.LockAcquired,
257+
ValidationErrors = response.ValidationErrors,
258+
ErrorCode = new ServiceAgentError(1, response.ErrorMessage)
259+
};
260+
}
261+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent;
66
/// Defines exceptions threw when doing an API call.
77
/// </summary>
88
/// <typeparam name="TException">The type of this API exception.</typeparam>
9+
[Obsolete("Try migrate to CqrsServiceAgent")]
910
public interface IApiException<out TException>
1011
where TException : Exception, IApiException<TException>
1112
{

0 commit comments

Comments
 (0)