Serializing and deserializing a collection of objects - c#

I'm trying to create an Rssfeed reader which saves info about a podcast to a JSON file and I'm having trouble serializing and deserializing to that file.
I realize that there are other threads regarding this subject, but I cannot grasp or comprehend how to apply it to my code or the reasoning behind it.
So I have a bit of code that creates a file if it doesn't exist and writes JSON data to it which looks like:
public void SaveFile(Podcast podcast)
{
try
{
JsonSerializer serializer = new JsonSerializer();
if(!File.Exists(#"C: \Users\Kasper\Desktop\Projektuppgift\Projektuppgift - Delkurs2\Projektet\Projektet\bin\Debug\podcasts.json"))
{
string json = JsonConvert.SerializeObject( new { Podcast = podcast });
StreamWriter sw = File.CreateText(#"C:\Users\Kasper\Desktop\Projektuppgift\Projektuppgift-Delkurs2\Projektet\Projektet\bin\Debug\podcasts.json");
using (JsonWriter writer = new JsonTextWriter(sw))
{
serializer.Serialize(writer, json);
}
}
else
{
var filepath = #"C:\Users\Kasper\Desktop\Projektuppgift\Projektuppgift-Delkurs2\Projektet\Projektet\bin\Debug\podcasts.json";
var jsonData = File.ReadAllText(filepath);
var podcasts = JsonConvert.DeserializeObject<List<Podcast>>(jsonData) ?? new List<Podcast>();
podcasts.Add(podcast);
jsonData = JsonConvert.SerializeObject(new {PodcastList = podcasts });
File.WriteAllText(filepath, jsonData);
}
}
catch (Exception ex)
{
Console.WriteLine("IO Exception ", ex.Message);
}
}
What I can't get to work is to deserialize from this file and add an object to it. Is there an easier way to add more data to the JSON file or am I missing something?
The Podcast class looks like this:
public class Podcast
{
public string url { get; set; }
public string name { get; set; }
public int updateInterval { get; set; }
public string category { get; set; }
//public Category category = new Category();
public List<Episode> episodes { get; set; }
public Podcast(string url, string name, Category category, List<Episode> episodes, int updateInterval)
{
this.url = url;
this.name = name;
this.category = category.name;
this.episodes = episodes;
this.updateInterval = updateInterval;
}
public Podcast(Podcast p)
{
this.url = p.url;
this.name = p.name;
this.category = p.category;
this.episodes = p.episodes;
this.updateInterval = p.updateInterval;
}
}

There appear to be a couple of issues here:
You are checking for the existence of a different file than the one you are reading/writing. The former filename has extra spaces in it. The best way to avoid this problem is to use a variable to contain the filename rather than hardcoding it in three separate places.
You are inconsistent about the JSON format you are writing and reading:
When you first create the file (in the first branch), you are writing a JSON object that contains a property Podcast which then contains a single podcast.
When you attempt to read the JSON file, you are treating the entire JSON as a list of podcasts.
After tacking the new podcast onto the list, you are writing the JSON as a single object containing a PodcastList property, which then contains the list.
You need to use a consistent JSON format. I would recommend breaking your code into smaller methods to read and write the podcasts.json file like this so that it is easier to reason about:
public static List<Podcast> ReadPodcastsFromFile(string filepath)
{
if (!File.Exists(filepath)) return new List<Podcast>();
string json = File.ReadAllText(filepath);
return JsonConvert.DeserializeObject<List<Podcast>>(json);
}
public static void WritePodcastsToFile(List<Podcast> podcasts, string filepath)
{
string json = JsonConvert.SerializeObject(podcasts);
// This will overwrite the file if it exists, or create a new one if it doesn't
File.WriteAllText(filepath, json);
}
Then, you can simplify your SaveFile method down to this (I would be tempted to rename it to SavePodcast):
public void SaveFile(Podcast podcast)
{
var filepath = #"C:\Users\Kasper\Desktop\Projektuppgift\Projektuppgift-Delkurs2\Projektet\Projektet\bin\Debug\podcasts.json";
List<Podcast> podcasts = ReadPodcastsFromFile(filepath);
podcasts.Add(podcast);
WritePodcastsToFile(podcasts, filepath);
}
Notice I've also removed the exception handling from SaveFile. You should move that up to wherever SaveFile is called, so that you can take appropriate action at that point if an exception is thrown, e.g.:
try
{
SaveFile(podcast);
}
catch (Exception ex)
{
// Show a message to the user indicating that the file did not save
}

I'm just still learning c# but it might be that you deserialise into a list of podcasts and when you serialise you're serliasing into an object type.

Related

How can I display my created error list using csvhelper

Apologies as this may seem a dumb question but I am missing something and not sure what to do.
As part of an application I'm making I am using CSVHelper for creating a file which contains details of any errors that may arise when using the application, such as a failure to connect to the database, values missing etc. I had been following this tutorial here: https://www.youtube.com/watch?v=fRaSeLYYrcQ and edited it more to suit my needs. I have an error logging file like so:
public class ErrorLogging
{
public string ErrorType { get; set; }
public string Relation { get; set; }
public string Message { get; set; }
public static List<ErrorLogging> GetErrors(string type, string relating, string message)
{
return new List<ErrorLogging>
{
new ErrorLogging
{
ErrorType = type,
Relation = relating,
Message = message
}
};
}
}
So here I have made it so the column headers in the csv file will be ErrorType, Relating and Message
I also have an export log file:
class ExportLog
{
public static void Log<T>(string path, string file, List<T>report)
{
var csvPath = Path.Combine(path + "\\", file);
using (var streamWriter = new StreamWriter(csvPath))
{
using (var csvWriter = new CsvWriter(streamWriter, CultureInfo.InvariantCulture))
{
csvWriter.WriteRecords(report);
}
}
}
}
In my main file program.cs I then use these by doing something like:
if (validateRoleTitles is not false)
{
log.Information("Roles found");
}
else
{
log.Warning("Roles could not be found");
ErrorLogging.GetErrors("Warning", "Database", "Not all roles found in the database");
}
And I now need to create the csv file with the list of errors by doing something like:
ExportLog.Log(config.ExportFolder, exportFile, "error logging list here");
However I'm not sure how to actually display these lists that I am creating if that makes sense?
As I can not do:
ExportLog.Log(config.ExportFolder, exportFile, ErrorLogging.GetErrors());
since it will be looking for parameters to be passed in. I know I am missing something very obvious here but can't figure out what, any help would be appreciated
So I ended up solving my issue, I had been going about it the wrong way, I did not need to have:
public static List<ErrorLogging> GetErrors(string type, string relating, string message)
{
return new List<ErrorLogging>
{
new ErrorLogging
{
ErrorType = type,
Relation = relating,
Message = message
}
};
}
in ErrorLogging.cs, it simply just needed to be:
public class ErrorLogging
{
public string ErrorType { get; set; }
public string Relation { get; set; }
public string Message { get; set; }
}
Then in program.cs I can declare a new list like so:
List<ErrorLogging> ErrorList = new List<ErrorLogging>();
and then when there is a point I would need to add to the list I can simply do something along the lines of:
ErrorList.Add(new ErrorLogging()
{
ErrorType = "Warning",
Relation = "Database",
Message = "Not all properties found in the database"
});
And then when it comes to creating the csv with the list of errors, I can check whether the list is empty or not, if not empty then create:
if (ErrorList.Any())
{
ExportLog.Log(config.LogFolder, errorLog, ErrorList);
}

How can I be notified if I deserialize XML to C# and there's no corresponding C# property?

I'm working with some third party XML that has no formally defined schema, only example XML. I have several thousand XML files from this third party. There is no guarantee that every possible element lies within one or more of these files. The third party service could send me a new file with a new element!
I can view these files and reverse engineer types relatively easily.
For example:
<MyObject>
<MyProperty>Some value</MyProperty>
</MyObject>
Could deserialize to
public class MyObject
{
public string MyProperty { get; set; }
}
No problems so far.
But what if I attempt to deserialize this:
<MyObject>
<MyProperty>Some value</MyProperty>
<MyOtherProperty>Some value</MyOtherProperty>
</MyObject>
into my class above? I want it to throw an exception, so I can be notified that my class does not accommodate MyOtherProperty.
Is there a way to do this?
I'd like to share the code I wrote using the accepted answer. Using the below utility method, I can deserialize without checks for unknown stuff, and with checks, by setting strict=true.
I hope readers find this useful!
public static T XmlDeserialize<T>(string xml, bool strict = false)
{
using (var stringReader = new StringReader(xml))
{
using (var xmlTextReader = new XmlTextReader(stringReader))
{
var xmlSerializer = new XmlSerializer(typeof(T));
if (strict)
{
var options = new XmlDeserializationEvents();
options.OnUnknownElement += (sender, args) =>
{
throw new XmlDeserializationException(xml, $"Unexpected Element {args.Element.LocalName} on line {args.LineNumber}.");
};
options.OnUnknownAttribute += (sender, args) =>
{
throw new XmlDeserializationException(xml, $"Unexpected Element: {args.Attr.LocalName} on line {args.LineNumber}.");
};
options.OnUnknownNode += (sender, args) =>
{
throw new XmlDeserializationException(xml, $"Unexpected Element: {args.LocalName} on line {args.LineNumber}.");
};
return (T)xmlSerializer.Deserialize(xmlTextReader, options);
}
return (T)xmlSerializer.Deserialize(xmlTextReader);
}
}
}
And that exception class I'm throwing looks like this:
public class XmlDeserializationException : Exception
{
public string Xml { get; private set; }
public XmlDeserializationException(
string xml, string message) : base (message)
{
Xml = xml;
}
}
I can check my logs and look up the line number in the actual xml. Works perfectly. Thanks, pfx.
The XmlSerializer has a Deserialize overload allowing to pass in an options element by which to hook to some events; eg. OnUnknownElement.
XmlDeserializationEvents options = new XmlDeserializationEvents();
options.OnUnknownElement += (sender, args) => {
XmlElement unknownElement = args.Element;
// throw an Exception with this info.
} ;
var o = serializer.Deserialize(xml, options) as MyObject;
The OnUnknowElement takes nested elements into account.
With the classes below
public class MyObject
{
public string MyProperty { get; set; }
public MyOtherObject Other { get; set; }
}
public class MyOtherObject
{
public string SomeProperty { get; set; }
}
and the following xml
<MyObject>
<MyProperty>Some value</MyProperty>
<Other>
<SomeProperty>...</SomeProperty>
<UFO>...</UFO>
</Other>
</MyObject>
The OnUnknowElement handler will trigger for the UFO element.
One way of doing so would be to use your current object model and create an XSD out of it. You can then check new files against that XSD and throw if it doesn't validate.
Extend your class with an XmlAnyElement container.
Any unknown elements will end up in that array.
After deserialization check whether that array is empty.
public class MyObject
{
[XmlAnyElement]
public XmlElement[] UnknownElements;
public string MyProperty { get; set; }
}

Get JSON Data in variable when only one item is returned

I am trying to get some currency values from an api. it's returning the data in the following format:
{"PKR_PKR":{"val":1}}
I want to show this value in textbox but there's an error
"Object reference not set to an instance of object".
I've tried the following code:
try
{
string endPoint = #"http:urlhere";
string ResultJson = "";
using (WebClient wc = new WebClient())
{
ResultJson = wc.DownloadString(endPoint);
}
JsonData values = JsonConvert.DeserializeObject<JsonData>(ResultJson);
txtBalanceRate.Text = values.CurrencyValue.ToString();
}
catch (Exception ex) { }
Class code:
class JsonData
{
public object CurrencyValue { get; set; }
}
**
UPDATE
**
Note: I can not update PKR_PKR Class becuase every time the name of variable is different for different currencies i.e. it can be USD_PKR , EUR_PKR etc
How can I resolve this?
FOLLOWING IS THE UPDATED CODE:
try
{
string endPoint = #"http://free.currencyconverterapi.com/api/v5/convert?q="+ddlCurrency.SelectedValue.ToString()+"_PKR&compact=y";
string ResultJson = "";
using (WebClient wc = new WebClient())
{
ResultJson = wc.DownloadString(endPoint);
}
RootObject rootObject = JsonConvert.DeserializeObject<RootObject>(ResultJson);
txtBalanceRate.Text = rootObject.PKR_PKR.val.ToString();
}
catch (Exception ex)
{
}
public class PKRPKR
{
public int val { get; set; }
}
public class RootObject
{
public PKRPKR PKR_PKR { get; set; }
}
If you are going to have dynamic object then you should try this out
dynamic data = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
Type typeOfDynamic = data.GetType();
if( typeOfDynamic.GetProperties().Where(p => p.Name.Equals("PKR_PKR")).Any())
{
console.WriteLine(data.PKR_PKR.val);
}
else if( typeOfDynamic.GetProperties().Where(p => p.Name.Equals("USD_PKR")).Any())
{
console.WriteLine(data.USD_PKR.val);
}
else if( typeOfDynamic.GetProperties().Where(p => p.Name.Equals("EUR_PKR")).Any())
{
console.WriteLine(data.EUR_PKR.val);
}
above way is not tried and tested but you can have try like this as you json is dynamic.
Above way is checking property exist or not and get val from dynamci object
Your class structure is incorrect can you please try below class structure
public class PKRPKR
{
public int val { get; set; }
}
public class RootObject
{
public PKRPKR PKR_PKR { get; set; }
}
RootObject rootObject = JsonConvert.DeserializeObject<RootObject>(json);
Console.WriteLine(rootObject.PKR_PKR.val);
Mostly if you see above class structure , you josn each node is represent as class, but I dont go in much detail as Visual studio can do it for me.
When comes to json to object conversion ,I make use of utility provided by Visual studio. which does conversion of json string to proper class structure. here is image of it
Read how to do it full here :
Visual Studio Generate Class From JSON or XML
If you dont have visual studio with this feature you can use this online utility : json2csharp
Note: I can not update PKR_PKR Class becuase evert time the name of
variable is different for different currencies i.e. it can be USD_PKR
, EUR_PKR etc How can I resolve this?
SOLUTION
if json string {"PKR_PKR":{"val":1}} is fixed in your case, you can use following solution for any currency name you got.
static void Main(string[] args)
{
string json1 = "{ \"PKR_PKR\":{ \"val\":1}}";
string json2 = "{ \"USD_PKR\":{ \"val\":2}}";
string json3 = "{ \"EUR_PKR\":{ \"val\":3}}";
JToken token1 = (JToken)JsonConvert.DeserializeObject(json1);
Console.WriteLine(token1.First().First()["val"]);
JToken token2 = (JToken)JsonConvert.DeserializeObject(json2);
Console.WriteLine(token2.First().First()["val"]);
JToken token3 = (JToken)JsonConvert.DeserializeObject(json3);
Console.WriteLine(token3.First().First()["val"]);
Console.ReadLine();
}
I think your receiving object should contain a dictionary, not a single string:
Check this
Or you have to improve your object structure implementing a root item which contains a PKR_PKR sub object

XML deserialization generic method

I have next XML file:
<Root>
<Document>
<Id>d639a54f-baca-11e1-8067-001fd09b1dfd</Id>
<Balance>-24145</Balance>
</Document>
<Document>
<Id>e3b3b4cd-bb8e-11e1-8067-001fd09b1dfd</Id>
<Balance>0.28</Balance>
</Document>
</Root>
I deserialize it to this class:
[XmlRoot("Root", IsNullable = false)]
public class DocBalanceCollection
{
[XmlElement("Document")]
public List<DocBalanceItem> DocsBalanceItems = new List<DocBalanceItem>();
}
where DocBalanceItem is:
public class DocBalanceItem
{
[XmlElement("Id")]
public Guid DocId { get; set; }
[XmlElement("Balance")]
public decimal? BalanceAmount { get; set; }
}
Here is my deserialization method:
public DocBalanceCollection DeserializeDocBalances(string filePath)
{
var docBalanceCollection = new DocBalanceCollection();
if (File.Exists(filePath))
{
var serializer = new XmlSerializer(docBalanceCollection.GetType());
TextReader reader = new StreamReader(filePath);
docBalanceCollection = (DocBalanceCollection)serializer.Deserialize(reader);
reader.Close();
}
return docBalanceCollection;
}
All works fine but I have many XML files. Besides writing Item classes I have to write ItemCollection classes for each of them. And also I have to implement DeserializeItems method for each.
Can I deserialize my XML files without creating ItemCollection classes? And can I write single generic method to deserialize all of them?
The only solution that comes to mind - make an interface for all these classes. Any ideas?
You can deserialize a generic List<T> just fine with XmlSerializer. However, first you need to add the XmlType attribute to your DocBalanceItem so it knows how the list elements are named.
[XmlType("Document")]
public class DocBalanceItem
{
[XmlElement("Id")]
public Guid DocId { get; set; }
[XmlElement("Balance")]
public decimal? BalanceAmount { get; set; }
}
Then modify your DeserializeDocBalances() method to return a List<T> and pass the serializer an XmlRootAttribute instance to instruct it to look for Root as the root element:
public List<T> DeserializeList<T>(string filePath)
{
var itemList = new List<T>();
if (File.Exists(filePath))
{
var serializer = new XmlSerializer(typeof(List<T>), new XmlRootAttribute("Root"));
TextReader reader = new StreamReader(filePath);
itemList = (List<T>)serializer.Deserialize(reader);
reader.Close();
}
return itemList;
}
Then you should be able to do
var list = DeserializeList<DocBalanceItem>("somefile.xml");
Since the method now returns a generic List<T>, you no longer need to create custom collections for every type.
P.S. - I tested this solution locally with the provided document, it does work.
Any stringable object can be deserialized by following method.
public static T genericDeserializeSingleObjFromXML<T>(T value, string XmalfileStorageFullPath)
{
T Tvalue = default(T);
try
{
XmlSerializer deserializer = new XmlSerializer(typeof(T));
TextReader textReader = new StreamReader(XmalfileStorageFullPath);
Tvalue = (T)deserializer.Deserialize(textReader);
textReader.Close();
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show("serialization Error : " + ex.Message);
}
return Tvalue;
}
In order to use this method you should already serialize the object in xml file.
Calling method is :
XmlSerialization.genericDeserializeSingleObjFromXML(new ObjectName(), "full path of the XML file");

Serializing a textbox in C# .net

I am a beginner with .NET environment.
I have a windows application with three textboxes and one button. When the user clicks on the button, i want all the textbox values to be serialized in an XML format to a file.
I tried doing it this way,
DialogResult dr = new DialogResult();
private void button1_Click(object sender, EventArgs e)
{
AddCustomer customer = new AddCustomer();
customer.textBox1.Text = textBox1.Text;
customer.textBox2.Text = textBox2.Text;
customer.textBox3.Text = textBox3.Text;
customer.textBox4.Text = textBox4.Text;
saveFileDialog1.InitialDirectory = #"D:";
saveFileDialog1.Filter = "Xml Files | *.xml";
if (saveFileDialog1.ShowDialog().Equals(DialogResult.OK))
{
SerializeToXML(customer);
}
}
public void SerializeToXML(AddCustomer customer)
{
XmlSerializer serializer = new XmlSerializer(typeof(AddCustomer));
TextWriter textWriter = new StreamWriter(#"D:\customer.xml");
serializer.Serialize(textWriter, customer);
textWriter.Close();
}
this returned system.invalidoperationexception was unhandled exception
any ideas?
Thanks,
Michael
You can't serialize controls instead you have to create Serializable component that represent TextBox values. (For more detail read MSDN article).
For instance,
[Serializable]
public class Customer
{
public string Name { get; set; }
public string Address {get;set;}
}
You shouldn't serialize the textbox object, only their values.
customer.textBox1 should be customer.text1 of type string. You then need to just assign customer.text1 = textbox1.text.
Also, mark your AddCustomer class with the [Serializable] attribute.
Edit: This is a code I just tried and it works fine. See if you can make it work in your solution.
Add new class Customer
[Serializable]
public class Customer
{
public string FullName { get; set; }
public string Age { get; set; }
}
Try to serialize it like this
Customer customer = new Customer();
customer.FullName = "John"; // or customer.FullName = textBox1.Text
customer.Age = "21"; // or customer.Age = textBox2.Text
XmlSerializer serializer = new XmlSerializer(typeof(Customer));
TextWriter textWriter = new StreamWriter(#"D:\customer.xml");
serializer.Serialize(textWriter, customer);
textWriter.Close();
After doing this, I got an xml file created with the following content.
<?xml version="1.0" encoding="utf-8"?>
<Customer xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<FullName>John</FullName>
<Age>21</Age>
</Customer>
Try and compare to see what you are doing wrong.
There are a lot of ways to write XML in .NET. Here is a way using XmlWriter that works for very simple content like in this case:
string text1 = /* get value of textbox */;
string text2 = /* get value of textbox */;
string text3 = /* get value of textbox */;
// Set indent=true so resulting file is more 'human-readable'
XmlWriterSettings settings = new XmlWriterSettings() { Indent = true };
// Put writer in using scope; after end of scope, file is automatically saved.
using (XmlWriter writer = XmlTextWriter.Create("file.xml", settings))
{
writer.WriteStartDocument();
writer.WriteStartElement("root");
writer.WriteElementString("text1", text1);
writer.WriteElementString("text2", text2);
writer.WriteElementString("text3", text3);
writer.WriteEndElement();
}
One note: you should avoid doing file operations on the UI thread as this can result in blocking behavior (e.g. the disk can be slow and cause the UI to freeze up while it writes the file, or it could be writing to a network location and hang for a while as it connects). It is best to put up a progress dialog and display a message "Please wait while file is saved..." and do the file operation in the background; a simple way is to post the background operation the thread pool using BeginInvoke/EndInvoke.
If you want to use the XmlSerializer instead, then you would follow these steps:
Create a public type to act as the root element of your document and mark it with XmlRoot.
Add elements/attributes made of either primitive/built-in types or your own public custom types which are also XML serializable, marking them with XmlElement or XmlAttribute as necessary.
To write the data out, use XmlSerializer.Serialize with an appropriate Stream, StreamWriter, or XmlWriter along with your root object.
To read the data back in, use XmlSerializer.Deseralize with an appropriate Stream, TextReader, or XmlReader, casting the return type back to your root object.
Full sample.
The type to serialize:
[XmlRoot("customer")]
public class CustomerData
{
// Must have a parameterless public constructor
public CustomerData()
{
}
[XmlElement("name")]
public string Name { get; set; }
[XmlElement("city")]
public string City { get; set; }
[XmlElement("company")]
public string Company { get; set; }
public override string ToString()
{
return
"Name=[" + this.Name + "] " +
"City=[" + this.City + "] " +
"Company=[" + this.Company + "]";
}
}
The code to read/write the data:
// Initialize the serializer to write and read the data
XmlSerializer serializer = new XmlSerializer(typeof(CustomerData));
// Initialize the data to serialize
CustomerData dataToWrite = new CustomerData()
{
Name = "Joel Spolsky",
City = "New York",
Company = "Fog Creek Software"
};
// Write it out
XmlWriterSettings settings = new XmlWriterSettings() { Indent = true };
using (XmlWriter writer = XmlTextWriter.Create("customer.xml", settings))
{
serializer.Serialize(writer, dataToWrite);
}
// Read it back in
CustomerData dataFromFile = null;
using (XmlReader reader = XmlTextReader.Create("customer.xml"))
{
dataFromFile = (CustomerData)serializer.Deserialize(reader);
}
Console.WriteLine(dataFromFile);

Categories

Resources