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 if
s to each of those methods.
Anyone able to advise how I would do this? Thanks
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.