parsing xml in c# using LINQ - c#

I have the following xml structure
<userlist>
<user Name="something">
<function name="funcname">
<picture name="pictname">
<curve name="curvename">
<name>NAME</name>
...
</curve>
</picture>
</function>
<function name="function2">
...
</function>
</user>
It goes on a bit more. I have written a function to extract of the "function" tags and place them in objects using code that simplifies to this:
from function in xmlDoc.Descendants("function")
select new FUNCTIONOBJECT {
do all the rest...
}.toList<FUNCTIONOBJECT>();
I am now trying to make it so that I only filter the functions for a given user. so the name attribute of the user is given. Can anyone tell me how I can make this work with LINQ?
My attempt was:
from user in xmlDoc.Descendants("user")
where user.Attribute("Name").Value == givenusername
select {
var functions =
from function in user.Descendants("function")
select new FUNCTIONOBJECT {
... more stuff
}.toList<FUNCTIONOBJECT>();
But this is wrong and doesnt work.
All help is good. I am pretty new to c# and still trying to wrap my head around xml parsing with LINQ.
EDIT:
updated version of what I have and still doesnt work:
XDocument xmlDoc = XDocument.Load(path);
var functionlist =
(from user in xmlDoc.Descendants("user")
where user.Attribute("Name").Value == username
select(
(from function in user.Descendants("function")
select new Function
{
name = function.Attribute("name").Value,
pictures =
(from picture in function.Descendants("picture")
select new Picture
{
name = picture.Attribute("name").Value,
layout = picture.Element("layout").Value,
curves =
(from curve in picture.Descendants("curve")
select new Curve
{
name = curve.Attribute("name").Value,
section = curve.Element("section").Value,
run = curve.Element("run").Value,
folder = curve.Element("folder").Value,
drivingpoint = curve.Element("drivingpoint").Value,
display = int.Parse(curve.Element("display").Value),
points =
(from point in curve.Descendants("point")
select new Point
{
id = point.Element("id").Value != null ? point.Element("id").Value : string.Empty,
direction = point.Element("direction").Value != null ? point.Element("direction").Value : string.Empty,
}).ToList<Point>(),
}).ToList<Curve>(),
}).ToList<Picture>(),
}).ToList<Function>(),
).toList();
}

Just few syntax mistakes. Otherwise, the content was correct. Its a bit tricky to learn C# and LINQ syntax at the same time (a language in a language). Here is the corrected code:
from user in xmlDoc.Descendants("user")
where user.Attribute("Name").Value == givenusername
select ((from function in user.Descendants("function") // When you do a "select something" "something" must have a value, so you can't begin with "{ var functions = ..."
select new FUNCTIONOBJECT
{
// more stuff
}).ToList(); // You don't have to specify <FUNCTIONOBJECT> because the compiler deduce it from the context (here there a new FUNCTIONOBJECT
But here, you will have a List<List<FUNCTIONOBJECT>>. Why? Because there is no information in the code that specify that only 1 user has the givenusername.
If it is the case, just split the code:
// Gets the user
var user = (from user in xmlDoc.Descendants("user")
where user.Attribute("Name").Value == givenusername
select user).Single(); // Get the only user that satisfy the condition (Throw an exception if no user has the given name or if multiple users have the given name)
// Gets its functions
List<FUNCTIONOBJECT> functions = (from function in user.Descendants("function")
select new FUNCTIONOBJECT
{
// more stuff
}).ToList();

Related

How to write XPath expression to select node name from its value

I'm trying to write an XPath expression to select the name of a node from its value in "qualities" and then select in "qualityNames" the value inside node whose name has previously captured.
E.g. In "qualities" - got value "4", take name "rarity3" then in "qualityNames" I got node named "rarity3" and take value "amazingrarity"
<result>
<status>1</status>
<qualities>
<Normal>0</Normal>
<rarity1>1</rarity1>
<rarity2>2</rarity2>
<vintage>3</vintage>
<rarity3>4</rarity3>
<rarity4>5</rarity4>
</qualities>
<qualityNames>
<Normal>Normal</Normal>
<rarity1>Genuine</rarity1>
<rarity2>rarity2</rarity2>
<vintage>Vintage</vintage>
<rarity3>amazingrarity</rarity3>
<rarity4>Unusual</rarity4>
</qualityNames>
</result>
I'm doing this in C# (It's a MVC App) and I'd prefer to use XPath because I'm indexing the XML and I haven't found a fastest way to query in-memory technique (this XML file has ~3MB and I'm using IndexingXPathNavigator).
Use the local-name() and text() functions + predicates. For value "4" it will be
//qualityNames/*[local-name()=local-name(//qualities/*[text() = '4'])]
Tested with http://www.xpathtester.com
Sounds like you want to create a dictionary of key/value pairs (assuming the node names are only needed to find matches and aren't important to your code).
If so, you can use the following:
var doc = XElement.Parse(#"<result>
<status>1</status>
<qualities>
<Normal>0</Normal>
<rarity1>1</rarity1>
<rarity2>2</rarity2>
<vintage>3</vintage>
<rarity3>4</rarity3>
<rarity4>5</rarity4>
</qualities>
<qualityNames>
<Normal>Normal</Normal>
<rarity1>Genuine</rarity1>
<rarity2>rarity2</rarity2>
<vintage>Vintage</vintage>
<rarity3>amazingrarity</rarity3>
<rarity4>Unusual</rarity4>
</qualityNames>
</result>");
var query = from quality in doc.XPathSelectElements("qualities/*")
join qualityName in doc.XPathSelectElements("qualityNames/*")
on quality.Name equals qualityName.Name
select new { Key = quality.Value, Value = qualityName.Value };
var qualities = query.ToDictionary(a => a.Key, a => a.Value);
var quality3 = qualities["3"];
// quality3 == "Vintage"
var quality4 = qualities["4"];
// quality4 == "amazingrarity"
EDIT: example of how to cache this dictionary
// add reference to System.Web dll
public Dictionary<string, string> GetQualities()
{
// assuming this code is in a controller
var qualities = this.HttpContext.Cache["qualities"] as Dictionary<string, string>;
if (qualities == null)
{
// LoadQualitiesFromXml() is the code above
qualities = LoadQualitiesFromXml();
this.HttpContext.Cache["qualities"] = qualities;
}
return qualities;
}
I think this is what you asked
var rarity3ValueInQualities = xml.SelectSingleNode("/result/qualities/rarity3").InnerText;
var rarity3ValueInqualityNames = xml.SelectSingleNode("/result/qualityNames/rarity3").InnerText;

LINQ to XML "System.Exception: Value cannot be null"

I am working with LINQ to XML in C#. I have the following code, the last line keeps throwing a System.Exception: Value cannot be null. I can't figure out what the problem is. I've tried everything.
AuthorizedToSign is a List. I was able to perform the same action using a bulky nested foreach loop.
I know for sure there are no errors as far as the XML file itself.
If anyone can help I would greatly appreciate it.
BusinessAccounts = (from a in accountRoot.Elements()
where (bool)a.Element("Business") == true
select new BusinessAccount()
{
OpenDate = (DateTime)a.Element("DateOpened"),
Password = a.Element("Password").Value,
Balance = (double)a.Element("Balance"),
AccountStatus = (Status)Enum.Parse(typeof(Status), a.Element("Status").Value),
//Element AuthToSign has a collection of sub-elements called "authName"
//couldn't get the code below to work
AuthorizedToSign = (from el in a.Element("AuthorizedToSign").Elements()
select el.Element("AuthName").Value).ToList()
}).ToList();
changing select el.Element("AuthName").Value)
to select (string)el.Element("AuthName"))
doesn't help.
The XML file has a lot of entries that look like this:
<?xml version="1.0" encoding="utf-8"?>
<Accounts>
<BusinessAccount>
<Business>true</Business>
<AccountNumber>34534456</AccountNumber>
<AccountBranchID>100</AccountBranchID>
<AccountName>Elgris Tech</AccountName>
<CompanyName>Elgris Tech</CompanyName>
<CompanyID>235</CompanyID>
<CreditLimit>50000</CreditLimit>
<DateOpened>2014-12-13T00:00:00</DateOpened>
<Balance>1200</Balance>
<Password>1234</Password>
<Status>Active</Status>
<AuthorizedToSign>
<AuthName>Yechiel</AuthName>
<AuthName>Lev</AuthName>
<AuthName>Roman</AuthName>
</AuthorizedToSign>
</BusinessAccount>
<PrivateAccount>
<Business>false</Business>
<AccountNumber>34534458</AccountNumber>
<AccountBranchID>100</AccountBranchID>
<AccountName>Yechiel L.</AccountName>
<CustomerName>Yechiel L.</CustomerName>
<CustomerAddress>2sadfasosa, CA</CustomerAddress>
<CustomerPhone>8-4268</CustomerPhone>
<CardNumber>304456</CardNumber>
<CreditLimit>10000</CreditLimit>
<DateOpened>1994-06-23T00:00:00</DateOpened>
<Balance>555000</Balance>
<Password>pass</Password>
<Status>Active</Status>
</PrivateAccount>
</Accounts>
UPDATE
According to your xml, PrivateAccount element does not have AuthorizedToSign node, thus referencing to AuthorizedToSign node throws an exception. So, in your case solution will be simple:
from a in accountRoot.Elements()
let authorized = a.Element("AuthorizedToSign")
where (bool)a.Element("Business")
select new BusinessAccount()
{
OpenDate = (DateTime)a.Element("DateOpened"),
Password = (string)a.Element("Password"),
Balance = (double)a.Element("Balance"),
AccountStatus = (Status)Enum.Parse(typeof(Status), (string)a.Element("Status")),
AuthorizedToSign = authorized == null ? null : // or new List<string>()
authorized.Elements()
.Select(auth => (string)auth)
.ToList()
};
Getting auth names with query syntax:
AuthorizedToSign = authorized == null ? null : // or new List<string>()
(from auth in authorized.Elements()
select (string)auth).ToList()

