Intersect list of list of object and list of object - c#

I have a list of transaction class :
class Transactions
{
public Transactions()
{
Products = new List<Product>();
}
public string Date { get; set; }
public string TransactionID { get; set; }
public List<Product> Products { get; set; }
}
And a Product Class:
class Product
{
public decimal ProductCode { get; set; }
}
I have a list of Products like this:
List<Product> unioned = product.Union(secondProduct).ToList();
And I want Intersect of unioned and transaction products,
This code does not work:
var intersection = transactions.Where(q => q.Products.Intersect(unioned).Any());
I think the reason is transaction products length is variant and union length is fixed.
How can I do it?

Intersect is using the default equality comparer, so will do a reference check - i.e. compare that the object references are the same.
You need to use the overload which allows you to specify an equality comparer:
Thus:
public class ProductComparer : IEqualityComparer<Product>
{
public bool Equals(Product x, Product y)
{
// TODO - Add null handling.
return x.ProductCode == y.ProductCode;
}
public int GetHashCode(Product obj)
{
return obj.ProductCode.GetHashCode();
}
}
And then:
var intersection = transactions
.Where(q => q.Products.Intersect(unioned, new ProductComparer()).Any());
This test will now pass:
[TestMethod]
public void T()
{
Product p = new Product { ProductCode = 10M };
List<Product> product = new List<Product> { p };
List<Product> secondProduct = new List<Product> { new Product { ProductCode = 20M } };
List<Product> unioned = product.Union(secondProduct).ToList();
var transaction = new Transactions();
// add a different object reference
transaction.Products.Add(new Product { ProductCode = 10M });
IList<Transactions> transactions = new List<Transactions> { transaction };
var intersection = transactions
.Where(q => q.Products.Intersect(unioned, new ProductComparer()).Any());
Assert.AreEqual(1, intersection.Count());
}

Try this to solution without using Intersect. I use ProductCode to check if the Product is the same:
transactions.Where(q => q.Products.Exists(x => unioned.Exists(z => z.ProductCode == x.ProductCode))).ToList();

You could do something like:
List<Product> allproductsTrans = new List<Product>();
transactions.ForEach(p => allproductsTrans.Concat(p.Products));
var result = allproductsTrans.Intersect(unioned);
but as Slava Utesinov said in the comments, the following code would do the same:
transactions.SelectMany(x => x.Products).Intersect(unioned)

Instead of using EqualityComparer, you can intersect ids, and then, if needed, find objects by ids.
Your example:
transactions.Where(q => q.Products.Select(x => x.ProductCode).Intersect(unioned.Select(x => x.ProductCode)).Any());
General case:
var ids = list1.Select(x => x.Id).Intersect(list2.Select(x => x.Id));
var result = list1.Where(x => ids.Contains(x.Id));

Related

Sort items of list by field of another table LINQ ASP.NET MVC

So, I have my Products table in SSMS with these properties:
public class Product
{
public int Id {get; set;}
public string Title { get; set; }
public decimal Price { get; set; }
}
and my Reports table:
public class Report
{
public int Id { get; set; }
public int ProductId { get; set; }
public ReportType ReportType { get; set; }
}
I want to return a List<Product> to my View that is sorted based on how many reports each Product has, but I can't figure out how to do it with LINQ. Any help/tip would be appreciated.
If you put nav props in this would be:
context.Products.Include(p => p.Reports).OrderBy(p => p.Reports.Count(*));
But as you have no nav props, perhaps something like:
context.Products.OrderBy(p => context.Reports.Count(r => r.ProductId == p.Id));
The query ends up looking like this for the latter:
SELECT *
FROM p
ORDER BY (SELECT COUNT(*) FROM r WHERE p.id = r.id)
and similar but with a left join, for the former
You could also do it on the client side
var dict = context.Reports.GroupBy(r => ProductId, (k,g) => new { ProductId, Count = g.Count() } )
.ToDictionary(at => at.ProductId, at => at.Count);
Then:
//or OrderByDescending if you want most reported products
var ret = context.Products.ToList().OrderBy(p => dict[p.ProductId]);
If you have some limited list of products:
var prods = context.Products.Where(...).ToList();
var prodIds = prods.Select(p => p.ProductId).ToArray();
var dict = context.Reports
.Where(r => prods.Contains(r.ProductId))
.GroupBy(r => ProductId, (k,g) => new { ProductId, Count = g.Count() } )
.ToDictionary(at => at.ProductId, at => at.Count)
var ret = prods.OrderBy(p => dict[p.ProductId]);

