How to use "let" to deal with missing XML nodes? - c#

I'm updating my question here based on some feedback I got in response to my original post. I'm trying to parse some xml in an XDocument and not getting the results that I expect. Based on feedback below I've added the two let statements below, but they're not working as expected and I assume that I've got them wrong somehow. The problem is that when the CoverArt and Biography are not present in the XML result I get nothing back from my query Here is the code that I am running:
public List<Album> ParseResults(XDocument inputDoc)
{
var albums = new List<Album>();
try
{
albums = (from item in inputDoc.Descendants("ALBUM")
select new Album
{
let CoverArt = item.Elements("URL").First(u => u.Attribute("TYPE").Value == "COVERART") ?? new XElement("COVERART")
let Biography = item.Elements("URL").First(u => u.Attribute("TYPE").Value == "ARTIST_BIOGRAPHY") ?? new XElement("ARTIST_BIOGRAPHY")
AlbumId = (string)item.Element("GN_ID"),
ArtistName = (string)item.Element("ARTIST"),
AlbumName = (string)item.Element("TITLE"),
TrackCount = (int)item.Element("TRACK_COUNT"),
Year = (string)item.Element("DATE"),
Genre = (string)item.Element("GENRE"),
CoverArt = item.Elements("URL").First(u => u.Attribute("TYPE").Value == "COVERART").Value.ToString(),
Biography = item.Elements("URL").First(u => u.Attribute("TYPE").Value == "ARTIST_BIOGRAPHY").Value.ToString(),
Tracks = item.Elements("TRACK")
.Select(t => new Track
{
AlbumId = (string)item.Element("GN_ID"),
TrackNumber = (int)t.Element("TRACK_NUM"),
TrackName = (string)t.Element("TITLE"),
TrackId = (string)t.Element("GN_ID"),
}).ToList()
}).ToList();
}
catch (Exception e)
{
}
return albums;
}
Here is the XML, I'm running it against. In this example there is no Biography in the XML that I'm querying:
<RESPONSE STATUS="OK">
<ALBUM>
<GN_ID>63074689-EDADA0FEDE93683CA03C6D38520A4D88</GN_ID>
<ARTIST>Green Day</ARTIST>
<TITLE>American Idiot</TITLE>
<PKG_LANG>ENG</PKG_LANG>
<DATE>2004</DATE>
<GENRE NUM="105222" ID="35474">Punk</GENRE>
<TRACK_COUNT>13</TRACK_COUNT>
<TRACK>
<TRACK_NUM>1</TRACK_NUM>
<GN_ID>63074690-456E41C113DC8354DC6B25421F2C7989</GN_ID>
<TITLE>American Idiot</TITLE>
</TRACK>
<TRACK>
<TRACK_NUM>2</TRACK_NUM>
<GN_ID>63074691-70EFB1E8EB31B5296D5822E55343EFA9</GN_ID>
<TITLE>Jesus Of Suburbia / City Of The Damned / I Don't Care / Dearly Beloved / Tales Of Another Broken Home</TITLE>
</TRACK>
<URL TYPE="COVERART" SIZE="THUMBNAIL" WIDTH="75" HEIGHT="75">http://akamai-b.cdn.cddbp.net/cds/2.0/cover/0A1A/BABF/DEBC/CF21_thumbnail_front.jpg</URL>
</ALBUM>
</RESPONSE>
</RESPONSES>
Can anyone help me out with this?

The problem is your XML has no URL elements, so the query is failing and throws an InvalidOperationException. Update the 2 URL lines in your query to the following:
CoverArt = (string)item.Elements("URL").FirstOrDefault(u => u.Attribute("TYPE").Value == "COVERART"),
Biography = (string)item.Elements("URL").FirstOrDefault(u => u.Attribute("TYPE").Value == "ARTIST_BIOGRAPHY"),
The query uses FirstOrDefault and will return null if the result isn't found, then casts it to a string. Alternately, you could use a let clause earlier in the query and assign it to item.Elements("URL") then do a ternary check before using it in the query, or return null. The result is the same, but the style chosen depends on what you want to do if it's null and gives you some more flexibility.

Related

Linq query causes null reference exception when accessing linked tables

