I'm trying to parse xml but certain elements aren't returning anything. the first block returns good data but the artist nodes and venue nodes all return null. im not sure what im doing wrong.
XDocument xml = XDocument.Parse(data);
var events = xml.Descendants("events");
foreach(var e in events)
{
foreach(var attribute in e.Descendants("event"))
{
string id = (string)attribute.Element("id");
string Title = (string)attribute.Element("title");
DateTime date = DateTime.Parse(((string)attribute.Element("datetime")), null, DateTimeStyles.RoundtripKind);
string TicketUrl = (string)attribute.Element("ticket_url");
TicketType TicketType = (TicketType)Enum.Parse(typeof(TicketType), (string)attribute.Element("ticket_type"));
TicketAvailability TicketAvailability = (TicketAvailability)Enum.Parse(typeof(TicketAvailability), (string)attribute.Element("ticket_status"));
DateTime TicketSaleDate = DateTime.Parse(((string)attribute.Element("on_sale_datetime")), null, DateTimeStyles.RoundtripKind);
DateTime LastAvailabilityCheck = DateTime.Now;
string FacebookRsvpUrl = (string)attribute.Element("facebook_rsvp_url");
string Description = (string)attribute.Element("description");
List<Artist> artistData = new List<Artist>();
Venue venue = new Venue();
foreach (var artistNode in attribute.Descendants("artist"))
{
var artistName = (string)artistNode.Attribute("name");
var junk = (string)artistNode.Attribute("mbid");
var ImageUrl = (string)artistNode.Attribute("image_url");
var ThumbnailUrl = (string)artistNode.Attribute("thumb_url");
var FaceBookUrl = (string)artistNode.Attribute("facebook_tour_dates_url");
artistData.Add(new Artist(artistName, ThumbnailUrl, ImageUrl, FaceBookUrl));
}
foreach (var venueNode in attribute.Descendants("venue"))
{
var VenueName = (string)venueNode.Element("name");
var City = (string)attribute.Element("city");
var Region = (string)venueNode.Attribute("region");
var Country = (string)venueNode.Attribute("country");
var GpsLocation = (string)venueNode.Attribute("latitude") + ", " + (string)attribute.Attribute("longitude");
venue = new Venue(VenueName, City, Region, Country, GpsLocation);
}
concerts.Add(new Concert(id, artistData, venue, date, TicketSaleDate, Description, FacebookRsvpUrl, Title, TicketUrl, TicketType, TicketAvailability, LastAvailabilityCheck));
}
}
sample data:
<?xml version="1.0" encoding="UTF-8"?>
<events>
<event>
<id>9203898</id>
<title>Fall Out Boy # DTE Energy Music Theatre in Clarkston, MI</title>
<datetime>2015-07-10T19:00:00</datetime>
<formatted_datetime>Friday, July 10, 2015 at 7:00PM</formatted_datetime>
<formatted_location>Clarkston, MI</formatted_location>
<ticket_url>http://www.bandsintown.com/event/9203898/buy_tickets?app_id=BandJunkieForWindows&artist=Fall+Out+Boy</ticket_url>
<ticket_type>Tickets</ticket_type>
<ticket_status>available</ticket_status>
<on_sale_datetime>2015-01-23T10:00:00</on_sale_datetime>
<facebook_rsvp_url>http://www.bandsintown.com/event/9203898?app_id=BandJunkieForWindows&artist=Fall+Out+Boy&came_from=67</facebook_rsvp_url>
<description></description>
<artists>
<artist>
<name>Fall Out Boy</name>
<mbid>516cef4d-0718-4007-9939-f9b38af3f784</mbid>
<image_url>http://www.bandsintown.com/FallOutBoy/photo/medium.jpg</image_url>
<thumb_url>http://www.bandsintown.com/FallOutBoy/photo/small.jpg</thumb_url>
<facebook_tour_dates_url>http://bnds.in/1n0W0qQ</facebook_tour_dates_url>
</artist>
</artists>
<venue>
<name>name</name>
<city>place</city>
<region>state</region>
<country>United States</country>
<latitude>142.748783</latitude>
<longitude>-183.380201</longitude>
</venue>
</event>
</events>
You're using Attribute, but your XML contains elements. Use Element instead...
foreach (var artistNode in attribute.Descendants("artist"))
{
var artistName = (string)artistNode.Element("name");
// etc...
Also as a sidenote, you can cast XElement and XAttribute to DateTime instead of calling DateTime.Parse:
DateTime TicketSaleDate = DateTime.Parse(((string)attribute.Element("on_sale_datetime")), null, DateTimeStyles.RoundtripKind);
can be
DateTime TicketSaleDate = (DateTime)attribute.Element("on_sale_datetime");
Related
i have 'created date' and 'closed date' in my file and i'm converting it in json so i have that dates in json.
in my method i have two parameter like from date and to date and i want to count particular column data of my file between from date and to date.so how can we write code to fetch it using linq.
i tried this...
public JsonResult StatusDerails(DateTime from,DateTime to)
{
string csvurl = WebConfigurationManager.AppSettings["csvfileurl"];
var lines = System.IO.File.ReadAllLines(csvurl).Skip(1);
List<Product> prdt = new List<Product>();
foreach (string line in lines)
{
Product c1 = new Product();
var split = line.Split(',');
c1.ID = Int32.Parse(split[0]);
c1.Area_Path = split[1];
c1.IterationPath = split[2];
c1.State = split[3];
c1.Reason = split[4];
c1.Priority = Int32.Parse(split[5]);
c1.Severity = split[6];
c1.Tags = split[7];
c1.Title = split[8];
c1.CreatedDate = split[9];
c1.CreatedBy = split[10];
c1.ResolvedDate = split[11];
c1.ResolvedBy = split[12];
c1.ClosedDate = split[13];
c1.AssignedTo = split[14];
prdt.Add(c1);
}
//var list = prdt.GroupBy(a=>a.AreaPath).Select(a=>new UIproduct() {
var productName = prdt.Select(a => a.Area_Path).Distinct();
List<StatusDetail> statusdetail = new List<StatusDetail>();
foreach (var Name in productName)
{
StatusDetail sd = new StatusDetail();
sd.CarryOver = prdt.Where(a => a.CreatedDate >= from.Date.ToString() && a.ClosedDate <= to.Date.ToShortDateString
}
return Json(statusdetail, JsonRequestBehavior.AllowGet);
}
The comparison of DateTime as string will not be a good option and that wont gives you the exact result, So I recommend you to change the type of CreatedDate and ClosedDate to DateTime. and compare two DateTime values in linq. I think instead of splitting json for creating object of certain types you can use json converters.
Fix for your scenario:
c1.CreatedDate = DateTime.Parse(split[9]);
c1.ClosedDate = DateTime.Parse(split[13]);
Don't forget to change the type in the class, Now its fine to use the linq as like the following:
sd.CarryOver = prdt.Where(a => a.CreatedDate >= from.Date && a.ClosedDate <= to.Date);
I have a xml that looks like this
<?xml version="1.0" encoding="utf-8" ?>
<Resorces>
<Resource id="3" name="loreum ipsum" downloadcount="5"></Resource>
<Resource id="2" name="loreum ipsum" downloadcount="9"></Resource>
</Resorces>
I have a class
public class Report
{
public int ResourceId {get; set; }
public string ResourceName { get; set; }
public int DownloadCount { get; set; }
}
I want to convert that xml into list of Report objects
I tried the below code
var resourceList = doc.Descendants("Resorces")
.First()
.Elements("Resource")
.ToList();
I get values like this,
How can I get it as list of objects?
Basically what you are missing is the part where you convert the Xml objects into your defined object of Report. This is how you can do it:
var resourceList = doc.Descendants("Resorces")
.First()
.Elements("Resource")
.Select(element => new Report()
{
ResourceId = (int)element.Attribute("id"),
ResourceName = (string)element.Attribute("name"),
DownloadCount = (int)element.Attribute("downloadcount")
})
.ToList();
I kept here the previous linq methods you called to keep it close to the original but as others said you can just get the Elements("Resource") from the doc root
XmlDocument newdoc = new XmlDocument();
newdoc.InnerXml = " <?xml version="1.0" encoding="utf-8" ?>
<Resorces>
<Resource id="3" name="loreum ipsum" downloadcount="5"></Resource>
<Resource id="2" name="loreum ipsum" downloadcount="9"></Resource>
</Resorces>";
List<string> list = new List <string>();
var selectnode = "Resorces/Resource";
var nodes = newdoc.SelectNodes(selectnode);
foreach (XmlNode nod in nodes)
{
string id = nod["id"].InnerText;
string name = nod["name"].InnerText;
string downloadcount = nod["downloadcount"].InnerText;
list.Add(id);
list.Add(name);
list.Add(downloadcount);
}
Console.WriteLine(list.Count);
You could use Elements method to get elements for a given element name.
XDocument doc = XDocument.Load(filepath);
var result = doc.Root
.Elements("Resource")
.Select(x=> new Report()
{
ResourceId = int.Parse( x.Attribute("id").Value),
ResourceName = (string)x.Attribute("name").Value,
DownloadCount = int.Parse( x.Attribute("downloadcount").Value)
})
.ToList();
Check this Demo
You can get by this
XDocument doc = XDocument.Load(xmlpath);
List<Report> resourceList = doc.Descendants("Resorces")
.First()
.Elements("Resource")
.Select(report => new Report()
{
ResourceId = (int)report.Attribute("id"),
ResourceName = (string)report.Attribute("name"),
DownloadCount = (int)report.Attribute("downloadcount")
}).ToList();
I need to format an XML to a given hierarchy from a list (List<>). The list contains, for the banking information, data spread across multiple rows as shown in the image.
The XML output needs to be formatted like this:
<ROOT>
<DocumentElement>
<Supplier>
<Company>STV</Company>
<Code>000199</Code>
<Name>TrafTrans</Name>
<BankAccounts>
<SupplierBankAccount>
<Bban>220-012510-63</Bban>
<Name>B1</Name>
</SupplierBankAccount>
<SupplierBankAccount>
<Bban>RUIL</Bban>
<Name>RUIL</Name>
</SupplierBankAccount>
</BankAccounts>
<SupplierAddresses>
<SupplierAddress>
<Type>PostalAddress</Type>
<Name>Loc TrafTrans</Name>
<IsDefault>true</IsDefault>
<AddressParts>
<SupplierAddressPart>
<AddressPartKey>STREET_NAME</AddressPartKey>
<AddressPartText>Somewhere</AddressPartText>
</SupplierAddressPart>
<SupplierAddressPart>
<AddressPartKey>COUNTRY</AddressPartKey>
<AddressPartText>SPAIN</AddressPartText>
</SupplierAddressPart>
</AddressParts>
</SupplierAddress>
</SupplierAddresses>
</Supplier>
</DocumentElement>
</ROOT>
I already have a method that converts a list to an XML and returns a string. But the problem is that this only formats one item from the list and there could be additional info in the following items.
public static string SuppliersToXML(List<SupplierItem> supplier)
{
CultureInfo ci = new CultureInfo("en-US");
XmlDocument doc = new XmlDocument();
var root = doc.CreateElement("ROOT");
var rootNode = doc.AppendChild(root);
var docElem = doc.CreateElement("DocumentElement");
var docElemNode = rootNode.AppendChild(docElem);
foreach (var item in supplier)
{
var supplierElem = doc.CreateElement("Supplier");
var companyElem = (XmlNode)doc.CreateElement("Company");
companyElem.InnerText = item.Company.ToString();
//more code...
supplierElem.AppendChild(companyElem);
//more code...
}
return doc.OuterXml;
}
I have found the answer myself, it may not be the prettiest code ever written. But it does the job.
public static string SuppliersToXML(List<SupplierItem> supplier)
{
//A distinct select is needed because bank info can be on multiple lines. So first a list is generated with correct info except for bank information
List<SupplierItem> distinctsupplier = supplier
.GroupBy(p => p.Code)
.Select(g => g.First())
.ToList();
CultureInfo ci = new CultureInfo("en-US");
XmlDocument doc = new XmlDocument();
var root = doc.CreateElement("ROOT");
var rootNode = doc.AppendChild(root);
var docElem = doc.CreateElement("DocumentElement");
var docElemNode = rootNode.AppendChild(docElem);
foreach (var item in distinctsupplier)
{
var supplierElem = doc.CreateElement("Supplier");
var companyElem = (XmlNode)doc.CreateElement("Company");
companyElem.InnerText = item.Company.ToString();
var codeElem = (XmlNode)doc.CreateElement("Code");
codeElem.InnerText = item.Code.ToString();
var codeName = (XmlNode)doc.CreateElement("Name");
codeName.InnerText = item.Name.ToString();
//supplieridentifier part
var supplierIdsElem = doc.CreateElement("SupplierIdentifiers");
var supplierIdElem = doc.CreateElement("SupplierIdentifier");
var supplierIdValueElem = (XmlNode)doc.CreateElement("SupplierIDValue");
supplierIdValueElem.InnerText = item.SupplierIDValue;
//supplieraddress part
var supplierAddressesElem = doc.CreateElement("SupplierAddresses");
var supplierAddressElem = doc.CreateElement("SupplierAddress");
var supplierTypeValueElem = (XmlNode)doc.CreateElement("Type");
supplierTypeValueElem.InnerText = item.Type;
var supplierNameValueElem = (XmlNode)doc.CreateElement("Name");
supplierNameValueElem.InnerText = item.AddressName;
var supplierDefaultValueElem = (XmlNode)doc.CreateElement("IsDefault");
supplierDefaultValueElem.InnerText = item.AddressIsDefault;
//address part
var AddressPartElem = doc.CreateElement("AddressParts");
var supplierAddressPartsElem = doc.CreateElement("SupplierAddressPart");
//Street
var AddressPartElemStreetKeyElem = (XmlNode)doc.CreateElement("AddressPartKey");
AddressPartElemStreetKeyElem.InnerText = item.AddressStreetKey;
var AddressPartElemStreetValueElem = (XmlNode)doc.CreateElement("AddressPartText");
AddressPartElemStreetValueElem.InnerText = item.AddressStreetText;
//Country
var AddressPartElemCountryKeyElem = (XmlNode)doc.CreateElement("AddressPartKey");
AddressPartElemCountryKeyElem.InnerText = item.AddressCountryKey;
var AddressPartElemCountryValueElem = (XmlNode)doc.CreateElement("AddressPartText");
AddressPartElemCountryValueElem.InnerText = item.AddressCountryText;
//add elements to supplierelem
supplierElem.AppendChild(companyElem);
supplierElem.AppendChild(codeElem);
supplierElem.AppendChild(codeName);
//bankaccounts part
var bankAccountElem = doc.CreateElement("BankAccounts");
//select all rows that contain multiple lines, so the bank info can be extracted, for a certain supplier
var bankInformation = supplier.Where(s => s.Code == item.Code).ToList();
foreach (var bankitem in bankInformation)
{
if (item.Code == bankitem.Code)
{
var bankAccountSupplElem = doc.CreateElement("SupplierBankAccount");
var bankAccountBbanElem = doc.CreateElement("Bban");
var bankAccountBbanValueElem = (XmlNode)doc.CreateElement("Bban");
bankAccountBbanValueElem.InnerText = bankitem.Bban;
var bankAccountNameElem = doc.CreateElement("Name");
var bankAccountNameValueElem = (XmlNode)doc.CreateElement("Name");
bankAccountNameValueElem.InnerText = bankitem.BankName;
var bankAccountElemNode = supplierElem.AppendChild(bankAccountElem);
var bankAccountSupplElemNode = bankAccountElemNode.AppendChild(bankAccountSupplElem);
bankAccountSupplElemNode.AppendChild(bankAccountBbanValueElem);
bankAccountSupplElemNode.AppendChild(bankAccountNameValueElem);
}
}
//add address elements to supplierelem
var supplierAddressesElemNode = supplierElem.AppendChild(supplierAddressesElem);
var supplierAddressElemNode = supplierAddressesElemNode.AppendChild(supplierAddressElem);
supplierAddressElemNode.AppendChild(supplierTypeValueElem);
supplierAddressElemNode.AppendChild(supplierNameValueElem);
supplierAddressElemNode.AppendChild(supplierDefaultValueElem);
//add addressparts to supplieraddressesnode
//Street
var AddressPartElemNode = supplierAddressElemNode.AppendChild(AddressPartElem);
var supplierAddressPartsElemNode = AddressPartElemNode.AppendChild(supplierAddressPartsElem);
supplierAddressPartsElemNode.AppendChild(AddressPartElemStreetKeyElem);
supplierAddressPartsElemNode.AppendChild(AddressPartElemStreetValueElem);
//Country (first reinitialize supplieraddresspart)
supplierAddressPartsElem = doc.CreateElement("SupplierAddressPart");
var supplierAddressPartsCountryElemNode = AddressPartElemNode.AppendChild(supplierAddressPartsElem);
supplierAddressPartsCountryElemNode.AppendChild(AddressPartElemCountryKeyElem);
supplierAddressPartsCountryElemNode.AppendChild(AddressPartElemCountryValueElem);
//add all supplierelements to docelemnode
docElemNode.AppendChild(supplierElem);
}
return doc.OuterXml;
}
<?xml version="1.0" standalone="yes"?>
<CompanyInfo>
<Employee name="Jon" deptId="123">
<Region name="West">
<Area code="96" />
</Region>
<Region name="East">
<Area code="88" />
</Region>
</Employee>
</CompanyInfo>
public class Employee
{
public string EmployeeName { get; set; }
public string DeptId { get; set; }
public List<string> RegionList {get; set;}
}
public class Region
{
public string RegionName { get; set; }
public string AreaCode { get; set; }
}
I am trying to read this XML data, so far I have tried this:
XDocument xml = XDocument.Load(#"C:\data.xml");
var xElement = xml.Element("CompanyInfo");
if (xElement != null)
foreach (var child in xElement.Elements())
{
Console.WriteLine(child.Name);
foreach (var item in child.Attributes())
{
Console.WriteLine(item.Name + ": " + item.Value);
}
foreach (var childElement in child.Elements())
{
Console.WriteLine("--->" + childElement.Name);
foreach (var ds in childElement.Attributes())
{
Console.WriteLine(ds.Name + ": " + ds.Value);
}
foreach (var element in childElement.Elements())
{
Console.WriteLine("------->" + element.Name);
foreach (var ds in element.Attributes())
{
Console.WriteLine(ds.Name + ": " + ds.Value);
}
}
}
}
This enables me to get each node, its attribute name and value and so I can save these data into the relevant field in database, but this seems a long winded way and
not flexible, for instance if the XML structure changes all those foreach statements needs revisiting, also it is difficult to filter the data this way,
I need to write certain if statements to filter the data (e.g get employees from West only etc...)
I was looking for a more flexible way, using linq, something like this:
List<Employees> employees =
(from employee in xml.Descendants("CompanyInfo")
select new employee
{
EmployeeName = employee.Element("employee").Value,
EmployeeDeptId = ?? get data,
RegionName = ?? get data,
AreaCode = ?? get data,,
}).ToList<Employee>();
But I am not sure how I can get the values from the child nodes and apply the filtering (to get the certain employees only). Is this possible? Any help is appreciated.
Thanks
var employees = (from e in xml.Root.Elements("Employee")
let r = e.Element("Region")
where (string)r.Attribute("name") == "West"
select new Employee
{
EmployeeName = (string)e.Attribute("employee"),
EmployeeDeptId = (string)e.Attribute("deptId"),
RegionName = (string)r.Attribute("name"),
AreaCode = (string)r.Element("Area").Attribute("code"),
}).ToList();
But it will still require query revision when XML file structure changes.
Edit
Query for multiple regions per employee:
var employees = (from e in xml.Root.Elements("Employee")
select new Employee
{
EmployeeName = (string)e.Attribute("employee"),
DeptId = (string)e.Attribute("deptId"),
RegionList = e.Elements("Region")
.Select(r => new Region {
RegionName = (string)r.Attribute("name"),
AreaCode = (string)r.Element("Area").Attribute("code")
}).ToList()
}).ToList();
You can then filter the list for employees from given region only:
var westEmployees = employees.Where(x => x.RegionList.Any(r => r.RegionName == "West")).ToList();
You can track the structure:
from employee in xml
.Element("CompanyInfo") // must be root
.Elements("Employee") // only directly children of CompanyInfo
or less strictly
from employee in xml.Descendants("Employee") // all employees at any level
And then get the information you want:
select new Employee
{
EmployeeName = employee.Attribute("name").Value,
EmployeeDeptId = employee.Attribute("deptId").Value,
RegionName = employee.Element("Region").Attribute("name").Value,
AreaCode = employee.Element("Region").Element("Area").Attribute("code").Value,
}
And with the additional info about multiple regions, assuming a List<Region> Regions property:
select new Employee
{
EmployeeName = employee.Attribute("name").Value,
EmployeeDeptId = employee.Attribute("deptId").Value,
//RegionName = employee.Element("Region").Attribute("name").Value,
//AreaCode = employee.Element("Region").Element("Area").Attribute("code").Value,
Regions = (from r in employee.Elements("Region") select new Region
{
Name = r.Attribute("name").Value,
Code = r.Element("Area").Attribute("code").Value,
}).ToList();
}
You can do the selection in one query and then the filtering in second or combine them both to one query:
Two queries:
// do te transformation
var employees =
from employee in xml.Descendants("CompanyInfo").Elements("Employee")
select new
{
EmployeeName = employee.Attribute("name").Value,
EmployeeDeptId = employee.Attribute("deptId").Value,
Regions = from region in employee.Elements("Region")
select new
{
Name = region.Attribute("name").Value,
AreaCode = region.Element("Area").Attribute("code").Value,
}
};
// now do the filtering
var filteredEmployees = from employee in employees
from region in employee.Regions
where region.AreaCode == "96"
select employee;
Combined one query (same output):
var employees2 =
from selectedEmployee2 in
from employee in xml.Descendants("CompanyInfo").Elements("Employee")
select new
{
EmployeeName = employee.Attribute("name").Value,
EmployeeDeptId = employee.Attribute("deptId").Value,
Regions = from region in employee.Elements("Region")
select new
{
Name = region.Attribute("name").Value,
AreaCode = region.Element("Area").Attribute("code").Value,
}
}
from region in selectedEmployee2.Regions
where region.AreaCode == "96"
select selectedEmployee2;
But there is one little thing you should consider adding. For robustness, you need to check existence of your elements and attributes then the selection will look like that:
var employees =
from employee in xml.Descendants("CompanyInfo").Elements("Employee")
select new
{
EmployeeName = (employee.Attribute("name") != null) ? employee.Attribute("name").Value : string.Empty,
EmployeeDeptId = (employee.Attribute("deptId") != null) ? employee.Attribute("deptId").Value : string.Empty,
Regions = (employee.Elements("Region") != null)?
from region in employee.Elements("Region")
select new
{
Name = (region.Attribute("name")!= null) ? region.Attribute("name").Value : string.Empty,
AreaCode = (region.Element("Area") != null && region.Element("Area").Attribute("code") != null) ? region.Element("Area").Attribute("code").Value : string.Empty,
}
: null
};
I am trying to parse the following XML files in to a list. Unfortunately it returns only one element
Sample XML
<Titles>
<Book Title ="Love Story" Author= "Erich Segal" Year = "1999"/>
<Book Title ="Code Complete" Author= "Steve McConnel" Year = "2004"/>
<Book Title ="Rework" Author = "Jaso Fried" Year = "2010"/>
<Book Title ="Delivering Happiness" Author= "Tony Hseigh" Year = "2011"/>
</Titles>
C# Code
public class BookInfo
{
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
}
XDocument xmlDoc = XDocument.Load(strXMLPath);
var b = from device in xmlDoc.Descendants("Titles")
select new BookInfo
{
Title = device.Element("Book").Attribute("Title").Value,
Author = device.Element("Book").Attribute("Author").Value,
Year = int.Parse(device.Element("Book").Attribute("Year").Value)
};
books = b.ToList();
I suspect you actually want to be finding descendants called "Book" rather than "Titles":
XDocument xmlDoc = XDocument.Load(strXMLPath);
var b = from book in xmlDoc.Descendants("Book")
select new BookInfo
{
Title = (string) book.Attribute("Title"),
Author = (string) book.Attribute("Author"),
Year = (int) book.Attribute("Year")
};
var books = b.ToList();
Or in non-query expression syntax:
XDocument xmlDoc = XDocument.Load(strXMLPath);
var books = xmlDoc.Descendants("Book")
.Select(book => new BookInfo
{
Title = (string) book.Attribute("Title"),
Author = (string) book.Attribute("Author"),
Year = (int) book.Attribute("Year")
})
.ToList();
EDIT: If you want all elements descending from Titles (e.g. to exclude "Book" elements from elsewhere), you'd want:
XDocument xmlDoc = XDocument.Load(strXMLPath);
var books = xmlDoc.Descendants("Titles")
.Descendants("Book")
.Select(book => /* same as before */)