다음을 통해 공유


VB.NET: Working with temporary files

Introduction

The .NET Framework provides an abundance of ways to work with creating, modifying and deleting files, here the focus is on creating and modifying files which other processes can not access during program execution along with automatically deleting these files on normal program termination or abnormal termination.

Basic file create/write operation

Hobbyist and students will find using System.IO.File.WriteAllBytes, System.IO.File.WriteAllLinesSystem.WriteAllText and My.Computer.FileSystem.WriteAllText will fit most if not their file write operation needs along with mirror image read methods. Professional developers uses these methods too along with working with a FileStream class.

Simple examples

Dim line = "This is some text in the file."
 
Dim fileName1 As String  = "Test1.txt"
Dim fileStream1 As FileStream = IO.File.Create(fileName1)
 
Dim info As Byte() = New  UTF8Encoding(True).GetBytes(line)
fileStream1.Write(info, 0, info.Length)
fileStream1.Close()
 
Dim fileStream2 As StreamWriter
Dim fileName2 = "Test2.txt"
fileStream2 = My.Computer.FileSystem.OpenTextFileWriter(fileName2, True)
fileStream2.WriteLine(line)
fileStream2.Close()
 
Dim fileName3 = "Test3.txt"
File.WriteAllText(fileName3, line)

 

Advance write/read operations

When a task involves considerable file read/write operations working with one or more files over time, minutes or hours where these files are exposed to users and there is a need to easily clean up afterwards basic methods tend to be problematic. Working with files in this manner a user may open and modify files which can cause unintended consequences within the application along with needing to handle cleanup of files after the operations have completed and there is a need to remove unnecessary files when the application crashes which is always a possibility.

By creating an instance of a FileStream with additional parameters (there are 15 overloads for the constructor)  file operations can be done so that if a user tries to access the file, access will be denied and if the application crashes (except for the computer abruptly losing power).

The following code module provides methods to a) generate a unique file name and create an instance of a FileStream setup to deny access and remove on program termination. Note that in this case GenerateRandomBaseName is hard coded for .xml, this can also be variable as in another method to create a random file name found here as part of the source code for this article.

Imports System.IO
Imports System.Runtime.CompilerServices
 
Namespace LanguageExtensions
  Public Module  ExtensionsMethods
    <DebuggerStepThrough()>
    <Extension()>
    Public Function  GenerateRandomXmlFile(sender As String, length As  Integer) As String
      Return $"{sender}{GenerateRandomBaseName(length)}.XML"
    End Function
    <DebuggerStepThrough()>
    Private Function  GenerateRandomBaseName(length As Integer) As  String
 
      Dim rand = New Random()
 
      Return CStr(Enumerable.Range(0, length).
        Select(Function(index) (Chr(Asc("A") + rand.Next(0, 26)))).
        ToArray)
 
    End Function
 
    ''' <summary>
    ''' Creates a file stream which will be removed from 
    ''' disk when the application closes
    ''' </summary>
    ''' <param name="fileName"></param>
    ''' <returns>FileStream marked for removal when app closes</returns>
    ''' <remarks>
    ''' </remarks>
    Public Function  FileStreamDeleteOnClose(fileName As String) As  FileStream
 
      Dim fileStream As New  FileStream(
        fileName,
        FileMode.Create,
        Security.AccessControl.FileSystemRights.Modify,
        FileShare.None,
        8,
        FileOptions.DeleteOnClose)
 
      File.SetAttributes(
        fileStream.Name,
        File.GetAttributes(fileStream.Name) Or
        FileAttributes.Temporary)
 
      Return fileStream
 
    End Function
 
  End Module
End Namespace

 

Simple example

Create a new file, populate with some XML data in one method then in another method add a new element followed by another method (to simulate what would happen in real life) retrieve the XML data.

Using the method and extension method in the previous code block create an instance of a FileStream in a class.

Privately scoped FileStream in the class.

Private _creator1 As FileStream

Then in the new constructor instantiate the FileStream with a random file name.

''' <summary>
''' Setup FileStream with a random file name marked
''' to remove on app crash or normal close of app.
''' </summary>
Public Sub  New()
 
  _creator1 = FileStreamDeleteOnClose(Path.Combine(
    AppDomain.CurrentDomain.BaseDirectory, "K1".GenerateRandomXmlFile(5)))
 
End Sub

Populate the file with some XML data.