Joining two lists of object optimization

I am looking for a way of optimizing my LINQ query.
Classes:
public class OffersObject
{
public List<SingleFlight> Flights { get; set; }
public List<Offer> Offers { get; set; } = new List<Offer>();
}
public class SingleFlight
{
public int Id { get; set; }
public string CarrierCode { get; set; }
public string FlightNumber { get; set; }
}
public class Offer
{
public int ProfileId { get; set; }
public List<ExtraOffer> ExtraOffers { get; set; } = new List<ExtraOffer>();
}
public class ExtraOffer
{
public List<int> Flights { get; set; }
public string Name { get; set; }
}
Sample object:
var sampleObject = new OffersObject
{
Flights = new List<SingleFlight>
{
new SingleFlight
{
Id = 1,
CarrierCode = "KL",
FlightNumber = "1"
},
new SingleFlight
{
Id = 2,
CarrierCode = "KL",
FlightNumber = "2"
}
},
Offers = new List<Offer>
{
new Offer
{
ProfileId = 41,
ExtraOffers = new List<ExtraOffer>
{
new ExtraOffer
{
Flights = new List<int>{1},
Name = "TEST"
},
new ExtraOffer
{
Flights = new List<int>{2},
Name = "TEST"
},
new ExtraOffer
{
Flights = new List<int>{1,2},
Name = "TEST"
}
}
}
}
};
Goal of LINQ query:
List of:
{ int ProfileId, string CommercialName, List<string> fullFlightNumbers }
FullFlightNumber should by created by "Id association" of a flight. It is created like: {CarrierCode} {FlightNumber}
What I have so far (works correctly, but not the fastest way I guess):
var result = sampleObject.Offers
.SelectMany(x => x.ExtraOffers,
(a, b) => {
return new
{
ProfileId = a.ProfileId,
Name = b.Name,
FullFlightNumbers = b.Flights.Select(f => $"{sampleObject.Flights.FirstOrDefault(fl => fl.Id == f).CarrierCode} {sampleObject.Flights.First(fl => fl.Id == f).FlightNumber}").ToList()
};
})
.ToList();
Final note
The part that looks wrong to me is:
.Select(f => $"{sampleObject.Flights.FirstOrDefault(fl => fl.Id == f)?.CarrierCode} {sampleObject.Flights.FirstOrDefault(fl => fl.Id == f)?.FlightNumber}").ToList()
I am basically looking for a way of "joining" those two lists of the OffersObject by Flight's Id.
Any tips appreciated.
If there will only be a few flights defined in sampleObject.Flights, a sequential search using a numeric key is hard to beat.
However, if the number of flights times the number of offers is substantial (1000s or more), I would suggest loading the list of flights into a dictionary with Id as the key for efficient lookup. Something like:
var flightLookup = sampleObject.Flights.ToDictionary(f => f.Id);
And then calculate your FullFlightNumbers as
FullFlightNumbers = b.Flights
.Select(flightId => {
flightLookup.TryGetValue(flightId, out SingleFlight flight);
return $"{flight?.CarrierCode} {flight?.FlightNumber}";
})
.ToList()
TryGetValue above will quietly return a null value for flight if no match is found. If you know that a match will always be present, the lookup cold alternately be coded as:
SingleFlight flight = flightLookup[flightId];
The above also uses a statement lambda. In short, lambda functions can have either expression or statement blocks as bodies. See the C# reference for more information.
I'd suggest replacing the double .FirstOrDefault() approach with .IntersectBy(). It is available in the System.Linq namespace, starting from .NET 6.
.IntersectBy() basically filters sampleObject.Flights by matching the flight ID for each flight in sampleObject with flight IDs in ExtraOffers.Flights.
In the code below, fl => fl.Id is the key selector for sampleObject.Flights (i.e. fl is a SingleFlight).
var result = sampleObject.Offers
.SelectMany(x => x.ExtraOffers,
(a, b) => {
return new
{
ProfileId = a.ProfileId,
Name = b.Name,
FullFlightNumbers = sampleObject.Flights
.IntersectBy(b.Flights, fl => fl.Id)
.Select(fl => fl.FullFlightNumber) // alternative 1
//.Select(fl => $"{fl.CarrierCode} {fl.FlightNumber}") // alternative 2
.ToList()
};
})
.ToList();
In my suggestion I have added the property FullFlightNumber to SingleFlight so that the Linq statement looks slightly cleaner:
public class SingleFlight
{
public int Id { get; set; }
public string CarrierCode { get; set; }
public string FlightNumber { get; set; }
public string FullFlightNumber => $"{CarrierCode} {FlightNumber}";
}
If defining SingleFlight.FullFlightNumber is not possible/desirable for you, the second alternative in the code suggestion can be used instead.
Example fiddle here.

