Skip to content

feat: add cqrs service agent implementation #93

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
/// <summary>
/// Response returned by <see cref="ICommand{TError}"/>.
/// </summary>
public abstract record CommandResponse : IValidationResponse, ILockableResponse
public record CommandResponse : IValidationResponse, ILockableResponse
{
/// <summary>
/// Check if validation fails.
Expand Down Expand Up @@ -69,6 +69,7 @@ public CommandResponse()
public CommandResponse(TError errorCode)
{
ErrorCode = errorCode;
ErrorMessage = errorCode.Name;
}

/// <summary>
Expand Down Expand Up @@ -173,9 +174,9 @@ private CommandResponse(TView response)
/// </summary>
/// <param name="view">The model to return.</param>
/// <returns>A <see cref="CommandResponse{TView, TError}"/> with given result.</returns>
public static CommandResponse<TView, TError> Success(TView view)
public static CommandResponse<TView, TError> Success(TView? view)
{
return new CommandResponse<TView, TError>(view);
return view is null ? Success() : new CommandResponse<TView, TError>(view);
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,17 @@ protected IActionResult HandleCommandResponse<TResponse, TError>(CommandResponse
private IActionResult HandleErrorCommandResponse<TError>(CommandResponse<TError> 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}")
Expand All @@ -90,6 +97,12 @@ protected virtual IActionResult CustomErrorCommandResponseMap<TError>(CommandRes
return MapErrorCommandResponseToPlainText(response);
}

private IActionResult MapErrorCommandResponseToCqrsResponse<TError>(CommandResponse<TError> response)
where TError : Enumeration
{
return BadRequest(response);
}

private IActionResult MapErrorCommandResponseToProblemDetails<TError>(CommandResponse<TError> response)
where TError : Enumeration
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,29 @@ public CommandEndpointHandler(IMediator mediator, IOptions<CqrsHttpOptions> 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(
$"Unsupported CommandErrorResponseType: {_options.CommandErrorResponseType}")
};
}

private static IResult HandleErrorCommandResponseWithCqrs(CommandResponse response)
{
return Results.BadRequest(response);
}

private static IResult HandleErrorCommandResponseWithPlainText(CommandResponse response)
{
if (response.IsValidationError)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;

namespace Cnblogs.Architecture.Ddd.Cqrs.AspNetCore;

/// <summary>
Expand All @@ -15,6 +17,11 @@ public enum ErrorResponseType
/// </summary>
ProblemDetails,

/// <summary>
/// Returns <see cref="CommandResponse"/>
/// </summary>
Cqrs,

/// <summary>
/// Handles command error by custom logic.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Cnblogs.Architecture.Ddd.Cqrs.DependencyInjection\Cnblogs.Architecture.Ddd.Cqrs.DependencyInjection.csproj"/>
<ProjectReference Include="..\Cnblogs.Architecture.Ddd.Cqrs.DependencyInjection\Cnblogs.Architecture.Ddd.Cqrs.DependencyInjection.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0"/>
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
</ItemGroup>
</Project>
261 changes: 261 additions & 0 deletions src/Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent/CqrsServiceAgent.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Service Agent for CQRS
/// </summary>
public abstract class CqrsServiceAgent
{
/// <summary>
/// The underlying <see cref="HttpClient"/>.
/// </summary>
protected HttpClient HttpClient { get; }

/// <summary>
/// Create a service agent for cqrs api.
/// </summary>
/// <param name="httpClient">The underlying HttpClient.</param>
protected CqrsServiceAgent(HttpClient httpClient)
{
HttpClient = httpClient;
}

/// <summary>
/// Execute a command with DELETE method.
/// </summary>
/// <param name="url">The url.</param>
/// <typeparam name="TResponse">Response type.</typeparam>
/// <returns>The response.</returns>
public async Task<CommandResponse<TResponse, ServiceAgentError>> DeleteCommandAsync<TResponse>(string url)
{
var response = await HttpClient.DeleteAsync(url);
return await HandleCommandResponseAsync<TResponse>(response);
}

/// <summary>
/// Execute a command with DELETE method.
/// </summary>
/// <param name="url">The route of the API.</param>
public async Task<CommandResponse<ServiceAgentError>> DeleteCommandAsync(string url)
{
var response = await HttpClient.DeleteAsync(url);
return await HandleCommandResponseAsync(response);
}

/// <summary>
/// Execute a command with POST method.
/// </summary>
/// <param name="url">The route of the API.</param>
public async Task<CommandResponse<ServiceAgentError>> PostCommandAsync(string url)
{
var response = await HttpClient.PostAsync(url, new StringContent(string.Empty));
return await HandleCommandResponseAsync(response);
}

/// <summary>
/// Execute a command with POST method and payload.
/// </summary>
/// <param name="url">The route of the API.</param>
/// <param name="payload">The request body.</param>
/// <typeparam name="TPayload">The type of request body.</typeparam>
public async Task<CommandResponse<ServiceAgentError>> PostCommandAsync<TPayload>(string url, TPayload payload)
{
var response = await HttpClient.PostAsJsonAsync(url, payload);
return await HandleCommandResponseAsync(response);
}

/// <summary>
/// Execute a command with POST method and payload.
/// </summary>
/// <param name="url">The route of the API.</param>
/// <param name="payload">The request body.</param>
/// <typeparam name="TResponse">The type of response body.</typeparam>
/// <typeparam name="TPayload">The type of request body.</typeparam>
/// <returns>The response body.</returns>
public async Task<CommandResponse<TResponse, ServiceAgentError>> PostCommandAsync<TResponse, TPayload>(
string url,
TPayload payload)
{
var response = await HttpClient.PostAsJsonAsync(url, payload);
return await HandleCommandResponseAsync<TResponse>(response);
}

/// <summary>
/// Execute a command with PUT method and payload.
/// </summary>
/// <param name="url">The route of API.</param>
public async Task<CommandResponse<ServiceAgentError>> PutCommandAsync(string url)
{
var response = await HttpClient.PutAsync(url, new StringContent(string.Empty));
return await HandleCommandResponseAsync(response);
}

/// <summary>
/// Execute a command with PUT method and payload.
/// </summary>
/// <param name="url">The route of API.</param>
/// <param name="payload">The request body.</param>
/// <typeparam name="TPayload">The type of request body.</typeparam>
/// <returns>The command response.</returns>
public async Task<CommandResponse<ServiceAgentError>> PutCommandAsync<TPayload>(string url, TPayload payload)
{
var response = await HttpClient.PutAsJsonAsync(url, payload);
return await HandleCommandResponseAsync(response);
}

/// <summary>
/// Execute a command with PUT method and payload.
/// </summary>
/// <param name="url">The route of API.</param>
/// <param name="payload">The request body.</param>
/// <typeparam name="TResponse">The type of response body.</typeparam>
/// <typeparam name="TPayload">The type of request body.</typeparam>
/// <returns>The response body.</returns>
public async Task<CommandResponse<TResponse, ServiceAgentError>> PutCommandAsync<TResponse, TPayload>(
string url,
TPayload payload)
{
var response = await HttpClient.PutAsJsonAsync(url, payload);
return await HandleCommandResponseAsync<TResponse>(response);
}

/// <summary>
/// Query item with GET method.
/// </summary>
/// <param name="url">The route of the API.</param>
/// <typeparam name="T">The type of item to get.</typeparam>
/// <returns>The query result, can be null if item does not exists or status code is 404.</returns>
public async Task<T?> GetItemAsync<T>(string url)
{
try
{
return await HttpClient.GetFromJsonAsync<T>(url);
}
catch (HttpRequestException e)
{
if (e.StatusCode == HttpStatusCode.NotFound)
{
return default;
}

throw;
}
}

/// <summary>
/// Batch get items with GET method.
/// </summary>
/// <param name="url">The route of the API.</param>
/// <param name="paramName">The name of id field.</param>
/// <param name="ids">The id list.</param>
/// <typeparam name="TResponse">The type of the query result item.</typeparam>
/// <typeparam name="TId">The type of the id.</typeparam>
/// <returns>A list of items that contains id that in <paramref name="ids"/>, the order or count of the items are not guaranteed.</returns>
public async Task<List<TResponse>> BatchGetItemsAsync<TResponse, TId>(
string url,
string paramName,
IEnumerable<TId> 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<List<TResponse>>(url) ?? new List<TResponse>();
}

/// <summary>
/// Get paged list of items based on url.
/// </summary>
/// <param name="url">The route of the API.</param>
/// <param name="pagingParams">The paging parameters, including page size and page index.</param>
/// <param name="orderByString">Specifies the order of items to return.</param>
/// <typeparam name="TItem">The type of items to query.</typeparam>
/// <returns>The paged list of items. An empty list is returned when there is no result.</returns>
public async Task<PagedList<TItem>> ListPagedItemsAsync<TItem>(
string url,
PagingParams? pagingParams = null,
string? orderByString = null)
{
return await ListPagedItemsAsync<TItem>(url, pagingParams?.PageIndex, pagingParams?.PageSize, orderByString);
}

/// <summary>
/// Get paged list of items based on url.
/// </summary>
/// <param name="url">The route of the API.</param>
/// <param name="pageIndex">The page index.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="orderByString">Specifies the order of items to return.</param>
/// <typeparam name="TItem">The type of items to query.</typeparam>
/// <returns>The paged list of items. An empty list is returned when there is no result.</returns>
public async Task<PagedList<TItem>> ListPagedItemsAsync<TItem>(
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<PagedList<TItem>>(url) ?? new PagedList<TItem>();
}

private static async Task<CommandResponse<TResponse, ServiceAgentError>> HandleCommandResponseAsync<TResponse>(
HttpResponseMessage httpResponseMessage)
{
if (httpResponseMessage.IsSuccessStatusCode)
{
var result = await httpResponseMessage.Content.ReadFromJsonAsync<TResponse>();
return CommandResponse<TResponse, ServiceAgentError>.Success(result);
}

var response = await httpResponseMessage.Content.ReadFromJsonAsync<CommandResponse>();
if (response is null)
{
return CommandResponse<TResponse, ServiceAgentError>.Fail(ServiceAgentError.UnknownError);
}

return new CommandResponse<TResponse, ServiceAgentError>
{
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<CommandResponse<ServiceAgentError>> HandleCommandResponseAsync(
HttpResponseMessage message)
{
if (message.IsSuccessStatusCode)
{
return CommandResponse<ServiceAgentError>.Success();
}

var response = await message.Content.ReadFromJsonAsync<CommandResponse>();
if (response is null)
{
return CommandResponse<ServiceAgentError>.Fail(ServiceAgentError.UnknownError);
}

return new CommandResponse<ServiceAgentError>
{
IsConcurrentError = response.IsConcurrentError,
IsValidationError = response.IsValidationError,
ErrorMessage = response.ErrorMessage,
LockAcquired = response.LockAcquired,
ValidationErrors = response.ValidationErrors,
ErrorCode = new ServiceAgentError(1, response.ErrorMessage)
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Cnblogs.Architecture.Ddd.Cqrs.ServiceAgent;
/// Defines exceptions threw when doing an API call.
/// </summary>
/// <typeparam name="TException">The type of this API exception.</typeparam>
[Obsolete("Try migrate to CqrsServiceAgent")]
public interface IApiException<out TException>
where TException : Exception, IApiException<TException>
{
Expand Down
Loading