File manipulation is a common and broad thing in the .NET world and in some cases can lead to certain exceptions. In this article, we will focus on files already in use, and see how we can avoid it.
Let’s dive in.
Working With Files in C#
In general, working with files in .NET involves various operations such as reading, writing, and manipulating files on the file system.
When working with files, it’s important to understand that multiple processes or applications can attempt to access the same file simultaneously. This concurrent access can lead to conflicts and issues, which is why it’s important to know if a file is in use before attempting to open, modify, or delete it. This can occur for multiple reasons:
- Another application is currently reading or writing to the file
- Our application has previously opened the file without adequately closing it
- The file is held open by the operating system or an antivirus program for security reasons.
Now that we know more about this exception and how it can occur, let’s try to reproduce it and see how we can avoid it.
Project Setup
We will start by creating a new console application in Visual Studio. We can do it using a Visual Studio template, or using the .NET CLI:
dotnet new console
To showcase the exception, we will mimic the use case where the file was not properly closed after its creation, and we try to access it again:
public static void ReadFile(string fileName) { File.Create(fileName); try { var fileContents = File.ReadAllText(fileName); Console.WriteLine("File contents: " + fileContents); } catch (IOException ex) { Console.WriteLine("Error reading the file: " + ex.Message); } Console.ReadLine(); }
First, we start by defining a new method named ReadFile()
. As its only parameter, we are sending the fileName
which holds the name of the file we wish to manipulate. Inside of the method we utilize the File.Create()
method to generate the desired file. Note that we are using this method without the using
statement. In this case, that is exactly what we want, since under the hood this will create a new stream and won’t close it.
Once we have our file prepared, we proceed to read its contents using File.ReadAllText()
. Lastly, to keep the program’s output window open after it finishes, we utilize Console.ReadLine()
.
Now if we run this application, we encounter an exception in the console output stating that "The process cannot access the file because it is being used by another process".
This occurs because we are trying to access an open file stream that was left open after File.Create()
method.
So let’s now see how we can avoid this exception in the future, or to be more precise, know about it on time.
How to Know if a File Is in Use
Currently, there is no way to modify a file and know if is it in use in a single operation without getting an exception. So a way we can work around it is to check if the file is in use before trying to modify it and react appropriately to the given result.
Using IOException to Know if a File Is in Use
The first thing we can do is to create a small helper method IsFileInUseGeneric()
. This method will check if the file is in use and return the appropriate boolean flag:
public static bool IsFileInUseGeneric(FileInfo file) { try { using var stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None); } catch (IOException) { return true; } return false; }
First, we introduce a new method, IsFileInUseGeneric()
which takes an FileInfo
object as its argument. Inside this method, we utilize a FileStream
to attempt to open the provided FileInfo
object. By doing so we are checking the availability for modifications.
When opening the file, it’s important to note that we specify FileAccess.Read
as the access property. In this way, we ensure that we won’t encounter an exception when attempting to open a read-only file with anything other than read access. If we catch an IOException
during this operation, we return a flag indicating that the file is already in use. In all other cases, we return false
.
After this method is ready, we can modify the original method to include the check:
public static void ReadFile(string fileName) { File.Create(fileName); if (IsFileInUseGeneric(new FileInfo(fileName))) { Console.WriteLine("File is in use by another process."); } else { try { var fileContents = File.ReadAllText(fileName); Console.WriteLine("File contents: " + fileContents); } catch (IOException ex) { Console.WriteLine("Error reading the file: " + ex.Message); } } Console.ReadLine(); }
Now, before doing any operations with the file, we are calling IsFileInUseGeneric()
. As its argument, we are passing a FileInfo
object created from the file we’ve previously generated. If IsFileInUseGeneric()
returns true
, we will log the relevant message. On the other hand, if it returns false, we will proceed with the modification.
Using HResult to Know if a File Is in Use
In the first approach we took we are catching the IOException
exception in the catch block. Although this approach will serve its purpose, it is also generic since we can get IOException
for other reasons that don’t relate to the file being used by another process.
What we can do to make this exception a little bit more exact is to use HResult
property of the IOException.
In general, HResult
is a 32-bit value, divided into three different fields: a severity code, a facility code, and an error code. The severity code indicates whether the return value represents information, warning, or error. The facility code identifies the area of the system responsible for the error. The error code is a unique number that represents the exception. In our case, we are interested in checking the error code:
public static bool IsFileInUse(FileInfo file) { try { using var stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.None); } catch (IOException e) when ((e.HResult & 0x0000FFFF) == 32) { return true; } return false; }
First, we create a small helper method IsFileInUse()
. The logic is the same, we are receiving an FileInfo
object and trying to open it.
The only difference is that now after the exception is caught we are checking HResult
property of it. By using ((e.HResult & 0x0000FFFF) == 32)
we check the lower 16 bits (0x0000FFFF
) of the error code. By following Microsoft documentation, when there is a file-sharing rule violation this value will be 32
.
Now, we have a more precise way to achieve our goal.
When to Use Which Approach?
The first thing that is important to have in mind with both approaches is race conditions. In our case, it refers to the fact that between checking if a file is in use and actually trying to modify it, there is that short period of time that the file can become unavailable again.
Now that we have this in mind, we can see when it is most suitable to choose each approach.
As we already said using generic IOException
will surely catch that the file is in use by another process. But since it is generic, it will also catch all other potential exceptions that can occur. Some of the examples can be that the path doesn’t exist, or we don’t have sufficient permissions to open it. So this approach is most suitable when we want to ensure we can access the file, and the exact reason why is not important to us.
On the other hand, by using HResult
we are making sure that we know the exact reasons why we cannot access the file. In this way, we get the advantage of pinpointing the exact reasons for our inability to access the file. This method provides a more detailed understanding of the underlying issues. Also, it allows us to make informed decisions based on the specific error codes. This can be particularly useful when we require precise knowledge about why the access is being denied.
Conclusion
In this article, we learned different ways to find out if a file is being used by another process. First, we looked at a more generic approach by using IOException. After that, we saw how we could go a little bit more in-depth with the help of its HResult property. Finally, we saw where it is appropriate to use which approach.