Search code examples
c#language-ext

How do I conditionally call code when using LanguageExt with query syntax?


The code here is an MRE, although very much simplified.

I have a method that takes a transaction and requests a refund. The method looks for the transaction in the database, then calls an external service to request the refund. If the refund were approved, it would update the transaction in the local database and return a success code. if anything went wrong, or the refund request was refused, it would return an appropriate code.

Ignoring the Trans.Type property for the moment, the following code does the job...

static async Task<States> Refund(int id) =>
  await (
    from transaction in GetTransaction(id).ToAsync()
    from _1 in GetOtherData().ToAsync()
    from result in ApproveRefund(transaction).ToAsync()
    from _2 in Save(transaction).ToAsync()
    select States.Success
  )
  .Match(s => s, s => s);

// Get the transaction from the database
static async Task<Either<States, Trans>> GetTransaction(int id) =>
  id switch {
    0 => new Trans(id, TransType.Open, 1.0M),
    1 => new Trans(id, TransType.Closed, 1.0M),
    2 => new Trans(id, TransType.Open, 1.5M),
    3 => new Trans(id, TransType.Closed, 1.5M),
    _ => States.TransactionNotFound
  };

// We need some extra data before calling the external service.
// There are actually about four methods that are called in turn,
// but for simplicity, I've only included one here
static async Task<Either<States, Unit>> GetOtherData() =>
  unit;

// For simplicity, approve any transaction less that £1.50
// In reality, this would seek approval from an external service
static async Task<Either<States, States>> ApproveRefund(Trans transaction) =>
  transaction.Amount < 1.5M
    ? Right<States, States>(States.Success)
    : Left<States, States>(States.RefundRefused);

// Save the changes. For this sample we assume this will always succeed
static async Task<Either<States, Unit>> Save(Trans transaction) {
  transaction.Refunded = DateTime.Now;
  return States.Success;
}

class Trans {
  public Trans(int id, TransType type, decimal amount, DateTime? refunded = null) {
    Id = id;
    Type = type;
    Amount = amount;
    Refunded = refunded;
  }

  public int Id { get; set; }
  public TransType Type { get; set; }
  public decimal Amount { get; set; }
  public DateTime? Refunded { get; set; }
}

enum TransType {
  Open,
  Closed
}

enum States {
  Success,
  TransactionNotFound,
  RefundRefused,
  // More specific error states would go here
}

We now have to handle two types of transaction, which we refer to as open and closed. For closed transactions, the workflow above remains the same. However, for open transactions, we do not need to call the external service, we just need to set the transaction's Refunded property to DateTime.Now and save.

I can't work out how to make this change.

I added the Trans.Type property, and would like to do something like the following...

static async Task<States> Refund(int id) =>
  await (
    from transaction in GetTransaction(id).ToAsync()
    if (transaction.Type == TransType.Closed) {
      from _1 in GetOtherData().ToAsync()
      from result in ApproveRefund(transaction).ToAsync()
    }
    from _2 in Save(transaction).ToAsync()
    select States.Success
  )
  .Match(s => s, s => s);

...but of course this does not compile.

I know it looks from this simple sample like I could just put an if in the ApproveRefund method, and only call the external service if the transaction were closed, but in reality the calls that precede it (in the sample represented by the GetOtherData method) should not be called. I don't really want to add ifs to each of those methods.

Anyone able to advise how I would do this? Thanks


Solution

  • Why not wrap the entire section that calls the external service in a method, and just bail out immediately if it's an open transaction?

    Renaming your ApproveRefund method to CallService, and adding a new ApproveRefund that checked if the call were necessary, and load the extra data if it were, then your main query would look like this...

    static async Task<States> Refund(int id) =>
      await (
        from transaction in GetTransaction(id).ToAsync()
        from _1 in ApproveRefund(transaction).ToAsync()
        from _2 in Save(transaction).ToAsync()
        select States.Success
      )
      .Match(s => s, s => s);
    

    ...where ApproveRefund looks like this...

    static async Task<Either<States, States>> ApproveRefund(Trans transaction) =>
      transaction.Type == TransType.Open
        ? Right<States, States>(States.Success)
        : await (
            from _ in GetOtherData().ToAsync()
            from result in CallService(transaction).ToAsync()
            select States.Success
          )
          .Match(Right<States, States>, Left<States, States>);
    

    This seems to work, although is perhaps not as elegant as it might be.