I saw a lot of discussion, but I don't know how to do it in a real world. I understand that validation duplication in the client and server is needed. But how to elegantly validate in server and return friendly messages to client.
I have a value object like this, it has some business rules.
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;
}
}
The command send by the client.
public class CreateInvoiceCommand : IRequest
{
public string OrderId { get; set; }
}
The application layer will create value object, but if command violates business rules, it will throw exception, this is not friendly to the client, imagine there are more business rules here, it will only return the first rule.
public class CreateInvoiceCommandHandler : IRequestHandler<CreateInvoiceCommand>
{
public Task<Unit> Handle(CreateInvoiceCommand command, CancellationToken cancellationToken)
{
var orderId = new OrderId(command.OrderId);
return Task.FromResult(Unit.Value);
}
}
So I validate the command when the request arrived. With FluentValidation, it can return friendly messages to client.
public class CreateInvoiceCommandValidator : AbstractValidator<CreateInvoiceCommand>
{
public CreateInvoiceCommandValidator()
{
RuleFor(c => c.OrderId).NotEmpty().MaximumLength(50);
//Other rules...
}
}
My question is, is there a way to solve the duplication and return friendly messages?
Should I remove the business rules in value object to avoid DRY, is this still DDD?
UPDATE
According to the answer and this, I tried something.
Now, the value object looks like 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;
}
}
The validator
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);
}
});
}
}
This solves my problem, but it's just a simple example, I'm not sure if this makes the value object too complicated when I have more business rules.