So I have the following linq query for a test blog:
var random = new Random();
var random = context.Blog.OrderBy(x => random.Next())
.Where(x => x.DisplayStatus == "Approved")
.Select(x => new Blog
{
Title = x.Title,
Content = x.Content,
AuthorId = x.AuthorId,
Author = x.Authors.AuthorName //null pointer exception here
}).Take(5).ToList()
The trouble is, it throws a null pointer exception when it hits 'x.Authors.AuthorName'. I cannot figure out why. I know the author is there because it works fine when I use linq expressions (from x in context.Blog ...etc), and it works in LinqPad. I can't use the linq expression, though, because I don't know how to declare 'OrderBy(x => random.Next())' without using a lambda expression.
this is the version that works without the random
var working = (from x in context.Blog
//NO known code to select random
where x.DisplayStatus == "Approved"
select new Blog
{
Title = x.Title,
Content = x.Content,
AuthorId = x.AuthorId,
Author = x.Authors.AuthorName //NO null pointer exception
}).Take(5).ToList()
Blog is a POCO class with no database relations.
Authors is a database entity class with AuthorName being a simple string.
Any idea on what is going on here?
I am not allowed to add a comment, so I ask here. What type is Authors? From the name I guess it could be some type of Collection.
What I often do if one Property might be null is to add a ?:
Author = x.Authors?.AuthorName
You can get random sort using Guid.NewGuid():
var random = context.Blog.OrderBy(x => Guid.NewGuid())
.Where(x => x.DisplayStatus == "Approved")
.Select(x => new Blog
{
Title = x.Title,
Content = x.Content,
AuthorId = x.AuthorId,
Author = x.Authors.AuthorName
}).Take(5).ToList()
Or SqlFunctions.Rand:
var random = context.Blog.OrderBy(x => SqlFunctions.Rand())
.Where(x => x.DisplayStatus == "Approved")
.Select(x => new Blog
{
Title = x.Title,
Content = x.Content,
AuthorId = x.AuthorId,
Author = x.Authors.AuthorName
}).Take(5).ToList()
Change your query to this:
var working = (from x in context.Blog
where x.DisplayStaus == "Approved"
select new Blog
{
Title = x.Title,
Content = x.Content,
AuthorId = x.AuthorId,
Author = x.Authors.AuthorName
})
.AsEnumerable().OrderBy(x => random.Next())
.Take(5).ToList();
Edit:
I wasn't 100% sure what the error is and wanted to wait for the OP to check if my answer actually does what I believe it does.
I assumed that Entity Framework will not translate your query to SQL and therefor lose the ability to automatically load x.Authors, by moving the OrderBy the first "half" of the query, can be translated to SQL while the AsEnumerable().OrderBy() will run in memory.
So AsEnumerable() will force the query to be translated and exceuted in SQL, and the following OrderBy will run in memory.
Edit2:
About how to do it on the SQL Server and not loading the whole query to memory, I am afraid I can't help you with that, a google search brought this
Linq to Entities, random order

Way to populate class object in single LINQ XML query?

