Retrieving XML attributes of unknown quantity - c#

I have requirement to read an XML file. I've never done anything with XML so it's all new territory for me. Please refer to the below XML sample.
-
<GPO xmlns="http://www.microsoft.com/GroupPolicy/Settings" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">+
<Identifier>
<Name>GB Local Server Access</Name>
<IncludeComments>true</IncludeComments>
<CreatedTime>2011-08-03T11:58:18</CreatedTime>
<ModifiedTime>2011-08-03T12:13:41</ModifiedTime>
<ReadTime>2014-10-21T11:32:49.5863908Z</ReadTime>+
<SecurityDescriptor>----------------------------
<FilterDataAvailable>true</FilterDataAvailable>-
<Computer>
<VersionDirectory>18</VersionDirectory>
<VersionSysvol>18</VersionSysvol>
<Enabled>true</Enabled>-
<ExtensionData>-
<Extension xsi:type="q1:SecuritySettings" xmlns:q1="http://www.microsoft.com/GroupPolicy/Settings/Security">-
<q1:RestrictedGroups>-
<q1:GroupName>
<SID xmlns="http://www.microsoft.com/GroupPolicy/Types">S-1-5-21-1547161642-1214440339-682003330-1141792</SID>
<Name xmlns="http://www.microsoft.com/GroupPolicy/Types">CRB\DKCPHGITSCOM</Name>
</q1:GroupName>-
<q1:Memberof>
<SID xmlns="http://www.microsoft.com/GroupPolicy/Types">S-1-5-32-544</SID>
<Name xmlns="http://www.microsoft.com/GroupPolicy/Types">BUILTIN\Administrators</Name>
</q1:Memberof>
</q1:RestrictedGroups>-
<q1:RestrictedGroups>-
<q1:GroupName>
<SID xmlns="http://www.microsoft.com/GroupPolicy/Types">S-1-5-21-1547161642-1214440339-682003330-1151</SID>
<Name xmlns="http://www.microsoft.com/GroupPolicy/Types">CRB\GB Administrators</Name>
</q1:GroupName>-
<q1:Memberof>
<SID xmlns="http://www.microsoft.com/GroupPolicy/Types">S-1-5-32-544</SID>
<Name xmlns="http://www.microsoft.com/GroupPolicy/Types">BUILTIN\Administrators</Name>
</q1:Memberof>
</q1:RestrictedGroups>
Please could you advise on the simplest method that I can use that will allow me to drill down to GPO.Computer.ExtensionData.Extension.RestrictedGroups and then FOR EACH instance of RestrictedGroups return the value of GroupName.Name and MemberOf.Name. I can then incorporate the logic to get this data into an array of some sort ready to be output.

You should use LINQ to XML.
Something like:
//preparing the reusable XName instances:
var q1Namespace = "http://www.microsoft.com/GroupPolicy/Settings/Security";
var groupNameElementName = XName.Get("GroupName", q1Namespace);
var memberOfElementName = XName.Get("Memberof", q1Namespace);
var nameElementName = XName.Get("Name", "http://www.microsoft.com/GroupPolicy/Types");
var data = XDocument.Load(filePath)
.Descendants(XName.Get("RestrictedGroups", q1Namespace))
.Select(group =>
new
{
GroupName =
group.Descendants(groupNameElementName)
.Select(gn => gn.Element(nameElementName).Value)
.FirstOrDefault(),
MemberOfName =
group.Descendants(memberOfElementName)
.Select(gn => gn.Element(nameElementName).Value)
.FirstOrDefault()
});
Then use it in foreach loop:
foreach (var d in data)
{
Console.WriteLine("Group name: {0}, member of name: {1}", d.GroupName, d.MemberOfName);
}
If you'd like to make it safe and provide default values for the names, use something like the below instead of the Select call which finds the Name element:
...
.SelectMany(gn => gn.Elements(nameElementName))
.Select(elem => elem.Value ?? "Name not found")
.DefaultIfEmpty("Name not found")
.FirstOrDefault()
This way you will protect yourself from the cases where the Name element does not exist or has no value.

Related

Extract values from XML with linq query

