Hi jewel,
That's an excellent SQL solution! Your use of CTEs and the window function for running balance calculation is really well-structured. I can see how you've broken down the problem into logical steps.
Here's how you can translate that exact SQL logic into C# LINQ while maintaining the same approach:
public JsonResult GetAccountStatement()
{
var purchases = GetCompanyPurchases(1);
var payments = GetCompanyPayments(1);
var targetDate = DateTime.Parse("2025-02-02");
var endDate = DateTime.Parse("2025-02-15");
var previousDay = targetDate.AddDays(-1);
// First CTE equivalent - Previous balance calculation
var previousPurchaseTotal = purchases
.Where(p => p.Companyid == 1 && p.Date <= previousDay)
.Sum(p => (decimal?)p.Value) ?? 0;
var previousPaymentTotal = payments
.Where(p => p.Companyid == 1 && p.Date <= previousDay)
.Sum(p => (decimal?)p.PaymentAmount) ?? 0;
var previousBalance = previousPaymentTotal - previousPurchaseTotal;
// Second CTE equivalent (cte1) - Purchase transactions
var purchaseTransactions = purchases
.GroupBy(p => new { p.Date, p.Companyid, p.Invoceno })
.Select(g => new
{
Date = (DateTime?)g.Key.Date,
Companyid = g.Key.Companyid,
Note = g.Key.Invoceno,
PurchaseAmount = (decimal?)g.Sum(p => p.Value),
PaymentAmount = (decimal?)null
});
// Second CTE equivalent (cte1) - Payment transactions
var paymentTransactions = payments
.GroupBy(p => new { p.Date, p.Companyid })
.Select(g => new
{
Date = (DateTime?)g.Key.Date,
Companyid = g.Key.Companyid,
Note = (string)null,
PurchaseAmount = (decimal?)null,
PaymentAmount = (decimal?)g.Sum(p => p.PaymentAmount)
});
// Third CTE equivalent (CTE3) - Combine all data
var allRecords = new List<object>();
// Previous balance record
allRecords.Add(new
{
Date = (DateTime?)null,
Companyid = 1,
Note = "Previous Balance",
PaymentAmount = (decimal?)null,
PurchaseAmount = (decimal?)null,
TransactionBalance = previousBalance
});
// Add filtered transactions from date range
var filteredTransactions = purchaseTransactions
.Where(t => t.Companyid == 1 && t.Date >= targetDate && t.Date <= endDate)
.Union(paymentTransactions.Where(t => t.Companyid == 1 && t.Date >= targetDate && t.Date <= endDate))
.Select(t => new
{
Date = t.Date,
Companyid = t.Companyid,
Note = t.Note,
PaymentAmount = t.PaymentAmount,
PurchaseAmount = t.PurchaseAmount,
TransactionBalance = (t.PaymentAmount ?? 0) - (t.PurchaseAmount ?? 0)
})
.OrderBy(t => t.Date);
allRecords.AddRange(filteredTransactions.Cast<object>());
// Final result with running balance (SUM() OVER equivalent)
var sortedRecords = allRecords
.OrderBy(r => ((dynamic)r).Date ?? DateTime.MinValue)
.ToList();
var finalResults = new List<object>();
var runningBalance = 0m;
foreach (dynamic record in sortedRecords)
{
runningBalance += record.TransactionBalance;
finalResults.Add(new
{
Date = record.Date,
Companyid = record.Companyid,
Note = record.Note,
PaymentAmount = record.PaymentAmount,
PurchaseAmount = record.PurchaseAmount,
Balance = runningBalance
});
}
return Json(finalResults);
}
Key points that mirror your SQL structure:
- CTE breakdown: Each of your CTEs is represented as a separate LINQ operation
- Previous balance logic: Matches your
DATEADD(DAY,-1,'2025-02-02')filtering - Union operations: Combines purchase and payment transactions like your
UNION ALL - Window function: The running balance calculation replicates your
SUM(balance) OVER(ORDER BY [Date]) - Grouping: Maintains your grouping logic for both purchases and payments
Your SQL solution with CTEs is actually very elegant for this type of financial reporting. The LINQ version gives you the same results while keeping the logic in your application layer. Both approaches are valid, it really depends on whether you prefer database-side or application-side processing for this business logic.
The running balance calculation using the window function in your SQL is particularly clean, that's often the most efficient way to handle cumulative calculations in SQL Server.
Hope this helps!