Read XML file with LINQ and create object with IEnumerable propety - c#

Here is my problem:
I have this XML file:
<?xml version="1.0" encoding="utf-8" ?>
<settings>
<app name="Application1">
<log name="Log1" path="d:\paths\" filename="Log1File"/>
<log name="Log2" path="d:\paths\"/>
<log name="log3" path="d:\paths\" filename="Log3File"/>
</app>
</settings>
And I'm trying to read it with LINQ and create object of this class:
public class Apps
{
public string Name { get; set; }
public IEnumerable<Logs> Logs { get; set; }
}
public class Logs
{
public string Name { get; set; }
public string Path { get; set; }
public string Filename { get; set; }
}
So far I managed to create this bit of code however looks like it only gets first log element mean time I need all log elements for each app element:
public static IEnumerable<Apps> GetAllApps()
{
var items = from a in db.Descendants("app")
orderby a.Attribute("name").Value
select new Apps
{
Name = a.Attribute("name").Value,
Logs = from b in a.Descendants("log")
select new Logs
{
Name = b.Attribute("name").Value,
Path = b.Attribute("path").Value,
Filename = b.Attribute("filename").Value
}
};
return items;
}

I would use serialization here
XmlSerializer ser = new XmlSerializer(typeof(Settings));
var result = (Settings)ser.Deserialize(stream);
[XmlRoot("settings")]
public class Settings
{
[XmlElement("app")]
public Apps[] apps;
}
public class Apps
{
[XmlAttribute("name")]
public string Name { get; set; }
[XmlElement("log")]
public Logs[] Logs { get; set; }
}
public class Logs
{
[XmlAttribute("name")]
public string Name { get; set; }
[XmlAttribute("path")]
public string Path { get; set; }
[XmlAttribute("filename")]
public string Filename { get; set; }
}

I used fluent API, but let you adapt as you prefer...
Problem is a NullReferenceException, as one of your Logs in xml has no "filename" attribute. And when you use "Value" on a null, you get a NRE.
So, check if the Attribute exists before trying to get it's value.
var it = db.Descendants("app")
.OrderBy(app => app.Attribute("name").Value)
.Select(app => new Apps() {
Name = app.Attribute("name").Value,
Logs = app.Descendants("log").Select(a =>
new Logs() {
Name = a.Attribute("name") != null ? a.Attribute("name").Value : null,
Path = a.Attribute("path") != null ? a.Attribute("path").Value : null,
Filename = a.Attribute("filename") != null ? a.Attribute("filename").Value : null
}).ToList()
}).ToList();

Related

C# parsing multiple json