How to have a changing name as Key and it's value as Count in select

So I'm not sure if this is possible, but I'm doing a final select query, and I've noticed that it would be easier for my data if I could do something like:
var finalQuery = selectQuery
.GroupBy(x => x.ProductName)
.Select(c => new
{
c.Key = c.Count()
}).ToList();
Therefore, I want the returned data to be something like:
[{
"Clothes" : 5,
"Shoes" : 7,
"Laptop" : 10
}]
My current query is:
var finalQuery = selectQuery
.GroupBy(x => x.ProductName)
.Select(c => new
{
ProductName = c.Key
ProductCount = c.Count()
}).ToList();
I thought something like c.Key.ToString() would do the trick, but it doesn't work. I'm guessing the "Key" must be a set value to work, and it can't necessarily be dynamically changed?
You can try this way
var finalQuery = selectQuery.GroupBy(p => p.ProductName).ToDictionary(g => g.Key, g => g.Count());
You should customize the string using your own code, or you can use an external library as NewtonSoft.Json.
Below is a sample code that does the trick for you.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class Test
{
public static void Main()
{
var products = new List<Product>()
{
new Product("a", "Clothes"),
new Product("b", "Shoes"),
new Product("c", "Clothes"),
new Product("d", "Clothes"),
new Product("e", "Shoes"),
new Product("f", "Shoes"),
new Product("g", "Laptop"),
new Product("h", "Laptop"),
new Product("h", "Shoes"),
};
var result = products
.GroupBy(p => p.Type)
.Select(
group => new
ProductCount(group.Key, group.Count())
// You can use also an object as the commented code below
//{
// Type = group.Key,
// Count = group.Count()
//}
)
.ToList();
Console.WriteLine(ProductCount.ToFormatedString(result));
}
class Product
{
public string Name { get; set; }
public string Type { get; set; }
public Product(string name, string type)
{
this.Name = name;
this.Type = type;
}
}
class ProductCount
{
public string Type { get; set; }
public int Count { get; set; }
public ProductCount(string type, int count)
{
this.Type = type;
this.Count = count;
}
public override string ToString()
{
return $"\"{this.Type}\" : {this.Count}";
}
public static string ToFormatedString(IEnumerable<ProductCount> products) // if you need a more generic method, u can make this an extension method and use Reflection
// Or u can use a nuget package that formats objects to json (e.g: Newtonsoft is a good library)
{
var sb = new StringBuilder();
sb.AppendLine("[{");
foreach (var item in products)
sb.AppendLine(item.ToString());
sb.AppendLine("}]");
return sb.ToString();
}
}
}

SequenceEqual linq group

