I have a function whose role is to return the name of a COM type; it works fine on x64, but crashes in the middle of iterating the COM types in the Excel type library (and MS-Project too, ..and probably other ones I'm not aware of), and I can't seem to be able to do anything to catch the exception it's throwing.
System.Reflection.TargetInvocationException was unhandled
Message: An unhandled exception of type 'System.Reflection.TargetInvocationException' occurred in mscorlib.dll
Additional information: Exception has been thrown by the target of an invocation.
This results in an AccessViolationException, which results in sudden death for the EXCEL.EXE host process.
I was able to add some debug output and identify exactly which member was blowing up:
Querying AsTypeName for member SheetPivotTableBeforeDiscardChanges
Member EXCEL.EXE;Excel.IAppEvents.SheetPivotTableBeforeDiscardChanges
(Object) added successfully
Querying AsTypeName for parameter Sh
Parameter EXCEL.EXE;Excel.IAppEvents.Sh (Object) added successfully
Querying AsTypeName for parameter TargetPivotTable
When the Excel type library starts loading, the entire VBA type library has successfully loaded. If I run my process in a MS-Word host, everything runs fine, so it looks like there's something about the type of the TargetPivotTable parameter of IAppEvents.SheetPivotTableBeforeDiscardChanges that doesn't like what I've written to query it.
case VarEnum.VT_USERDEFINED:
int href;
unchecked
{
if (Marshal.SizeOf(typeof (IntPtr)) == sizeof (long))
{
href = (int) desc.lpValue.ToInt64();
}
else
{
href = desc.lpValue.ToInt32();
}
}
ITypeInfo refTypeInfo;
info.GetRefTypeInfo(href, out refTypeInfo); // <~ boom
return GetTypeName(refTypeInfo);
Here's the relevant part of the stack trace a contributor was able to get from a MS-Project host:
System.Runtime.InteropServices.COMException (0x80004005): Error HRESULT E_FAIL has been returned from a call to a COM component.
at System.Runtime.InteropServices.ComTypes.ITypeInfo.GetRefTypeInfo(Int32 hRef, ITypeInfo& ppTI)
at Rubberduck.Parsing.Symbols.ReferencedDeclarationsCollector.GetTypeName(TYPEDESC desc, ITypeInfo info) in c:\Users\Andrew\Documents\GitHub\Rubberduck\Rubberduck.Parsing\Symbols\ReferencedDeclarationsCollector.cs:line 95
at Rubberduck.Parsing.Symbols.ReferencedDeclarationsCollector.GetTypeName(TYPEDESC desc, ITypeInfo info) in c:\Users\Andrew\Documents\GitHub\Rubberduck\Rubberduck.Parsing\Symbols\ReferencedDeclarationsCollector.cs:line 89
at Rubberduck.Parsing.Symbols.ReferencedDeclarationsCollector.CreateFieldDeclaration(ITypeInfo info, Int32 fieldIndex, DeclarationType typeDeclarationType, QualifiedModuleName typeQualifiedModuleName, Declaration moduleDeclaration)
The code gets a VT_PTR, so it gets a TYPEDESC out of that pointer (line 89), and then recurses; the same code then gets a VT_USERDEFINED, so it gets an ITypeInfo out of the lpValue of the TYPEDESC (line 95), and normally I get that ITypeInfo and fetch the type's name. For some reason in a 32-bit context GetRefTypeInfo refuses to cooperate and sends everything into flames.
For full context, here's the entire relevant code:
private string GetTypeName(TYPEDESC desc, ITypeInfo info)
{
var vt = (VarEnum)desc.vt;
TYPEDESC tdesc;
switch (vt)
{
case VarEnum.VT_PTR:
tdesc = (TYPEDESC)Marshal.PtrToStructure(desc.lpValue, typeof(TYPEDESC));
return GetTypeName(tdesc, info);
case VarEnum.VT_USERDEFINED:
int href;
unchecked
{
if (Marshal.SizeOf(typeof (IntPtr)) == sizeof (long))
{
href = (int) desc.lpValue.ToInt64();
}
else
{
href = desc.lpValue.ToInt32();
}
}
ITypeInfo refTypeInfo;
info.GetRefTypeInfo(href, out refTypeInfo);
return GetTypeName(refTypeInfo);
case VarEnum.VT_CARRAY:
tdesc = (TYPEDESC)Marshal.PtrToStructure(desc.lpValue, typeof(TYPEDESC));
return GetTypeName(tdesc, info) + "()";
default:
string result;
if (TypeNames.TryGetValue(vt, out result))
{
return result;
}
break;
}
return "Object";
}
private string GetTypeName(ITypeInfo info)
{
string typeName;
string docString; // todo: put the docString to good use?
int helpContext;
string helpFile;
info.GetDocumentation(-1, out typeName, out docString, out helpContext, out helpFile);
return typeName;
}
Is there anything in that switch block that looks like it's waiting to blow up in a 32-bit context?
Related
I have created a SSIS custom data flow component that performs the simple task of converting dates from a COBOL mainframe date types, that are in the format of CYYMMDD, to a SQL Server supported format of YYYYMMDD. There are error rows because the incoming COBOL formatted dates can be invalid dates (i.e., 2-29-2015, 4-31-2016, 9-31-2010, etc). These bad dates are from user derived input fields that do not have a date mask on them and I cannot add a date mask because the application belongs to a 3rd party data vendor.
My problem:
The custom component will not re-direct error rows. I found a posting on MSDN that explains how to re-direct error rows:
https://msdn.microsoft.com/en-us/library/ms136009.aspx
I used this code as a guide and modified it to suit my needs of being able to execute over multiple selected input columns (MS's example only contemplates one input column). When I execute the process, I get the following two errors:
[ConvertCobolDates [2]] Error: System.ArgumentException: Value does not fall within the expected range.
at Microsoft.SqlServer.Dts.Pipeline.Wrapper.IDTSBuffer100.DirectErrorRow(Int32 hRow, Int32 lOutputID, Int32 lErrorCode, Int32 lErrorColumn)
at Microsoft.SqlServer.Dts.Pipeline.PipelineBuffer.DirectErrorRow(Int32 outputID, Int32 errorCode, Int32 errorColumn)
at SSIS.Convert.CobolDate.DataFlow.ConvertCobolDateDataFlow.ProcessInput(Int32 inputID, PipelineBuffer buffer)
at Microsoft.SqlServer.Dts.Pipeline.ManagedComponentHost.HostProcessInput(IDTSManagedComponentWrapper100 wrapper, Int32 inputID, IDTSBuffer100 pDTSBuffer, IntPtr bufferWirePacket)
[SSIS.Pipeline] Error: SSIS Error Code DTS_E_PROCESSINPUTFAILED. The ProcessInput method on component "ConvertCobolDates" (2) failed with error code 0x80070057 while processing input "Input" (4). The identified component returned an error from the ProcessInput method. The error is specific to the component, but the error is fatal and will cause the Data Flow task to stop running. There may be error messages posted before this with more information about the failure.
Also, I don't know if I am missing code to specifically redirect the output or if the error handling is being done incorrectly - it does not show up in the Configure Error Output screen. Any assistance is greatly appreciated!
enter image description here
Note: I suspect the error is in one of the following places: ProvideComponentProperties or ProcessInput.
public override void ProvideComponentProperties()
{
try
{
// Perform the base class' method
base.ProvideComponentProperties();
// Start out clean, remove anything put on by the base class
base.RemoveAllInputsOutputsAndCustomProperties();
// Set component information
ComponentMetaData.Name = "ConvertCobolDates";
ComponentMetaData.Description = "Data Flow task that converts COBOL date types into SQL Server Date types for each row flowing through the component.";
ComponentMetaData.ContactInfo = "Contact Info.";
ComponentMetaData.UsesDispositions = true; // As a rule, components should support error dispositions - they make it easier to troubleshoot problems with the data
// Create input objects. This allows the custom component to have a 'Success' input data flow line
IDTSInput100 input = ComponentMetaData.InputCollection.New();
input.Name = "Input";
input.ErrorRowDisposition = DTSRowDisposition.RD_RedirectRow; // Use RD_RedirectRow is ComponentMetaData.UsesDispositions = true. Otherwise, use RD_NotUsed
input.ErrorOrTruncationOperation = "Either a bad date has been detected or an input column(s) has been selected that does not contain dates.";
// Create output objects. This allows the custom component to have a 'Success' output data flow line
IDTSOutput100 output = ComponentMetaData.OutputCollection.New();
output.Name = "Output";
output.SynchronousInputID = input.ID; //Synchronous transformation
output.ExclusionGroup = 1;
// Create output objects. This allows the custom component to have a 'Error' output data flow line
IDTSOutput100 errorOutput = ComponentMetaData.OutputCollection.New();
errorOutput.IsErrorOut = true;
errorOutput.Name = "ErrorOutput";
errorOutput.SynchronousInputID = input.ID;
errorOutput.ExclusionGroup = 1;
}
catch (Exception ex)
{
bool bolCancel = false;
ComponentMetaData.FireError(0, ComponentMetaData.Name, ex.Message, "", 0, out bolCancel);
throw;
}
}
public override void ProcessInput(int inputID, PipelineBuffer buffer)
{
IDTSInput100 input = ComponentMetaData.InputCollection.GetObjectByID(inputID);
// This code assumes the component has two outputs, one the default,
// the other the error output. If the intErrorOutputIndex returned from GetErrorOutputInfo
// is 0, then the default output is the second output in the collection.
int intDefaultOutputID = -1;
int intErrorOutputID = -1;
int intErrorOutputIndex = -1;
int intErrorColumnIndex = -1;
bool bolValidDate = false;
GetErrorOutputInfo(ref intErrorOutputID, ref intErrorOutputIndex);
if (intErrorOutputIndex == 0)
intDefaultOutputID = ComponentMetaData.OutputCollection[1].ID;
else
intDefaultOutputID = ComponentMetaData.OutputCollection[0].ID;
// Process each incoming row
while (buffer.NextRow())
{
try
{
for (int i = 0; i < inputBufferColumnIndex.Length; i++)
{
if (!buffer.IsNull(inputBufferColumnIndex[i]))
{
// Get the name of the current column that is being processed
string strColName = this.ComponentMetaData.InputCollection[0].InputColumnCollection[i].Name;
// Get the current row number that is being processed
int intCurRow = buffer.CurrentRow + 2; // Buffer.CurrentRow is zero bounded and the first row is a header row, which is skipped. Adjust by two to account for this
// Ideally, your code should detect potential exceptions before they occur, rather
// than having a generic try/catch block such as this. However, because the error or truncation implementation is specific to each component,
// this sample focuses on actually directing the row, and not a single error or truncation.
// Get the ID of the PipelineBuffer column that may cause an error. This is required for redirecting error rows
intErrorColumnIndex = this.ComponentMetaData.InputCollection[0].InputColumnCollection[i].ID;
string strCobolDate = buffer.GetString(inputBufferColumnIndex[i]);
string strConvertedCobolDate = ConvertCobolDate(strCobolDate, strColName, intCurRow);
DateTime dtConvertedSQLDate;
// Validate that the date is correct. This detects bad dates (e.g., 2-30-2016, 4-31-2015, etc.) that are inputted from the user
// Throw an error if the date is bad
bolValidDate = DateTime.TryParse(strConvertedCobolDate, out dtConvertedSQLDate);
if (!bolValidDate)
{
// Validation failed, throw an exception and redirect the error row
throw new Exception();
}
else if (bolValidDate)
{
// validation passed. Direct the column back to its corresponding row within the pipeline buffer
buffer[inputBufferColumnIndex[i]] = dtConvertedSQLDate.ToShortDateString();
}
}
}
// Unless an exception occurs, direct the row to the default
buffer.DirectRow(intDefaultOutputID);
}
catch(Exception)
{
// Has the user specified to redirect the row?
if (input.ErrorRowDisposition == DTSRowDisposition.RD_RedirectRow)
{
// Yes, direct the row to the error output.
buffer.DirectErrorRow(intErrorOutputID, 0, intErrorColumnIndex);
}
else if (input.ErrorRowDisposition == DTSRowDisposition.RD_FailComponent || input.ErrorRowDisposition == DTSRowDisposition.RD_NotUsed)
{
// No, the user specified to fail the component, or the error row disposition was not set.
throw new Exception("An error occurred, and the DTSRowDisposition is either not set, or is set to fail component.");
}
else
{
// No, the user specified to ignore the failure so direct the row to the default output.
buffer.DirectRow(intDefaultOutputID);
}
}
}
}
After some pain staking research, and help from a friend, the problem has been identified - the errorCode of 0 (specified on the MSDN article I previosuly posted) that is being passed into the DirectErrorRow function is incorrect [it is actually a negative number (in this case: -1071628258)]. This was a difficult bug to fix because the compiler was outputting a generic out of bounds error without specifying both the argument and value that was out of bounds (see below).
'System.ArgumentException: Value does not fall within the expected range.' ... message truncated to reduce post length
I thought that the compiler error was referring to the actual bad date that it was unable to convert and so I spent all of my time focusing on the intErrorColumnIndex, which the MSDN article lists as:
// TODO: Add code to include the intErrorColumnIndex.
I assumed that the errorCode of 0 that was provided by Microsoft was correct. On a hunch, my friend said to try retrieving the actual error code and that worked! Thus, the errorCode is probably bounded somewhere between negative infinity to -1. Microsoft's MSDN article on directing error rows needs to be corrected.
Me: 1
Microsoft: 0
The solution is as follows in the catch block:
catch(Exception)
{
// Has the user specified to redirect the row?
if (input.ErrorRowDisposition == DTSRowDisposition.RD_RedirectRow)
{
// Yes, get the error code
int DTS_E_ERRORTRIGGEREDREDIRECTION = -1;
unchecked
{
DTS_E_ERRORTRIGGEREDREDIRECTION = (int)0xC020401E;
}
// Direct the row to the error output
buffer.DirectErrorRow(intErrorOutputID, DTS_E_ERRORTRIGGEREDREDIRECTION, intErrorColumnIndex);
}
else if (input.ErrorRowDisposition == DTSRowDisposition.RD_FailComponent || input.ErrorRowDisposition == DTSRowDisposition.RD_NotUsed)
{
// No, the user specified to fail the component, or the error row disposition was not set
throw new Exception("An error occurred, and the DTSRowDisposition is either not set or is set to fail component.");
}
else
{
// No, the user specified to ignore the failure so direct the row to the default output
buffer.DirectRow(intDefaultOutputID);
}
}
I have a very weird issue occurring only in production environment.
The exception has the message
"Delegate to an instance method cannot have null 'this'".
The method where the exception is being thrown is very simple, and worked for a long time, so
the problem must be an obscure dependency in the environment, or something like that...
I'm using ASP.NET Web API, hosted in Azure, and the action method of controller is executed via AJAX.
Here is the code where the exception was thrown:
public class BlacklistService : IBlacklistService
{
public bool Verify(string blacklist, string message)
{
if (string.IsNullOrEmpty(blacklist)) return true;
var split = blacklist.ToLower().Split(';'); // exception is thrown here
return !split.Any(message.Contains);
}
}
Here is the relevant part of stack trace:
at System.MulticastDelegate.ThrowNullThisInDelegateToInstance()
at System.MulticastDelegate.CtorClosed(Object target, IntPtr methodPtr)
at MyApp.Business.Services.BlacklistService.Verify(String blacklist, String message)
at MyApp.Business.Services.ContactMessageFactory.GetVerifiedStatus(String mensagem)
at MyApp.Business.Services.ContactMessageFactory.GetMailMessage(ContactForm contactForm)
at MyApp.Business.ContactEmailService.Send(ContactForm contactForm)
Someone can figure out the possible causes of this exception? Thanks in advance.
The problem lays with the fact that message is actually null. You can reproduce this quite easily:
void Main()
{
Verify("hello", null);
}
public bool Verify(string blacklist, string message)
{
if (string.IsNullOrEmpty(blacklist)) return true;
var split = blacklist.ToLower().Split(';'); // exception is thrown here
return !split.Any(message.Contains);
}
What happens is that message.Contains is passed to Func<string, bool> constructor via the method group conversion, it looks like this:
Func<string, bool> func = ((string)null).Contains;
return !split.Any(func);
And that is what causes MulticastDelegate to go bananas. You can also see that in the generated IL:
IL_0028: ldftn System.String.Contains
IL_002E: newobj System.Func<System.String,System.Boolean>..ctor
IL_0033: call System.Linq.Enumerable.Any
In order for this not to happen, make sure you null check message as well:
public bool Verify(string blacklist, string message)
{
if (string.IsNullOrEmpty(blacklist)) return true;
if (string.IsNullOrEmpty(message)) return false;
var split = blacklist.ToLower().Split(';'); // exception is thrown here
return !split.Any(message.Contains);
}
The delegate having a null this is the method string.Contains() used towards the end, which uses your message variable as the this pointer. In other words, there is a call made where message is null.
Fails when message is null. Can use this
return !split.Any(part => (message != null && message.Contains(part)));
When debugging an application I always get the following error when break on exception is enabled in Visual Studio. This is really bugging me, since we work with break on exception. The funny thing is, that it still works when I continue (the StringCollection is loaded).
The Message is:
Could not load file or assembly 'System.XmlSerializers,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' or
one of its dependencies. The system cannot find the file specified.
Here is the code that is causing the exception (designer generated)
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public global::System.Collections.Specialized.StringCollection Mru {
get {
return ((global::System.Collections.Specialized.StringCollection)(this["Mru"]));
}
set {
this["Mru"] = value;
}
}
I tried to create an empty test application that shows the error, but the exception didn't occur. Our project is huge so it tough to find the cause. Maybe someone on this site has a clue how to solve this.
Just an explanation for why this exception is thrown. You can repro the exception with this sample Windows Forms app. Start by adding a setting named "Setting" of type StringCollection. Click the dots in the Value column and enter a couple of strings. Make the form class code look like this:
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
protected override void OnFormClosing(FormClosingEventArgs e) {
Properties.Settings.Default.Setting[0] = DateTime.Now.ToString();
Properties.Settings.Default.Save();
base.OnFormClosing(e);
}
}
Debug + Exceptions, tick the Thrown checkbox for CLR exceptions. Run the form and close it, the debugger will stop when the exception is thrown. The top of the call stack looks like this:
mscorlib.dll!System.Reflection.Assembly.nLoad(System.Reflection.AssemblyName fileName, string codeBase, System.Security.Policy.Evidence assemblySecurity, System.Reflection.Assembly locationHint, ref System.Threading.StackCrawlMark stackMark, bool throwOnFileNotFound, bool forIntrospection) + 0x2c bytes
mscorlib.dll!System.Reflection.Assembly.InternalLoad(System.Reflection.AssemblyName assemblyRef, System.Security.Policy.Evidence assemblySecurity, ref System.Threading.StackCrawlMark stackMark, bool forIntrospection) + 0x80 bytes
mscorlib.dll!System.Reflection.Assembly.Load(System.Reflection.AssemblyName assemblyRef) + 0x1d bytes
System.Xml.dll!System.Xml.Serialization.TempAssembly.LoadGeneratedAssembly(System.Type type = {Name = "StringCollection" FullName = "System.Collections.Specialized.StringCollection"}, string defaultNamespace = null, out System.Xml.Serialization.XmlSerializerImplementation contract = null) + 0xcd bytes
System.Xml.dll!System.Xml.Serialization.XmlSerializer.XmlSerializer(System.Type type = {Name = "StringCollection" FullName = "System.Collections.Specialized.StringCollection"}, string defaultNamespace = null) + 0x105 bytes
You can see the XmlSerializer class hunting for an assembly that contains the XML serializer for the StringCollection class. The LoadGeneratedAssembly method looks like this with the boring bits removed:
internal static Assembly LoadGeneratedAssembly(Type type, string defaultNamespace, out XmlSerializerImplementation contract)
{
...
AssemblyName parent = GetName(type.Assembly, true);
partialName = Compiler.GetTempAssemblyName(parent, defaultNamespace);
parent.Name = partialName;
parent.CodeBase = null;
parent.CultureInfo = CultureInfo.InvariantCulture;
try
{
serializer = Assembly.Load(parent); // <=== here
}
catch (Exception exception)
{
...
}
....
}
And Compiler.GetTempAssemblyName():
internal static string GetTempAssemblyName(AssemblyName parent, string ns)
{
return (parent.Name + ".XmlSerializers" + (((ns == null) || (ns.Length == 0)) ? "" : ("." + ns.GetHashCode())));
}
This GetTempAssemblyName is the evil-doer in this case. The StringCollection class lives in the System.dll assembly, the method generates the name "System.XmlSerializers". This method is designed to find the assembly for your own classes, the one generated by Sgen.exe. Like WindowsApplication1.XmlSerializers.dll for your sample program. But StringCollection is a class in the .NET Framework, the assembly name it generates just isn't valid. There isn't actually a "System.XmlSerializers.dll" assembly in the framework.
Feedback reports about this behavior at connect.microsoft.com have all been closed with "By Design". It was, the original designers considered the cost of preventing the exception too high and decided to just catch the exception. Which all works fine, the exception is indeed caught. You just happen to see it because you got the Thrown checkbox turned on in the Debug + Exceptions dialog.
Making the Xml serialization code behave differently here is not an option. It would have been easy enough for them to simply filter out types in the System.dll assembly, but that's a potentially never-ending battle, there are a lot more assemblies in the framework. A workaround is to use your own class to store the setting instead of using a StringCollection.
As this really seems to be part of the normal operation (see also:
XmlSerializer giving FileNotFoundException at constructor), I can only offer two workarounds:
Disable this specific exception: goto Debug/Exceptions, click Add, Type: C++ Exceptions, Name: EEFileLoadException (if this is the exception you're seeing), uncheck the Thrown checkbox for this exception.
Change the type of the setting to string and access it e.g. like so:
var mru = Settings.Default.Mru.Split('|');
Settings.Default.Mru = string.Join("|", mru.ToArray());
You are catching too many exceptions, the System.XmlSerializer will always throw this exception as part of it's normal operation, it is caught and handled by the class itself. Change your debugging options to only catch your exceptions, not exceptions that are caught and handled within the .net farmework classes.
When an exception is thrown (while debugging in the IDE), i have the opportunity to view details of the exception:
But in code if i call exception.ToString() i do not get to see those useful details:
System.Data.SqlClient.SqlException (0x80131904): Could not find stored procedure 'FetchActiveUsers'.
[...snip stack trace...]
But Visual Studio has some magic where it can copy the exception to the clipboard:
Which gives the useful details:
System.Data.SqlClient.SqlException was unhandled by user code
Message=Could not find stored procedure 'FetchActiveUsers'.
Source=.Net SqlClient Data Provider
ErrorCode=-2146232060
Class=16
LineNumber=1
Number=2812
Procedure=""
Server=vader
State=62
StackTrace:
[...snip stack trace...]
InnerException:
Well i want that!
What would be the contents of:
String ExceptionToString(Exception ex)
{
//todo: Write useful routine
return ex.ToString();
}
that can accomplish the same magic. Is there a .NET function built in somewhere? Does Exception have a secret method somewhere to convert it to a string?
ErrorCode is specific to ExternalException, not Exception and LineNumber and Number are specific to SqlException, not Exception. Therefore, the only way to get these properties from a general extension method on Exception is to use reflection to iterate over all of the public properties.
So you'll have to say something like:
public static string GetExceptionDetails(this Exception exception) {
var properties = exception.GetType()
.GetProperties();
var fields = properties
.Select(property => new {
Name = property.Name,
Value = property.GetValue(exception, null)
})
.Select(x => String.Format(
"{0} = {1}",
x.Name,
x.Value != null ? x.Value.ToString() : String.Empty
));
return String.Join("\n", fields);
}
(Not tested for compliation issues.)
.NET 2.0 compatible answer:
public static string GetExceptionDetails(this Exception exception)
{
PropertyInfo[] properties = exception.GetType()
.GetProperties();
List<string> fields = new List<string>();
foreach(PropertyInfo property in properties) {
object value = property.GetValue(exception, null);
fields.Add(String.Format(
"{0} = {1}",
property.Name,
value != null ? value.ToString() : String.Empty
));
}
return String.Join("\n", fields.ToArray());
}
I first tried Jason's answer (at the top), which worked pretty well, but I also wanted:
To loop iteratively through inner exceptions and indent them.
Ignore null properties and increases readability of the output.
It includes the metadata in the Data property. (if any) but excludes the Data property itself. (its useless).
I now use this:
public static void WriteExceptionDetails(Exception exception, StringBuilder builderToFill, int level)
{
var indent = new string(' ', level);
if (level > 0)
{
builderToFill.AppendLine(indent + "=== INNER EXCEPTION ===");
}
Action<string> append = (prop) =>
{
var propInfo = exception.GetType().GetProperty(prop);
var val = propInfo.GetValue(exception);
if (val != null)
{
builderToFill.AppendFormat("{0}{1}: {2}{3}", indent, prop, val.ToString(), Environment.NewLine);
}
};
append("Message");
append("HResult");
append("HelpLink");
append("Source");
append("StackTrace");
append("TargetSite");
foreach (DictionaryEntry de in exception.Data)
{
builderToFill.AppendFormat("{0} {1} = {2}{3}", indent, de.Key, de.Value, Environment.NewLine);
}
if (exception.InnerException != null)
{
WriteExceptionDetails(exception.InnerException, builderToFill, ++level);
}
}
Call like this:
var builder = new StringBuilder();
WriteExceptionDetails(exception, builder, 0);
return builder.ToString();
This comprehensive answer handles writing out:
The Data collection property found on all exceptions (The accepted answer does not do this).
Any other custom properties added to the exception.
Recursively writes out the InnerException (The accepted answer does not do this).
Writes out the collection of exceptions contained within the AggregateException.
It also writes out the properties of the exceptions in a nicer order. It's using C# 6.0 but should be very easy for you to convert to older versions if necessary.
public static class ExceptionExtensions
{
public static string ToDetailedString(this Exception exception)
{
if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}
return ToDetailedString(exception, ExceptionOptions.Default);
}
public static string ToDetailedString(this Exception exception, ExceptionOptions options)
{
var stringBuilder = new StringBuilder();
AppendValue(stringBuilder, "Type", exception.GetType().FullName, options);
foreach (PropertyInfo property in exception
.GetType()
.GetProperties()
.OrderByDescending(x => string.Equals(x.Name, nameof(exception.Message), StringComparison.Ordinal))
.ThenByDescending(x => string.Equals(x.Name, nameof(exception.Source), StringComparison.Ordinal))
.ThenBy(x => string.Equals(x.Name, nameof(exception.InnerException), StringComparison.Ordinal))
.ThenBy(x => string.Equals(x.Name, nameof(AggregateException.InnerExceptions), StringComparison.Ordinal)))
{
var value = property.GetValue(exception, null);
if (value == null && options.OmitNullProperties)
{
if (options.OmitNullProperties)
{
continue;
}
else
{
value = string.Empty;
}
}
AppendValue(stringBuilder, property.Name, value, options);
}
return stringBuilder.ToString().TrimEnd('\r', '\n');
}
private static void AppendCollection(
StringBuilder stringBuilder,
string propertyName,
IEnumerable collection,
ExceptionOptions options)
{
stringBuilder.AppendLine($"{options.Indent}{propertyName} =");
var innerOptions = new ExceptionOptions(options, options.CurrentIndentLevel + 1);
var i = 0;
foreach (var item in collection)
{
var innerPropertyName = $"[{i}]";
if (item is Exception)
{
var innerException = (Exception)item;
AppendException(
stringBuilder,
innerPropertyName,
innerException,
innerOptions);
}
else
{
AppendValue(
stringBuilder,
innerPropertyName,
item,
innerOptions);
}
++i;
}
}
private static void AppendException(
StringBuilder stringBuilder,
string propertyName,
Exception exception,
ExceptionOptions options)
{
var innerExceptionString = ToDetailedString(
exception,
new ExceptionOptions(options, options.CurrentIndentLevel + 1));
stringBuilder.AppendLine($"{options.Indent}{propertyName} =");
stringBuilder.AppendLine(innerExceptionString);
}
private static string IndentString(string value, ExceptionOptions options)
{
return value.Replace(Environment.NewLine, Environment.NewLine + options.Indent);
}
private static void AppendValue(
StringBuilder stringBuilder,
string propertyName,
object value,
ExceptionOptions options)
{
if (value is DictionaryEntry)
{
DictionaryEntry dictionaryEntry = (DictionaryEntry)value;
stringBuilder.AppendLine($"{options.Indent}{propertyName} = {dictionaryEntry.Key} : {dictionaryEntry.Value}");
}
else if (value is Exception)
{
var innerException = (Exception)value;
AppendException(
stringBuilder,
propertyName,
innerException,
options);
}
else if (value is IEnumerable && !(value is string))
{
var collection = (IEnumerable)value;
if (collection.GetEnumerator().MoveNext())
{
AppendCollection(
stringBuilder,
propertyName,
collection,
options);
}
}
else
{
stringBuilder.AppendLine($"{options.Indent}{propertyName} = {value}");
}
}
}
public struct ExceptionOptions
{
public static readonly ExceptionOptions Default = new ExceptionOptions()
{
CurrentIndentLevel = 0,
IndentSpaces = 4,
OmitNullProperties = true
};
internal ExceptionOptions(ExceptionOptions options, int currentIndent)
{
this.CurrentIndentLevel = currentIndent;
this.IndentSpaces = options.IndentSpaces;
this.OmitNullProperties = options.OmitNullProperties;
}
internal string Indent { get { return new string(' ', this.IndentSpaces * this.CurrentIndentLevel); } }
internal int CurrentIndentLevel { get; set; }
public int IndentSpaces { get; set; }
public bool OmitNullProperties { get; set; }
}
Top Tip - Logging Exceptions
Most people will be using this code for logging. Consider using Serilog with my Serilog.Exceptions NuGet package which also logs all properties of an exception but does it faster and without reflection in the majority of cases. Serilog is a very advanced logging framework which is all the rage at the time of writing.
Top Tip - Human Readable Stack Traces
You can use the Ben.Demystifier NuGet package to get human readable stack traces for your exceptions or the serilog-enrichers-demystify NuGet package if you are using Serilog. If you are using .NET Core 2.1, then this feature comes built in.
For people who don't want to mess with overriding, this simple non-intrusive method might be enough:
public static string GetExceptionDetails(Exception exception)
{
return "Exception: " + exception.GetType()
+ "\r\nInnerException: " + exception.InnerException
+ "\r\nMessage: " + exception.Message
+ "\r\nStackTrace: " + exception.StackTrace;
}
It does not show the SQLException-specific details you want, though...
There is no secret method. You could probably just override the ToString() method and build the string you want.
Things like ErrorCode and Message are just properties of the exception that you can add to the desired string output.
Update: After re-reading your question and thinking more about this, Jason's answer is more likely what you are wanting. Overriding the ToString() method would only be helpful for exceptions that you created, not already implemented ones. It doesn't make sense to sub class existing exceptions just to add this functionality.
For displaying some details to user you should use ex.Message. For displaying to developers you will probably need ex.Message and ex.StackTrace.
There is no 'secret' method, you could consider Message property to be best fit for user friendly message.
Also be careful that in some case you may have inner exception in exception you catch which would be also useful to log.
You will probably have to manually construct that string by concatenating the various fields you are interested in.
Each left-side name is property in the Exception. If you want to display Message field, you can do
return ex.Message;
Pretty simple. Likewise, the StackTrace can be displayed as below link.
A complete example of StackTrace: http://msdn.microsoft.com/en-us/library/system.exception.stacktrace.aspx
and Exception class: http://msdn.microsoft.com/en-us/library/system.exception.aspx
I think the exception serialization to JSON is nice option. Sample result:
{
"Errors": [{
"Source": ".Net SqlClient Data Provider",
"Number": -1,
"Class": 20,
"Server": "111.168.222.70",
"Message": "A transport-level error has occurred when receiving results from the server. (provider: Session Provider, error: 19 - Physical connection is not usable)"
}
],
"ClientConnectionId": "b1854037-51e4-4943-94b4-72b7bb4c6ab7",
"ClassName": "System.Data.SqlClient.SqlException",
"Message": "A transport-level error has occurred when receiving results from the server. (provider: Session Provider, error: 19 - Physical connection is not usable)",
"Data": {
"HelpLink.ProdName": "Microsoft SQL Server",
"HelpLink.EvtSrc": "MSSQLServer",
"HelpLink.EvtID": "-1",
"HelpLink.BaseHelpUrl": "http://go.microsoft.com/fwlink",
"HelpLink.LinkId": "20476"
},
"InnerException": null,
"HelpURL": null,
"StackTraceString": " at System.Data.SqlClient.SqlConnection.OnError ... DbExecutionStrategy.Execute[TResult](Func`1 operation)",
"RemoteStackTraceString": null,
"RemoteStackIndex": 0,
"ExceptionMethod": "8\nOnError\nSystem.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\nSystem.Data.SqlClient.SqlConnection\nVoid OnError(System.Data.SqlClient.SqlException, Boolean, System.Action`1[System.Action])",
"HResult": -2146232060,
"Source": ".Net SqlClient Data Provider",
"WatsonBuckets": null
}
If you call ToString on Exception object, you get the class name appended by the message, followed by inner exception and then the stack trace.
className + message + InnerException + stackTrace
Given that, InnerException and StackTrace are only added if they are not null. Also, the fields you have mentioned in the screenshot are not part of standard Exception class. Yes, exception does offer a public property called "Data", that contain additional user-defined information about the exception.
In visual studio that sort of information can be outputted by a debugger visualizer.
I assume that because it is possible to write your own debugger visualizer:
http://msdn.microsoft.com/en-us/library/e2zc529c.aspx
That in theory, if your can reverse engineer the built-in debugger visualizer for exceptions (if your can work out where they are stored) then you could use the same functionality.
EDIT:
Here is a post about where the debugger visualizers are kept: Where do I find Microsoft.VisualStudio.DebuggerVisualizers?
You might be able to use it for your own purposes.
I have a webservice that wraps a COM object's functions for the purpose of reducing RPC communication errors. The parameter that I pass to a particular web service is a string[]. A problem seems to lie in the COM object function, which requires an object reference.
Some errors will show on the webpage because they come from the COMClass (error 87), others are compiler errors. I've tried to indicate which is which below.
[WebMethod(EnableSession=true)]
public int WSFunc(string[] StringArray){
//object StringArr = (object)((object[])StringArr); //returns error 87 in webpage log
//object StringArr = (object)StringArr; //returns error 87 in webpage log
//public override int COMClass.Func(ref object pStringList)
return COMClass.Func(ref StringArr);
//return COMClass.Func(ref StringArray); //Doesn't build, cannot convert from 'ref string[]' to 'ref object' (CS1503)
//return COMClass.Func(ref (object)StringArr); //Doesn't build, A ref or out argument must be an assignable variable (CS1510)
}
Also, this produces the same result (error 87 - parameter incorrect):
[WebMethod(EnableSession=true)]
public string WSFunc(string[] StringArray){
object[] StringObj = new object[StringArray.Length];
for(int i = 0; i < StringArray.Length; i++){
StringObj[i] = StringArray[i];
}
object MyString = (object)StringObj;
return ComClass.Func(ref MyString);
}
Has anyone else run into this issue?
I think this article should help you setup your marshalling for the com interop.
http://msdn.microsoft.com/en-us/library/aa645736(v=vs.71).aspx