Way to populate class object in single LINQ XML query? - c#

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"]
});

Related

What is the best way to get the percentage for an object in linq list and map it to JSON?

As a result of a join in linq I am getting a list of RoleViewModel objects. What I want to do after is to get the percentage for each WorkRole in the list and map the work role's percentage and it's name to a json.
So, if I have two objects total in var list - one has RoleName "Role1" and the other has RoleName "Role2", what's the best way to get a JSON like :
myObj = {
"rolename":"Role1",
"perc":50
},
{
"rolename":"Role2",
"perc":50
},
Here is the query for my list :
var list= list1.
Join(db.WorkRolesUsersDetails,
o => o.WorkRoleId, od => od.WorkRoleId,
(o, od) => new
{
WorkRoleId = o.WorkRoleId,
RoleName = o.RoleName,
RoleDescription = o.RoleDescription,
CompanyId = o.CompanyId,
WRUDId = od.WRUDId,
UserDetailsId = od.UserDetailsId,
FocusStart = od.FocusStart,
FocusEnd = od.FocusEnd
}).ToList()
.Select(item => new RoleViewModel(
item.WorkRoleId,
item.RoleName,
item.RoleDescription,
item.CompanyId,
item.WRUDId,
item.UserDetailsId,
item.FocusStart,
item.FocusEnd)).ToList();
So, any tips on how can I do what I want in the best and easiest way? I am new to c#.
It should work like that:
var perclist = list.GroupBy(i=>i.RoleName)
.Select(i=>
new {
rolename=i.Key,
perc = ((double)(i.Count()) / (double)(list.Count()) )*100
});
var json = JsonConvert.SerializeObject(perclist);
I user Json.NET for serialization

Join table with object list

I have a table, lets say tblCar with all the related columns like Id, Make, Model, Color etc.
I have a search model for car containing two params Id and Model.
public class CarSearch
{
public int Id { get; set; }
public string Model { get; set; }
}
var carSearchObjets = new List<CarSearch>();
With list of primitive data (like Id list), to get cars with those Ids I could have done:
var idList = new List<int> { 1, 2, 3 };
var carsFromQuery = context.Cars.Where(x => idList.Contains(x.Id);
But if I have to fetch all the cars with Id and model from the list, how do I do it? Simple join cannot be done between in memory objects and tables.
I need something like,
from m in context.Cars
join n in carSearchObjets
on new { Id = n.Id, Model = n.Model } equals new { Id = m.Id, Model = m.Model }
select m;
This obviously won't work.
Please ignore any typos.And if you need more info or the question is not clear, let me know.
One (ugly-but-working) way to manage that is to use concatenation with a "never used" concat char.
I mean a char that should never appear in the datas. This is always dangerous, as... never is never sure, but you've got the idea.
For example, we'll say that our "never used" concat char will be ~
This is not good for perf, but at least working :
var carSearchObjectsConcatenated = carSearchObjets.Select(m => new { m.Id + "~" + m.Model});
then you can use Contains again (concatenating on the db too) : you'll need to use SqlFunctions.StringConvert if you wanna concatenate string and numbers on the db side.
var result = context.Cars.Where(m =>
carSearchObjectsConcatenated.Contains(SqlFunctions.StringConvert((double)m.Id) + "~" + m.Model);
EDIT
Another solution would be to use PredicateBuilder, as mentionned by Sorax, or to build your own Filter method if you don't want a third party lib (but PredicateBuilder is really fine).
Something like that in a static class :
public static IQueryable<Car> FilterCars(this IQueryable<Car> cars, IEnumerable<SearchCar> searchCars)
{
var parameter = Expression.Parameter(typeof (Car), "m");
var idExpression = Expression.Property(parameter, "Id");
var modelExpression = Expression.Property(parameter, "Model");
Expression body = null;
foreach (var search in searchCars)
{
var idConstant = Expression.Constant(search.Id);
var modelConstant = Expression.Constant(search.Model);
Expression innerExpression = Expression.AndAlso(Expression.Equal(idExpression, idConstant), Expression.Equal(modelExpression, modelConstant));
body = body == null
? innerExpression
: Expression.OrElse(body, innerExpression);
}
var lambda = Expression.Lambda<Func<Car, bool>>(body, new[] {parameter});
return cars.Where(lambda);
}
usage
var result = context.Cars.FilterCars(carSearchObjets);
this will generate an sql looking like
select ...
from Car
where
(Id = 1 And Model = "ax") or
(Id = 2 And Model = "az") or
(Id = 3 And Model = "ft")
'PredicateBuilder' might be helpful.
var predicate = PredicateBuilder.False<Car>();
carSearchObjects
.ForEach(a => predicate = predicate.Or(p => p.Id == a.Id && p.Model == a.Model));
var carsFromQuery = context.Cars.AsExpandable().Where(predicate);
Note the text in the link regarding EF:
If you're using Entity Framework, you'll need the complete LINQKit -
for the AsExpandable functionality. You can either reference
LINQKit.dll or copy LINQKit's source code into your application.
Old school solution..
//in case you have a
List<CarSearch> search_list; //already filled
List<Cars> cars_found = new List<Cars>();
foreach(CarSearch carSearch in search_list)
{
List<Cars> carsFromQuery = context.Cars.Where(x => x.Id == carSearch.Id && x.Model == carSearch.Model).ToList();
cars_found.AddRange(carsFromQuery);
}
Abd don't worry about the for loops.
I landed up passing in an xml list as a parameter to the sql query and joined to that:
var xml = new XElement("Cars", yourlist.Select(i => new XElement("Car", new XElement("Id", i.Id), new XElement("Model", i.Model))));
var results = Cars
.FromSql("SELECT cars.*"
+ "FROM #xml.nodes('/Cars/Car') Nodes(Node)"
+ "JOIN Cars cars on cars.Id = Nodes.Node.value('Id[1]', 'int') and cars.Model = Nodes.Node.value('Model[1]', 'varchar(100)')",
new SqlParameter("#xml", new SqlXml(xml.CreateReader())));
For entity-framework-core users I created a nuget package extension:
EntityFrameworkCore.SqlServer.Extensions.Contains

How to use "let" to deal with missing XML nodes?

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.

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();

Categories

Resources