Making a list of customers who buy the top N items from a supplier
As said in a comment this may be done better and faster with an SQL script depending on how the involved data sets are created, but below are some optimization suggestions.



You should maybe consider some input check:

private void CustomersBuyingRange(string supplier, int topN)
{
  if (string.IsNullOrWhiteSpace(supplier)) return;
  if (topN &lt;= 0) throw new ArgumentOutOfRangeException(nameof(topN));




Optimizations:

1)


  // Get a list of products from a supplier.
  string[] products = productDetails
                      .Where(x =&gt; x.SupplierID == supplier.ToUpper())
                      .Select(x =&gt; x.ProductCode).ToArray();



supplier.ToUpper() is called repeatedly for every element in the vector. Consider to do it once:

  supplier = supplier.ToUpper();

  // Get a list of products from a supplier.
  var products = productDetails
                      .Where(x =&gt; x.SupplierID == supplier)
                      .Select(x =&gt; x.ProductCode);


As shown, there is also no need to populate an array with the result here.



2)


  var customers = customerDetails
      .Where(x =&gt; x.LastInvoiceDate &gt; DateTime.Now.AddMonths(-3).Date)
      .OrderBy(x =&gt; x.CustomerName)
      .ToList();



Again you create a new DateTime object for each customer you evaluate in the Where() call. Consider to create that date once:

    DateTime minDate = DateTime.Now.AddMonths(-3).Date;
    // We only want customers that have had an order in the last 3 months.
    var customers = customerDetails
        .Where(x =&gt; x.LastInvoiceDate &gt; minDate)
        .OrderBy(x =&gt; x.CustomerName);


You can then reuse it in the last loop too.



3)


    for (int i = 0; i &lt; topN; i++)
    {
      row[topSellers[i]] = detailedOrderLines
          .Any(x =&gt; x.CustomerID == customer.CustomerID &amp;&amp;
               x.ProductCode == topSellers[i] &amp;&amp;
               x.InvoiceDate &gt; DateTime.Now.AddMonths(-3).Date
               ) ? "Yes" : "No";
    }



Here you should be able to run a foreach loop instead:

      foreach (var topSeller in topSellers)
      {
        row[topSeller] = detailedOrderLines
            .Any(x =&gt; x.CustomerID == customer.CustomerID &amp;&amp;
                 x.ProductCode == topSeller &amp;&amp;
                 x.InvoiceDate &gt; minDate) ? "Yes" : "No";
      }




4)


foreach (var customer in customers)
  {
    DataRow row = table.NewRow();
    row["CustomerID"] = customer.CustomerID;
    row["Customer"] = customer.CustomerName;
    row["AccountManager"] = customer.AccountManager;
    for (int i = 0; i &lt; topN; i++)
    {
      row[topSellers[i]] = detailedOrderLines
          .Any(x =&gt; x.CustomerID == customer.CustomerID &amp;&amp;
               x.ProductCode == topSellers[i] &amp;&amp;
               x.InvoiceDate &gt; DateTime.Now.AddMonths(-3).Date
               ) ? "Yes" : "No";
    }
    table.Rows.Add(row);
  }



Here you keep require the entire detailedOrderLines although you previously when creating the topSellers actually grouped those by ProductCode. So if you instead of selecting topSellers as strings select them as anonymous objects like:

  // Rank the products by distribution and then by sales,
  // then add the required amount to an array.
  var topProducts = detailedOrderLines
                          .Where(x =&gt; products.Contains(x.ProductCode))
                          .GroupBy(x =&gt; x.ProductCode)
                          .Select(x =&gt; new
                          {
                            ProductCode = x.FirstOrDefault().ProductCode,
                            DeliveredQty = x.Sum(p =&gt; p.DeliveredQty),
                            Distribution = x.Select(c =&gt; c.CustomerID).Distinct().Count(),
                            OrderLines = x.ToList()
                          })
                          .OrderByDescending(x =&gt; x.Distribution)
                          .ThenByDescending(x =&gt; x.DeliveredQty)
                          .Take(topN).ToList();


you only have to query the subset of orderLines that belongs to the current ProductCode:

    foreach (var customer in customers)
    {
      DataRow row = table.NewRow();
      row["CustomerID"] = customer.CustomerID;
      row["Customer"] = customer.CustomerName;
      row["AccountManager"] = customer.AccountManager;

      foreach (var topProduct in topProducts)
      {
        row[topProduct.ProductCode] = topProduct.OrderLines.Any(x =&gt; x.CustomerID == customer.CustomerID &amp;&amp; x.InvoiceDate &lt; minDate) ? "Yes" : "No";
      }

      table.Rows.Add(row);
    }




All in all the method could then be something like:

private void CustomersBuyingRange(string supplier, int topN)
{
  if (string.IsNullOrWhiteSpace(supplier)) return;
  if (topN &lt;= 0) throw new ArgumentOutOfRangeException(nameof(topN));

  supplier = supplier.ToUpper();

  // Get a list of products from a supplier.
  var products = productDetails
                      .Where(x =&gt; x.SupplierID == supplier)
                      .Select(x =&gt; x.ProductCode);

  // Rank the products by distribution and then by sales,
  // then add the required amount to an array.
  var topProducts = detailedOrderLines
                          .Where(x =&gt; products.Contains(x.ProductCode))
                          .GroupBy(x =&gt; x.ProductCode)
                          .Select(x =&gt; new
                          {
                            ProductCode = x.FirstOrDefault().ProductCode,
                            DeliveredQty = x.Sum(p =&gt; p.DeliveredQty),
                            Distribution = x.Select(c =&gt; c.CustomerID).Distinct().Count(),
                            OrderLines = x.ToList()
                          })
                          .OrderByDescending(x =&gt; x.Distribution)
                          .ThenByDescending(x =&gt; x.DeliveredQty)
                          .Take(topN).ToList();

  using (DataTable table = new DataTable("Customers"))
  {
    table.Columns.Add("CustomerID", typeof(string));
    table.Columns.Add("Customer", typeof(string));
    table.Columns.Add("AccountManager", typeof(string));
    // Add the columns with the products.
    foreach (var topProduct in topProducts)
    {
      table.Columns.Add(topProduct.ProductCode, typeof(string));
    }

    DateTime minDate = DateTime.Now.AddMonths(-3).Date;
    // We only want customers that have had an order in the last 3 months.
    var customers = customerDetails
        .Where(x =&gt; x.LastInvoiceDate &gt; minDate)
        .OrderBy(x =&gt; x.CustomerName);


    foreach (var customer in customers)
    {
      DataRow row = table.NewRow();
      row["CustomerID"] = customer.CustomerID;
      row["Customer"] = customer.CustomerName;
      row["AccountManager"] = customer.AccountManager;

      foreach (var topProduct in topProducts)
      {
        row[topProduct.ProductCode] = topProduct.OrderLines.Any(x =&gt; x.CustomerID == customer.CustomerID &amp;&amp; x.InvoiceDate &lt; minDate) ? "Yes" : "No";
      }

      table.Rows.Add(row);
    }

    Export.ExportToExcel(table, true);
  }
}




Disclaimer: I haven't had the opportunity to test the above, so don't hang me if it doesn't improve anything or if I have misunderstood something.