Reading from XML file in memory using XmlSerializer - c#

I use code below to open zip archive in memory.
using (var leagueFile = File.OpenRead(openFileDialog.FileName))
using (var package = new ZipArchive(leagueFile, ZipArchiveMode.Read))
{
foreach (var team in package.Entries)
{
if (team.Name.EndsWith(".xml"))
{
_xmlHandler.Import<Player>(team.FullName, Encoding.UTF8);
//...
}
}
}
When I try to deserialize with my Import<T>() method, app crashes due to file wasn't found.
public T Import<T>(string fileName, Encoding encoding) where T : class, new()
{
var serializer = new XmlSerializer(typeof(T));
serializer.UnknownNode += serializer_UnknownNode;
serializer.UnknownAttribute += serializer_UnknownAttribute;
var reader = XmlReader.Create(new StreamReader(fileName, encoding));
var po = (T)serializer.Deserialize(reader);
return po;
}
The problem is that app is searching for fileName in the bin directory of application. Not in stream (?) of zip archive. Is there a way to do this with XmlSerializer class?

Modify your code as below:
using (var leagueFile = File.OpenRead(openFileDialog.FileName))
using (var package = new ZipArchive(leagueFile, ZipArchiveMode.Read))
{
foreach (var team in package.Entries)
{
if (team.Name.EndsWith(".xml"))
{
using(var xmlStream = team.Open())
{
_xmlHandler.Import<Player>(xmlStream, Encoding.UTF8);
//...
}
}
}
}
public T Import<T>(Stream input, Encoding encoding) where T : class, new()
{
var serializer = new XmlSerializer(typeof(T));
serializer.UnknownNode += serializer_UnknownNode;
serializer.UnknownAttribute += serializer_UnknownAttribute;
var reader = XmlReader.Create(input);
var po = (T)serializer.Deserialize(reader);
return po;
}

Related

C# ZipArchive - How to nest internal .zip files without writing to disk