New to linq and XML and looking for a way to return values of
<AddtlInf></AddtlInf> as a list which I will then concatenate the values to a single string.
XML below:
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<Document xmlns='urn:iso:std:iso:20022:tech:xsd:pain.002.001.03' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
<CstmrPmtStsRpt>
<GrpHdr>
<MsgId>21233519</MsgId>
<CreDtTm>2018-11-29T09:28:00</CreDtTm>
<InitgPty>
<Nm>Standard Bank SA</Nm>
<Id>
<OrgId>
<BICOrBEI>SBZAZAJJXXX</BICOrBEI>
</OrgId>
</Id>
</InitgPty>
</GrpHdr>
<OrgnlGrpInfAndSts>
<OrgnlMsgId>Domestic Base-612742-300749</OrgnlMsgId>
<OrgnlMsgNmId>PAIN.001.001.03</OrgnlMsgNmId>
<OrgnlCreDtTm>2018-11-29T12:23:06</OrgnlCreDtTm>
<OrgnlNbOfTxs>1</OrgnlNbOfTxs>
<OrgnlCtrlSum>17500.00</OrgnlCtrlSum>
<GrpSts>RJCT</GrpSts>
<StsRsnInf>
<Rsn>
<Cd>NARR</Cd>
</Rsn>
<AddtlInf>Duplicate File</AddtlInf>
<AddtlInf>Error: 6789</AddtlInf>
<AddtlInf>Not Processed</AddtlInf>
</StsRsnInf>
</OrgnlGrpInfAndSts>
</CstmrPmtStsRpt>
</Document>
So far I have:
var info = doc.Descendants(ns + "CstmrPmtStsRpt")
.Descendants(ns + "OrgnlGrpInfAndSts")
.Descendants(ns + "StsRsnInf")
.Select(r => new
{
Info = r.Element(ns + "AddtlInf").Value
}).ToList();
But this only returns "Duplicate File", the first value. How do I fix this?
You need to access the sub-elements of StsRsnInf:
var info = doc.Descendants(ns + "CstmrPmtStsRpt")
.Descendants(ns + "OrgnlGrpInfAndSts")
.Descendants(ns + "StsRsnInf")
.SelectMany(r => r.Elements(ns + "AddtlInf").Select(s => new
{
Info = s.Value
})).ToList();
By using Element(without trailing s), you only access a single element instead of several, hence your result does only contain the value of the first element.
By using SelectMany as opposed to Select you can return several elements from sub-enumerations.
In your code, r.Element will always return you the first element of the collection.
Instead try to use r.Elements, which will return you the collection of "AddtlInf" elements.
For detailed information about this, have a look at this link!
You may use SelectMany in Linq.
var result = (string)xdoc.Descendants("member")
.FirstOrDefault(x => (string)x.Element("name") == "responseCode")
?.Element("value");
var info = xdoc.Descendants("CstmrPmtStsRpt")
.Descendants("OrgnlGrpInfAndSts")
.Descendants("StsRsnInf")
.SelectMany(r=> r.Descendants("AddtlInf")).ToList();
Console.WriteLine(info.Count); // prints 3.
Dotnet fiddle here: https://dotnetfiddle.net/IKd7go
As an alternative this seems easier to me:
var list = doc.XPathSelectElements
("Document/CstmrPmtStsRpt/OrgnlGrpInfAndSts/StsRsnInf/AddtlInf")
.Select(n => n.Value);

Flatten results from hierachical XML file using Linq to XML