How to update LINQ-to-SQL Single XML Record?

So basically I have this field in XML that I want to update. It is parsed out through XML and I am not sure how to update this single record from this single instance.
var dataContext =
new RequestFormsDBDataContext(ConfigManager.Data.ConnectionString);
var userForm = (
from i in dataContext.RequestFormItems
join c in dataContext.RequestFormInstances on i.TypeGuid equals c.TypeGuid
where i.Id == FormID
select new {
i.Id,
XmlData = XElement.Parse(i.XML)
}
).ToList();
// Parsing out XML Data
var userFormParced = (
from i in userForm
select new FormItem {
FormId = i.Id,
DateTimeCompleted = i.XmlData.Element("DateTimeCompleted").Value
}
).FirstOrDefault();
RFDateTimeCompleted = userFormParced.DateTimeCompleted;
// Code that isnt working
userFormParced.DateTimeCompleted = "New Data";
dataContext.SubmitChanges();
This won't work because you aren't changing the instance that you retrieve from the database. You are creating a new object using the values from the original - twice - and then changing it. The LINQ-to-SQL context has no knowledge that you're changing database rows, just some unrelated XML that you've constructed.
Firstly, the value you retrieve from the database is being put into an XElement
XmlData = XElement.Parse(i.XML)
and then you retrieve the 'value' from a node and put it into another new object
select new FormItem
{
FormId = i.Id,
DateTimeCompleted = i.XmlData.Element("DateTimeCompleted").Value
}
It's lost any reference to the LINQ-to-SQL context by this stage. Instead you need to change it in place.
If you're using XML in your database, you should be using XML columns which map to XML properties in your LINQ-to-SQL objects. You're not, so we'll implement a workaround.
Try something like this.
// just get the item
var userForm = (
from i in dataContext.RequestFormItems
join c in dataContext.RequestFormInstances on i.TypeGuid equals c.TypeGuid
where i.Id == FormID
select i).FirstOrDefault();
// parse the XML
var xml = XElement.Parse(userForm.XML);
RFDateTimeCompleted = xml.Element("DateTimeCompleted").Value;
xml.Element("DateTimeCompleted").Value = "New Data";
// and finally, because you're again just changing XML
// unrelated to the context, update the original object
userForm.XML = xml.ToString();
context.SubmitChanges();

