2

I decided to code my new project in Clean Architecture, with which I'm not very familiar. I'm using this template https://github.com/JasonGT/CleanArchitecture

I have an Entity Withdrawal.cs which has a BTCAddress ReceiveAddress property:

public class Withdrawal
{
    ///ctor...

    public Guid Id { get; private set; }

    public decimal FiatAmountRequested { get; private set; }
    public Currency FiatCurrency { get; private set; }

    public BTCAddress ReceiveAddress { get; private set; }
}

I extracted BTCAddress into a ValueObject because a BTC address isn't an ordinary string, there is validation logic needed.

BTCAddress.cs

public class BTCAddress
{
    public BTCAddress(string address)
    {
        Address = address;
    }

    public string Address { get; private set; }

    public override string ToString()
    {
        return Address;
    }
}

As you can see, I haven't implemented any validations on the address. I must use an external library in order to validate. My question is can I add a dependency in BTCAddress on let's say IBTCAddressValidator and implement this interface in the Infrastructure layer, or it isn't permitted to have dependencies on interfaces in the Domain Layer?

If not I guess I should have the dependency of this interface in every Use Case (CreateWithdrawalRequestUseCase for example) that has to create a BTCAddress object. Or create a CreateBTCAddressUseCase and let other use cases be depended on it, which doesn't sound right to me. Or create some kind of a helper class for creating this value object.

What is the right way to add validation from external library in my value object?

EDIT:

Here is my RequestWithdrawalCommand

using System;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using DepositWithdraw.Application.Common.Interfaces;
using DepositWithdraw.Domain.Entities;
using DepositWithdraw.Domain.ValueObjects;
using FluentValidation;
using MediatR;

namespace DepositWithdraw.Application.Withdrawals.Commands.RequestWithdrawal
{
    public class RequestWithdrawalCommand : IRequest, IMapTo<Withdrawal>
    {
        public string ExternalId { get; set; }
        public DateTime ExternalDateTime { get; set; }

        public Client Client { get; set; }

        public decimal FiatAmountRequested { get; set; }
        public FiatCurrency FiatCurrency { get; set; }

        public URL CallbackUrl { get; set; }
        public BTCAddress ReceiveAddress { get; set; }

        public class RequestWithdrawalCommandValidator : AbstractValidator<RequestWithdrawalCommand>
        {
            public RequestWithdrawalCommandValidator(IBTCAddressValidator btcAddressValidator)
            {
                RuleFor(p => p.FiatAmountRequested)
                    .GreaterThan(0)
                    .WithMessage("Requested fiat amount must be positive.");

                RuleFor(p => p.ReceiveAddress)
                    .Must(address => address != null && btcAddressValidator.IsValid(address.ToString()))
                    .WithMessage("Invalid BTC address.");
            }
        }

        public class RequestWithdrawalCommandHandler : AsyncRequestHandler<RequestWithdrawalCommand>
        {
            private readonly IApplicationDbContext _db;
            private readonly IRatesRepository _ratesRepository;
            private readonly IBTCWalletService _BTCWalletService;
            private readonly IMapper _mapper;

            public RequestWithdrawalCommandHandler(
                IApplicationDbContext db,
                IRatesRepository ratesRepository,
                IBTCWalletService BTCWalletService,
                IMapper mapper)
            {
                _db = db;
                _ratesRepository = ratesRepository;
                _BTCWalletService = BTCWalletService;
                _mapper = mapper;
            }

            protected override async Task Handle(RequestWithdrawalCommand request, CancellationToken cancellationToken)
            {
                var btcRate = _ratesRepository.GetBtcRate(request.Client.Id, request.FiatCurrency.ToString());
                var btcAmount = request.FiatAmountRequested / btcRate.BuyRate;

                var withdrawal = _mapper.Map<Withdrawal>(request);
                await _BTCWalletService.SendBTC(request.ReceiveAddress, btcAmount);
            }
        }
    }
}

This code works as expected, The ReceiveAddress is validated, but I don't like the fact that I would have to repeat this validation process everywhere I'm using BTCAddress.cs. In this case I don't see the benefit from extracting it in a ValueObject, I can achieve the same thing keeping the ReceiveAddress as a string.

Reath
  • 491
  • 5
  • 16
  • 1
    Things like this can become very opinionated. You could always keep the validator separate and validate the value object where it is used. That way it separates concerns. – Nkosi Jan 12 '20 at 12:42
  • Can you provide a little more detail about this use case and how it would use the validator so we get a clearer understanding of the bigger picture. – Nkosi Jan 12 '20 at 12:46
  • @Nkosi a client calls my API with a request to withdraw some bitcoins to a given address. In the `CreateWithdrawalRequestUseCase` I want to validate the address that he gave me, before sending him the bitcoins. If you want, I can go into more details, but in short that's all. In my understandings, it would be best if a `BTCAddress` knows when it is valid, and when it is not. But the validation logic is long and complex and I want to use an external library for that – Reath Jan 12 '20 at 13:23
  • Update the shown code with what you just explained. – Nkosi Jan 12 '20 at 13:26
  • `But the validation logic is long and complex`. All the more reason to have it in its own service – Nkosi Jan 12 '20 at 13:26
  • @Nkosi Added the code you requested. Thank you for the attention. – Reath Jan 13 '20 at 14:03

0 Answers0