2

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.

1

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.

  • So I shouldn't throw an exception in value object and instead return the validation result. Is it similar to this(Solution 4: Execute / CanExecute pattern)? – LiTang Aug 22 at 5:45
  • Yep, exactly like Solution 4. You could throw an exception in Value object, but you catch it in a higher level enclosing object like an aggregate, application service or command handler, and send a collapsed data structure to the front-end. – Subhash Bhushan Aug 22 at 5:55
  • And raising Exceptions is generally considered a suitable mechanism for bubbling up errors - exceptions can be too expensive for business validations. OP already has validators which can return collection of validation errors without throwing and catching exceptions. – Fabio Aug 22 at 22:30
  • @Fabio agree. Validators are much better and easier to control. – Subhash Bhushan Aug 23 at 0:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.