10
\$\begingroup\$

I use Autofac a lot and for everything and when you make a mistake and forget to register a dependency etc. it'll tell you exaclty what's wrong. Although its exceptions are very helpful, the exception strings are at the same time hard to read because it's a large blob of text:

System.Exception: Blub ---> Autofac.Core.DependencyResolutionException: An error occurred during the activation of a particular registration. See the inner exception for details. Registration: Activator = User (ReflectionActivator), Services = [UserQuery+User], Lifetime = Autofac.Core.Lifetime.CurrentScopeLifetime, Sharing = None, Ownership = OwnedByLifetimeScope ---> None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'UserQuery+User' can be invoked with the available services and parameters: Cannot resolve parameter 'System.String name' of constructor 'Void .ctor(System.String)'. (See inner exception for details.) ---> Autofac.Core.DependencyResolutionException: None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'UserQuery+User' can be invoked with the available services and parameters: Cannot resolve parameter 'System.String name' of constructor 'Void .ctor(System.String)'. at Autofac.Core.Activators.Reflection.ReflectionActivator.GetValidConstructorBindings(IComponentContext context, IEnumerable`1 parameters)

To find the reason for this exception in such a string isn't easy. This is better done by some tool so I created. It reads the string for me and presents it in a more friendly way. I implemented it as a Debugger Visualizer.

Screenshot-1


DebuggerVisualizer

The ExceptionVisualizer is virtually a single function that shows the WPF window with the exception strings:

public class ExceptionVisualizer : DialogDebuggerVisualizer
{
    protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
    {
        var data = (IEnumerable<ExceptionInfo>)objectProvider.GetObject();

        var window = new Window
        {
            Title = "Exception Visualizer",
            Width = SystemParameters.WorkArea.Width * 0.4,
            Height = SystemParameters.WorkArea.Height * 0.6,
            Content = new DebuggerVisualizers.ExceptionControl
            {
                DataContext = new ExceptionControlModel
                {
                    Exceptions = data
                },
                HorizontalAlignment = HorizontalAlignment.Stretch
            },
            WindowStartupLocation = WindowStartupLocation.CenterScreen
        };

        window.ShowDialog();
    }

    public static void TestShowVisualizer(object objectToVisualize)
    {
        var visualizerHost = new VisualizerDevelopmentHost(objectToVisualize, typeof(ExceptionVisualizer));
        visualizerHost.ShowVisualizer();
    }
}  

It receives a collection of ExceptionInfos that I create with these helpers that parse the string by removing the stack trace and extracting exception names and messages. I know I could use the Exception object to extract exception names and messages but I'm going to reuse this parser in another tools later for parsing and searching logs so I didn't want to have two solutions.

public class ExceptionParser
{
    public static string RemoveStackStrace(string exceptionString)
    {
        // Stack-trace begins at the first 'at'
        return Regex.Split(exceptionString, @"^\s{3}at", RegexOptions.Multiline).First();
    }

    public static IEnumerable<ExceptionInfo> ParseExceptions(string exceptionString)
    {
        // Exceptions start with 'xException:' string and end either with '$' or '--->' if an inner exception follows.
        return
            Regex
                .Matches(exceptionString, @"(?<exception>(^|\w+)?Exception):\s(?<message>(.|\n)+?)(?=( --->|$))", RegexOptions.ExplicitCapture)
                .Cast<Match>()
                .Select(m => new ExceptionInfo { Name = m.Groups["exception"].Value, Message = m.Groups["message"].Value });
    }
}

public static class EnumerableExtensions
{
    public static IEnumerable<T> Reverse<T>(this IEnumerable<T> source) => new Stack<T>(source);
}

The DTO is

[Serializable]
public class ExceptionInfo
{
    public string Name { get; set; }

    public string Message { get; set; }

    public override string ToString()
    {
        return Name + Environment.NewLine + Message;
    }
}

GUI

On the UI side there is simple a WPF.UserControl with a ListBox and two TextBoxes. The Close button closes the window and the Copy button copies the list to Clipboard and contains a small animation that lets the button shrink and grow back to its original size. In order to keep it in the middle I also animate the right Margin.

<UserControl x:Class="Reusable.Apps.ExceptionControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Reusable.Apps"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800" Background="#FF404040"
             >
    <UserControl.Resources>
        <local:ExceptionControlModel x:Key="DesignViewModel" />
        <Style TargetType="TextBlock" x:Key="NameStyle">
            <Setter Property="FontSize" Value="20"/>
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="FontFamily" Value="Consolas"/>
            <Setter Property="Foreground" Value="DarkOrange"/>
            <Setter Property="Margin" Value="0,10,0,0" />
        </Style>
        <Style TargetType="TextBlock" x:Key="MessageStyle">
            <Setter Property="FontSize" Value="16"/>
            <Setter Property="FontFamily" Value="Segoe UI"/>
            <Setter Property="TextWrapping" Value="Wrap"/>
            <Setter Property="Margin" Value="0,5,0,0" />
            <Setter Property="Foreground" Value="WhiteSmoke"/>
        </Style>
        <Style x:Key="Theme" TargetType="{x:Type Control}">
            <Setter Property="Background" Value="#FF404040"></Setter>
        </Style>
    </UserControl.Resources>
    <UserControl.CommandBindings>
        <CommandBinding Command="Close"></CommandBinding>
    </UserControl.CommandBindings>
    <Grid >
        <Grid.RowDefinitions>
            <RowDefinition  />
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ListBox ItemsSource="{Binding Exceptions}" d:DataContext="{Binding Source={StaticResource DesignViewModel}}" Style="{StaticResource Theme}" Grid.Row="0" BorderThickness="0">
            <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                    <Setter Property="Width" Value="{Binding (Grid.ActualWidth), RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Grid}}}" />
                </Style>
            </ListBox.ItemContainerStyle>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <Border>
                            <TextBlock Text="{Binding Name}" Style="{StaticResource NameStyle}"  />
                        </Border>
                        <TextBlock Text="{Binding Message}" Style="{StaticResource MessageStyle}" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <DockPanel Grid.Row="1" HorizontalAlignment="Right" >
            <DockPanel.Resources>
                <Style TargetType="Button">
                    <Setter Property="Margin" Value="0,5,10,5" />
                    <Setter Property="Width" Value="100"/>
                    <Setter Property="Height" Value="25"></Setter>
                    <Setter Property="FontSize" Value="15"/>
                </Style>
            </DockPanel.Resources>
            <Button
                Content="Copy"
                Command="{x:Static local:ExceptionControlModel.CopyCommand}"
                CommandParameter="{Binding}">
                <Button.Triggers>
                    <EventTrigger RoutedEvent="Button.Click">
                        <EventTrigger.Actions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetProperty="Width" From="100" To="90" Duration="0:0:0.25"/>
                                    <DoubleAnimation Storyboard.TargetProperty="Width" From="90" To="100" Duration="0:0:0.25"/>
                                </Storyboard>
                            </BeginStoryboard>
                            <BeginStoryboard>
                                <Storyboard>
                                    <ThicknessAnimation Storyboard.TargetProperty="Margin" From="0,5,10,5" To="0,5,15,5" Duration="0:0:0.25"/>
                                    <ThicknessAnimation Storyboard.TargetProperty="Margin" From="0,5,15,5" To="0,5,10,5" Duration="0:0:0.25"/>
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger.Actions>
                    </EventTrigger>
                </Button.Triggers>
            </Button>
            <Button 
                Content="Close"
                Command="{x:Static local:ExceptionControlModel.CloseCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" />
        </DockPanel>
    </Grid>
</UserControl>

and this is its model with some design-time data:

public class ExceptionControlModel
{
    public static readonly ICommand CloseCommand = CommandFactory<Window>.Create(p => p.Close());

    public static readonly ICommand CopyCommand = CommandFactory<ExceptionControlModel>.Create(p => p.CopyToClipboard());

    public IEnumerable<ExceptionInfo> Exceptions { get; set; } = new[]
    {
        // This is design-time data.
        new ExceptionInfo {Name = "DependencyResolutionException", Message = "An error occurred during the activation of a particular registration. See the inner exception for details. Registration: Activator = User (ReflectionActivator), Services = [UserQuery+User], Lifetime = Autofac.Core.Lifetime.CurrentScopeLifetime, Sharing = None, Ownership = OwnedByLifetimeScope"},
        new ExceptionInfo {Name = "DependencyResolutionException", Message = "None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'UserQuery+User' can be invoked with the available services and parameters: Cannot resolve parameter 'System.String name' of constructor 'Void .ctor(System.String)'."},
    };

    private void CopyToClipboard()
    {
        Clipboard.SetText(Exceptions.Join(Environment.NewLine + Environment.NewLine));
    }
}

The command creation is supported with this helper factory that passes a strong type to the handler delegate:

public static class CommandFactory<T>
{
    public static ICommand Create([NotNull] Action<T> execute)
    {
        if (execute == null) throw new ArgumentNullException(nameof(execute));

        return new Command(parameter => execute((T)parameter));
    }

    public static ICommand Create([NotNull] Action<T> execute, [NotNull] Predicate<object> canExecute)
    {
        if (execute == null) throw new ArgumentNullException(nameof(execute));
        if (canExecute == null) throw new ArgumentNullException(nameof(canExecute));

        return new Command(parameter => execute((T)parameter), parameter => canExecute((T)parameter));
    }

    private class Command : ICommand
    {
        private readonly Action<object> _execute;

        private readonly Predicate<object> _canExecute;

        public Command(Action<object> execute) : this(execute, _ => true) { }

        public Command(Action<object> execute, Predicate<object> canExecute)
        {
            _execute = execute ?? throw new ArgumentNullException(nameof(execute));
            _canExecute = canExecute;
        }

        #region ICommand

        public event EventHandler CanExecuteChanged
        {
            add => CommandManager.RequerySuggested += value;
            remove => CommandManager.RequerySuggested -= value;
        }

        [DebuggerStepThrough]
        public bool CanExecute(object parameter) => _canExecute(parameter);

        [DebuggerStepThrough]
        public void Execute(object parameter) => _execute(parameter);

        #endregion
    }
}

Example

I use the following code for testing where I try to resolve an instance of the User class which is missing a dependency:

internal class ExceptionVisualizerExperiment
{
    public static void Run()
    {
        try
        {
            try
            {
                var builder = new ContainerBuilder();
                builder.RegisterType<User>();
                var container = builder.Build();
                container.Resolve<User>();
                throw new DivideByZeroException("Blub");
            }
            catch (Exception ex)
            {
                throw new Exception("Blub", ex);

            }
        }
        catch (Exception ex)
        {
            var exceptionString = ex.ToString();
            exceptionString = ExceptionParser.RemoveStackStrace(exceptionString);
            var exceptions = ExceptionParser.ParseExceptions(exceptionString); 
            ExceptionVisualizer
                .TestShowVisualizer(EnumerableExtensions.Reverse(exceptions));
        }
    }


    public static void TestShowVisualizer(object objectToVisualize)
    {
        var visualizerHost = new VisualizerDevelopmentHost(objectToVisualize, typeof(ExceptionVisualizer));
        visualizerHost.ShowVisualizer();
    }
}

internal class User
{
    public User(string name) { }
}

Running

There is one more component that is required to run it in Visual Studio. It's the custom object-source for the exception serialization:

public class ExceptionVisualizerObjectSource : VisualizerObjectSource
{       
    public override void GetData(object target, Stream outgoingData)
    {
        var exceptionString = target.ToString();
        exceptionString = ExceptionParser.RemoveStackStrace(exceptionString);
        var exceptions = ExceptionParser.ParseExceptions(exceptionString).Reverse();
        Serialize(outgoingData, exceptions);
    }
}

This needs to be registered with:

[assembly: DebuggerVisualizer(
    visualizer: typeof(ExceptionVisualizer),
    visualizerObjectSource: typeof(ExceptionVisualizerObjectSource),
    Target = typeof(Exception),
    Description = "Exception Visualizer")]

So what do you think about the parsing of the excpetion string and the UI? It's my first WPF application for a long time so it's probably not the state of the art. Is there anything you would improve either in the back and or front end?


As always, you can also find it on my GitHub under Reusable.DebuggerVisualizers. The Console code is here.

\$\endgroup\$
  • 1
    \$\begingroup\$ Is there any reason behind parsing exception by string instead of navigating the layers with .InnerException? Are they coming from a log file? \$\endgroup\$ – Xiaoy312 Dec 3 '18 at 23:11
  • 1
    \$\begingroup\$ @Xiaoy312 tl;dr? ;-) I wrote about it - I want to reuse this parser later for evaluating logs too so I didn't want to have two different solutions. \$\endgroup\$ – t3chb0t Dec 4 '18 at 7:27
  • \$\begingroup\$ @t3chb0t How does your parser handle aggregate exceptions? docs.microsoft.com/en-us/dotnet/api/… \$\endgroup\$ – dfhwze Jun 10 at 13:17
5
\$\begingroup\$
    public static string RemoveStackStrace(string exceptionString)
    {
        // Stack-trace begins at the first 'at'
        return Regex.Split(exceptionString, @"^\s{3}at", RegexOptions.Multiline).First();
    }

I assume the method name is a typo for RemoveStackTrace.

Regex seems overkill, as does \s{3} instead of simply " at". Is there any circumstance in which .Net would localise the spaces to something else but not the at?

(Also: yes, the at is localised. This library won't be so helpful for parsing error messages generated by computers running in a non-English locale).


    public static IEnumerable<ExceptionInfo> ParseExceptions(string exceptionString)
    {
        // Exceptions start with 'xException:' string and end either with '$' or '--->' if an inner exception follows.

That's not necessarily true, particularly with legacy libraries. There's a tonne of weirdly named exceptions for compatibility with Visual Basic, not to mention ExceptionCollection. And who knows what third party libraries do, without checking? In your use case it might be true, but perhaps it would be worth logging a warning if there's no match.


public static class EnumerableExtensions
{
    public static IEnumerable<T> Reverse<T>(this IEnumerable<T> source) => new Stack<T>(source);
}

What's wrong with System.Linq.Enumerable.Reverse?


Otherwise, looks good, and possibly worth stealing borrowing. I'm not sure: can it be packaged as a nuget in such a way that it's just necessary to add a reference to it and it installs itself automatically?

\$\endgroup\$
  • \$\begingroup\$ I think it needs to be packaged as *.vsix to be installable because it'd be a VS extension. However, I haven't tried doing this yet. \$\endgroup\$ – t3chb0t Jul 25 at 11:52
  • 1
    \$\begingroup\$ I'm mucking around with custom Roslyn analysers at the moment, and they can be packaged as Vsix to be installed in VS globally or as nuget, installing themselves in the projects they're added to. I wonder what other features work both ways, and whether debugger visualisers are one of them. \$\endgroup\$ – Peter Taylor Jul 25 at 11:55
  • 1
    \$\begingroup\$ @t3chb0t after all our latest discoveries in LINQ, we should at least verify its source before using Reverse: codereview.stackexchange.com/questions/224752/… ;-) \$\endgroup\$ – dfhwze Jul 25 at 11:57
  • \$\begingroup\$ I actually abandoned VS and use virtually exclusively Rider. I wonder whether it'd be possible to port this code to be useful there ;-] \$\endgroup\$ – t3chb0t Jul 25 at 12:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.