Skip to content

業務例外発生時にexceptionIdとexceptionValuesが返却されるようにする #2746

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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
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 @@ -8,15 +8,15 @@ namespace Dressca.ApplicationCore.ApplicationService;
/// </summary>
public class CatalogBrandNotExistingInRepositoryException : BusinessException
{
private const string ErrorCode = "CatalogBrandNotExistingInRepository";
private const string ExceptionId = "catalogBrandNotFound";

/// <summary>
/// 見つからなかったカタログブランド ID を指定して
/// <see cref="CatalogBrandNotExistingInRepositoryException"/> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="catalogBrandIds">見つからなかったカタログブランド ID 。</param>
public CatalogBrandNotExistingInRepositoryException(IEnumerable<long> catalogBrandIds)
: base(new BusinessError(ErrorCode, string.Format(Messages.CatalogBrandIdDoesNotExist, string.Join(",", catalogBrandIds))))
: base(new BusinessError(ExceptionId, new ErrorMessage(Messages.CatalogBrandIdDoesNotExist, string.Join(",", catalogBrandIds))))
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ namespace Dressca.ApplicationCore.ApplicationService;
/// </summary>
public class CatalogCategoryNotExistingInRepositoryException : BusinessException
{
private const string ErrorCode = "CatalogCategoryNotExistingInRepository";
private const string ExceptionId = "catalogCategoryNotFound";

/// <summary>
/// 見つからなかったカタログカテゴリ ID を指定して
/// <see cref="CatalogCategoryNotExistingInRepositoryException"/> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="catalogCategoryIds">見つからなかったカタログカテゴリ Id 。</param>
public CatalogCategoryNotExistingInRepositoryException(IEnumerable<long> catalogCategoryIds)
: base(new BusinessError(ErrorCode, string.Format(Messages.CatalogCategoryIdDoesNotExist, string.Join(",", catalogCategoryIds))))
: base(new BusinessError(ExceptionId, new ErrorMessage(Messages.CatalogCategoryIdDoesNotExist, string.Join(",", catalogCategoryIds))))
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ namespace Dressca.ApplicationCore.ApplicationService;
/// </summary>
public class CatalogItemNotExistingInRepositoryException : BusinessException
{
private const string ErrorCode = "CatalogItemNotExistingInRepository";
private const string ExceptionId = "catalogIdNotFound";

/// <summary>
/// 見つからなかったカタログアイテム Id を指定して
/// <see cref="CatalogItemNotExistingInRepositoryException"/> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="catalogItemIds">見つからなかったカタログアイテム Id 。</param>
public CatalogItemNotExistingInRepositoryException(IEnumerable<long> catalogItemIds)
: base(new BusinessError(ErrorCode, string.Format(Messages.CatalogItemIdDoesNotExistInRepository, string.Join(",", catalogItemIds))))
: base(new BusinessError(ExceptionId, new ErrorMessage(Messages.CatalogItemIdDoesNotExistInRepository, string.Join(",", catalogItemIds))))
{
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
using Dressca.ApplicationCore.Resources;
using Dressca.SystemCommon;

namespace Dressca.ApplicationCore.Assets;

/// <summary>
/// アセットが存在しないことを表す例外クラスです。
/// </summary>
public class AssetNotFoundException : Exception
public class AssetNotFoundException : BusinessException
{
private const string ExceptionId = "assetNotFound";

/// <summary>
/// 見つからなかったアセットコードを指定して
/// <see cref="AssetNotFoundException"/> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="assetCode">見つからなかった買い物かご Id 。</param>
public AssetNotFoundException(string assetCode)
: base(string.Format(Messages.AssetNotFound, assetCode))
: base(new BusinessError(ExceptionId, new ErrorMessage(Messages.AssetNotFound, assetCode)))
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ namespace Dressca.ApplicationCore.Authorization;
/// </summary>
public class PermissionDeniedException : BusinessException
{
private const string ErrorCode = "PermissionDenied";
private const string ExceptionId = "permissionDenied";

/// <summary>
/// 実行を試みた操作を指定して
/// <see cref="PermissionDeniedException"/> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="operationName">実行を試みた操作の名称。</param>
public PermissionDeniedException([CallerMemberName] string operationName = "")
: base(new BusinessError(ErrorCode, string.Format(Messages.PermissionDenied, operationName)))
: base(new BusinessError(ExceptionId, new ErrorMessage(Messages.PermissionDenied, operationName)))
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ namespace Dressca.ApplicationCore.Baskets;
/// </summary>
public class CatalogItemNotExistingInBasketException : BusinessException
{
private const string ErrorCode = "CatalogItemNotExistingInBasket";
private const string ExceptionId = "catalogItemIdDoesNotExistInBasket";

/// <summary>
/// 見つからなかったカタログアイテム Id を指定して
/// <see cref="CatalogItemNotExistingInBasketException"/> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="catalogItemIds">見つからなかったカタログアイテム Id 。</param>
public CatalogItemNotExistingInBasketException(IEnumerable<long> catalogItemIds)
: base(new BusinessError(ErrorCode, string.Format(Messages.CatalogItemIdDoesNotExistInBasket, string.Join(",", catalogItemIds))))
: base(new BusinessError(ExceptionId, new ErrorMessage(Messages.CatalogItemIdDoesNotExistInBasket, string.Join(",", catalogItemIds))))
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ namespace Dressca.ApplicationCore.Ordering;
/// </summary>
public class EmptyBasketOnCheckoutException : BusinessException
{
private const string ErrorCode = "EmptyBasketOnCheckout";
private const string ExceptionId = "basketIsEmptyOnCheckout";

/// <summary>
/// <see cref="EmptyBasketOnCheckoutException"/> クラスの新しいインスタンスを初期化します。
/// </summary>
public EmptyBasketOnCheckoutException()
: base(new BusinessError(ErrorCode, Messages.BasketIsEmptyOnCheckout))
: base(new BusinessError(ExceptionId, new ErrorMessage(Messages.BasketIsEmptyOnCheckout)))
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ namespace Dressca.ApplicationCore.Ordering;
/// </summary>
public class NullBasketOnCheckoutException : BusinessException
{
private const string ErrorCode = "NullBasketOnCheckout";
private const string ExceptionId = "basketIsNullOnCheckout";

/// <summary>
/// <see cref="NullBasketOnCheckoutException"/> クラスの新しいインスタンスを初期化します。
/// </summary>
public NullBasketOnCheckoutException()
: base(new BusinessError(ErrorCode, Messages.BasketIsNullOnCheckout))
: base(new BusinessError(ExceptionId, new ErrorMessage(Messages.BasketIsNullOnCheckout)))
{
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
using Dressca.ApplicationCore.Resources;
using Dressca.SystemCommon;

namespace Dressca.ApplicationCore.Ordering;

/// <summary>
/// 注文情報が存在しないことを表す例外クラスです。
/// </summary>
public class OrderNotFoundException : Exception
public class OrderNotFoundException : BusinessException
{
private const string ExceptionId = "orderNotFound";

/// <summary>
/// 見つからなかった注文 Id と購入者 Id を指定して
/// <see cref="OrderNotFoundException"/> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="orderId">見つからなかった注文 Id 。</param>
/// <param name="buyerId">見つからなかった購入者 Id 。</param>
public OrderNotFoundException(long orderId, string buyerId)
: base(string.Format(Messages.OrderNotFound, orderId, buyerId))
: base(new BusinessError(ExceptionId, new ErrorMessage(Messages.OrderNotFound, orderId, buyerId)))
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Dressca.SystemCommon;
/// </summary>
public class BusinessError
{
private readonly List<string> errorMessages = [];
private List<ErrorMessage> errorMessages = [];

/// <summary>
/// <see cref="BusinessError"/> クラスの新しいインスタンスを初期化します。
Expand All @@ -22,50 +22,48 @@ public BusinessError()
/// エラーコードとエラーメッセージのリストを指定して
/// <see cref="BusinessError"/> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="errorCode">
/// <param name="exceptionId">
/// エラーコード。
/// <see langword="null"/> を指定した場合は空の文字列 ("") として取り扱います。
/// </param>
/// <param name="errorMessages">エラーメッセージのリスト。</param>
public BusinessError(string? errorCode, params string[] errorMessages)
public BusinessError(string? exceptionId, params ErrorMessage[] errorMessages)
{
this.ErrorCode = errorCode ?? string.Empty;
if (errorMessages is not null)
{
this.errorMessages.AddRange(errorMessages);
}
this.ExceptionId = exceptionId ?? string.Empty;

this.errorMessages.AddRange(errorMessages);
}

/// <summary>
/// エラーコードを取得します。
/// </summary>
public string ErrorCode { get; private set; }
public string ExceptionId { get; private set; }

/// <summary>
/// エラーメッセージのリストを取得します
/// エラーメッセージの情報を取得します
/// </summary>
public IReadOnlyList<string> ErrorMessages => this.errorMessages.AsReadOnly();
public IReadOnlyList<ErrorMessage> ErrorMessages => this.errorMessages.AsReadOnly();

/// <summary>
/// エラーメッセージを追加します。
/// </summary>
/// <param name="errorMessage">エラーメッセージ。</param>
public void AddErrorMessage(string? errorMessage)
=> this.errorMessages.Add(errorMessage ?? string.Empty);
public void AddErrorMessage(ErrorMessage errorMessage)
=> this.errorMessages.Add(errorMessage);

/// <summary>
/// エラーメッセージのリストを追加します。
/// </summary>
/// <param name="errorMessages">エラーメッセージのリスト。</param>
public void AddErrorMessages(params string[] errorMessages)
=> this.errorMessages.AddRange(errorMessages);
public void AddErrorMessages(params ErrorMessage[] errorMessages)
=> this.errorMessages.AddRange(errorMessages);

/// <inheritdoc/>
public override string ToString()
{
Dictionary<string, string[]> data = new Dictionary<string, string[]>
{
{ this.ErrorCode, this.errorMessages.ToArray() },
{ this.ExceptionId, this.errorMessages.Select(e => e.Message).ToArray() },
};

return JsonSerializer.Serialize(data, DefaultJsonSerializerOptions.GetInstance());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ public BusinessErrorCollection()
public void AddOrMerge(BusinessError newBusinessError)
{
ArgumentNullException.ThrowIfNull(newBusinessError);
if (this.businessErrors.TryGetValue(newBusinessError.ErrorCode, out var businessError))
if (this.businessErrors.TryGetValue(newBusinessError.ExceptionId, out var businessError))
{
businessError.AddErrorMessages([.. newBusinessError.ErrorMessages]);
}
else
{
this.businessErrors.Add(newBusinessError.ErrorCode, newBusinessError);
this.businessErrors.Add(newBusinessError.ExceptionId, newBusinessError);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,23 @@ public BusinessException(Exception? innerException, params BusinessError[] busin
/// <inheritdoc/>
public override string Message => $"{base.Message}{Environment.NewLine}{this.businessErrors}";

/// <summary>
/// この例外オブジェクトの保持する業務エラーの情報を取得します。
/// </summary>
public IEnumerable<BusinessError> GetBusinessErrors => this.businessErrors;

/// <summary>
/// この例外オブジェクトの保持するエラーコードとエラーメッセージのリストを取得します。
/// </summary>
/// <returns>エラーコードとエラーメッセージのリスト。</returns>
public IEnumerable<(string ErrorCode, string ErrorMessage)> GetErrors()
public IEnumerable<(string ExceptionId, string ErrorMessage)> GetErrors()
{
foreach (var businessError in this.businessErrors)
{
var errorCode = businessError.ErrorCode;
var exceptionId = businessError.ExceptionId;
foreach (var errorMessage in businessError.ErrorMessages)
{
yield return new(errorCode, errorMessage);
yield return new(exceptionId, errorMessage.Message);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace Dressca.SystemCommon;

/// <summary>
/// エラーメッセージの情報を保持するクラスです。
/// </summary>
public class ErrorMessage
{
/// <summary>
/// <see cref="ErrorMessage"/> クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="errorMessage">
/// エラーメッセージ。<br />
/// このパラメーターにはメッセージ用の定数クラスで定義した値を指定します。
/// ユーザーやDB等の外部からの入力値は指定しないでください。
/// </param>
/// <param name="errorMessageValues">エラーメッセージのプレースホルダーの値。</param>
public ErrorMessage(string errorMessage, params object[] errorMessageValues)
{
this.ErrorMessageValues = errorMessageValues;
this.Message = errorMessage is null ? string.Empty : string.Format(errorMessage, this.ErrorMessageValues);
}

/// <summary>
/// エラーメッセージを表す文字列を取得します。
/// </summary>
public string Message { get; private set; }

/// <summary>
/// エラーメッセージのプレースホルダーの値を取得します。
/// </summary>
public object[] ErrorMessageValues { get; }

/// <inheritdoc/>
public override string ToString()
{
return this.Message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,20 @@ protected override ProblemDetails CreateProblemDetails(ExceptionContext context)
{
// 開発用のフィルターでは例外のスタックトレースも返却する。
ArgumentNullException.ThrowIfNull(context);
return this.problemDetailsFactory.CreateValidationProblemDetails(
var problemDetails = this.problemDetailsFactory.CreateValidationProblemDetails(
context.HttpContext,
context.ModelState,
statusCode: (int)HttpStatusCode.BadRequest,
title: Messages.BusinessExceptionHandled,
detail: context.Exception.ToString());

if (context.Exception is BusinessException businessEx)
{
// 暫定の実装として、1つ目のBusinessErrorのexceptionIdとexceptionValuesを設定
problemDetails.Extensions.Add("exceptionId", businessEx.GetBusinessErrors.FirstOrDefault()?.ExceptionId ?? string.Empty);
problemDetails.Extensions.Add("exceptionValues", businessEx.GetBusinessErrors.FirstOrDefault()?.ErrorMessages.FirstOrDefault()?.ErrorMessageValues ?? []);
}

return problemDetails;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,19 @@ public BusinessExceptionFilter(
protected override ProblemDetails CreateProblemDetails(ExceptionContext context)
{
ArgumentNullException.ThrowIfNull(context);
return this.problemDetailsFactory.CreateValidationProblemDetails(
var problemDetails = this.problemDetailsFactory.CreateValidationProblemDetails(
context.HttpContext,
context.ModelState,
statusCode: (int)HttpStatusCode.BadRequest,
title: Messages.BusinessExceptionHandled);

if (context.Exception is BusinessException businessEx)
{
// 暫定の実装として、1つ目のBusinessErrorのexceptionIdとexceptionValuesを設定
problemDetails.Extensions.Add("exceptionId", businessEx.GetBusinessErrors.FirstOrDefault()?.ExceptionId ?? string.Empty);
problemDetails.Extensions.Add("exceptionValues", businessEx.GetBusinessErrors.FirstOrDefault()?.ErrorMessages.FirstOrDefault()?.ErrorMessageValues ?? []);
}

return problemDetails;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ public void OnException(ExceptionContext context)
{
this.logger.LogInformation(Events.BusinessExceptionHandled, businessEx, Messages.BusinessExceptionHandled);
var errors = businessEx.GetErrors();
foreach (var (errorCode, errorMessage) in errors)

foreach (var (exceptionId, errorMessage) in errors)
{
context.ModelState.AddModelError(errorCode, errorMessage);
context.ModelState.AddModelError(exceptionId, errorMessage);
}

var validationProblem = this.CreateProblemDetails(context);
Expand Down
Loading