kgrzybek / sample-dotnet-core-cqrs-api

Sample .NET Core REST API CQRS implementation with raw SQL and DDD using Clean Architecture.
https://www.kamilgrzybek.com/design/simple-cqrs-implementation-with-raw-sql-and-ddd/
MIT License
2.88k stars 646 forks source link

How to handle unique constraint with user friendly message #3

Closed fuine6 closed 5 years ago

fuine6 commented 5 years ago

Sorry, my English is poor.

If the customer's email has unique constraint. When call RegisterCustomer api with duplicate email, the method in UnitOfWork.CommitAsync will throw DbUpdateException, how to handle this and return friendly message to frontend, like this

{
   errorCode: 409001
   message: Duplicated email.
}

the errorCode is unique which defined by application, and has a api document for frontend, like this

errorCode message
409001 Duplicated email
401001 Unauthorized
...... ......

frontend use errorCode to show user friendly message.

I found most solution is,

public class RegisterCustomerCommandHandler : IRequestHandler<RegisterCustomerCommand, CustomerDto>
{
        ......

        public async Task<CustomerDto> Handle(RegisterCustomerCommand request, CancellationToken cancellationToken)
        {
            var customer = new Customer(request.Email, request.Name, this._customerUniquenessChecker);

        if (EmailAlreadyExist())
        {
        throw new EmailAlreadyExistException(request.Email);
        }

            await this._customerRepository.AddAsync(customer);

            await this._unitOfWork.CommitAsync(cancellationToken);

            return new CustomerDto { Id = customer.Id.Value };
        }
}

and use a ExceptionFilter to catch EmailAlreadyExistException.

But this can't handle concurrent insert, if two requests send at same time with same email, two requests will pass EmailAlreadyExist, the method in UnitOfWork.CommitAsync will throw DbUpdateException, this solution don't solve problem.

Maybe this happen rarely, I can return a response with 500(Internal Server Error), and let user try again.

Maybe I can use a lock in database, but most suggest don't do this, because of performance.

Maybe I can use Optimistic Concurrency Control, so I need write my code like this

public class RegisterCustomerCommandHandler : IRequestHandler<RegisterCustomerCommand, CustomerDto>
{
        ......

        public async Task<CustomerDto> Handle(RegisterCustomerCommand request, CancellationToken cancellationToken)
        {
            var customer = new Customer(request.Email, request.Name, this._customerUniquenessChecker);

        if (EmailAlreadyExist())
        {
        throw new EmailAlreadyExistException(request.Email);
        }

            await this._customerRepository.AddAsync(customer);

       try
       {
        await this._unitOfWork.CommitAsync(cancellationToken);
       }
       catch(DbUpdateException)
       {
        //do something
       }

            return new CustomerDto { Id = customer.Id.Value };
        }
}

But it may has many change in a database transaction, I can't know who throw the DbUpdateException.

If I has a situation, user can update same record, and it happen frequently, user may complain.

How to handle this gracefully.

kgrzybek commented 5 years ago

If for certain use cases you expect concurrent calls whose simultaneous write breaks business rules, you can use one more method besides the methods you mentioned - repeat command processing.

Then validation on the command handler / domain model will detect broken rule (because side effect of another command is already persisted) and the application will return an appropriate message.

The important thing is to have always all validation on the application side and treat database as "last line of defense" against such situations.

You can use the Polly library to implement this behavior

fuine6 commented 5 years ago

The important thing is to have always all validation on the application side and treat database as "last line of defense" against such situations.

I think this is a good idea. It helped me a lot.