Home > Mobile >  Joining two lists of object optimization
Joining two lists of object optimization

Time:04-09

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:

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 => $"{mainList.Flights.FirstOrDefault(fl => fl.Id == f)?.CarrierCode} {mainList.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.

CodePudding user response:

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.

CodePudding user response:

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.

  • Related