How to retrieve the name of the attribute dynamically without specifying the name of the attribute ?

I am developing asp.net mobile application. I am using LINQ to XML to query XML file. I am using the following query to retrieve the name & value of the query dynamically as follows
var TotalManifolds = from MF in FieldRoot.Element("FIELD-DEFINITION").Element("MANIFOLDS").Elements("MANIFOLD")
join SLT in FieldRoot.Element("FIELD-DEFINITION").Element("SLOTS").Elements("SLOT")
on (string)MF.Attribute("MID") equals (string)SLT.Attribute("PARENT")
select new
{
SlotName = (string)SLT.Attribute("NAME").Value,
SlotValue = (string)SLT.Attribute("NAME").Value
};
In the following statement of above query I want to retrieve the name of the attribute dynamically without explicitly specifying the name of the attribute
SlotName = (string)SLT.Attribute("NAME").Value
Here I am explicitly specifying the name. I want to code which can dynamically retrieve the name of the attribute. I am new to Linq to xml. Can you please tell how this can be done programatically ? or can you provide me the link through which I can resolve the above issue ?
It seems you are looking for something like:
// ...
select new
{
SlotName = SLT.Attributes().First().Name,
SlotValue = SLT.Attributes().First().Value
};
If I understand you correctly, you could always pass a variable in to the LINQ query:
var string attrName = "NAME"; // specify whatever value you need ...
// wrap the query below in a function, if it will be reused...
var TotalManifolds = from MF in FieldRoot.Element("FIELD-DEFINITION").Element("MANIFOLDS").Elements("MANIFOLD")
join SLT in FieldRoot.Element("FIELD-DEFINITION").Element("SLOTS").Elements("SLOT")
on (string)MF.Attribute("MID") equals (string)SLT.Attribute("PARENT")
select new
{
SlotName = (string)SLT.Attribute(attrName).Value,
SlotValue = (string)SLT.Attribute(attrName).Value
};