I currently have the following C# with Linq to XML code working but I'm hoping there is a better way to form the Linq query to then be able to extract the results into a flat structure.
Here are the current results formatted as I would like them to appear:
7990697: result_status = Complete
7990756: result_status = Incomplete
I want to be able to access the results using the syntax result.resultStatusId instead of result.meta.ToList()[0].resultStatusId
var xDoc = XDocument.Parse(#"<fs_response status='success' timestamp='2014-07-10 14:38:39'>
<headings>
<heading for='result_status'>Result Status</heading>
<heading for='1'>First Name</heading>
<heading for='2'>Last Name</heading>
</headings>
<results>
<result id='7990697'>
<metas>
<meta id='result_status'>Complete</meta>
</metas>
<items>
<item id='1' index='3' type='text'>
<value>Bugs</value>
</item>
<item id='2' index='4' type='text'>
<value>Bunny</value>
</item>
</items>
</result>
<result id='7990756'>
<metas>
<meta id='result_status'>Incomplete</meta>
</metas>
<items>
<item id='1' index='3' type='text'>
<value>Yogi</value>
</item>
<item id='2' index='4' type='text'>
<value>Bear</value>
</item>
</items>
</result>
</results>
<total_results>2</total_results>
</fs_response>");
var results =
from x in xDoc.Elements("fs_response").Elements("results").Elements("result")
select new
{
id = x.Attribute("id").Value,
meta = from m in x.Elements("metas").Elements("meta")
where m.Attribute("id").Value == "result_status"
select new
{
resultStatusId = m.Attribute("id").Value,
resultStatus = m.Value
}
};
results.Dump();
foreach (var result in results.ToList())
{
Console.WriteLine("{0}: {1} = {2}",
result.id,
result.meta.ToList()[0].resultStatusId,
result.meta.ToList()[0].resultStatus);
//Console.WriteLine("{0}: {1} = {2}",
// result.id,
// result.resultStatusId,
// result.resultStatus);
}
I would change the query to something like this:
var results = xDoc.Element("fs_response")
.Element("results")
.Elements("result")
.Select (e => new
{
id = e.Attribute("id").Value,
meta = e.Element("metas")
.Elements("meta")
.Where (m =>
m.Attribute("id").Value == "result_status")
.Select (m => new
{
resultStatusId = m.Attribute("id").Value,
resultStatus = m.Value
})
.Single()
});
Some notes:
Notice the change in a few locations from Elements to Element. This indicates that you know there will only be one result with that name, or if you only want the first with that name. Use Elements when you expect multiple results. In some cases in the example above, I'm making assumptions about your xml schema (such as the fact that there will only be one metas Element per result). This may perform faster as it will not have to continue searching for Elements of a specific name once it has found one.
Also see the use of Single(). This indicates that you are expecting one and only one element with the id attribute equal to result_status. If you expect 0 or 1, you can use SingleOrDefault(), or if you want the first in a potential list of matching elements you can use First() or FirstOrDefault(). Once you add this, you will no longer have to call ToList()[0] in the results:
Console.WriteLine("{0}: {1} = {2}",
result.id,
result.meta.resultStatusId,
result.meta.resultStatus);
var results =
from x in xDoc.Elements("fs_response").Elements("results").Elements("result")
from m in x.Elements("metas").Elements("meta")
where m.Attribute("id").Value == "result_status"
select new
{
id = x.Attribute("id").Value,
resultStatusId = m.Attribute("id").Value,
resultStatus = m.Value,
};
foreach (var result in results.ToList())
{
Console.WriteLine("{0}: {1} = {2}", result.id, result.resultStatusId, result.resultStatus);
}

How do I parse text from complex type xml in c# using XDocument?

<?xml version="1.0" encoding="UTF-8"?>
<meta>
<field type="xs-string" name="AssetId">TF00000002</field>
<field type="xs-string" name="Title">TitleOfAsset</field>
</meta>
I have this XML loaded in to a XDocument using the function
XDocument doc = XDocument.Parse(xmlData)
However, I want to be able to retrieve the text fields "TF00000002" and "TitleOfAsset" ... How do I go about doing this?
templateMetaData.assetID = doc
.Descendants()
.Where(p => p.Name.LocalName == "AssetId")
.ToString();
returns:
System.Linq.Enumerable+WhereEnumerableIterator`1[System.Xml.Linq.XElement]
Can anyone shine a light on this?
In your query, you are calling ToString on an IEnumerable<XElement> which will never give you the expected result, instead look for field elements under your Root and get their value:
var values = doc.Root
.Elements("field")
.Select(element => (string)element);
If you want to access your values using the name attribute you can use Dictionary:
var values = doc.Root
.Elements("field")
.ToDictionary(x => (string)x.Attribute("name"), x => (string)x);
Then you can access the value of AssetId:
var id = values["AssetId"];

Get attributes Name and Value of element in C# through System.Linq

I have one custom config file.
<Students>
<student>
<Detail Name="abc" Class="1st Year">
<add key="Main" value="web"/>
<add key="Optional" value="database"/>
</Detail>
</student>
</Students>
I read this file through the IConfigurationHandler interface implementation.
When I read the childNode attributes of Detail element. It return me below result into Immediate Window of IDE.
elem.Attributes.ToObjectArray()
{object[2]}
[0]: {Attribute, Name="key", Value="Main"}
[1]: {Attribute, Name="value", Value="web"}
When I try to write on Console
Console.WriteLine("Value '{0}'",elem.Attributes.ToObjectArray());
it does return me
Value : 'System.Configuration.ConfigXmlAttribute'
elem.Attributes.Item(1) method gives me the Name and Value detail but here I need to pass the index value of attribute which I don't know currently.
I want to get Name and value of attribute through LINQ query and individual display on Console for each childNode attribute as follows:
Value : Name="Key" and Value="Main"
Name="value", Value="web"
How can I achieve that?
If you want to use this Xml Library you can get all the students and their details with this code:
XElement root = XElement.Load(file); // or .Parse(string)
var students = root.Elements("student").Select(s => new
{
Name = s.Get("Detail/Name", string.Empty),
Class = s.Get("Detail/Class", string.Empty),
Items = s.GetElements("Detail/add").Select(add => new
{
Key = add.Get("key", string.Empty),
Value = add.Get("value", string.Empty)
}).ToArray()
}).ToArray();
Then to iterate over them use:
foreach(var student in students)
{
Console.WriteLine(string.Format("{0}: {1}", student.Name, student.Class));
foreach(var item in student.Items)
Console.WriteLine(string.Format(" Key: {0}, Value: {1}", item.Key, item.Value));
}
You can use a Linq Select and string.Join to get the output you want.
string.Join(Environment.NewLine,
elem.Attributes.ToObjectArray()
.Select(a => "Name=" + a.Name + ", Value=" + a.Value)
)
This will get all the attributes of the children of the Detail element as you state in your question.
XDocument x = XDocument.Parse("<Students> <student> <Detail Name=\"abc\" Class=\"1st Year\"> <add key=\"Main\" value=\"web\"/> <add key=\"Optional\" value=\"database\"/> </Detail> </student> </Students>");
var attributes = x.Descendants("Detail")
.Elements()
.Attributes()
.Select(d => new { Name = d.Name, Value = d.Value }).ToArray();
foreach (var attribute in attributes)
{
Console.WriteLine(string.Format("Name={0}, Value={1}", attribute.Name, attribute.Value));
}
If you have the Attributes in an object[] as you wrote, which can be mocked by
var Attributes = new object[]{
new {Name="key", Value="Main"},
new {Name="value", Value="web"}
};
then the issue is that you have anonymous types whose names can't be extracted easily.
Take a look at this code (you can paste it into the main() method of a LinqPad editor window to execute it):
var linq=from a in Attributes
let s = string.Join(",",a).TrimStart('{').TrimEnd('}').Split(',')
select new
{
Value = s[0].Split('=')[1].Trim(),
Name = s[1].Split('=')[1].Trim()
};
//linq.Dump();
Since you cannot access the Name and Value properties of the variable Attributes inside the object[] array because the compiler hides them from you you, the trick is here to use the Join(",", a) method to get around this limitation.
All you need to do afterwards is to trim and split the resulting string and finally create a new object with Value and Name properties.
You can try it out if you uncomment the linq.Dump(); line in LinqPad - it returns what you want and it is furthermore queryable by Linq statements.

Update XML file with Linq

I'm having trouble trying to update my xml file with a new value. I have a class Person, which only contains 2 strings, name and description. I populate this list and write it as an XML file. Then I populate a new list, which contains many of the same names, but some of them contains descriptions that the other list did not contain. How can I check if the name in the current XML file contains a value other than "no description", which is the default for "nothing"?
Part of the xml file:
<?xml version="1.0" encoding="utf-8"?>
<Names>
<Person ID="2">
<Name>Aaron</Name>
<Description>No description</Description>
</Person>
<Person ID="2">
<Name>Abdi</Name>
<Description>No description</Description>
</Person>
</Names>
And this is the method for writing the list to the xml file:
public static void SaveAllNames(List<Person> names)
{
XDocument data = XDocument.Load(#"xml\boys\Names.xml");
foreach (Person person in names)
{
XElement newPerson = new XElement("Person",
new XElement("Name", person.Name),
new XElement("Description", person.Description)
);
newPerson.SetAttributeValue("ID", GetNextAvailableID());
data.Element("Names").Add(newPerson);
}
data.Save(#"xml\boys\Names.xml");
}
In the foreach loop how do I check if the person's name is already there, and then check if the description is something other than "no description", and if it is, update it with the new information?
I'm not sure I understand properly what you want, but I'm assuming you want to update the description only when the name is already there and the description is currently No description (which you should probably change to an empty string, BTW).
You could put all the Persons into a Dictionary based by name:
var doc = …;
var persons = doc.Root.Elements()
.ToDictionary(x => (string)x.Element("Name"), x => x);
and then query it:
if (persons.ContainsKey(name))
{
var description = persons[name].Element("Description");
if (description.Value == "No description")
description.Value = newDescription;
}
That is, if you care about performance. If you don't, you don't need the dictionary:
var person = doc.Root.Elements("Person")
.SingleOrDefault(x => (string)x.Element("Name") == name);
if (person != null)
{
var description = person.Element("Description");
if (description.Value == "No description")
description.Value = newDescription;
}
You can use the Nodes-Method on XElement and check manually.
But i will advise you to use the XPathEvaluate-Extension Method
For XPath expression take a look at:
How to check if an element exists in the xml using xpath?
I think you could create a peoplelist which only contains people not in the xml.
like ↓
var containlist = (from p in data.Descendants("Name") select p.Value).ToList();
var result = (from p in peoplelist where !containlist.Contains(p.Name) select p).ToList();
so that , you would no need to change anything with your exist method ...
just call it after..
SaveAllNames(result);

Categories

Resources