Public Sub  PopulateTempFile()
  Dim xmlData As XDocument =
      <?xml version="1.0" standalone="yes"?>
      <Customers>
        <Customer>
          <CustomerID>100</CustomerID>
          <CompanyName>Alfreds Futterkiste</CompanyName>
        </Customer>
        <Customer>
          <CustomerID>101</CustomerID>
          <CompanyName>Ana Trujillo Emparedados y helados</CompanyName>
        </Customer>
        <Customer>
          <CustomerID>102</CustomerID>
          <CompanyName>Bilido Comidas preparadas</CompanyName>
        </Customer>
        <Customer>
          <CustomerID>103</CustomerID>
          <CompanyName>Centro comercial Moctezuma</CompanyName>
        </Customer>
      </Customers>
 
  Dim byteArray As Byte() = Encoding.ASCII.GetBytes(xmlData.ToString)
 
  _creator1.Write(byteArray, 0, byteArray.Length)
 
End Sub

To add a new element/Customer the following method is executed on the same FileStream. Note the first assertion checks to see if there is data in the stream, if not call the method above.

There are several important things to understand:

  • Data is read from the stream into a StringBuilder which is an efficient method to concatenate strings.
  • Once data has been read in the string from the StringBuilder object is feed to the Parse method of a XDocument using literal notation to access elements the string is then converted to a List(Of Customer).
  • Next a lambda statement is used to get the last primary key and then increment the key by one to be used when writing the new Customer to the stream. Normally this would be a bad idea as outside processes or mix access to the file can cause issues e.g. two customers added at exactly the same time and use the same primary key. This is only possible in this case with poor judgement as outside access is denied. 
  • Using XML literals and embedded expressions an new XML structure is created then written to the stream. Working with XML literals and embedded expressions should be experimented with besides simply constructing structures as done here as both are very powerful and exclusive to VB.NET.
  • There are two custom Delegates used to allow the calling form to get notifications of the work being performed in another class.
Public Sub  AddNewCustomer(newCustomer As Customer)
  If _creator1.Length = 0 Then
    PopulateTempFile()
  End If
 
  _creator1.Position = 0
 
  Dim streamReader As StreamReader
  streamReader = New  StreamReader(_creator1)
  streamReader.BaseStream.Seek(0, SeekOrigin.Begin)
 
  Dim customerDataBuilder As New  StringBuilder
 
  While (streamReader.Peek > -1)
    customerDataBuilder.Append(streamReader.ReadLine())
  End While
 
 
  Dim customersLambdaTyped = (
      From customer In  XDocument.Parse(customerDataBuilder.ToString())...<Customer>).
      Select(Function(customer)
            Return New  Customer With  {
           .Name = customer.<CompanyName>.Value,
           .Identifier = CInt(customer.<CustomerID>.Value)}
          End Function).ToList()
 
 
  '
  ' Get last identifier and increment by 1
  ' Since we have an exclusive lock there are no 
  ' issues of colliding with another section of code
  ' grabbing the same identifier
  '
  Dim id = customersLambdaTyped.Select(Function(customer) customer.Identifier).Max() + 1
 
  newCustomer.Identifier = id
 
  '
  ' Add customer to existing list
  '
  customersLambdaTyped.Add(newCustomer)
 
  '
  ' Transform to XML using xml literals and embedded expressions
  '
  Dim doc As XDocument =
      <?xml version="1.0" standalone="yes"?>
      <Customers><%= From currentCustomer In  customersLambdaTyped Select
        <Customer>
          <CustomerID><%= currentCustomer.Identifier %></CustomerID>
          <CompanyName><%= currentCustomer.Name %></CompanyName>
        </Customer> %>
      </Customers>
 
  doc.Declaration.Version = "1.0"
  doc.Declaration.Encoding = "utf-8"
 
  '
  ' Rewrite the file rather than appending
  '
  Dim byteArray As Byte() = Encoding.ASCII.GetBytes(doc.ToString)
  _creator1.SetLength(0)
  _creator1.Write(byteArray, 0, byteArray.Length)
 
  ExamineCustomersFromXmlFile()
 
End Sub

Moving on to another method to examine the XML with an added Customer.

