私はたくさんの議論を見ましたが、現実の世界でそれを行う方法がわかりません。クライアントとサーバーで検証の重複が必要であることを理解しています。しかし、サーバーでエレガントに検証し、フレンドリーなメッセージをクライアントに返す方法。
このような値オブジェクトがあり、いくつかのビジネスルールがあります。
public class OrderId : ValueObject<OrderId>
{
public string Value { get; }
public OrderId(string value)
{
if (string.IsNullOrWhiteSpace(value) || value.Length > 50)
{
throw new ArgumentException(nameof(value), "error message here");
}
Value = value;
}
}
クライアントが送信したコマンド。
public class CreateInvoiceCommand : IRequest
{
public string OrderId { get; set; }
}
アプリケーションレイヤーは値オブジェクトを作成しますが、コマンドがビジネスルールに違反している場合は例外をスローします。これはクライアントに好都合ではありません。ここにさらに多くのビジネスルールがあると想像してください。最初のルールのみを返します。
public class CreateInvoiceCommandHandler : IRequestHandler<CreateInvoiceCommand>
{
public Task<Unit> Handle(CreateInvoiceCommand command, CancellationToken cancellationToken)
{
var orderId = new OrderId(command.OrderId);
return Task.FromResult(Unit.Value);
}
}
そのため、リクエストが到着したときにコマンドを検証します。 FluentValidationを使用すると、フレンドリーなメッセージをクライアントに返すことができます。
public class CreateInvoiceCommandValidator : AbstractValidator<CreateInvoiceCommand>
{
public CreateInvoiceCommandValidator()
{
RuleFor(c => c.OrderId).NotEmpty().MaximumLength(50);
//Other rules...
}
}
私の質問は、重複を解決してフレンドリーなメッセージを返す方法はありますか?
DRYを回避するために値オブジェクトのビジネスルールを削除する必要がありますか?これはまだDDDですか?
[〜#〜]更新[〜#〜]
answer および this によると、私は何かを試しました。
これで、値オブジェクトは次のようになります
public class OrderId : ValueObject<OrderId>
{
public string Value { get; }
public OrderId(string value)
{
if (!CanCreate(value, out var errorMessages))
{
throw new ArgumentException(nameof(value), string.Join(".", errorMessages));
}
Value = value;
}
public static bool CanCreate(string orderId, out List<string> errorMessages)
{
errorMessages = new List<string>();
if (string.IsNullOrWhiteSpace(orderId))
{
errorMessages.Add("can not be null or empty");
}
if (orderId?.Length > 50)
{
errorMessages.Add("should not be longer than 50 characters");
}
return errorMessages.Count == 0;
}
}
バリデーター
public class CreateInvoiceCommandValidator : AbstractValidator<CreateInvoiceCommand>
{
public CreateInvoiceCommandValidator()
{
RuleFor(c => c.OrderId).IsOrderId();
}
}
public static class ValidatorExtensions
{
public static IRuleBuilderInitial<T, string> IsOrderId<T>(this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder.Custom((orderId, context) =>
{
if (!OrderId.CanCreate(orderId, out var errorMessages))
{
foreach (var errorMessage in errorMessages)
context.AddFailure($"'{context.DisplayName}' " + errorMessage);
}
});
}
}
これは私の問題を解決しますが、これは単なる例です。ビジネスルールが増えたときに、値オブジェクトが複雑すぎるかどうかはわかりません。
検証は間違いなくドメイン層にあります。すべてのドメイン関連の検証は、集計、エンティティ、および値オブジェクトに深く埋め込まれます。また、例外を発生させることは、(カスタムメッセージやエラーコードの代わりに)エラーを発生させるための適切なメカニズムと一般的に考えられています。
ただし、クライアントに例外が発生しないようにする必要があります。
この問題に対する過去の私の解決策は、errors
ハッシュを返すことでした。
たとえば、コマンドハンドラー(または私が呼び出すアプリケーションサービス)がAggregateのメソッドを呼び出すと、Aggregateは囲まれたエンティティと値オブジェクトの初期化を処理します。次に、集計は、例外をキャッチしてerrors
ハッシュを入力するのに最適な場所です。
検証は、コンストラクター、カスタム検証クラス、または明示的に呼び出すこともできます。ただし、すべての検証を実行した後、コードはerrors
ハッシュが空かどうかを確認します。そうでない場合は、ハッシュとともに呼び出し先にすぐに戻ります。
アプリケーションサービスまたはコマンドハンドラーは、エラーをそのままコントローラーに返し、パッケージ化してクライアントに返します。または、エラーが埋め込まれたカスタムドメイン固有のResponse
オブジェクトを作成することもできます。
どちらを選択しても、errors
ハッシュは次のようになります。
{
'email': [
'is required'
],
'username': [
'can not be the same as email',
'should not be longer than 254 characters'
],
'password': [
'should contain at least one number',
'should contain at least one special character',
'cannot be a dictionary Word'
]
}
この構造は最も基本的な形式ですが、必要に応じて複雑さを追加できます。たとえば、アプリケーションが多言語システムの場合、英語のメッセージの代わりにエラーコードを埋め込むことができます。