Refactor LINQ ... multiplying property and child property from a list
I am still coming to terms with LINQ and I am trying to refactor the following foreach loop into it's LINQ equivalent. Here's the foreach loop that I am trying to convert;
var NetTotal = 0M;
foreach (var cheque in ListOfCheques)
{
var exchangeRate = (from exc in cheque.ExchangeRates
where exc.Type == EnumExchangeRate.ForCheque
select exc).FirstOrDefault();
NetTotal = exchangeRate != null ? NetTotal + cheque.NetAmount * exchangeRate.Rate : NetTotal + cheque.NetAmount;
}
return NetTotal ;
and the LINQ code that I've come up with;
var NetTotal = (from cheque in ListOfCheques
join exc in ListOfCheques.SelectMany(b => b.ExchangeRates) on cheque.ID equals exrate.Cheque.ID into chequeWithRate
where income.ExchangeRates.Select(x => x.Type).Equals(EnumExchangeRate.ForCheque)
from ur in chequeWithRate.DefaultIfEmpty()
select ur).Sum(x => x.Cheque.NetAmount * x.Rate);
return Ne开发者_JAVA百科tTotal;
The important points that I am struggling with;
- It's possible that the "ExchangeRates" list within the Cheque class does not exist, i.e. it does not need an exchange rate.
- If there is no exchange rate found it should default to 1. How can I set that... I was hoping to set it within DefaultIfEmpty(1).
Any help is greatly appreciated.
Like any refactoring, just chip away piece by piece. Firstly, rather than foreach
just use a Sum()
like so.
return ListOfCheques.Sum(c =>
{
var exchangeRate = (from exc in c.ExchangeRates
where exc.Type == EnumExchangeRate.ForCheque
select exc).FirstOrDefault();
return c.NetAmount * (exchangeRate ?? new ExchangeRate(){ Rate = 1 }).Rate;
});
(it'd be nice if the default value for the ExchangeRate.Rate property was 1)
I'd rewrite the exchangeRate function to a simple format. Are you sure you want FirstOrDefault
and not SingleOrDefault
?
var exchangeRate = c.ExchangeRates.
FirstOrDefault(ex => ex.Type == EnumExchangeRate.ForCheque);
and then this can be swapped into the first statement, leaving the resulting final product below.
A one liner if you want it to be!
return ListOfCheques.Sum(c => c.NetAmount *
(c.ExchangeRates.FirstOrDefault(ex => ex.Type == EnumExchangeRate.ForCheque)
?? new ExchangeRate() { Rate = 1 }).Rate);
edit
Clarification on the ?? new ExchangeRate()
Rather than doing the != null ? (amount * rate) : (rate)
I prefer to coalesce the ExchangeRate
object with a new such object with Rate = 1. I think this provides a smoother and cleaner piece of code. I'd strongly suggest you make the default Rate be 1.0, and then you can simply coalesce with a new ExchangeRate()
, without needing to set the Rate property.
To set a default value for the Rate
in a new ExchangeRate object, just put the initializer inside the constructor
class ExchangeRate
{
public ExchangeRate()
{
this.Rate = 1.0;
}
// other stuff
}
How about this?
var query =
from cheque in ListOfCheques
let rate =
cheque.ExchangeRates
.Where(exc => exc.Type == EnumExchangeRate.ForCheque)
.Select(exc => exc.Rate)
.DefaultIfEmpty(1.0M)
.First()
select rate * cheque.NetAmount;
var NetTotal = query.Sum();
Your LINQ query example given in your question has "extra" stuff that you didn't explain so I've only included the stuff from your foreach
loop.
Do you need this?
var query = from cheque in ListOfCheques
let excRates = cheque.ExchangeRates ?? Enumerable.Empty()
let rate = excRates.Where(x => x.Type == Something).Select(x => x.Rate).FirstOrDefault() ?? 1
select cheque.NetAmount * rate;
var netTotal = query.Sum();
If Rate is nulllable, you can either adept that in the let statement by making it nullable (e.g. Select(x => new int?(x.Rate)) or remove ?? 1 and adept it in your select. which will make:
var query = from cheque in ListOfCheques
let excRates = cheque.ExchangeRates ?? Enumerable.Empty()
let rate = excRates.Where(x => x.Type == Something).Select(x => x.Rate).FirstOrDefault()
select cheque.NetAmount * (rate != 0 ? rate : 1);
精彩评论