views:

351

answers:

3

Hey,

I have implemented a csv file builder that takes in an xml document applies a xsl transform to it and appends it to a file.

public class CsvBatchPrinter : BaseBatchPrinter
{
 public CsvBatchPrinter() : base(".csv")
 {
  RemoveDiatrics = false;
 }

 protected override void PrintDocuments(System.Collections.Generic.List<XmlDocument> documents, string xsltFileName, string directory, string tempImageDirectory)
 {
  base.PrintDocuments(documents, xsltFileName, directory, tempImageDirectory);

  foreach (var file in new DirectoryInfo(tempImageDirectory).GetFiles())
  {
   var destination = directory + file.Name;
   if (!File.Exists(destination))
    file.CopyTo(destination);
  }
 }

 protected override void PrintDocument(XmlDocument document, string xsltFileName, string directory, string tempImageDirectory)
 {
  StringUtils.EscapeQuotesInXmlNode(document);
  if (RemoveDiatrics)
  {
   var docXml = StringUtils.RemoveDiatrics(document.OuterXml);
   document = new XmlDocument();
   document.LoadXml(docXml);
  }

  using (var writer = new StreamWriter(string.Format("{0}{1}{2}", directory, "batch", FileExtension), true, Encoding.ASCII))
  {
   Transform(document, xsltFileName, writer);
  }
 }

 public bool RemoveDiatrics { get; set; }
}

I have a large number of xml documents to add to this csv file and after multiple calls to it, it occasionally throws an IOException The process cannot access the file 'batch.csv' because it is being used by another process.

Would this be be some sort of locking issue?

Could it be solved by:

lock(this)
{
    using (var writer = new StreamWriter(string.Format("{0}{1}{2}", directory, "batch", FileExtension), true, Encoding.ASCII))
    {
        Transform(document, xsltFileName, writer);
    }
}

EDIT:

Here is my stack trace:

at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)
at System.IO.StreamWriter.CreateFile(String path, Boolean append)
at System.IO.StreamWriter..ctor(String path, Boolean append, Encoding encoding, Int32 bufferSize)
at System.IO.StreamWriter..ctor(String path, Boolean append, Encoding encoding)
at Receipts.Facade.Utilities.BatchPrinters.CsvBatchPrinter.PrintDocument(XmlDocument document, String xsltFileName, String directory, String tempImageDirectory) in CsvBatchPrinter.cs:line 37
at Receipts.Facade.Utilities.BatchPrinters.BaseBatchPrinter.PrintDocuments(List`1 documents, String xsltFileName, String directory, String tempImageDirectory) in BaseBatchPrinter.cs:line 30
at Receipts.Facade.Utilities.BatchPrinters.CsvBatchPrinter.PrintDocuments(List`1 documents, String xsltFileName, String directory, String tempImageDirectory) in CsvBatchPrinter.cs:line 17
at Receipts.Facade.Utilities.BatchPrinters.BaseBatchPrinter.Print(List`1 documents, String xsltFileName, String destinationDirectory, String tempImageDirectory) in BaseBatchPrinter.cs:line 23
at Receipts.Facade.Modules.FinanceDocuments.FinanceDocumentActuator`2.printXmlFiles(List`1 xmlDocuments, String tempImagesDirectory) in FinanceDocumentActuator.cs:line 137

and my base class:

public abstract class BaseBatchPrinter : IBatchPrinter
{
 private static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
 protected readonly string FileExtension;

 protected BaseBatchPrinter(string fileExtension)
 {
  FileExtension = fileExtension;
 }

 public void Print(List<XmlDocument> documents, string xsltFileName, string destinationDirectory, string tempImageDirectory)
 {
  Log.InfoFormat("Printing to directory: {0}", destinationDirectory);
  PrintDocuments(documents, xsltFileName, destinationDirectory, tempImageDirectory);
 }

 protected virtual void PrintDocuments(List<XmlDocument> documents, string xsltFileName, string directory, string tempImageDirectory)
 {
  foreach (var document in documents)
  {
   PrintDocument(document, xsltFileName, directory, tempImageDirectory);
  }
 }

 /// <summary>
 /// Needs to Call Transform(XmlDocument document, string xsltFileName, string directory)
 /// </summary>
 protected abstract void PrintDocument(XmlDocument document, string xsltFileName, string directory, string tempImageDirectory);

 protected void Transform(XmlDocument document, string xsltFileName, StreamWriter writer)
 {
  //TODO: look into XslCompiledTransform to replace the XslTransform
  var xslTransform = new XslTransform();
  xslTransform.Load(xsltFileName);
  xslTransform.Transform(createNavigator(document), null, writer);
 }

 protected string CreateFileName(string directory, XmlDocument doc)
 {
  var conId = createNavigator(doc).SelectSingleNode(Config.SELECT_CONSTITUENT_ID_XPATH).Value;
  return string.Format(@"{0}{1}{2}", directory, conId, FileExtension.IndexOf('.') > -1 ? FileExtension : "." + FileExtension);
 }

 protected XPathNavigator createNavigator(XmlDocument document)
 {
  return document.DocumentElement == null ? document.CreateNavigator() : document.DocumentElement.CreateNavigator();
 }
}

Cheers.

A: 

Only one thing can cause this. Another process is writing to the file. The other process could even be your own.

I suggest you add try/catch blocks around the areas that can write that file. In the catch block, throw a new exception, adding the full file path:

var filePath = string.Format("{0}{1}{2}", directory, "batch", FileExtension);
try {
    using (var writer = new StreamWriter(filepath, true, Encoding.ASCII)) {
        Transform(...);
    }
}
catch (IOException ex) {
    throw new Exception(
        String.Format("Exception while transforming file {0}", filePath),
        ex);
}

Post the complete exception you receive, including any InnerException and stack trace.

You also want to make sure of whether your file copy could even be writing to batch.csv.

John Saunders
I removed the full file path was for brevity. And yes the only process that was accessing the file at all was my process, which is using a single Thread.
zonkflut
You may have only one thread, but you do have a stack, and you have overloaded methods. Try my suggestion and look at the stack trace.
John Saunders
I'm almost out of ideas, but is there any chance that CreateFileName could create the same filename as used by your overridden PrintDocument method? Also, get a copy of Process Monitor from http://technet.microsoft.com/en-us/sysinternals/default.aspx and watch your program when it runs.
John Saunders
The CreateFileName method is not used by the CsvBatchPrinter at all. I'll get Process Monitor and have a look.Cheers
zonkflut
What is the file size of the file that you are writing?
Kane
should it matter? but it ends up close to 20mb by the time it has been called a few hundred times.
zonkflut
A: 

If you are willing, how about trying the following mods to the code to aid more in finding the fault than as a permanent fix.

Add an overload to print document which takes a StreamWriter param. Modify base.PrintDocuments as follows:

using ( var writer = new StreamWriter ( string.Format ( "{0}{1}{2}", directory, "batch", FileExtension ), true, Encoding.ASCII ) )
{
    foreach ( var document in documents )
    {
        PrintDocument ( document, xsltFileName, directory, tempImageDirectory, writer );
    }
}

protected override void PrintDocument ( XmlDocument document, string xsltFileName, string directory, string tempImageDirectory, StreamWriter writer )
{
    if ( RemoveDiatrics )
    {
        var docXml = document.OuterXml;
        document = new XmlDocument ( );
        document.LoadXml ( docXml );
    }

   Transform ( document, xsltFileName, writer );        
}

If this fails...always the pessimist...I would be looking at Transform as the a possible culprit, though I know it does nothing with the TextWriter other than, well, write with it. Anyway, give it a shot if you have time, creating the writer once may be the solution (quick fix), let us know if it works.

Simon Wilson
A: 

Hey all thanks for your responses.

I have come up with a solution that works for me. A little bit hacky but it does the job.

protected override void PrintDocument(XmlDocument document, string xsltFileName, string directory, string tempImageDirectory)
{
    StringUtils.EscapeQuotesInXmlNode(document);
if (RemoveDiatrics)
{
    var docXml = StringUtils.RemoveDiatrics(document.OuterXml);
    document = new XmlDocument();
    document.LoadXml(docXml);
}

IOException ex = null;
for (var attempts = 0; attempts < 10; attempts++)
{
    ex = tryWriteToFile(document, directory, xsltFileName);
    if (ex == null)
        break;
}

if (ex != null)
    throw new ApplicationException("Cannot write to file", ex);
}

private IOException tryWriteToFile(XmlDocument document, string directory, string xsltFileName)
{
try
{
    using (var writer = new StreamWriter(new FileStream(string.Format("{0}{1}{2}", directory, "batch", FileExtension), FileMode.Append, FileAccess.Write, FileShare.Read), Encoding.ASCII))
    {
        Transform(document, xsltFileName, writer);
 writer.Close();
    }
    return null;
}
catch (IOException e)
{
    return e;
}
}

Basically the idea behind it is to attempt to run it a couple of times and if the problem persists throw the error.

Gets me through the issue

zonkflut