I write a simple program using c# 3.5 and linq .
i have class
public class Product
{
public Product()
{
}
public int RoleId { get; set; }
public int ObjectId { get; set; }
public bool Read { get; set; }
public override bool Equals(object obj)
{
return Equals((Product) obj);
}
public bool Equals(Product other)
{
return ObjectId == other.ObjectId && Read == other.Read;
}
}
I am trying to compair list.
List<Product> products = new List<Product>()
{
new Product { RoleId = 1, ObjectId = 2, Read = false },
new Product { RoleId = 2, ObjectId = 1, Read = false },
new Product { RoleId = 1, ObjectId = 1, Read = true }
};
var groupedCustomerList = products.GroupBy(u => u.RoleId)
.Select(grp => grp.ToList()).ToList();
var firstGroup = groupedCustomerList.ElementAt(0);
List<Product> productsListSearch = new List<Product>()
{
new Product {ObjectId = 1, Read = true },
new Product {ObjectId = 2, Read = false }
};
var result= productsListSearch.SequenceEqual(firstGroup);
Why the result is incorrect ?
I need to sort the items?
They are not sequence-equal because the objects come in different order. If you change productsListSearch like this
List<Product> productsListSearch = new List<Product>()
{
new Product {ObjectId = 2, Read = false }
, new Product {ObjectId = 1, Read = true }
};
SequenceEqual would return True.
Demo on ideone.
Generally, you should not rely on the order of items in LINQ-generated groups, unless you set the order yourself:
var groupedCustomerList = products.GroupBy(u => u.RoleId)
.Select(grp => grp.OrderBy(p => ...).ToList()).ToList();
Note: your Product class overrides Equals without overriding GetHashCode. This will be problematic if you decide to use Product in hash sets or as keys of hash-based dictionaries.
The lists are in different orders. Change second list to
List<Product> productsListSearch = new List<Product>()
{
new Product {ObjectId = 2, Read = false },
new Product {ObjectId = 1, Read = true }
};
and the result will be true
SequenceEquals means the sequence has to be equal. You have the same elements in both collections but in a different sequence.
I assume that your second data set is how you expect to see your main data once it is grouped and sorted. In order for that to happen you need to change this line so that your grouped items will be sorted:
var groupedCustomerList = products.GroupBy (u => u.RoleId)
.Select (u => u.OrderBy (x => x.ObjectId));
And this is a cleaner looking way to get the first element:
var firstGroup = groupedCustomerList.First ();

Union in entity framework

I have two tables: Vehicles and Workers.
Vehicle(Id, Number)
Workers(Id, Name, ContractorVehicleNumber)
I would like to write lambda query to return all the vehicles and the contractor vehicles. Something like in sql:
SELECT Id, Number
FROM Vehicle
UNION
SELECT NULL, ContractorVehicleNumber
FROM Workers
This is what I made:
public IQueryable<Vehicle> Get(bool includeContractorVehicles)
{
IQueryable<Vehicle> query = GetQuery();
if (includeContractorVehicles == true)
{
WorkerRepository rep = new WorkerRepository();
IQueryable<Vehicle> contractorsVehicles = rep.GetWirkers().
Select(x => new Vehicle()
{
VehicleNumber = x.ContractorVehicleNumber
});
query = query.Union(contractorsVehicles);
}
return query;
}
But I get an exception:
The entity or complex type 'XXXXXXXX' cannot be constructed in a LINQ to Entities query.
You cannot construct mapped entity type in projection. Your former example will work only if you create a new special type used for projection:
public class VehicleResult
{
public string Number { get; set; }
... // If you don't need more then one column you can use simple type instead of custom class
}
And your method will look like:
public IQueryable<VehicleResult> Get(bool includeContractorVehicles)
{
IQueryable<VehicleResult> query = GetQuery().Select(v => new VehicleResult { ... });
if (includeContractorVehicles == true)
{
WorkerRepository rep = new WorkerRepository();
IQueryable<VehicleResult> contractorsVehicles = rep.GetWorkers().
Select(x => new VehicleResult()
{
Number = x.ContractorVehicleNumber
});
query = query.Union(contractorsVehicles);
}
return query;
}
You cant create entities in the select statement. Try this instead:
public class VehicleDTO
{
public int Id { get; set; }
public int Number { get; set; }
}
public IQueryable<VehicleDTO> Get(bool includeContractorVehicles)
{
var query = GetQuery().Select(x => new VehicleDTO(){ ID = c.ID, Number = c.Number });
if (includeContractorVehicles)
{
WorkerRepository rep = new WorkerRepository();
var contractorsVehicles = rep.GetWirkers().
Select(x => new VehicleDTO(){ Number = x.ContractorVehicleNumber});
query = query.Union(contractorsVehicles);
}
return query;
}
Also are you sure you want a Union and not a Concat ?

Categories

Resources