Public Sub  ExamineCustomersFromXmlFile()
 
  If _creator1.Length = 0 Then
    PopulateTempFile()
  End If
 
  '
  ' Rewind to start of temporary file
  '
  _creator1.Position = 0
 
  '
  ' Read data from  temporary file into a StringBuilder
  ' to parse into either a anonymous type or strong typed
  ' list.
  '
  Dim streamReader As StreamReader
  streamReader = New  StreamReader(_creator1)
  streamReader.BaseStream.Seek(0, SeekOrigin.Begin)
 
  Dim customerDataBuilder As New  StringBuilder
 
  While (streamReader.Peek > -1)
    customerDataBuilder.Append(streamReader.ReadLine())
  End While
 
  '
  ' Example of reading data anonymously and strong typed
  '
 
 
  Dim customersLinqAnonymous = (
      From customer In  XDocument.Parse(customerDataBuilder.ToString())...<Customer>
      Select
        Name = customer.<CompanyName>.Value,
        Identifier = CInt(customer.<CustomerID>.Value)
      ).ToList
 
  Dim customersLambdaTyped = (
      From customer In  XDocument.Parse(customerDataBuilder.ToString())...<Customer>).
      Select(Function(customer)
            Return New  Customer With  {
             .Name = customer.<CompanyName>.Value,
             .Identifier = CInt(customer.<CustomerID>.Value)}
          End Function).ToList()
  
  RaiseEvent CustomersEventHandler(Me, New  CustomerArgs(customersLambdaTyped))
 
End Sub

That is the entire process from start to finish other then the calls used to invoke the methods which are done in a form.

Although the entire example was performed with a XML structure anything well thought out and placed into a file stream can follow a similar pattern. In the repository containing source code there is a short example using comma delimited data for an example.

Crash proof 

As stated before, once the application terminates under normal circumstances the file generated using methods provided will be removed. If the application crashes the file is also removed. To test this the following method Crash will create, write a line of text then crash using Environment.FailFast. No code below that line will execute but the file will not be removed because the FileStream has not been setup to know to remove the file once finished with it.

Public Sub  Crash(line As  String)
  Dim fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Crashed.txt")
 
  Try
    Using fs As  New FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite)
      Using fw As  New StreamWriter(fs)
        fw.Write(line)
      End Using
    End Using
 
    Environment.FailFast(Nothing)
 
    '
    ' never get here
    '
    Dim crashVar = CInt(line)
 
    File.Delete(fileName)
 
  Catch ex As Exception
    '
    ' never get here
    '
    If File.Exists(fileName) Then
      File.Delete(fileName)
    End If
 
  End Try
 
End Sub

 

In contrast the following calls Environment.FailFast with the FileStream setup to remove once the application has terminated. Adding a try-catch will produce the exact same result.

Public Sub  QuickUse(peopleList As List(Of Person), Optional failFast As Boolean  = False)
 
  Dim fileName = Path.GetTempFileName()
 
  If failFast Then
    Console.WriteLine(fileName)
  End If
 
 
  RaiseEvent PeekEventHandler(Me, New  PeekArgs($"Temporary file name: {fileName}"))
  RaiseEvent PeekEventHandler(Me, New  PeekArgs(""))
 
  Using fileStream = New  FileStream(fileName,
                   FileMode.Open,
                   FileAccess.ReadWrite,
                   FileShare.None,
                   4096,
                   FileOptions.DeleteOnClose)
 
    Using streamWriter = New  StreamWriter(fileStream)
 
      '
      ' Write each person comma delimited
      '
      For index As Integer  = 0 To  peopleList.Count - 1
        If index = 1 AndAlso failFast Then
          Console.WriteLine("Fail")
          Environment.FailFast(Nothing)
        End If
        streamWriter.WriteLine($"{peopleList(index).FirstName},{peopleList(index).LastName}")
      Next
 
      streamWriter.Flush()
 
      '
      ' Read data back
      '
 
      Dim streamReader As StreamReader
 
      streamReader = New  StreamReader(fileStream)
      streamReader.BaseStream.Seek(0, SeekOrigin.Begin)
 
      While (streamReader.Peek > -1)
 
        Dim nameParts = streamReader.ReadLine().Split(","c)
 
        RaiseEvent PeekEventHandler(
          Me, New  PeekArgs($"{nameParts(0)} - {nameParts(1)}"))
 
      End While
 
    End Using
 
  End Using
 
End Sub

 

Summary

This how to has provided steps to create a FileStream which can not be altered outside the application and is guaranteed to be removed on program termination. This type of operation is more geared to advance/intensive file task while the basic usage section methods are for 90 percent of file operations a coder will perform typically.

See also

Source code

Clone or download the following GitHub repository.

  • Microsoft Visual Studio 2017 or later is required while older versions of Visual Studio will work but under Visual Studio 2015 will need some changes.