The JSON data is as follows:
{"Sucess":true,
"Code":0,
"Msg":"Sucess",
"Data":{
"UserDayRanking":
{
"UserID":11452112,
"UserCharm":0,
"UserName":"gay",
"UserGender":1,
"UserLevel":36,
"UserPhoto":"http://res.xxx.com/2020/3/16/63719926625601201487545U11452112.jpeg",
"Ranking":0,
"IsNobility":0,
"NobilityType":0,
"NobilityLevel":0,
"UserShowStyle":null,
"LiveLevelUrl":null,
"IsStealth":false},
"DayRankingList":[
{
"UserID":3974854,
"UserCharm":114858,
"UserName":"jack",
"UserGender":1,
"UserLevel":91,
"UserPhoto":"http://res.xxx.com/2020/2/15/63717400601924412312384U3974854.jpeg",
"Ranking":2,
"IsNobility":1,
"NobilityType":1,
"NobilityLevel":3,
"UserShowStyle":
{
"NameColor":100102,
"BorderColor":100403,
"LiangMedal":0,
"DztCountDown":0,
"Mounts":100204,
"LiveLevelCode":0,
"LiveRights":null
},
"LiveLevelUrl":null,
"IsStealth":false
},
{"UserID":6231512,
"UserCharm":22644,
"UserName":"red.girl",
"UserGender":1,
"UserLevel":57,
"UserPhoto":"http://res.xxx.com/2019/11/20/63709843050801519858823U6231512.jpeg",
"Ranking":3,
"IsNobility":0,
"NobilityType":0,
"NobilityLevel":0,
"UserShowStyle":{
"NameColor":0,
"BorderColor":0,
"LiangMedal":0,
"DztCountDown":0,
"Mounts":0,
"LiveLevelCode":0,
"LiveRights":null
},
"LiveLevelUrl":null,
"IsStealth":false}
],
"LiveCharmSwitch":1,
"IsSelf":false
}
}
I want to use c # extraction
"UserID": 3974854,
"UserCharm": 114858,
"UserName": "jack",
"UserID":6231512,
"UserCharm":22644,
"UserName":"red.girl",
That is to extract UserID, UserCharm, UserName,This json has many layers,
What I want after the extraction is,id is sorted in order
id = 1, UserID = 3974854, UserCharm = 114858, UserName = jack
id = 2, UserID = 6231512, UserCharm = 22644, UserName = red.girl
I use the following code, but only extract the first one
string json = #"{"Sucess":true,"Code":0,"Msg":"Sucess","Data":{"UserDayRanking":{"UserID":11452112,"UserCharm":0,"UserName":"gay","UserGender":1,"UserLevel":36,"UserPhoto":"http://res.xxx.com/2020/3/16/63719926625601201487545U11452112.jpeg","Ranking":0,"IsNobility":0,"NobilityType":0,"NobilityLevel":0,"UserShowStyle":null,"LiveLevelUrl":null,"IsStealth":false},"DayRankingList":[{"UserID":3974854,"UserCharm":114858,"UserName":"jack","UserGender":1,"UserLevel":91,"UserPhoto":"http://res.xxx.com/2020/2/15/63717400601924412312384U3974854.jpeg","Ranking":2,"IsNobility":1,"NobilityType":1,"NobilityLevel":3,"UserShowStyle":{"NameColor":100102,"BorderColor":100403,"LiangMedal":0,"DztCountDown":0,"Mounts":100204,"LiveLevelCode":0,"LiveRights":null},"LiveLevelUrl":null,"IsStealth":false},{"UserID":6231512,"UserCharm":22644,"UserName":"red.girl","UserGender":1,"UserLevel":57,"UserPhoto":"http://res.xxx.com/2019/11/20/63709843050801519858823U6231512.jpeg","Ranking":3,"IsNobility":0,"NobilityType":0,"NobilityLevel":0,"UserShowStyle":{"NameColor":0,"BorderColor":0,"LiangMedal":0,"DztCountDown":0,"Mounts":0,"LiveLevelCode":0,"LiveRights":null},"LiveLevelUrl":null,"IsStealth":false}],"LiveCharmSwitch":1,"IsSelf":false}}";
List<Info> jobInfoList = JsonConvert.DeserializeObject<List<Info>>(z);
foreach (Info jobInfo in jobInfoList)
{
//Console.WriteLine("UserName:" + jobInfo.UserName);
}
public class Info
{
public string UserCharm { get; set; }
public string UserName { get; set; }
public data DayRankingList { get; set; }
}
public class data
{
public int UserID { get; set; }
public string UserCharm { get; set; }
public string UserName { get; set; }
public string UserGender { get; set; }
public string UserLevel { get; set; }
}
The above code only shows username = jack,Never show username = red.girl
As it looks to me then you want some details from your JSON has the which is in DayRankingList. As you only want some data then we can use a tool like http://json2csharp.com/ to create our classes and then remove what we don't need. Then we end up with the following classes.
public class DayRankingList
{
public int UserID { get; set; }
public int UserCharm { get; set; }
public string UserName { get; set; }
}
public class Data
{
public List<DayRankingList> DayRankingList { get; set; }
}
public class RootObject
{
public Data Data { get; set; }
}
Which you can deserialise like this
string json = .....
var root = JsonConvert.DeserializeObject<RootObject>(json);
Then if you wish, you can extract the inner data into a new List<> and then just work on that.
List<DayRankingList> rankingLists = root.Data.DayRankingList;
//Do something with this, such as output it
foreach(DayRankingList drl in rankingLists)
{
Console.WriteLine(String.Format("UserId {0} UserCharm {1} UserName {2}",drl.UserId, drl.UserCharm, drl.UserName));
}
You can use Json.Linq to parse your JSON into JObject and enumerate DayRankingList items (since it's an array). Then convert every item into data class and order the result sequence by UserID
var jObject = JObject.Parse(json);
var rankingList = (jObject["Data"] as JObject)?.Property("DayRankingList");
var list = rankingList.Value
.Select(rank => rank.ToObject<data>())
.OrderBy(item => item?.UserID);
foreach (var user in list)
Console.WriteLine($"{user.UserID} {user.UserName}");
Another way is copy your JSON, go to Edit->Paste Special->Paste JSON as classes menu in Visual Studio and generate a proper class hierarchy (I've got 5 classes, they are quite long to post here), then use them during deserialization
The most type-safe way is to define the class structure that you want, like jason.kaisersmith suggested.
To have the final format you need, though, you might want to do an extra Linq Order and Select, to include the id:
var finalList = rankingLists.OrderBy(rl => rl.UserId).Select((value, index) => new
{
id = index,
value.UserId,
value.UserCharm,
value.UserName
});
foreach (var drl in finalList)
{
Console.WriteLine($"Id = {drl.id}, UserId = {drl.UserId}, UserCharm = {drl.UserCharm}, UserName = {drl.UserName}");
}

C# - Read json to subclass fields

I'm trying to make a Config.cs class to use on my project.
Structure is supposed to consist of categories of settings. For example, Config.LogOnDetails should hold the values for MySQL login.
Here is my current structure.
public class Config
{
public string pPath;
public string configPath;
public string configFilePath;
public class LogOnDetails
{
public string MySQLDatabaseName { get; set; }
public string MySQLUser { get; set; }
public string MySQLPassword { get; set; }
public string MySQLAddress { get; set; }
}
public Config()
{
pPath = Directory.GetCurrentDirectory();
configPath = Path.Combine(pPath, #"/config");
configFilePath = Path.Combine(configPath, "/config.json");
//If it doesn't exist, create a directory for the configuration to be stored in
if (!Directory.Exists(configPath))
{
Directory.CreateDirectory("config");
}
if (!File.Exists(configFilePath))
{
string json = JsonConvert.SerializeObject(this);
File.WriteAllText(configFilePath, json);
Console.WriteLine("Created a new blank config file!");
}
}
}
Here is how I'm trying to load the config to the class.
//Initialize configuration
Config.LogOnDetails logOnDetails = new Config.LogOnDetails();
//Load config from json
config.LogOnDetails = JsonConvert.DeserializeObject<Config.LogOnDetails>(config.configFilePath);
But this doesn't seem to work and looks like I don't understand subclasses properly. How can I organize my class so it will work?
json example:
{
"pPath": null,
"configPath": null,
"configFilePath": null,
"MySQLDatabaseName": null,
"MySQLUser": null,
"MySQLPassword": null,
"MySQLAddress": null
}
First off, I'm going to start with a general point. The Config class should know nothing about how it is stored, or where it is stored. That is a completely separate "concern". See Separation of concerns
Start off with the definition of what you want to store. That seems to be your MySql info, and some other info. These should all be individual classes (To be clear, you can nest them, but there is no need to and complicates the answer a little):
public class LogOnDetails
{
public string MySQLDatabaseName { get; set; }
public string MySQLUser { get; set; }
public string MySQLPassword { get; set; }
public string MySQLAddress { get; set; }
}
You can have another one:
public class Settings
{
public string Locale { get; set; }
}
And you can compose these into a master config object
public class Config
{
public string SomeTopLevelProp {get; set; }
public LogOnDetails LogOnDetails { get; set; }
public Settings Settings { get; set; }
}
The way to serialize and deserialize this is fairly straightforward
var config = new Config()
{
SomeTopLevelProp = "ABCDEF",
LogOnDetails = new LogOnDetails()
{
MySqlDatabaseName = "Foo" ,
MySQLUser = "MyUser"
// snip the rest of the props
},
Settings = new Settings
{
Locale = "en-GB"
}
}
var json = JsonConvert.SerializeObject(config );
var mySettingDeserialized = JsonConvert.DeserializeObject<Config>(json);
I have purposely left out the writing to a file part - you seem to know how to do that - but keep it outside of the Config class. For example a separate classes, perhaps just with 2 static methods which knows how/where to store the config
public static class ConfigLoader
{
public static void StoreConfig(Config config, string location) {... }
public static Config LoadConfig(string location) {... }
}
A note on security - storing your database password as plain text in a json config file is generally a bad idea. You might consider encrypting it, and storing that and decrypting it when using the value.

MongoDB Unable to determine the serialization information for the expression error

My data is having following structure
public enum ParamType
{
Integer=1,
String=2,
Boolean=3,
Double=4
}
public class Gateway
{
public int _id { get; set; }
public string SerialNumber { get; set; }
public List<Device> Devices { get; set; }
}
public class Device
{
public string DeviceName { get; set; }
public List<Parameter> Parameters { get; set; }
}
public class Parameter
{
public string ParamName { get; set; }
public ParamType ParamType { get; set; }
public string Value { get; set; }
}
I filled 10 document objects of Gateway in a MongoDB database.
Now I want to query all those gateways which contains a device having Parameter with ParamName as "Target Temperature" and whose Value > 15.
I created following queries
var parameterQuery = Query.And(Query<Parameter>.EQ(p => p.ParamName, "Target Temperature"), Query<Parameter>.GT(p => int.Parse(p.Value), 15));
var deviceQuery = Query<Device>.ElemMatch(d => d.Parameters, builder => parameterQuery);
var finalQuery = Query<Gateway>.ElemMatch(g => g.Devices, builder => deviceQuery);
But when I run this, it is giving an exception
Unable to determine the serialization information for the expression: (Parameter p) => Int32.Parse(p.Value)
Please suggest where I am wrong.
As the error suggests, you can't use Int32.Parse inside your query. This lambda expression is used to get out the name of the property and it doesn't understand what Int32.Parse is.
If you are querying a string, you need to use a string value for comparison:
var parameterQuery = Query.And(Query<Parameter>.EQ(p => p.ParamName, "Target Temperature"), Query<Parameter>.GT(p => p.Value, "15"));
However, that's probably not what you want to do since you're using GT. To be treated as a number for this comparison you need the value to actually be an int in mongo, so you would need to change the type of your property:
public class Parameter
{
public string ParamName { get; set; }
public ParamType ParamType { get; set; }
public int Value { get; set; }
}
var parameterQuery = Query.And(Query<Parameter>.EQ(p => p.ParamName, "Target Temperature"), Query<Parameter>.GT(p => p.Value, 15));
Summary
I ran into this when I was modifying a list. It appears that Linq First/FirstOrDefault was not handled well by MongoDB for me. I changed to be an array index var update = Builders<Movie>.Update.Set(movie => movie.Movies[0].MovieName, "Star Wars: A New Hope"); Note: This is in Asp.Net 5 using MongoDB.Driver 2.2.0.
Full Example
public static void TypedUpdateExample() {
var client = new MongoClient("mongodb://localhost:27017");
var database = client.GetDatabase("test");
var collection = database.GetCollection<Movie>("samples");
//Create some sample data
var movies = new Movie {
Name = "TJ",
Movies = new List<MovieData>
{
new MovieData {
MovieName = "Star Wars: The force awakens"
}
}
};
collection.InsertOne(movies);
//create a filter to retreive the sample data
var filter = Builders<Movie>.Filter.Eq("_id", movies.Id);
//var update = Builders<Movie>.Update.Set("name", "A Different Name");
//TODO:LP:TSTUDE:Check for empty movies
var update = Builders<Movie>.Update.Set(movie => movie.Movies[0].MovieName, "Star Wars: A New Hope");
collection.UpdateOne(filter, update);
}
public class Movie {
[BsonId]
public ObjectId Id { get; set; }
public string Name { get; set; }
public List<MovieData> Movies { get; set; }
}
public class MovieData {
[BsonId]
public ObjectId Id { get; set; }
public string MovieName { get; set; }
}

How to load XML Elements using LINQ from XDocument into a class (not using Descendants)

How to load XML Elements using LINQ from XDocument into a c# class. I don't want to use XDocument.Descendants because the settings are not repeated and occur only once in the XML.
This works but I think there must be another way where I don't have to use IEnumerable or use ToArray() and use the first element [0]. Any suggestions?
CODE
public class BackupItem // class needed so LINQ reads XML into an editable DbGrid
{
public bool Backup { get; set; }
public bool IncludeSubDir { get; set; }
public string BackupLabel { get; set; }
public string BackupPath { get; set; }
}
public class BackupSettings
{
public bool IncludeDateStamp { get; set; }
public bool DatePrefix { get; set; }
public bool DateSuffix { get; set; }
public string DateFormat { get; set; }
public bool ZipCompress { get; set; }
public bool ZipPrefix { get; set; }
public bool ZipSuffix { get; set; }
public string ZipText { get; set; }
public bool BackupToSiblingFolder { get; set; }
public string SiblingFolder { get; set; }
public bool BackupToFolder { get; set; }
public bool IncludeFullPath { get; set; }
public string BackupFolder { get; set; }
}
// use a LINQ query to load xml file into datagridview and make datagrid editable (create class with get/set)
var q = from arg in GvXMLDoc.Descendants("BackupItem")
select new BackupItem()
{
Backup = (bool)arg.Element("IncludeDirectory"),
IncludeSubDir = (bool)arg.Element("IncludeSubDirectories"),
BackupLabel = (string)arg.Element("BackupLabel"),
BackupPath = (string)arg.Element("BackupPath")
};
dataGridView1.DataSource = q.ToList();
// load global variable
IEnumerable<BackupSettings> GvBackupSettings = from arg in GvXMLDoc.Element("Backup").Elements("Settings")
select new BackupSettings()
{
IncludeDateStamp = (bool)arg.Element("IncludeDateStamp"),
DatePrefix = (bool)arg.Element("DatePrefix"),
DateSuffix = (bool)arg.Element("DateSuffix"),
DateFormat = (string)arg.Element("DateFormat"),
ZipCompress = (bool)arg.Element("ZipCompress"),
ZipPrefix = (bool)arg.Element("ZipPrefix"),
ZipSuffix = (bool)arg.Element("ZipSuffix"),
ZipText = (string)arg.Element("ZipText"),
BackupToSiblingFolder = (bool)arg.Element("BackupToSiblingFolder"),
SiblingFolder = (string)arg.Element("SiblingFolder"),
BackupToFolder = (bool)arg.Element("BackupToFolder"),
IncludeFullPath = (bool)arg.Element("IncludeFullPath"),
BackupFolder = (string)arg.Element("BackupFolder")
};
I can access the values but it seems a very 'messy way'.
var s = GvBackupSettings.ToArray()[0].DateSuffix;
var t = GvBackupSettings.ToArray()[0].DateFormat;
XML FILE
<?xml version='1.0'?>
<Backup>
<Settings>
<IncludeDateStamp>true</IncludeDateStamp>
<DatePrefix>false</DatePrefix>
<DateSuffix>true</DateSuffix>
<DateFormat>yyyy-MM-dd h.m.s</DateFormat>
<ZipCompress>false</ZipCompress>
<ZipPrefix>false</ZipPrefix>
<ZipSuffix>false</ZipSuffix>
<ZipText></ZipText>
<BackupToSiblingFolder>true</BackupToSiblingFolder>
<SiblingFolder>backups</SiblingFolder>
<BackupToFolder>false</BackupToFolder>
<IncludeFullPath>true</IncludeFullPath>
<BackupFolder>C:\\backup</BackupFolder>
</Settings>
<BackupItem>
<IncludeDirectory>true</IncludeDirectory>
<IncludeSubDirectories>true</IncludeSubDirectories>
<BackupLabel>Backup1</BackupLabel>
<BackupPath>C:\TestFiles\Xml\Samples</BackupPath>
</BackupItem>
<BackupItem>
<IncludeDirectory>true</IncludeDirectory>
<IncludeSubDirectories>false</IncludeSubDirectories>
<BackupLabel>Backup2</BackupLabel>
<BackupPath>C:\TestFiles\Xml\Samples</BackupPath>
</BackupItem>
</Backup>
EDIT 1
I also am trying to deserialize the XML (thanks for idea) so I don't have to cast each item. This does not work.. I don't want to have to run the XSD tool either and have a huge class file...
XmlRootAttribute rootAttribute = new XmlRootAttribute("Backup");
XmlSerializer deserializer = new XmlSerializer((typeof(BackupSettings)), rootAttribute);
GvBackupSettings = (BackupSettings)deserializer.Deserialize(XmlReader.Create(GvXMLFileName));
EDIT 2
Ended up serializing and deserializing xml as suggested below using stardard XSD.exe tool to generate the c# class. Especially as I had to read and write to same xml file. Note: Make sure you verify/modify the generated xsd file first.
You don't need a query if you just need the first element:
XElement settings = GvXMLDoc.Element("Backup").Element("Settings");
BackupSettings GvBackupSettings = new BackupSettings
{
IncludeDateStamp = (bool)settings.Element("IncludeDateStamp"),
DatePrefix = (bool)settings.Element("DatePrefix"),
...
};
var s = GvBackupSettings.DateSuffix;
var t = GvBackupSettings.DateFormat;

Linq to XML query returining a list of strings

I have the following class
public class CountrySpecificPIIEntity
{
public string Country { get; set; }
public string CreditCardType { get; set; }
public String Language { get; set; }
public List<String> PIIData { get; set; }
}
I have the following XML that I want to query using Linq-to-xml and shape in to the class above
<?xml version="1.0" encoding="utf-8" ?>
<piisettings>
<piifilter country="DE" creditcardype="37" language="ALL" >
<filters>
<filter>FIRSTNAME</filter>
<filter>SURNAME</filter>
<filter>STREET</filter>
<filter>ADDITIONALADDRESSINFO</filter>
<filter>ZIP</filter>
<filter>CITY</filter>
<filter>STATE</filter>
<filter>EMAIL</filter>
</filters>
</piifilter>
<piifilter country="DE" creditcardype="37" Language="en" >
<filters>
<filter>EMAIL</filter>
</filters>
</piifilter>
</piisettings>
I'm using the following query but I'm having trouble with the last attribute i.e. PIIList.
var query = from pii in xmlDoc.Descendants("piifilter")
select new CountrySpecificPIIEntity
{
Country = pii.Attribut("country").Value,
CreditCardType = pii.Attribute("creditcardype").Value,
Language = pii.Attribute("Language").Value,
PIIList = (List<string>)pii.Elements("filters")
};
foreach (var entity in query)
{
Debug.Write(entity.Country);
Debug.Write(entity.CreditCardType);
Debug.Write(entity.Language);
Debug.Write(entity.PIIList);
}
How Can I get the query to return a List to be hydrated in to the PIIList property.
var query = from pii in xmlDoc.Descendants("piifilter")
select new CountrySpecificPIIEntity
{
Country = pii.Attribut("country").Value,
CreditCardType = pii.Attribute("creditcardype").Value,
Language = pii.Attribute("Language").Value,
PIIList = pii.Elements("filters").Select(xe => xe.Value).ToList();
};
i am not sure, can you check you class definition once to check if ?
public List<String> PIIList { get; set; }
if this is correct then try this:
select new CountrySpecificPIIEntity
{
Country = pii.Attribute("country").Value,
CreditCardType = pii.Attribute("creditcardype").Value,
Language = pii.Attribute("Language").Value,
PIIList = pii.Elements("filters").Cast<string>().ToList()
}

Categories