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.

有帮助吗?

解决方案

Validations are definitely in the domain layer. All domain related validations are to be deeply embedded in Aggregates, Entities, and Value Objects. And raising Exceptions is generally considered a suitable mechanism for bubbling up errors (instead of custom messages or error codes).

However, you wouldn't want to let the exception bubble up to the client.

My solution in the past to this problem has been to return a errors hash.

For example, say the Command Handler (or Application Service, as I would call it) invokes a method in the Aggregate, The Aggregate handles initialization of enclosed entities and value objects. Then the aggregate is the best place to catch exceptions and populate the errors hash.

The validations could be in constructors, custom validator classes, or can even be explicitly invoked. But after running all validations, the code checks if the errors hash is empty. If it is not, returns immediately to the callee with the hash.

The Application Service or Command Handler can then either return the errors as-is to the Controller to package and return to the client. Or it could even construct a custom domain-specific Response object with errors embedded.

Whichever you choose, the errors hash could be something like this:

{
    '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'
    ]
}

This structure is the most basic format, but you can add complexity as necessary. For instance, you can embed error codes instead of English messages, if your application is a multi-lingual system.

许可以下: CC-BY-SA归因
scroll top