I have a class that contains 2 properties of the same type. decimal NetAmount
and decimal GrossAmount
I would like to initialize it using either GrossAmount
or NetAmount
and based on the one specified calculate the second one.
Which way is the most elegant and why? (parameter validation is omitted for brevity)
1
public class TaxedPrice
{
private TaxedPrice()
{ }
public decimal NetAmount { get; private set; }
public decimal GrossAmount { get; private set; }
public static TaxedPrice FromNet(decimal netAmount, decimal taxRate)
{
return new TaxedPrice
{
NetAmount = decimal.Round(netAmount, 2, MidpointRounding.AwayFromZero),
GrossAmount = decimal.Round(netAmount.ApplyTax(taxRate), 2, MidpointRounding.AwayFromZero)
};
}
public static TaxedPrice FromGross(decimal grossAmount, decimal taxRate)
{
return new TaxedPrice
{
GrossAmount = decimal.Round(grossAmount, 2, MidpointRounding.AwayFromZero),
NetAmount = decimal.Round(grossAmount.RemoveTax(taxRate), 2, MidpointRounding.AwayFromZero)
};
}
}
2
public class TaxedPrice
{
public TaxedPrice(decimal netAmount, decimal grossAmount, decimal taxRate)
{
if (grossAmount != default)
{
GrossAmount = decimal.Round(grossAmount, 2, MidpointRounding.AwayFromZero);
NetAmount = decimal.Round(grossAmount.RemoveTax(taxRate), 2, MidpointRounding.AwayFromZero);
}
else if (netAmount != default)
{
NetAmount = decimal.Round(netAmount, 2, MidpointRounding.AwayFromZero);
GrossAmount = decimal.Round(netAmount.ApplyTax(taxRate), 2, MidpointRounding.AwayFromZero);
}
else
{
throw new InvalidOperationException($"Either {nameof(netAmount)} or {grossAmount} must be set.");
}
}
public decimal NetAmount { get; }
public decimal GrossAmount { get; }
}
3
public class TaxedPrice
{
public enum Type
{
Gross,
Net
}
public TaxedPrice(decimal amount, Type type, decimal taxRate)
{
if (type == Type.Gross)
{
GrossAmount = decimal.Round(amount, 2, MidpointRounding.AwayFromZero);
NetAmount = decimal.Round(amount.RemoveTax(taxRate), 2, MidpointRounding.AwayFromZero);
}
else if (type == Type.Net)
{
NetAmount = decimal.Round(amount, 2, MidpointRounding.AwayFromZero);
GrossAmount = decimal.Round(amount.ApplyTax(taxRate), 2, MidpointRounding.AwayFromZero);
}
}
public decimal NetAmount { get; }
public decimal GrossAmount { get; }
}
4
public class TaxedPrice
{
public TaxedPrice(decimal amount, bool fromGross, decimal taxRate)
{
if (fromGross)
{
GrossAmount = decimal.Round(amount, 2, MidpointRounding.AwayFromZero);
NetAmount = decimal.Round(amount.RemoveTax(taxRate), 2, MidpointRounding.AwayFromZero);
}
else
{
NetAmount = decimal.Round(amount, 2, MidpointRounding.AwayFromZero);
GrossAmount = decimal.Round(amount.ApplyTax(taxRate), 2, MidpointRounding.AwayFromZero);
}
}
public decimal NetAmount { get; }
public decimal GrossAmount { get; }
}
How it looks like from caller's side:
// 1
var taxedPrice = TaxedPrice.FromNet(2.123m, 0.23m);
// 2
var taxedPrice = new TaxedPrice(2.123m, default, 0.23m); // uses the first one to calculate the second one
var taxedPrice2 = new TaxedPrice(2.123m, 1.11m, 0.23m); // uses the first one to calculate the second one
var taxedPrice3 = new TaxedPrice(default, 1.11m, 0.23m); // uses the second one to calculate the first one
// 3
var taxedPrice = new TaxedPrice(2.123m, TaxedPrice.Type.Net, 0.23m);
// 4
var taxedPrice = new TaxedPrice(2.123m, false, 0.23m);
Extensions for tax:
public static class TaxExtensions
{
public static decimal ApplyTax(this decimal netPrice, decimal taxRate)
{
return netPrice * (taxRate + 1);
}
public static decimal RemoveTax(this decimal grossPrice, decimal taxRate)
{
return grossPrice / (taxRate + 1);
}
}
Mapping perspective
In my upper layer I pass those prices in POCOs/DTOs like:
public class PriceDTO
{
public decimal NetAmount { get; set; }
public decimal GrossAmount { get; set; }
}
And I have to check there as well which one was passed to decide from which to calculate. So in case of 1 mapping would look like:
if (priceDto.GrossAmount != default)
return TaxedPrice.FromGross(priceDto.GrossAmount, taxRate);
else if (priceDto.NetAmount != default)
return TaxedPrice.FromNet(priceDto.NetAmount, taxRate);
else
// error
In case of 2 (no need to check in the mapping code)
return new TaxedPrice(priceDto.NetAmount, priceDto.GrossAmount, taxRate)
3 - there's a check as well
4 - same like 1 and 3
And I agree this could be a struct
instead.
RemoveTax
andApplyTax
. Are these methods extensions? \$\endgroup\$decimal
type that add/remove tax from the price. \$\endgroup\$