My code is very inefficient for this simple Linq usage

I have the following method that is supposed to parse information from an XML response and return a collection of users.
I've opted into creating a Friend class and returning a List<Friend> to the calling method.
Here's what I have so far, but I noticed that the ids.ToList().Count method parses every single id element to a List, then does it again in the for conditional. It's just super ineffective.
public List<Friend> FindFriends()
{
List<Friend> friendList = new List<Friend>();
var friends = doc.Element("ipb").Element("profile").Element("friends").Elements("user");
var ids = from fr in friends
select fr.Element("id").Value;
var names = from fr in friends
select fr.Element("name").Value;
var urls = from fr in friends
select fr.Element("url").Value;
var photos = from fr in friends
select fr.Element("photo").Value;
if (ids.ToList().Count > 0)
{
for (int i = 0; i < ids.ToList().Count; i++)
{
Friend buddy = new Friend();
buddy.ID = ids.ToList()[i];
buddy.Name = names.ToList()[i];
buddy.URL = urls.ToList()[i];
buddy.Photo = photos.ToList()[i];
friendList.Add(buddy);
}
}
return friendList;
}
First question - do you have to return a List<Friend>? Can you return an IEnumerable<Friend> instead? If so, performance gets a lot better:
IEnumerable<Friend> FindFriends()
{
return doc.Descendants("user").Select(user => new Friend {
ID = user.Element("id").Value,
Name = user.Element("name").Value,
Url = user.Element("url").Value,
Photo = user.Element("photo").Value
});
}
Rather than actually creating new buckets and stuffing values into them, this creates a projection, or a new object that simply contains all of the logic for how to create the new Friend objects without actually creating them. They get created when the caller eventually starts to foreach over the IEnumerable. This is called "deferred execution".
This also makes one assumption - All the <user> nodes in your XML fragment are friends. If that isn't true, the first part of the XML selection might need to be a little more complex.
And as #anon points out, even if you do need to return a List<Friend> for some reason not obvious from the information you've provided, you can just call .ToList() at the end of the return statement. This will just execute the projection I described above straight into a new bucket, so you only ever create one.
Why do you need the separate ids/names/urls/photos variables? Combine it all. You can eliminate the ToList() call if you don't need a List.
List<Friend> friendList = (from f in doc.Element("ipb").Element("profile").Element("friends").Elements("user")
select new Friend() {
ID = f.Element("id").Value,
Name = f.Element("name").Value,
URL = f.Element("url").Value,
Photo = f.Element("photo").Value
}).ToList();
return friendList;

Categories