I need to create a zip file in memory, then send the zip file to the client. However, there are cases where the created zip file will need to contain other zip files that were also generated in memory. For instance, the file structure might look like this:
SendToClient.zip
InnerZip1.zip
File1.xml
File2.xml
InnerZip2.zip
File3.xml
File4.xml
I've been attempting to use the System.IO.Compression.ZipArchive library. I cannot use the System.IO.Compression.ZipFile library because my project's version of .NET is not compatible with it.
Here's an example of what I've tried.
public Stream GetMemoryStream() {
var memoryStream = new MemoryStream();
string fileContents = "Lorem ipsum dolor sit amet";
string entryName = "Lorem.txt";
string innerZipName = "InnerZip.zip";
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) {
ZipArchiveEntry entry = archive.CreateEntry(Path.Combine(innerZipName, entryName), CompressionLevel.Optimal);
using (var writer = new StreamWriter(entry.Open())) {
writer.Write(fileContents);
}
}
return memoryStream
}
However, this just puts Lorem.txt in a folder called "Inner.zip" (instead of in an actual zip file).
I can create an empty inner zip file if I create an entry called "Inner.zip" without writing to it. I can't add anything to it, though, and writing to an entry called "Inner.zip\Lorem.txt" afterward just creates a folder again (alongside the identically named empty .zip file).
I've also tried creating a separate archive, serializing it with a memory stream, then writing that to the original archive as a .zip.
public Stream CreateOuterZip() {
var memoryStream = new MemoryStream();
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) {
ZipArchiveEntry entry = archive.CreateEntry("Outer.zip", CompressionLevel.NoCompression);
using (var writer = new BinaryWriter(entry.Open())) {
writer.Write(GetMemoryStream().ToArray());
}
}
return memoryStream;
}
This just creates an invalid .zip file that windows doesn't know how to open, though.
Thanks in advance!
So I created a FileStream instead of a MemoryStream so the code can be tested easier
public static Stream CreateOuterZip()
{
string fileContents = "Lorem ipsum dolor sit amet";
// Final zip file
var fs = new FileStream(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SendToClient.zip"), FileMode.OpenOrCreate);
// Create inner zip 1
var innerZip1 = new MemoryStream();
using (var archive = new ZipArchive(innerZip1, ZipArchiveMode.Create, true))
{
var file1 = archive.CreateEntry("File1.xml");
using (var writer = new BinaryWriter(file1.Open()))
{
writer.Write(fileContents); // Change fileContents to real XML content
}
var file2 = archive.CreateEntry("File2.xml");
using (var writer = new BinaryWriter(file2.Open()))
{
writer.Write(fileContents); // Change fileContents to real XML content
}
}
// Create inner zip 2
var innerZip2 = new MemoryStream();
using (var archive = new ZipArchive(innerZip2, ZipArchiveMode.Create, true))
{
var file3 = archive.CreateEntry("File3.xml");
using (var writer = new BinaryWriter(file3.Open()))
{
writer.Write(fileContents); // Change fileContents to real XML content
}
var file4 = archive.CreateEntry("File4.xml");
using (var writer = new BinaryWriter(file4.Open()))
{
writer.Write(fileContents); // Change fileContents to real XML content
}
}
using (var archive = new ZipArchive(fs, ZipArchiveMode.Create, true))
{
// Create inner zip 1
var innerZipEntry = archive.CreateEntry("InnerZip1.zip");
innerZip1.Position = 0;
using (var s = innerZipEntry.Open())
{
innerZip1.WriteTo(s);
}
// Create inner zip 2
var innerZipEntry2 = archive.CreateEntry("InnerZip2.zip");
innerZip2.Position = 0;
using (var s = innerZipEntry2.Open())
{
innerZip2.WriteTo(s);
}
}
fs.Close();
return fs; // The file is written, can probably just close this
}
You can obviously modify this method to return a MemoryStream, or change the method to Void to just have the zip file written out to disk
You should create ZipArchive for internal zip file also. Write it to stream (memorystream). And after write this stream as general stream into main zip.
static Stream Inner() {
var memoryStream = new MemoryStream();
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) {
var demoFile = archive.CreateEntry("foo2.txt");
using (var entryStream = demoFile.Open())
using (var streamWriter = new StreamWriter(entryStream)) {
streamWriter.Write("Bar2!");
}
}
return memoryStream;
}
static void Main(string[] args) {
using (var memoryStream = new MemoryStream()) {
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) {
var demoFile = archive.CreateEntry("foo.txt");
using (var entryStream = demoFile.Open())
using (var streamWriter = new StreamWriter(entryStream)) {
streamWriter.Write("Bar!");
}
var zip = archive.CreateEntry("inner.zip");
using (var entryStream = zip.Open()) {
var inner = Inner();
inner.Seek(0, SeekOrigin.Begin);
inner.CopyTo(entryStream);
}
}
using (var fileStream = new FileStream(#"d:\test.zip", FileMode.Create)) {
memoryStream.Seek(0, SeekOrigin.Begin);
memoryStream.CopyTo(fileStream);
}
}
Thanks to this answer.

Update an existing ooxml file with C# and System.IO.Compression.ZipArchive

I am trying to update an nearly empty docx/ooxml file. I dont get an exception, the code is running through but the file is not modified.
I think I am missing something :/
The code below is not the actual code, but a simplified example. It is also running without an exception but the file is not updated.
using (var fs = new FileStream(docxFile, FileMode.Open))
{
var archive = new ZipArchive(fs, ZipArchiveMode.Update);
foreach (var entry in archive.Entries)
{
if (entry.Name == "[Content_Types].xml")
{
using (var entryStream = entry.Open())
{
var doc = new XmlDocument();
doc.Load(entryStream);
var element = doc.CreateElement("Override");
element.SetAttribute("PartName", "/docProps/example.xml");
doc.DocumentElement.AppendChild(element);
doc.Save(entryStream);
entryStream.Flush();
}
break;
}
}
}
Seems like it was late yesterday :(
Either use using with the ZipArchive instance or calling Dispose after the loop will do the job ;)
using (var fs = new FileStream(docxFile, FileMode.Open))
{
using (var archive = new ZipArchive(fs, ZipArchiveMode.Update))
{
foreach (var entry in archive.Entries)
{
if (entry.Name == "[Content_Types].xml")
{
using (var entryStream = entry.Open())
{
var doc = new XmlDocument();
doc.Load(entryStream);
var element = doc.CreateElement("Override");
element.SetAttribute("PartName", "/docProps/example.xml");
doc.DocumentElement.AppendChild(element);
doc.Save(entryStream);
entryStream.Flush();
}
break;
}
}
}
}

Saving XDocument to file in PCL C#

I'm working within my PCL library and need to serialise a class and output to a file. I'm very short on space, so don't have the space for PCLStorage.
Currently I'm using this for the serialisation. IFilePath returns a file path from the non-PCL part.
IFilePath FilePath;
public void SerializeObject<T>(T serializableObject, string fileName)
{
if (serializableObject == null) { return; }
try
{
using (var ms = new MemoryStream())
{
var xmlDocument = new XDocument();
using (var writer = xmlDocument.CreateWriter())
{
var serialize = new DataContractSerializer(typeof(T));
serialize.WriteObject(writer, serializableObject);
xmlDocument.Save(ms, SaveOptions.None);
}
}
}
catch (Exception ex)
{
//Log exception here
}
}
When I try to save, nothing is showing. I have a feeling it's because I'm not outputting the stream to a file, but I'm at a loss as how to do this.
You are trying to save to a file, an action which is specific for each platform.
PCLStorage is implementing this functionality for each platform and this is what you will have to do also if you can"t use it.
In you case what you have to do is to create the stream (in each platform) in your non pcl code and then pass it to your function which will look like this:
public void SerializeObject<T>(T serializableObject, Stream fileStream)
{
if (serializableObject == null) { return; }
try
{
var xmlDocument = new XDocument();
using (var writer = xmlDocument.CreateWriter())
{
var serialize = new DataContractSerializer(typeof(T));
serialize.WriteObject(writer, serializableObject);
xmlDocument.Save(fileStream, SaveOptions.None);
}
}
catch (Exception ex)
{
//Log exception here
}
}
more on pcl here.
Problem is that your variable ms in using (var ms = new MemoryStream()) is empty and does not point to any file location of which MemoryStream does not receive a filepath as argument. I propose you use a StreamWriter instead and pass the your FileStream to it. Example
Use your fileName to create a FileStream which inherits from the Stream class then replace the Memory stream with the newly created filestream like this.
using(FileStream stream = File.OpenWrite(fileName))
{
var xmlDocument = new XDocument();
using (var writer = xmlDocument.CreateWriter())
{
var serialize = new DataContractSerializer(typeof(T));
serialize.WriteObject(writer, serializableObject);
xmlDocument.Save(stream, SaveOptions.None);
}
}
Hope this helps.

Can I decompress and deserialize a file using streams?

My application serializes an object using Json.Net, compresses the resulting JSON, then saves this to file. Additionally the application can load an object from one of these files. These objects can be tens of Mb in size and I'm concerned about memory usage, due to the way the existing code creates large strings and byte arrays:-
public void Save(MyClass myObject, string filename)
{
var json = JsonConvert.SerializeObject(myObject);
var bytes = Compress(json);
File.WriteAllBytes(filename, bytes);
}
public MyClass Load(string filename)
{
var bytes = File.ReadAllBytes(filename);
var json = Decompress(bytes);
var myObject = JsonConvert.DeserializeObject<MyClass>(json);
}
private static byte[] Compress(string s)
{
var bytes = Encoding.Unicode.GetBytes(s);
using (var ms = new MemoryStream())
{
using (var gs = new GZipStream(ms, CompressionMode.Compress))
{
gs.Write(bytes, 0, bytes.Length);
gs.Close();
return ms.ToArray();
}
}
}
private static string Decompress(byte[] bytes)
{
using (var msi = new MemoryStream(bytes))
{
using (var mso = new MemoryStream())
{
using (var gs = new GZipStream(msi, CompressionMode.Decompress))
{
gs.CopyTo(mso);
return Encoding.Unicode.GetString(mso.ToArray());
}
}
}
}
I was wondering if the Save/Load methods could be replaced with streams? I've found examples of using streams with Json.Net but am struggling to get my head around how to fit in the additional compression stuff.
JsonSerializer has methods to serialize from a JsonTextReader and to a StreamWriter, both of which can be created on top of any sort of stream, including a GZipStream. Using them, you can create the following extension methods:
public static partial class JsonExtensions
{
// Buffer sized as recommended by Bradley Grainger, https://faithlife.codes/blog/2012/06/always-wrap-gzipstream-with-bufferedstream/
// But anything smaller than 85,000 bytes should be OK, since objects larger than that go on the large object heap. See:
// https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/large-object-heap
const int BufferSize = 8192;
// Disable writing of BOM as per https://datatracker.ietf.org/doc/html/rfc8259#section-8.1
static readonly Encoding DefaultEncoding = new UTF8Encoding(false);
public static void SerializeToFileCompressed(object value, string path, JsonSerializerSettings settings = null)
{
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
SerializeCompressed(value, fs, settings);
}
public static void SerializeCompressed(object value, Stream stream, JsonSerializerSettings settings = null)
{
using (var compressor = new GZipStream(stream, CompressionMode.Compress))
using (var writer = new StreamWriter(compressor, DefaultEncoding, BufferSize))
{
var serializer = JsonSerializer.CreateDefault(settings);
serializer.Serialize(writer, value);
}
}
public static T DeserializeFromFileCompressed<T>(string path, JsonSerializerSettings settings = null)
{
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
return DeserializeCompressed<T>(fs, settings);
}
public static T DeserializeCompressed<T>(Stream stream, JsonSerializerSettings settings = null)
{
using (var compressor = new GZipStream(stream, CompressionMode.Decompress))
using (var reader = new StreamReader(compressor))
using (var jsonReader = new JsonTextReader(reader))
{
var serializer = JsonSerializer.CreateDefault(settings);
return serializer.Deserialize<T>(jsonReader);
}
}
}
See Performance Tips: Optimize Memory Usage in the Json.NET documentation.
For those looking for an idea how to use the extensions from #dbc in uwp apps, I modified the code to this - where the StorageFile is a file you have access to write to.
public static async void SerializeToFileCompressedAsync(object value, StorageFile file, JsonSerializerSettings settings = null)
{
using (var stream = await file.OpenStreamForWriteAsync())
SerializeCompressed(value, stream, settings);
}
public static void SerializeCompressed(object value, Stream stream, JsonSerializerSettings settings = null)
{
using (var compressor = new GZipStream(stream, CompressionMode.Compress))
using (var writer = new StreamWriter(compressor))
{
var serializer = JsonSerializer.CreateDefault(settings);
serializer.Serialize(writer, value);
}
}
public static async Task<T> DeserializeFromFileCompressedAsync<T>(StorageFile file, JsonSerializerSettings settings = null)
{
using (var stream = await file.OpenStreamForReadAsync())
return DeserializeCompressed<T>(stream, settings);
}
public static T DeserializeCompressed<T>(Stream stream, JsonSerializerSettings settings = null)
{
using (var compressor = new GZipStream(stream, CompressionMode.Decompress))
using (var reader = new StreamReader(compressor))
using (var jsonReader = new JsonTextReader(reader))
{
var serializer = JsonSerializer.CreateDefault(settings);
return serializer.Deserialize<T>(jsonReader);
}
}

Simultaneously writing and validating XML

I have a Write method that serializes objects which use XmlAttributes. It's pretty standard like so:
private bool WriteXml(DirectoryInfo dir)
{
var xml = new XmlSerializer(typeof (Composite));
_filename = Path.Combine(dir.FullName, _composite.Symbol + ".xml");
using (var xmlFile = File.Create(_filename))
{
xml.Serialize(xmlFile, _composite);
}
return true;
}
Apart from trying to read the file I have just written out (with a Schema validator), can I perform XSD validation WHILE the XML is being written?
I can mess around with memory streams before writing it to disk, but it seems in .Net there is usually an elegant way of solving most problems.
The way I've done it is like this for anyone interested:
public Composite Read(Stream stream)
{
_errors = null;
var settings = new XmlReaderSettings();
using (var fileStream = File.OpenRead(XmlComponentsXsd))
{
using (var schemaReader = new XmlTextReader(fileStream))
{
settings.Schemas.Add(null, schemaReader);
settings.ValidationType = ValidationType.Schema;
settings.ValidationEventHandler += OnValidationEventHandler;
using (var xmlReader = XmlReader.Create(stream, settings))
{
var serialiser = new XmlSerializer(typeof (Composite));
return (Composite) serialiser.Deserialize(xmlReader);
}
}
}
}
private ValidationEventArgs _errors = null;
private void OnValidationEventHandler(object sender, ValidationEventArgs validationEventArgs)
{
_errors = validationEventArgs;
}
Then instead of writing the XML to file, using a memory stream do something like:
private bool WriteXml(DirectoryInfo dir)
{
var xml = new XmlSerializer(typeof (Composite));
var filename = Path.Combine(dir.FullName, _composite.Symbol + ".xml");
// first write it to memory
var memStream = new MemoryStream();
xml.Serialize(memStream, _composite);
memStream.Position = 0;
Read(memStream);
if (_errors != null)
{
throw new Exception(string.Format("Error writing to {0}. XSD validation failed : {1}", filename, _errors.Message));
}
memStream.Position = 0;
using (var outFile = File.OpenWrite(filename))
{
memStream.CopyTo(outFile);
}
memStream.Dispose();
return true;
}
That way you're always validating against the schema before anything is written to disk.

Categories