Given the following XML snippet, is there a way to both query and populate a class object in one LINQ statement? It's confusing because of the need to select using attribute values.
<data>
<array>
<item key="0">
<map>
<item key="mrid">53030</item>
<item key="mrtitle">GeneralFeedback</item>
</map>
</item>
</array>
</data>
Class:
public class Incident
{
public int ID { get; set; }
public string Title { get; set; }
}
Current (working) code (where result is the XML snippet as a string):
var data = XDocument.Parse(result);
var id = from item in data.Descendants("item")
where item.Attribute("key").Value == "mrid"
select item.Value;
var title = from item in data.Descendants("item")
where item.Attribute("key").Value == "mrtitle"
select item.Value;
var incident = new Incident
{
ID = Convert.ToInt32(id.FirstOrDefault()),
Title = title.FirstOrDefault()
};
Based on the answers given I learned some useful things and came up with this variation:
var incidents = data.Descendants("map")
.Select(i => i.Descendants("item")
.ToDictionary(m => m.Attribute("key").Value, m => m.Value))
.Where(i => i.ContainsKey("mrid")
&& i.ContainsKey("mrtitle"))
.Select(i => new Incident
{
ID = int.Parse(i["mrid"]),
Title = i["mrtitle"]
});
One thing I really like is that this creates an IEnumerable that allows for multiple incidents being present in the XML data.
is there a way to both query and populate a class object in one LINQ statement?
Yes, well sorta ... and it remains quite ugly. The below "single" multi-step LINQ statement ensures only the items that belong to the same map element get selected. Like your code sample, it will blow up in your face if the items with the required key values are missing (or the "mrid" element is not an int).
var key_vals = new List<string> { "mrid", "mrtitle" };
var xdoc = XDocument.Load(#"c:\temp\test.xml");
var incidents = xdoc.Descendants("map").Select(map => {
var items = map.Descendants("item").Where(i => key_vals.Contains(i.Attribute("key").Value));
var idItem = items.Where(x => x.Attribute("key").Value == "mrid").First();
var titleItem = items.Where(x => x.Attribute("key").Value == "mrtitle").First();
return new Incident {
ID = int.Parse(idItem.Value),
Title = titleItem.Value
};
});
foreach (var i in incidents)
Console.WriteLine("ID = {0}, Title = {1}", i.ID, i.Title);
It will produce the output below for your given xml input file:
ID = 53030, Title = GeneralFeedback
Check out this post to learn how to convert your XML schema to a C# class
Generate C# class from XML
Then you can use your new type and de-serialize your XML to a class
XmlSerializer serializer = new XmlSerializer(typeof(Incident));
using (StringReader reader = new StringReader(xmlDocumentText))
{
Incident incident= (Incident)(serializer.Deserialize(reader));
}
Alex has already given a perfect answer, but I find this a little more readable (:
The Where clause ensures each item found, has the keys required to construct an Incident.
var incidents = xdoc.Root
.Element("array")
.Elements("item")
.Select(i => i.Element("map")
.Elements("item")
.ToDictionary(m => m.Attribute("key").Value,
m => m.Value))
.Where(i => i.ContainsKey("mrid")
&& i.ContainsKey("mrtitle"))
.Select(i => new Incident
{
ID = int.Parse(i["mrid"]),
Title = i["mrtitle"]
});

Use where condition for retrieving the xml data with C#

I want to fetch the data from an xml file. I am fetching the id of node from the previous page. And on next page I want to display the data from xml of that id. I am passing id of node using query string but when I run my code its give me this error
System.Xml.XmlException: Data at the root level is invalid. Line 1, position 1.
Here is my code
XElement xid = XElement.Parse(Request.QueryString["ID"]);
// var id = Request.QueryString["ID"];
var doc = XDocument.Load(Server.MapPath("~/Data/BlogContent.xml"));
var result = doc.Descendants("post")
.Where(x => x.Element("id") == xid)
.Select(x => new
{
id = x.Element("id").Value,
title = x.Element("title").Value,
Discription = x.Element("Discription").Value,
dt = x.Element("dt").Value,
mnt = x.Element("mnt").Value,
yr = x.Element("yr").Value
}).OrderByDescending(x => x.id).Take(5);
Repeater1.DataSource = result;
Repeater1.DataBind();
Here is my xml
<?xml version="1.0" encoding="utf-8"?>
<content>
<post>
<id>1</id>
<title>fds</title>
<Discription>fdsafsdf</Discription>
<dt>21</dt>
<mnt>6</mnt>
<yr>2013</yr>
</post>
</content>
Please tell me where I am going wrong
You don't indicate the line causing the error, but I bet it's this one:
XElement xid = XElement.Parse(Request.QueryString["ID"]);
Most likely "ID" in your query string is an identifier of some sort, not XML - hence the error.
Something like this is what you want:
string xid = Request.QueryString["ID"];
Then you can use it in your where clause.
Also, there's an error in your where clause - you're trying to compare an XElement to a value - you need to get the value of the XElement using it's Value property:
Where(x => x.Element("id").Value == xid)
XElement.Value returns a string - so simply take the string value from the query string and use it in the comparison in your where clause.
Everything Put Together
string xid = Request.QueryString["ID"];
var doc = XDocument.Load(Server.MapPath("~/Data/BlogContent.xml"));
var result = doc.Descendants("post")
.Where(x => x.Element("id").Value == xid)
.Select(x => new
{
id = x.Element("id").Value,
title = x.Element("title").Value,
Discription = x.Element("Discription").Value,
dt = x.Element("dt").Value,
mnt = x.Element("mnt").Value,
yr = x.Element("yr").Value
}).OrderByDescending(x => x.id).Take(5);
Repeater1.DataSource = result;
Repeater1.DataBind();

Trying to read XML attributes via LINQ

xDoc variable loads the XML content but I am not able to retrieve any information. It comes back NULL:
var xDoc = XDocument.Load(Config.CredentialFileName);
//method 1
IEnumerable<XElement> rows = from row in xDoc.Descendants("domain")
where (string)row.Attribute("name") == "TEST"
select row;
//method 2
var list = xDoc.Descendants("domain")
.Select(d => new
{
name = d.Attribute("name").Value,
username = d.Attribute("username").Value,
password = d.Attribute("password").Value //,
})
.Where(a => a.name == "TEST")
.ToList();
XML file:
<domains>
<domain name="TEST" userName="test" password="tSEvmlsmwEkjSxUwrCVf3G6"/>
</domains>
Thank you
Your first method works just fine with xml you provided. Make sure you are parsing xml with exactly same structure. Also check that you have at least one domain element with name equal to TEST. And make sure you don't have namespaces defined in your xml.
Second method has typo in userName attribute name (you have lower case username):
var list = xDoc.Descendants("domain")
.Select(d => new {
name = d.Attribute("name").Value,
username = d.Attribute("userName").Value, // <-- typo here
password = d.Attribute("password").Value
})
.Where(a => a.name == "TEST")
.ToList();
Also, I'd recommend to use casting instead of reading node Value property, because getting this property will throw an exception if node not exist.
var domains = from d in xDoc.Descendants("domain")
let name = (string)d.Attribute("name")
where name == "TEST"
select new {
Name = name,
Username = (string)d.Attribute("userName"),
Password = (string)d.Attribute("password")
};
See below.
var xDoc= XElement.Load(Config.CredentialFileName);
var result = xDoc.Elements("domain").Where(x => x.Attribute("name").Value.Equals("TEST")).ToList();

using linq to order tickets by lowest date where ticket is not closed?

I have an object called Ticket with that contains a list of objects called TicketActions. The Ticket object has a field called Date_Closed and the Actions object has a field called Action_Date:
Ticket
Date_Closed
TicketActions
-Action_Date
What I'm trying to do is order a List of tickets (List) based on the latest date of each Action in ascending order where the Ticket does not have a value for Date_Closed. The goal is to load this list into a listview and show tickets in a way that displays tickets in order on the page, placing the ones that have gone the longest without an action at the top. Does that make sense?
Here is what I ended up with so far that isn't working:
protected List<FullTicket> BuildTickets(int ticketsToShow)
{
using (var db = new SupportLogDBDataContext())
{
var result =
(from ticket in db.Support_Tickets
join status in db.Ticket_Statuses on ticket.Status_ID equals status.ID
select new FullTicket
{
TicketID = ticket.ID,
DateOpened = (DateTime)ticket.Date_Opened,
DateClosed = (DateTime)ticket.Date_Closed,
Subject = ticket.Subject,
Status = new KeyPair { Key = status.Status, Value = status.ID },
CreatedBy = new GuidPair { Key = ticket.Reported_By, Value = (Guid)ticket.AD_GUID },
TicketActions =
(from a in db.Ticket_Actions
where a.Ticket_ID == ticket.ID
select a).ToList()
}).Take(ticketsToShow).ToList();
result.OrderBy(i => i.TicketActions.Where(i.DateClosed == null).Max()); //error on this line (invalid arguments)
return result;
}
}
People reply quick here!
Try this:
var result = (from ticket in tickets
where !ticket.DateClosed.HasValue
select ticket).OrderByDescending(t => (from a in t.TicketActions
select a.ActionDate).Max());
From here you can take as many as you need.
David B's analysis is slightly off. The line...
result.OrderBy(i => i.TicketActions.Where(i.DateClosed == null).Max());
... will not compile because the argument to the Where method is not a lambda expression or delegate.
I would suggest this solution (assuming that the relevant property of the TicketAction type is ActionDate):
return result.Where(i => i.DateClosed == null)
.OrderBy(i => i.TicketActions.Max(a => a.ActionDate));
Or, in query comprehension syntax:
return from i in result
where i.DateClosed == null
orderby i.TicketActions.Max(a => a.ActionDate)
select i;
Here is some simple code.
var sorted = tickets.Where(t => t.DateClosed == null)
.OrderBy(t => t.TicketActions.Max(ta => ta.Action_Date.Ticks));
Sorry, I prefer LINQ function syntax, but if you want it in query syntax, it shouldn't be too hard to convert.
result.OrderBy(i => i.TicketActions.Where(i.DateClosed == null).Max());
This line generates an error because TicketActions.Max() is not defined.
You need to project TicketAction into something that can be Max'd. For example:
result.OrderBy(i =>
i.TicketActions
.Where(ta => i.DateClosed == null)
.Select(ta => ta.Id)
.Max()
);
Also note:
OrderBy does not modify its source. OrderBy returns an ordered IEnumerable, which you didn't assign anywhere.
OrderBy's enumerable is deferred, and you want a List result instead, so you should call ToList.
You are accessing Ticket.TicketActions outside of the query. This will cause one database round trip per ticket to load that property.
Here is a modification to your query that avoids the problems mentioned above by ordering and using navigational properties within the query.
from ticket in db.Support_Tickets
where ticket.DateClosed == null
let lastDate = ticket.TicketActions
.Select(ta => ta.ActionDate)
.OrderByDescending(date => date)
.FirstOrDefault()
let ticketStatus = ticket.TicketStatus
order by lastDate
select new FullTicket
{
...
}

Categories

Resources