37

The System.Exception.HResult property is protected. How can I peek inside an exception and get the HResult without resorting to reflection or other ugly hacks?


Here's the situation:
I want to write a backup tool, which opens and reads files on a system. I open the file with FileAccess.Read and FileShare.ReadWrite, according to this guidance, because I don't care if the file is open for writing at the time I read it.

In some cases, when a file I am reading is open by another app, the System.IO.FileStream.Read() method throws a System.IO.IOException, "The process cannot access the file because another process has locked a portion of the file". This is error 33, or I think HResult 0x80070021. [EDIT: I believe this can be returned when another process calls LockFileEx to lock a byte range within a file.]

I'd like to pause and retry when I get this error. I think this is the appropriate action to take here. If the locking process releases the byte-range lock quickly, then I can proceed reading the file.

How can I distinguish an IOException for this reason, from others? I can think of these ways:

  • private reflection - don't wanna do that. Perf will stink.
  • call Exception.ToString() and parse the string. Feels hacky. Won't work in i18n versions.

I don't like these options. Isn't there a better, cleaner way?


I just searched around and found System.Runtime.InteropServices.Marshal.GetHRForException. Will that return a uint like 0x80070021?

Community
  • 1
  • 1
Cheeso
  • 189,189
  • 101
  • 473
  • 713
  • 4
    > private reflection - don't wanna do that. Perf will stink. - Exception perf stinks anyway, so I wouldn't worry about the perf aspect. Reflection does - however - require FullTrust, is ugly, and is unsupported and prone to breakage - which is why you shouldn't do it. – Mark Brackett Aug 07 '09 at 20:14

6 Answers6

58

For .Net Framework 4.5 and above, you can use the Exception.HResult property:

int hr = ex.HResult;

For older versions, you can use Marshal.GetHRForException to get back the HResult, but this has significant side-effects and is not recommended:

int hr = Marshal.GetHRForException(ex);
Mitch
  • 21,223
  • 6
  • 63
  • 86
JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
  • Thanks a lot! This really ease the development in case of C#/COM interoperability. – rds Dec 10 '10 at 10:16
  • 2
    +1 Eww, requires full trust... But it is a solution nonetheless. – reSPAWNed Jul 17 '12 at 09:00
  • 3
    Beware of side-effects: "Note that the **GetHRForException** method sets the **IErrorInfo** of the current thread. This can cause unexpected results for methods like the **ThrowExceptionForHR** methods that default to using the **IErrorInfo** of the current thread if it is set." – HugoRune Jun 17 '13 at 09:38
  • 1
    I strongly recommend you do not use GetHRForException. It caused a very strange problem that took us months to understand: http://stackoverflow.com/a/40242031/5844190 – Alsty Oct 25 '16 at 14:01
11

For what it's worth, System.Exception.HResult is no longer protected in .NET 4.5 -- only the setter is protected. That doesn't help with code that might be compiled with more than one version of the framework.

UweBaemayr
  • 1,861
  • 1
  • 18
  • 21
5

You can also use the ISerializable interface:

static class IOExceptionExtensions
{
    public static int GetHResult(this IOException ex)
    {
        var info = new SerializationInfo(typeof (IOException), new FormatterConverter());
        ex.GetObjectData(info, new StreamingContext());
        return info.GetInt32("HResult");
    }
}
Maxence
  • 12,868
  • 5
  • 57
  • 69
0

Necromancing.
Or you can just fetch the protected property by reflection:

private static int GetHresult(System.Exception exception)
{
    int retValue = -666;

    try
    {
        System.Reflection.PropertyInfo piHR = typeof(System.Exception).GetProperty("HResult", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);

        if (piHR != null)
        {
            object o = piHR.GetValue(exception, null);
            retValue = System.Convert.ToInt32(o);
        }
    }
    catch (Exception ex)
    {
    }

    return retValue;
}
Stefan Steiger
  • 78,642
  • 66
  • 377
  • 442
0

Does CanRead property help in this case?
i.e. call CanRead, if that returns true, call Read()

shahkalpesh
  • 33,172
  • 3
  • 63
  • 88
  • Nope, CanRead is true. I believe 80070021 is a transient error. If I am reading the doc correctly, to handle it the recommended practice is to "wait a while and retry." – Cheeso Jun 13 '09 at 21:47
  • Is it possible that you opened the file for reading & someone else opened it as well (using FileShare.Read), the 1st caller can't read it anymore? Is that what you mean by transient? – shahkalpesh Jun 13 '09 at 21:50
  • No, what I mean is, another process has called FileLock or FileLockEx (http://msdn.microsoft.com/en-us/library/aa365203.aspx) on the file to lock a range within the file. This is sometimes called a byte-range lock. At some point the locking process will release the range lock. That is what I mean by "Transient." – Cheeso Jun 13 '09 at 22:23
  • Thanks Cheeso. Coming from a VB background, I thought that file can be locked completely by 1 reader. Never thought that a range of characters can be locked for reading. – shahkalpesh Jun 13 '09 at 22:42
0

Have you profiled either of these cases? I'd imagine that the reflection method isn't all that slow, especially relative to all the other works your app will be doing and how often this exception will be likely to occur.

If it turns out to be a bottleneck you can look into caching some of the reflection operations or generate dynamic IL to retrieve the property.

Kevin Pullin
  • 13,122
  • 3
  • 24
  • 33